helpers.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. import argparse
  2. import binascii
  3. import grp
  4. import msgpack
  5. import os
  6. import pwd
  7. import re
  8. import sys
  9. import time
  10. from datetime import datetime, timezone, timedelta
  11. from fnmatch import translate
  12. from operator import attrgetter
  13. import fcntl
  14. from . import hashindex
  15. from . import chunker
  16. from . import crypto
  17. class Error(Exception):
  18. """Error base class"""
  19. exit_code = 1
  20. def get_message(self):
  21. return 'Error: ' + type(self).__doc__.format(*self.args)
  22. class ExtensionModuleError(Error):
  23. """The Borg binary extension modules do not seem to be properly installed"""
  24. class UpgradableLock:
  25. class ReadLockFailed(Error):
  26. """Failed to acquire read lock on {}"""
  27. class WriteLockFailed(Error):
  28. """Failed to acquire write lock on {}"""
  29. def __init__(self, path, exclusive=False):
  30. self.path = path
  31. try:
  32. self.fd = open(path, 'r+')
  33. except IOError:
  34. self.fd = open(path, 'r')
  35. try:
  36. if exclusive:
  37. fcntl.lockf(self.fd, fcntl.LOCK_EX)
  38. else:
  39. fcntl.lockf(self.fd, fcntl.LOCK_SH)
  40. # Python 3.2 raises IOError, Python3.3+ raises OSError
  41. except (IOError, OSError):
  42. if exclusive:
  43. raise self.WriteLockFailed(self.path)
  44. else:
  45. raise self.ReadLockFailed(self.path)
  46. self.is_exclusive = exclusive
  47. def upgrade(self):
  48. try:
  49. fcntl.lockf(self.fd, fcntl.LOCK_EX)
  50. # Python 3.2 raises IOError, Python3.3+ raises OSError
  51. except (IOError, OSError):
  52. raise self.WriteLockFailed(self.path)
  53. self.is_exclusive = True
  54. def release(self):
  55. fcntl.lockf(self.fd, fcntl.LOCK_UN)
  56. self.fd.close()
  57. def check_extension_modules():
  58. from . import platform
  59. if (hashindex.API_VERSION != 2 or
  60. chunker.API_VERSION != 2 or
  61. crypto.API_VERSION != 2 or
  62. platform.API_VERSION != 2):
  63. raise ExtensionModuleError
  64. class Manifest:
  65. MANIFEST_ID = b'\0' * 32
  66. def __init__(self, key, repository):
  67. self.archives = {}
  68. self.config = {}
  69. self.key = key
  70. self.repository = repository
  71. @classmethod
  72. def load(cls, repository, key=None):
  73. from .key import key_factory
  74. cdata = repository.get(cls.MANIFEST_ID)
  75. if not key:
  76. key = key_factory(repository, cdata)
  77. manifest = cls(key, repository)
  78. data = key.decrypt(None, cdata)
  79. manifest.id = key.id_hash(data)
  80. m = msgpack.unpackb(data)
  81. if not m.get(b'version') == 1:
  82. raise ValueError('Invalid manifest version')
  83. manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items())
  84. manifest.timestamp = m.get(b'timestamp')
  85. if manifest.timestamp:
  86. manifest.timestamp = manifest.timestamp.decode('ascii')
  87. manifest.config = m[b'config']
  88. return manifest, key
  89. def write(self):
  90. self.timestamp = datetime.utcnow().isoformat()
  91. data = msgpack.packb(StableDict({
  92. 'version': 1,
  93. 'archives': self.archives,
  94. 'timestamp': self.timestamp,
  95. 'config': self.config,
  96. }))
  97. self.id = self.key.id_hash(data)
  98. self.repository.put(self.MANIFEST_ID, self.key.encrypt(data))
  99. def prune_within(archives, within):
  100. multiplier = {'H': 1, 'd': 24, 'w': 24*7, 'm': 24*31, 'y': 24*365}
  101. try:
  102. hours = int(within[:-1]) * multiplier[within[-1]]
  103. except (KeyError, ValueError):
  104. # I don't like how this displays the original exception too:
  105. raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within)
  106. if hours <= 0:
  107. raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
  108. target = datetime.now(timezone.utc) - timedelta(seconds=hours*60*60)
  109. return [a for a in archives if a.ts > target]
  110. def prune_split(archives, pattern, n, skip=[]):
  111. last = None
  112. keep = []
  113. if n == 0:
  114. return keep
  115. for a in sorted(archives, key=attrgetter('ts'), reverse=True):
  116. period = to_localtime(a.ts).strftime(pattern)
  117. if period != last:
  118. last = period
  119. if a not in skip:
  120. keep.append(a)
  121. if len(keep) == n:
  122. break
  123. return keep
  124. class Statistics:
  125. def __init__(self):
  126. self.osize = self.csize = self.usize = self.nfiles = 0
  127. def update(self, size, csize, unique):
  128. self.osize += size
  129. self.csize += csize
  130. if unique:
  131. self.usize += csize
  132. def print_(self, label, cache):
  133. total_size, total_csize, unique_size, unique_csize = cache.chunks.summarize()
  134. print()
  135. print(' Original size Compressed size Deduplicated size')
  136. print('%-15s %20s %20s %20s' % (label, format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize)))
  137. print('All archives: %20s %20s %20s' % (format_file_size(total_size), format_file_size(total_csize), format_file_size(unique_csize)))
  138. def show_progress(self, item=None, final=False):
  139. if not final:
  140. path = remove_surrogates(item[b'path']) if item else ''
  141. if len(path) > 43:
  142. path = '%s...%s' % (path[:20], path[-20:])
  143. msg = '%9s O %9s C %9s D %-43s' % (
  144. format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize), path)
  145. else:
  146. msg = ' ' * 79
  147. print(msg, end='\r')
  148. sys.stdout.flush()
  149. def get_keys_dir():
  150. """Determine where to repository keys and cache"""
  151. return os.environ.get('BORG_KEYS_DIR',
  152. os.path.join(os.path.expanduser('~'), '.borg', 'keys'))
  153. def get_cache_dir():
  154. """Determine where to repository keys and cache"""
  155. return os.environ.get('BORG_CACHE_DIR',
  156. os.path.join(os.path.expanduser('~'), '.cache', 'borg'))
  157. def to_localtime(ts):
  158. """Convert datetime object from UTC to local time zone"""
  159. return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])
  160. def parse_timestamp(timestamp):
  161. """Parse a ISO 8601 timestamp string"""
  162. if '.' in timestamp: # microseconds might not be pressent
  163. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=timezone.utc)
  164. else:
  165. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
  166. def update_excludes(args):
  167. """Merge exclude patterns from files with those on command line.
  168. Empty lines and lines starting with '#' are ignored, but whitespace
  169. is not stripped."""
  170. if hasattr(args, 'exclude_files') and args.exclude_files:
  171. if not hasattr(args, 'excludes') or args.excludes is None:
  172. args.excludes = []
  173. for file in args.exclude_files:
  174. patterns = [line.rstrip('\r\n') for line in file if not line.startswith('#')]
  175. args.excludes += [ExcludePattern(pattern) for pattern in patterns if pattern]
  176. file.close()
  177. def adjust_patterns(paths, excludes):
  178. if paths:
  179. return (excludes or []) + [IncludePattern(path) for path in paths] + [ExcludePattern('*')]
  180. else:
  181. return excludes
  182. def exclude_path(path, patterns):
  183. """Used by create and extract sub-commands to determine
  184. whether or not an item should be processed.
  185. """
  186. for pattern in (patterns or []):
  187. if pattern.match(path):
  188. return isinstance(pattern, ExcludePattern)
  189. return False
  190. # For both IncludePattern and ExcludePattern, we require that
  191. # the pattern either match the whole path or an initial segment
  192. # of the path up to but not including a path separator. To
  193. # unify the two cases, we add a path separator to the end of
  194. # the path before matching.
  195. class IncludePattern:
  196. """Literal files or directories listed on the command line
  197. for some operations (e.g. extract, but not create).
  198. If a directory is specified, all paths that start with that
  199. path match as well. A trailing slash makes no difference.
  200. """
  201. def __init__(self, pattern):
  202. self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep
  203. def match(self, path):
  204. return (path+os.path.sep).startswith(self.pattern)
  205. def __repr__(self):
  206. return '%s(%s)' % (type(self), self.pattern)
  207. class ExcludePattern(IncludePattern):
  208. """Shell glob patterns to exclude. A trailing slash means to
  209. exclude the contents of a directory, but not the directory itself.
  210. """
  211. def __init__(self, pattern):
  212. if pattern.endswith(os.path.sep):
  213. self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep+'*'+os.path.sep
  214. else:
  215. self.pattern = os.path.normpath(pattern)+os.path.sep+'*'
  216. # fnmatch and re.match both cache compiled regular expressions.
  217. # Nevertheless, this is about 10 times faster.
  218. self.regex = re.compile(translate(self.pattern))
  219. def match(self, path):
  220. return self.regex.match(path+os.path.sep) is not None
  221. def __repr__(self):
  222. return '%s(%s)' % (type(self), self.pattern)
  223. def timestamp(s):
  224. """Convert a --timestamp=s argument to a datetime object"""
  225. try:
  226. # is it pointing to a file / directory?
  227. ts = os.stat(s).st_mtime
  228. return datetime.utcfromtimestamp(ts)
  229. except OSError:
  230. # didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support.
  231. for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S+00:00',
  232. '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
  233. '%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
  234. '%Y-%m-%d', '%Y-%j',
  235. ):
  236. try:
  237. return datetime.strptime(s, format)
  238. except ValueError:
  239. continue
  240. raise ValueError
  241. def is_cachedir(path):
  242. """Determines whether the specified path is a cache directory (and
  243. therefore should potentially be excluded from the backup) according to
  244. the CACHEDIR.TAG protocol
  245. (http://www.brynosaurus.com/cachedir/spec.html).
  246. """
  247. tag_contents = b'Signature: 8a477f597d28d172789f06886806bc55'
  248. tag_path = os.path.join(path, 'CACHEDIR.TAG')
  249. try:
  250. if os.path.exists(tag_path):
  251. with open(tag_path, 'rb') as tag_file:
  252. tag_data = tag_file.read(len(tag_contents))
  253. if tag_data == tag_contents:
  254. return True
  255. except OSError:
  256. pass
  257. return False
  258. def format_time(t):
  259. """Format datetime suitable for fixed length list output
  260. """
  261. if abs((datetime.now() - t).days) < 365:
  262. return t.strftime('%b %d %H:%M')
  263. else:
  264. return t.strftime('%b %d %Y')
  265. def format_timedelta(td):
  266. """Format timedelta in a human friendly format
  267. """
  268. # Since td.total_seconds() requires python 2.7
  269. ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
  270. s = ts % 60
  271. m = int(ts / 60) % 60
  272. h = int(ts / 3600) % 24
  273. txt = '%.2f seconds' % s
  274. if m:
  275. txt = '%d minutes %s' % (m, txt)
  276. if h:
  277. txt = '%d hours %s' % (h, txt)
  278. if td.days:
  279. txt = '%d days %s' % (td.days, txt)
  280. return txt
  281. def format_file_mode(mod):
  282. """Format file mode bits for list output
  283. """
  284. def x(v):
  285. return ''.join(v & m and s or '-'
  286. for m, s in ((4, 'r'), (2, 'w'), (1, 'x')))
  287. return '%s%s%s' % (x(mod // 64), x(mod // 8), x(mod))
  288. def format_file_size(v):
  289. """Format file size into a human friendly format
  290. """
  291. if abs(v) > 10**12:
  292. return '%.2f TB' % (v / 10**12)
  293. elif abs(v) > 10**9:
  294. return '%.2f GB' % (v / 10**9)
  295. elif abs(v) > 10**6:
  296. return '%.2f MB' % (v / 10**6)
  297. elif abs(v) > 10**3:
  298. return '%.2f kB' % (v / 10**3)
  299. else:
  300. return '%d B' % v
  301. def format_archive(archive):
  302. return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
  303. class IntegrityError(Error):
  304. """Data integrity error"""
  305. def memoize(function):
  306. cache = {}
  307. def decorated_function(*args):
  308. try:
  309. return cache[args]
  310. except KeyError:
  311. val = function(*args)
  312. cache[args] = val
  313. return val
  314. return decorated_function
  315. @memoize
  316. def uid2user(uid, default=None):
  317. try:
  318. return pwd.getpwuid(uid).pw_name
  319. except KeyError:
  320. return default
  321. @memoize
  322. def user2uid(user, default=None):
  323. try:
  324. return user and pwd.getpwnam(user).pw_uid
  325. except KeyError:
  326. return default
  327. @memoize
  328. def gid2group(gid, default=None):
  329. try:
  330. return grp.getgrgid(gid).gr_name
  331. except KeyError:
  332. return default
  333. @memoize
  334. def group2gid(group, default=None):
  335. try:
  336. return group and grp.getgrnam(group).gr_gid
  337. except KeyError:
  338. return default
  339. def posix_acl_use_stored_uid_gid(acl):
  340. """Replace the user/group field with the stored uid/gid
  341. """
  342. entries = []
  343. for entry in acl.decode('ascii').split('\n'):
  344. if entry:
  345. fields = entry.split(':')
  346. if len(fields) == 4:
  347. entries.append(':'.join([fields[0], fields[3], fields[2]]))
  348. else:
  349. entries.append(entry)
  350. return ('\n'.join(entries)).encode('ascii')
  351. class Location:
  352. """Object representing a repository / archive location
  353. """
  354. proto = user = host = port = path = archive = None
  355. ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
  356. r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
  357. r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
  358. file_re = re.compile(r'(?P<proto>file)://'
  359. r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
  360. scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
  361. r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
  362. def __init__(self, text):
  363. self.orig = text
  364. if not self.parse(text):
  365. raise ValueError
  366. def parse(self, text):
  367. m = self.ssh_re.match(text)
  368. if m:
  369. self.proto = m.group('proto')
  370. self.user = m.group('user')
  371. self.host = m.group('host')
  372. self.port = m.group('port') and int(m.group('port')) or None
  373. self.path = m.group('path')
  374. self.archive = m.group('archive')
  375. return True
  376. m = self.file_re.match(text)
  377. if m:
  378. self.proto = m.group('proto')
  379. self.path = m.group('path')
  380. self.archive = m.group('archive')
  381. return True
  382. m = self.scp_re.match(text)
  383. if m:
  384. self.user = m.group('user')
  385. self.host = m.group('host')
  386. self.path = m.group('path')
  387. self.archive = m.group('archive')
  388. self.proto = self.host and 'ssh' or 'file'
  389. return True
  390. return False
  391. def __str__(self):
  392. items = []
  393. items.append('proto=%r' % self.proto)
  394. items.append('user=%r' % self.user)
  395. items.append('host=%r' % self.host)
  396. items.append('port=%r' % self.port)
  397. items.append('path=%r' % self.path)
  398. items.append('archive=%r' % self.archive)
  399. return ', '.join(items)
  400. def to_key_filename(self):
  401. name = re.sub('[^\w]', '_', self.path).strip('_')
  402. if self.proto != 'file':
  403. name = self.host + '__' + name
  404. return os.path.join(get_keys_dir(), name)
  405. def __repr__(self):
  406. return "Location(%s)" % self
  407. def canonical_path(self):
  408. if self.proto == 'file':
  409. return self.path
  410. else:
  411. if self.path and self.path.startswith('~'):
  412. path = '/' + self.path
  413. elif self.path and not self.path.startswith('/'):
  414. path = '/~/' + self.path
  415. else:
  416. path = self.path
  417. return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
  418. self.host,
  419. ':{}'.format(self.port) if self.port else '',
  420. path)
  421. def location_validator(archive=None):
  422. def validator(text):
  423. try:
  424. loc = Location(text)
  425. except ValueError:
  426. raise argparse.ArgumentTypeError('Invalid location format: "%s"' % text)
  427. if archive is True and not loc.archive:
  428. raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
  429. elif archive is False and loc.archive:
  430. raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
  431. return loc
  432. return validator
  433. def read_msgpack(filename):
  434. with open(filename, 'rb') as fd:
  435. return msgpack.unpack(fd)
  436. def write_msgpack(filename, d):
  437. with open(filename + '.tmp', 'wb') as fd:
  438. msgpack.pack(d, fd)
  439. fd.flush()
  440. os.fsync(fd.fileno())
  441. os.rename(filename + '.tmp', filename)
  442. def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'):
  443. for key in keys:
  444. if isinstance(d.get(key), bytes):
  445. d[key] = d[key].decode(encoding, errors)
  446. return d
  447. def remove_surrogates(s, errors='replace'):
  448. """Replace surrogates generated by fsdecode with '?'
  449. """
  450. return s.encode('utf-8', errors).decode('utf-8')
  451. _safe_re = re.compile(r'^((\.\.)?/+)+')
  452. def make_path_safe(path):
  453. """Make path safe by making it relative and local
  454. """
  455. return _safe_re.sub('', path) or '.'
  456. def daemonize():
  457. """Detach process from controlling terminal and run in background
  458. """
  459. pid = os.fork()
  460. if pid:
  461. os._exit(0)
  462. os.setsid()
  463. pid = os.fork()
  464. if pid:
  465. os._exit(0)
  466. os.chdir('/')
  467. os.close(0)
  468. os.close(1)
  469. os.close(2)
  470. fd = os.open('/dev/null', os.O_RDWR)
  471. os.dup2(fd, 0)
  472. os.dup2(fd, 1)
  473. os.dup2(fd, 2)
  474. class StableDict(dict):
  475. """A dict subclass with stable items() ordering"""
  476. def items(self):
  477. return sorted(super(StableDict, self).items())
  478. if sys.version < '3.3':
  479. # st_mtime_ns attribute only available in 3.3+
  480. def st_mtime_ns(st):
  481. return int(st.st_mtime * 1e9)
  482. # unhexlify in < 3.3 incorrectly only accepts bytes input
  483. def unhexlify(data):
  484. if isinstance(data, str):
  485. data = data.encode('ascii')
  486. return binascii.unhexlify(data)
  487. else:
  488. def st_mtime_ns(st):
  489. return st.st_mtime_ns
  490. unhexlify = binascii.unhexlify
  491. def bigint_to_int(mtime):
  492. """Convert bytearray to int
  493. """
  494. if isinstance(mtime, bytes):
  495. return int.from_bytes(mtime, 'little', signed=True)
  496. return mtime
  497. def int_to_bigint(value):
  498. """Convert integers larger than 64 bits to bytearray
  499. Smaller integers are left alone
  500. """
  501. if value.bit_length() > 63:
  502. return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True)
  503. return value