helpers.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. from __future__ import with_statement
  2. import argparse
  3. from datetime import datetime, timedelta
  4. from fnmatch import fnmatchcase
  5. from operator import attrgetter
  6. import grp
  7. import os
  8. import pwd
  9. import re
  10. import stat
  11. import sys
  12. import time
  13. import urllib
  14. def purge_split(archives, pattern, n, skip=[]):
  15. items = {}
  16. keep = []
  17. for a in archives:
  18. key = to_localtime(a.ts).strftime(pattern)
  19. items.setdefault(key, [])
  20. items[key].append(a)
  21. for key, values in sorted(items.items(), reverse=True):
  22. if n and values[0] not in skip:
  23. values.sort(key=attrgetter('ts'), reverse=True)
  24. keep.append(values[0])
  25. n -= 1
  26. return keep
  27. class Statistics(object):
  28. def __init__(self):
  29. self.osize = self.csize = self.usize = self.nfiles = 0
  30. def update(self, size, csize, unique):
  31. self.osize += size
  32. self.csize += csize
  33. if unique:
  34. self.usize += csize
  35. def print_(self):
  36. print 'Number of files: %d' % self.nfiles
  37. print 'Original size: %d (%s)' % (self.osize, format_file_size(self.osize))
  38. print 'Compressed size: %s (%s)'% (self.csize, format_file_size(self.csize))
  39. print 'Unique data: %d (%s)' % (self.usize, format_file_size(self.usize))
  40. # OSX filenames are UTF-8 Only so any non-utf8 filenames are url encoded
  41. if sys.platform == 'darwin':
  42. def encode_filename(name):
  43. try:
  44. name.decode('utf-8')
  45. return name
  46. except UnicodeDecodeError:
  47. return urllib.quote(name)
  48. else:
  49. encode_filename = str
  50. class Counter(object):
  51. __slots__ = ('v',)
  52. def __init__(self, value=0):
  53. self.v = value
  54. def inc(self, amount=1):
  55. self.v += amount
  56. def dec(self, amount=1):
  57. self.v -= amount
  58. def __cmp__(self, x):
  59. return cmp(self.v, x)
  60. def __repr__(self):
  61. return '<Counter(%r)>' % self.v
  62. def get_keys_dir():
  63. """Determine where to store keys and cache"""
  64. return os.environ.get('DARC_KEYS_DIR',
  65. os.path.join(os.path.expanduser('~'), '.darc', 'keys'))
  66. def get_cache_dir():
  67. """Determine where to store keys and cache"""
  68. return os.environ.get('DARC_CACHE_DIR',
  69. os.path.join(os.path.expanduser('~'), '.darc', 'cache'))
  70. def deferrable(f):
  71. def wrapper(*args, **kw):
  72. callback = kw.pop('callback', None)
  73. if callback:
  74. data = kw.pop('callback_data', None)
  75. try:
  76. res = f(*args, **kw)
  77. except Exception, e:
  78. callback(None, e, data)
  79. else:
  80. callback(res, None, data)
  81. else:
  82. return f(*args, **kw)
  83. return wrapper
  84. def error_callback(res, error, data):
  85. if res:
  86. raise res
  87. def to_localtime(ts):
  88. """Convert datetime object from UTC to local time zone"""
  89. return ts - timedelta(seconds=time.altzone)
  90. def exclude_path(path, patterns):
  91. """Used by create and extract sub-commands to determine
  92. if an item should be processed or not
  93. """
  94. for pattern in (patterns or []):
  95. if pattern.match(path):
  96. return isinstance(pattern, ExcludePattern)
  97. return False
  98. class IncludePattern(object):
  99. """--include PATTERN
  100. >>> py = IncludePattern('*.py')
  101. >>> foo = IncludePattern('/foo')
  102. >>> py.match('/foo/foo.py')
  103. True
  104. >>> py.match('/bar/foo.java')
  105. False
  106. >>> foo.match('/foo/foo.py')
  107. True
  108. >>> foo.match('/bar/foo.java')
  109. False
  110. >>> foo.match('/foobar/foo.py')
  111. False
  112. >>> foo.match('/foo')
  113. True
  114. """
  115. def __init__(self, pattern):
  116. self.pattern = self.dirpattern = pattern
  117. if not pattern.endswith(os.path.sep):
  118. self.dirpattern += os.path.sep
  119. def match(self, path):
  120. dir, name = os.path.split(path)
  121. return (path == self.pattern
  122. or (dir + os.path.sep).startswith(self.dirpattern)
  123. or fnmatchcase(name, self.pattern))
  124. def __repr__(self):
  125. return '%s(%s)' % (type(self), self.pattern)
  126. class ExcludePattern(IncludePattern):
  127. """
  128. """
  129. def walk_path(path, skip_inodes=None):
  130. st = os.lstat(path)
  131. if skip_inodes and (st.st_ino, st.st_dev) in skip_inodes:
  132. return
  133. yield path, st
  134. if stat.S_ISDIR(st.st_mode):
  135. for f in os.listdir(path):
  136. for x in walk_path(os.path.join(path, f), skip_inodes):
  137. yield x
  138. def format_time(t):
  139. """Format datetime suitable for fixed length list output
  140. """
  141. if (datetime.now() - t).days < 365:
  142. return t.strftime('%b %d %H:%M')
  143. else:
  144. return t.strftime('%b %d %Y')
  145. def format_timedelta(td):
  146. """Format timedelta in a human friendly format
  147. >>> from datetime import datetime
  148. >>> t0 = datetime(2001, 1, 1, 10, 20, 3, 0)
  149. >>> t1 = datetime(2001, 1, 1, 12, 20, 4, 100000)
  150. >>> format_timedelta(t1 - t0)
  151. '2 hours 1.10 seconds'
  152. """
  153. # Since td.total_seconds() requires python 2.7
  154. ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
  155. s = ts % 60
  156. m = int(ts / 60) % 60
  157. h = int(ts / 3600) % 24
  158. txt = '%.2f seconds' % s
  159. if m:
  160. txt = '%d minutes %s' % (m, txt)
  161. if h:
  162. txt = '%d hours %s' % (h, txt)
  163. if td.days:
  164. txt = '%d days %s' % (td.days, txt)
  165. return txt
  166. def format_file_mode(mod):
  167. """Format file mode bits for list output
  168. """
  169. def x(v):
  170. return ''.join(v & m and s or '-'
  171. for m, s in ((4, 'r'), (2, 'w'), (1, 'x')))
  172. return '%s%s%s' % (x(mod / 64), x(mod / 8), x(mod))
  173. def format_file_size(v):
  174. """Format file size into a human friendly format
  175. """
  176. if v > 1024 * 1024 * 1024:
  177. return '%.2f GB' % (v / 1024. / 1024. / 1024.)
  178. elif v > 1024 * 1024:
  179. return '%.2f MB' % (v / 1024. / 1024.)
  180. elif v > 1024:
  181. return '%.2f kB' % (v / 1024.)
  182. else:
  183. return '%d B' % v
  184. class IntegrityError(Exception):
  185. """
  186. """
  187. def memoize(function):
  188. cache = {}
  189. def decorated_function(*args):
  190. try:
  191. return cache[args]
  192. except KeyError:
  193. val = function(*args)
  194. cache[args] = val
  195. return val
  196. return decorated_function
  197. @memoize
  198. def uid2user(uid):
  199. try:
  200. return pwd.getpwuid(uid).pw_name
  201. except KeyError:
  202. return None
  203. @memoize
  204. def user2uid(user):
  205. try:
  206. return pwd.getpwnam(user).pw_uid
  207. except KeyError:
  208. return None
  209. @memoize
  210. def gid2group(gid):
  211. try:
  212. return grp.getgrgid(gid).gr_name
  213. except KeyError:
  214. return None
  215. @memoize
  216. def group2gid(group):
  217. try:
  218. return grp.getgrnam(group).gr_gid
  219. except KeyError:
  220. return None
  221. class Location(object):
  222. """Object representing a store / archive location
  223. >>> Location('ssh://user@host:1234/some/path::archive')
  224. Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')
  225. >>> Location('file:///some/path::archive')
  226. Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')
  227. >>> Location('user@host:/some/path::archive')
  228. Location(proto='ssh', user='user', host='host', port=22, path='/some/path', archive='archive')
  229. >>> Location('/some/path::archive')
  230. Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')
  231. """
  232. proto = user = host = port = path = archive = None
  233. ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
  234. r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
  235. r'(?P<path>[^:]*)(?:::(?P<archive>.+))?')
  236. file_re = re.compile(r'(?P<proto>file)://'
  237. r'(?P<path>[^:]*)(?:::(?P<archive>.+))?')
  238. scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
  239. r'(?P<path>[^:]*)(?:::(?P<archive>.+))?')
  240. def __init__(self, text):
  241. if not self.parse(text):
  242. raise ValueError
  243. def parse(self, text):
  244. m = self.ssh_re.match(text)
  245. if m:
  246. self.proto = m.group('proto')
  247. self.user = m.group('user')
  248. self.host = m.group('host')
  249. self.port = m.group('port') and int(m.group('port')) or 22
  250. self.path = m.group('path')
  251. self.archive = m.group('archive')
  252. return True
  253. m = self.file_re.match(text)
  254. if m:
  255. self.proto = m.group('proto')
  256. self.path = m.group('path')
  257. self.archive = m.group('archive')
  258. return True
  259. m = self.scp_re.match(text)
  260. if m:
  261. self.user = m.group('user')
  262. self.host = m.group('host')
  263. self.path = m.group('path')
  264. self.archive = m.group('archive')
  265. self.proto = self.host and 'ssh' or 'file'
  266. if self.proto == 'ssh':
  267. self.port = 22
  268. return True
  269. return False
  270. def __str__(self):
  271. items = []
  272. items.append('proto=%r' % self.proto)
  273. items.append('user=%r' % self.user)
  274. items.append('host=%r' % self.host)
  275. items.append('port=%r' % self.port)
  276. items.append('path=%r'% self.path)
  277. items.append('archive=%r' % self.archive)
  278. return ', '.join(items)
  279. def to_key_filename(self):
  280. name = re.sub('[^\w]', '_', self.path).strip('_')
  281. if self.proto != 'file':
  282. name = self.host + '__' + name
  283. return os.path.join(get_keys_dir(), name)
  284. def __repr__(self):
  285. return "Location(%s)" % self
  286. def location_validator(archive=None):
  287. def validator(text):
  288. try:
  289. loc = Location(text)
  290. except ValueError:
  291. raise argparse.ArgumentTypeError('Invalid location format: "%s"' % text)
  292. if archive is True and not loc.archive:
  293. raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
  294. elif archive is False and loc.archive:
  295. raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
  296. return loc
  297. return validator