helpers.py 11 KB

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