helpers.py 10 KB

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