helpers.py 10 KB

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