helpers.py 11 KB

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