helpers.py 11 KB

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