1
2
3
4 """
5 | This file is part of the web2py Web Framework
6 | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 File operations
10 ---------------
11 """
12
13 import storage
14 import os
15 import re
16 import tarfile
17 import glob
18 import time
19 import datetime
20 import logging
21 from http import HTTP
22 from gzip import open as gzopen
23
24
25 __all__ = [
26 'parse_version',
27 'read_file',
28 'write_file',
29 'readlines_file',
30 'up',
31 'abspath',
32 'mktree',
33 'listdir',
34 'recursive_unlink',
35 'cleanpath',
36 'tar',
37 'untar',
38 'tar_compiled',
39 'get_session',
40 'check_credentials',
41 'w2p_pack',
42 'w2p_unpack',
43 'w2p_pack_plugin',
44 'w2p_unpack_plugin',
45 'fix_newlines',
46 'make_fake_file_like_object',
47 ]
48
49
50 -def parse_semantic(version="Version 1.99.0-rc.1+timestamp.2011.09.19.08.23.26"):
51 """Parses a version string according to http://semver.org/ rules
52
53 Args:
54 version(str): the SemVer string
55
56 Returns:
57 tuple: Major, Minor, Patch, Release, Build Date
58
59 """
60 re_version = re.compile('(\d+)\.(\d+)\.(\d+)(\-(?P<pre>[^\s+]*))?(\+(?P<build>\S*))')
61 m = re_version.match(version.strip().split()[-1])
62 if not m:
63 return None
64 a, b, c = int(m.group(1)), int(m.group(2)), int(m.group(3))
65 pre_release = m.group('pre') or ''
66 build = m.group('build') or ''
67 if build.startswith('timestamp'):
68 build = datetime.datetime.strptime(build.split('.',1)[1], '%Y.%m.%d.%H.%M.%S')
69 return (a, b, c, pre_release, build)
70
71 -def parse_legacy(version="Version 1.99.0 (2011-09-19 08:23:26)"):
72 """Parses "legacy" version string
73
74 Args:
75 version(str): the version string
76
77 Returns:
78 tuple: Major, Minor, Patch, Release, Build Date
79
80 """
81 re_version = re.compile('[^\d]+ (\d+)\.(\d+)\.(\d+)\s*\((?P<datetime>.+?)\)\s*(?P<type>[a-z]+)?')
82 m = re_version.match(version)
83 a, b, c = int(m.group(1)), int(m.group(2)), int(m.group(3)),
84 pre_release = m.group('type') or 'dev'
85 build = datetime.datetime.strptime(m.group('datetime'), '%Y-%m-%d %H:%M:%S')
86 return (a, b, c, pre_release, build)
87
89 """Attempts to parse SemVer, fallbacks on legacy
90 """
91 version_tuple = parse_semantic(version)
92 if not version_tuple:
93 version_tuple = parse_legacy(version)
94 return version_tuple
95
97 """Returns content from filename, making sure to close the file explicitly
98 on exit.
99 """
100 f = open(filename, mode)
101 try:
102 return f.read()
103 finally:
104 f.close()
105
106
108 """Writes <value> to filename, making sure to close the file
109 explicitly on exit.
110 """
111 f = open(filename, mode)
112 try:
113 return f.write(value)
114 finally:
115 f.close()
116
117
119 """Applies .split('\n') to the output of `read_file()`
120 """
121 return read_file(filename, mode).split('\n')
122
123
131
132
133 -def listdir(
134 path,
135 expression='^.+$',
136 drop=True,
137 add_dirs=False,
138 sort=True,
139 maxnum = None,
140 ):
168
169
179
180
182 """Turns any expression/path into a valid filename. replaces / with _ and
183 removes special characters.
184 """
185
186 items = path.split('.')
187 if len(items) > 1:
188 path = re.sub('[^\w\.]+', '_', '_'.join(items[:-1]) + '.'
189 + ''.join(items[-1:]))
190 else:
191 path = re.sub('[^\w\.]+', '_', ''.join(items[-1:]))
192 return path
193
194
196
197 if not hasattr(tarfile.TarFile, 'extractall'):
198 from tarfile import ExtractError
199
200 class TarFile(tarfile.TarFile):
201
202 def extractall(self, path='.', members=None):
203 """Extract all members from the archive to the current working
204 directory and set owner, modification time and permissions on
205 directories afterwards. `path' specifies a different directory
206 to extract to. `members' is optional and must be a subset of the
207 list returned by getmembers().
208 """
209
210 directories = []
211 if members is None:
212 members = self
213 for tarinfo in members:
214 if tarinfo.isdir():
215
216
217
218
219 try:
220 os.makedirs(os.path.join(path,
221 tarinfo.name), 0777)
222 except EnvironmentError:
223 pass
224 directories.append(tarinfo)
225 else:
226 self.extract(tarinfo, path)
227
228
229
230 directories.sort(lambda a, b: cmp(a.name, b.name))
231 directories.reverse()
232
233
234
235 for tarinfo in directories:
236 path = os.path.join(path, tarinfo.name)
237 try:
238 self.chown(tarinfo, path)
239 self.utime(tarinfo, path)
240 self.chmod(tarinfo, path)
241 except ExtractError, e:
242 if self.errorlevel > 1:
243 raise
244 else:
245 self._dbg(1, 'tarfile: %s' % e)
246
247 _cls = TarFile
248 else:
249 _cls = tarfile.TarFile
250
251 tar = _cls(filename, 'r')
252 ret = tar.extractall(path, members)
253 tar.close()
254 return ret
255
256
257 -def tar(file, dir, expression='^.+$', filenames=None):
258 """Tars dir into file, only tars file that match expression
259 """
260
261 tar = tarfile.TarFile(file, 'w')
262 try:
263 if filenames is None:
264 filenames = listdir(dir, expression, add_dirs=True)
265 for file in filenames:
266 tar.add(os.path.join(dir, file), file, False)
267 finally:
268 tar.close()
269
271 """Untar file into dir
272 """
273
274 _extractall(file, dir)
275
276
277 -def w2p_pack(filename, path, compiled=False, filenames=None):
278 """Packs a web2py application.
279
280 Args:
281 filename(str): path to the resulting archive
282 path(str): path to the application
283 compiled(bool): if `True` packs the compiled version
284 filenames(list): adds filenames to the archive
285 """
286 filename = abspath(filename)
287 path = abspath(path)
288 tarname = filename + '.tar'
289 if compiled:
290 tar_compiled(tarname, path, '^[\w\.\-]+$')
291 else:
292 tar(tarname, path, '^[\w\.\-]+$', filenames=filenames)
293 w2pfp = gzopen(filename, 'wb')
294 tarfp = open(tarname, 'rb')
295 w2pfp.write(tarfp.read())
296 w2pfp.close()
297 tarfp.close()
298 os.unlink(tarname)
299
301 if not os.path.exists('welcome.w2p') or os.path.exists('NEWINSTALL'):
302 try:
303 w2p_pack('welcome.w2p', 'applications/welcome')
304 os.unlink('NEWINSTALL')
305 logging.info("New installation: created welcome.w2p file")
306 except:
307 logging.error("New installation error: unable to create welcome.w2p file")
308
309
311
312 if filename=='welcome.w2p':
313 create_welcome_w2p()
314 filename = abspath(filename)
315 path = abspath(path)
316 if filename[-4:] == '.w2p' or filename[-3:] == '.gz':
317 if filename[-4:] == '.w2p':
318 tarname = filename[:-4] + '.tar'
319 else:
320 tarname = filename[:-3] + '.tar'
321 fgzipped = gzopen(filename, 'rb')
322 tarfile = open(tarname, 'wb')
323 tarfile.write(fgzipped.read())
324 tarfile.close()
325 fgzipped.close()
326 else:
327 tarname = filename
328 untar(tarname, path)
329 if delete_tar:
330 os.unlink(tarname)
331
332
334 """Packs the given plugin into a w2p file.
335 Will match files at::
336
337 <path>/*/plugin_[name].*
338 <path>/*/plugin_[name]/*
339
340 """
341 filename = abspath(filename)
342 path = abspath(path)
343 if not filename.endswith('web2py.plugin.%s.w2p' % plugin_name):
344 raise Exception("Not a web2py plugin name")
345 plugin_tarball = tarfile.open(filename, 'w:gz')
346 try:
347 app_dir = path
348 while app_dir[-1] == '/':
349 app_dir = app_dir[:-1]
350 files1 = glob.glob(
351 os.path.join(app_dir, '*/plugin_%s.*' % plugin_name))
352 files2 = glob.glob(
353 os.path.join(app_dir, '*/plugin_%s/*' % plugin_name))
354 for file in files1 + files2:
355 plugin_tarball.add(file, arcname=file[len(app_dir) + 1:])
356 finally:
357 plugin_tarball.close()
358
359
366
367
369 """Used to tar a compiled application.
370 The content of models, views, controllers is not stored in the tar file.
371 """
372
373 tar = tarfile.TarFile(file, 'w')
374 for file in listdir(dir, expression, add_dirs=True):
375 filename = os.path.join(dir, file)
376 if os.path.islink(filename):
377 continue
378 if os.path.isfile(filename) and file[-4:] != '.pyc':
379 if file[:6] == 'models':
380 continue
381 if file[:5] == 'views':
382 continue
383 if file[:11] == 'controllers':
384 continue
385 if file[:7] == 'modules':
386 continue
387 tar.add(filename, file, False)
388 tar.close()
389
390
393
394
396 """Checks that user is authorized to access other_application"""
397 if request.application == other_application:
398 raise KeyError
399 try:
400 session_id = request.cookies['session_id_' + other_application].value
401 session_filename = os.path.join(
402 up(request.folder), other_application, 'sessions', session_id)
403 osession = storage.load_storage(session_filename)
404 except Exception, e:
405 osession = storage.Storage()
406 return osession
407
408 -def set_session(request, session, other_application='admin'):
409 """Checks that user is authorized to access other_application"""
410 if request.application == other_application:
411 raise KeyError
412 session_id = request.cookies['session_id_' + other_application].value
413 session_filename = os.path.join(
414 up(request.folder), other_application, 'sessions', session_id)
415 storage.save_storage(session,session_filename)
416
417 -def check_credentials(request, other_application='admin',
418 expiration=60 * 60, gae_login=True):
419 """Checks that user is authorized to access other_application"""
420 if request.env.web2py_runtime_gae:
421 from google.appengine.api import users
422 if users.is_current_user_admin():
423 return True
424 elif gae_login:
425 login_html = '<a href="%s">Sign in with your google account</a>.' \
426 % users.create_login_url(request.env.path_info)
427 raise HTTP(200, '<html><body>%s</body></html>' % login_html)
428 else:
429 return False
430 else:
431 t0 = time.time()
432 dt = t0 - expiration
433 s = get_session(request, other_application)
434 r = (s.authorized and s.last_time and s.last_time > dt)
435 if r:
436 s.last_time = t0
437 set_session(request,s,other_application)
438 return r
439
440
442 regex = re.compile(r'''(\r
443 |\r|
444 )''')
445 for filename in listdir(path, '.*\.(py|html)$', drop=False):
446 rdata = read_file(filename, 'rb')
447 wdata = regex.sub('\n', rdata)
448 if wdata != rdata:
449 write_file(filename, wdata, 'wb')
450
451
452 -def copystream(
453 src,
454 dest,
455 size,
456 chunk_size=10 ** 5,
457 ):
458 """
459 this is here because I think there is a bug in shutil.copyfileobj
460 """
461 while size > 0:
462 if size < chunk_size:
463 data = src.read(size)
464 else:
465 data = src.read(chunk_size)
466 length = len(data)
467 if length > size:
468 (data, length) = (data[:size], size)
469 size -= length
470 if length == 0:
471 break
472 dest.write(data)
473 if length < chunk_size:
474 break
475 dest.seek(0)
476 return
477
478
480 class LogFile(object):
481 def write(self, value):
482 pass
483
484 def close(self):
485 pass
486 return LogFile()
487
488
489 from settings import global_settings
490
491
492
504