helpers.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  1. import argparse
  2. from collections import namedtuple
  3. from functools import wraps
  4. import grp
  5. import os
  6. import stat
  7. import textwrap
  8. import pwd
  9. import re
  10. from shutil import get_terminal_size
  11. import sys
  12. import platform
  13. import time
  14. import unicodedata
  15. import io
  16. import errno
  17. import logging
  18. from .logger import create_logger
  19. logger = create_logger()
  20. from datetime import datetime, timezone, timedelta
  21. from fnmatch import translate
  22. from operator import attrgetter
  23. from . import __version__ as borg_version
  24. from . import hashindex
  25. from . import chunker
  26. from . import crypto
  27. from . import shellpattern
  28. import msgpack
  29. import msgpack.fallback
  30. import socket
  31. # return codes returned by borg command
  32. # when borg is killed by signal N, rc = 128 + N
  33. EXIT_SUCCESS = 0 # everything done, no problems
  34. EXIT_WARNING = 1 # reached normal end of operation, but there were issues
  35. EXIT_ERROR = 2 # terminated abruptly, did not reach end of operation
  36. class Error(Exception):
  37. """Error base class"""
  38. # if we raise such an Error and it is only catched by the uppermost
  39. # exception handler (that exits short after with the given exit_code),
  40. # it is always a (fatal and abrupt) EXIT_ERROR, never just a warning.
  41. exit_code = EXIT_ERROR
  42. # show a traceback?
  43. traceback = False
  44. def get_message(self):
  45. return type(self).__doc__.format(*self.args)
  46. class ErrorWithTraceback(Error):
  47. """like Error, but show a traceback also"""
  48. traceback = True
  49. class IntegrityError(ErrorWithTraceback):
  50. """Data integrity error"""
  51. class ExtensionModuleError(Error):
  52. """The Borg binary extension modules do not seem to be properly installed"""
  53. class NoManifestError(Error):
  54. """Repository has no manifest."""
  55. class PlaceholderError(Error):
  56. """Formatting Error: "{}".format({}): {}({})"""
  57. def check_extension_modules():
  58. from . import platform
  59. if hashindex.API_VERSION != 2:
  60. raise ExtensionModuleError
  61. if chunker.API_VERSION != 2:
  62. raise ExtensionModuleError
  63. if crypto.API_VERSION != 2:
  64. raise ExtensionModuleError
  65. if platform.API_VERSION != 2:
  66. raise ExtensionModuleError
  67. class Manifest:
  68. MANIFEST_ID = b'\0' * 32
  69. def __init__(self, key, repository, item_keys=None):
  70. from .archive import ITEM_KEYS
  71. self.archives = {}
  72. self.config = {}
  73. self.key = key
  74. self.repository = repository
  75. self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS
  76. @classmethod
  77. def load(cls, repository, key=None):
  78. from .key import key_factory
  79. from .repository import Repository
  80. from .archive import ITEM_KEYS
  81. try:
  82. cdata = repository.get(cls.MANIFEST_ID)
  83. except Repository.ObjectNotFound:
  84. raise NoManifestError
  85. if not key:
  86. key = key_factory(repository, cdata)
  87. manifest = cls(key, repository)
  88. data = key.decrypt(None, cdata)
  89. manifest.id = key.id_hash(data)
  90. m = msgpack.unpackb(data)
  91. if not m.get(b'version') == 1:
  92. raise ValueError('Invalid manifest version')
  93. manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items())
  94. manifest.timestamp = m.get(b'timestamp')
  95. if manifest.timestamp:
  96. manifest.timestamp = manifest.timestamp.decode('ascii')
  97. manifest.config = m[b'config']
  98. # valid item keys are whatever is known in the repo or every key we know
  99. manifest.item_keys = frozenset(m.get(b'item_keys', [])) | ITEM_KEYS
  100. return manifest, key
  101. def write(self):
  102. self.timestamp = datetime.utcnow().isoformat()
  103. data = msgpack.packb(StableDict({
  104. 'version': 1,
  105. 'archives': self.archives,
  106. 'timestamp': self.timestamp,
  107. 'config': self.config,
  108. 'item_keys': tuple(self.item_keys),
  109. }))
  110. self.id = self.key.id_hash(data)
  111. self.repository.put(self.MANIFEST_ID, self.key.encrypt(data))
  112. def list_archive_infos(self, sort_by=None, reverse=False):
  113. # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts
  114. ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts')
  115. archives = []
  116. for name, values in self.archives.items():
  117. ts = parse_timestamp(values[b'time'].decode('utf-8'))
  118. id = values[b'id']
  119. archives.append(ArchiveInfo(name=name, id=id, ts=ts))
  120. if sort_by is not None:
  121. archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse)
  122. return archives
  123. def prune_within(archives, within):
  124. multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365}
  125. try:
  126. hours = int(within[:-1]) * multiplier[within[-1]]
  127. except (KeyError, ValueError):
  128. # I don't like how this displays the original exception too:
  129. raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within)
  130. if hours <= 0:
  131. raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
  132. target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
  133. return [a for a in archives if a.ts > target]
  134. def prune_split(archives, pattern, n, skip=[]):
  135. last = None
  136. keep = []
  137. if n == 0:
  138. return keep
  139. for a in sorted(archives, key=attrgetter('ts'), reverse=True):
  140. period = to_localtime(a.ts).strftime(pattern)
  141. if period != last:
  142. last = period
  143. if a not in skip:
  144. keep.append(a)
  145. if len(keep) == n:
  146. break
  147. return keep
  148. class Statistics:
  149. def __init__(self):
  150. self.osize = self.csize = self.usize = self.nfiles = 0
  151. self.last_progress = 0 # timestamp when last progress was shown
  152. def update(self, size, csize, unique):
  153. self.osize += size
  154. self.csize += csize
  155. if unique:
  156. self.usize += csize
  157. summary = """\
  158. Original size Compressed size Deduplicated size
  159. {label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}"""
  160. def __str__(self):
  161. return self.summary.format(stats=self, label='This archive:')
  162. def __repr__(self):
  163. return "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>".format(cls=type(self).__name__, hash=id(self), self=self)
  164. @property
  165. def osize_fmt(self):
  166. return format_file_size(self.osize)
  167. @property
  168. def usize_fmt(self):
  169. return format_file_size(self.usize)
  170. @property
  171. def csize_fmt(self):
  172. return format_file_size(self.csize)
  173. def show_progress(self, item=None, final=False, stream=None, dt=None):
  174. now = time.time()
  175. if dt is None or now - self.last_progress > dt:
  176. self.last_progress = now
  177. columns, lines = get_terminal_size()
  178. if not final:
  179. msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self)
  180. path = remove_surrogates(item[b'path']) if item else ''
  181. space = columns - len(msg)
  182. if space < len('...') + len(path):
  183. path = '%s...%s' % (path[:(space // 2) - len('...')], path[-space // 2:])
  184. msg += "{0:<{space}}".format(path, space=space)
  185. else:
  186. msg = ' ' * columns
  187. print(msg, file=stream or sys.stderr, end="\r", flush=True)
  188. def get_keys_dir():
  189. """Determine where to repository keys and cache"""
  190. xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
  191. keys_dir = os.environ.get('BORG_KEYS_DIR', os.path.join(xdg_config, 'borg', 'keys'))
  192. if not os.path.exists(keys_dir):
  193. os.makedirs(keys_dir)
  194. os.chmod(keys_dir, stat.S_IRWXU)
  195. return keys_dir
  196. def get_cache_dir():
  197. """Determine where to repository keys and cache"""
  198. xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(os.path.expanduser('~'), '.cache'))
  199. cache_dir = os.environ.get('BORG_CACHE_DIR', os.path.join(xdg_cache, 'borg'))
  200. if not os.path.exists(cache_dir):
  201. os.makedirs(cache_dir)
  202. os.chmod(cache_dir, stat.S_IRWXU)
  203. with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
  204. fd.write(textwrap.dedent("""
  205. Signature: 8a477f597d28d172789f06886806bc55
  206. # This file is a cache directory tag created by Borg.
  207. # For information about cache directory tags, see:
  208. # http://www.brynosaurus.com/cachedir/
  209. """).lstrip())
  210. return cache_dir
  211. def to_localtime(ts):
  212. """Convert datetime object from UTC to local time zone"""
  213. return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])
  214. def parse_timestamp(timestamp):
  215. """Parse a ISO 8601 timestamp string"""
  216. if '.' in timestamp: # microseconds might not be present
  217. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=timezone.utc)
  218. else:
  219. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
  220. def load_excludes(fh):
  221. """Load and parse exclude patterns from file object. Lines empty or starting with '#' after stripping whitespace on
  222. both line ends are ignored.
  223. """
  224. patterns = (line for line in (i.strip() for i in fh) if not line.startswith('#'))
  225. return [parse_pattern(pattern) for pattern in patterns if pattern]
  226. def update_excludes(args):
  227. """Merge exclude patterns from files with those on command line."""
  228. if hasattr(args, 'exclude_files') and args.exclude_files:
  229. if not hasattr(args, 'excludes') or args.excludes is None:
  230. args.excludes = []
  231. for file in args.exclude_files:
  232. args.excludes += load_excludes(file)
  233. file.close()
  234. class PatternMatcher:
  235. def __init__(self, fallback=None):
  236. self._items = []
  237. # Value to return from match function when none of the patterns match.
  238. self.fallback = fallback
  239. def add(self, patterns, value):
  240. """Add list of patterns to internal list. The given value is returned from the match function when one of the
  241. given patterns matches.
  242. """
  243. self._items.extend((i, value) for i in patterns)
  244. def match(self, path):
  245. for (pattern, value) in self._items:
  246. if pattern.match(path):
  247. return value
  248. return self.fallback
  249. def normalized(func):
  250. """ Decorator for the Pattern match methods, returning a wrapper that
  251. normalizes OSX paths to match the normalized pattern on OSX, and
  252. returning the original method on other platforms"""
  253. @wraps(func)
  254. def normalize_wrapper(self, path):
  255. return func(self, unicodedata.normalize("NFD", path))
  256. if sys.platform in ('darwin',):
  257. # HFS+ converts paths to a canonical form, so users shouldn't be
  258. # required to enter an exact match
  259. return normalize_wrapper
  260. else:
  261. # Windows and Unix filesystems allow different forms, so users
  262. # always have to enter an exact match
  263. return func
  264. class PatternBase:
  265. """Shared logic for inclusion/exclusion patterns.
  266. """
  267. PREFIX = NotImplemented
  268. def __init__(self, pattern):
  269. self.pattern_orig = pattern
  270. self.match_count = 0
  271. if sys.platform in ('darwin',):
  272. pattern = unicodedata.normalize("NFD", pattern)
  273. self._prepare(pattern)
  274. @normalized
  275. def match(self, path):
  276. matches = self._match(path)
  277. if matches:
  278. self.match_count += 1
  279. return matches
  280. def __repr__(self):
  281. return '%s(%s)' % (type(self), self.pattern)
  282. def __str__(self):
  283. return self.pattern_orig
  284. def _prepare(self, pattern):
  285. raise NotImplementedError
  286. def _match(self, path):
  287. raise NotImplementedError
  288. # For PathPrefixPattern, FnmatchPattern and ShellPattern, we require that the pattern either match the whole path
  289. # or an initial segment of the path up to but not including a path separator. To unify the two cases, we add a path
  290. # separator to the end of the path before matching.
  291. class PathPrefixPattern(PatternBase):
  292. """Literal files or directories listed on the command line
  293. for some operations (e.g. extract, but not create).
  294. If a directory is specified, all paths that start with that
  295. path match as well. A trailing slash makes no difference.
  296. """
  297. PREFIX = "pp"
  298. def _prepare(self, pattern):
  299. self.pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep
  300. def _match(self, path):
  301. return (path + os.path.sep).startswith(self.pattern)
  302. class FnmatchPattern(PatternBase):
  303. """Shell glob patterns to exclude. A trailing slash means to
  304. exclude the contents of a directory, but not the directory itself.
  305. """
  306. PREFIX = "fm"
  307. def _prepare(self, pattern):
  308. if pattern.endswith(os.path.sep):
  309. pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep + '*' + os.path.sep
  310. else:
  311. pattern = os.path.normpath(pattern) + os.path.sep + '*'
  312. self.pattern = pattern
  313. # fnmatch and re.match both cache compiled regular expressions.
  314. # Nevertheless, this is about 10 times faster.
  315. self.regex = re.compile(translate(self.pattern))
  316. def _match(self, path):
  317. return (self.regex.match(path + os.path.sep) is not None)
  318. class ShellPattern(PatternBase):
  319. """Shell glob patterns to exclude. A trailing slash means to
  320. exclude the contents of a directory, but not the directory itself.
  321. """
  322. PREFIX = "sh"
  323. def _prepare(self, pattern):
  324. sep = os.path.sep
  325. if pattern.endswith(sep):
  326. pattern = os.path.normpath(pattern).rstrip(sep) + sep + "**" + sep + "*" + sep
  327. else:
  328. pattern = os.path.normpath(pattern) + sep + "**" + sep + "*"
  329. self.pattern = pattern
  330. self.regex = re.compile(shellpattern.translate(self.pattern))
  331. def _match(self, path):
  332. return (self.regex.match(path + os.path.sep) is not None)
  333. class RegexPattern(PatternBase):
  334. """Regular expression to exclude.
  335. """
  336. PREFIX = "re"
  337. def _prepare(self, pattern):
  338. self.pattern = pattern
  339. self.regex = re.compile(pattern)
  340. def _match(self, path):
  341. # Normalize path separators
  342. if os.path.sep != '/':
  343. path = path.replace(os.path.sep, '/')
  344. return (self.regex.search(path) is not None)
  345. _PATTERN_STYLES = set([
  346. FnmatchPattern,
  347. PathPrefixPattern,
  348. RegexPattern,
  349. ShellPattern,
  350. ])
  351. _PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES)
  352. def parse_pattern(pattern, fallback=FnmatchPattern):
  353. """Read pattern from string and return an instance of the appropriate implementation class.
  354. """
  355. if len(pattern) > 2 and pattern[2] == ":" and pattern[:2].isalnum():
  356. (style, pattern) = (pattern[:2], pattern[3:])
  357. cls = _PATTERN_STYLE_BY_PREFIX.get(style, None)
  358. if cls is None:
  359. raise ValueError("Unknown pattern style: {}".format(style))
  360. else:
  361. cls = fallback
  362. return cls(pattern)
  363. def timestamp(s):
  364. """Convert a --timestamp=s argument to a datetime object"""
  365. try:
  366. # is it pointing to a file / directory?
  367. ts = os.stat(s).st_mtime
  368. return datetime.utcfromtimestamp(ts)
  369. except OSError:
  370. # didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support.
  371. for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S+00:00',
  372. '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
  373. '%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
  374. '%Y-%m-%d', '%Y-%j',
  375. ):
  376. try:
  377. return datetime.strptime(s, format)
  378. except ValueError:
  379. continue
  380. raise ValueError
  381. def ChunkerParams(s):
  382. chunk_min, chunk_max, chunk_mask, window_size = s.split(',')
  383. if int(chunk_max) > 23:
  384. # do not go beyond 2**23 (8MB) chunk size now,
  385. # COMPR_BUFFER can only cope with up to this size
  386. raise ValueError('max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)')
  387. return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size)
  388. def CompressionSpec(s):
  389. values = s.split(',')
  390. count = len(values)
  391. if count < 1:
  392. raise ValueError
  393. # --compression algo[,level]
  394. name = values[0]
  395. if name in ('none', 'lz4', ):
  396. return dict(name=name)
  397. if name in ('zlib', 'lzma', ):
  398. if count < 2:
  399. level = 6 # default compression level in py stdlib
  400. elif count == 2:
  401. level = int(values[1])
  402. if not 0 <= level <= 9:
  403. raise ValueError
  404. else:
  405. raise ValueError
  406. return dict(name=name, level=level)
  407. raise ValueError
  408. def PrefixSpec(s):
  409. return replace_placeholders(s)
  410. def dir_is_cachedir(path):
  411. """Determines whether the specified path is a cache directory (and
  412. therefore should potentially be excluded from the backup) according to
  413. the CACHEDIR.TAG protocol
  414. (http://www.brynosaurus.com/cachedir/spec.html).
  415. """
  416. tag_contents = b'Signature: 8a477f597d28d172789f06886806bc55'
  417. tag_path = os.path.join(path, 'CACHEDIR.TAG')
  418. try:
  419. if os.path.exists(tag_path):
  420. with open(tag_path, 'rb') as tag_file:
  421. tag_data = tag_file.read(len(tag_contents))
  422. if tag_data == tag_contents:
  423. return True
  424. except OSError:
  425. pass
  426. return False
  427. def dir_is_tagged(path, exclude_caches, exclude_if_present):
  428. """Determines whether the specified path is excluded by being a cache
  429. directory or containing user-specified tag files. Returns a list of the
  430. paths of the tag files (either CACHEDIR.TAG or the matching
  431. user-specified files).
  432. """
  433. tag_paths = []
  434. if exclude_caches and dir_is_cachedir(path):
  435. tag_paths.append(os.path.join(path, 'CACHEDIR.TAG'))
  436. if exclude_if_present is not None:
  437. for tag in exclude_if_present:
  438. tag_path = os.path.join(path, tag)
  439. if os.path.isfile(tag_path):
  440. tag_paths.append(tag_path)
  441. return tag_paths
  442. def format_line(format, data):
  443. try:
  444. return format.format(**data)
  445. except Exception as e:
  446. raise PlaceholderError(format, data, e.__class__.__name__, str(e))
  447. def replace_placeholders(text):
  448. """Replace placeholders in text with their values."""
  449. current_time = datetime.now()
  450. data = {
  451. 'pid': os.getpid(),
  452. 'fqdn': socket.getfqdn(),
  453. 'hostname': socket.gethostname(),
  454. 'now': current_time.now(),
  455. 'utcnow': current_time.utcnow(),
  456. 'user': uid2user(os.getuid(), os.getuid())
  457. }
  458. return format_line(text, data)
  459. def safe_timestamp(item_timestamp_ns):
  460. try:
  461. return datetime.fromtimestamp(bigint_to_int(item_timestamp_ns) / 1e9)
  462. except OverflowError:
  463. # likely a broken file time and datetime did not want to go beyond year 9999
  464. return datetime(9999, 12, 31, 23, 59, 59)
  465. def format_time(t):
  466. """use ISO-8601 date and time format
  467. """
  468. return t.strftime('%a, %Y-%m-%d %H:%M:%S')
  469. def format_timedelta(td):
  470. """Format timedelta in a human friendly format
  471. """
  472. # Since td.total_seconds() requires python 2.7
  473. ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
  474. s = ts % 60
  475. m = int(ts / 60) % 60
  476. h = int(ts / 3600) % 24
  477. txt = '%.2f seconds' % s
  478. if m:
  479. txt = '%d minutes %s' % (m, txt)
  480. if h:
  481. txt = '%d hours %s' % (h, txt)
  482. if td.days:
  483. txt = '%d days %s' % (td.days, txt)
  484. return txt
  485. def format_file_size(v, precision=2):
  486. """Format file size into a human friendly format
  487. """
  488. return sizeof_fmt_decimal(v, suffix='B', sep=' ', precision=precision)
  489. def sizeof_fmt(num, suffix='B', units=None, power=None, sep='', precision=2):
  490. for unit in units[:-1]:
  491. if abs(round(num, precision)) < power:
  492. if isinstance(num, int):
  493. return "{}{}{}{}".format(num, sep, unit, suffix)
  494. else:
  495. return "{:3.{}f}{}{}{}".format(num, precision, sep, unit, suffix)
  496. num /= float(power)
  497. return "{:.{}f}{}{}{}".format(num, precision, sep, units[-1], suffix)
  498. def sizeof_fmt_iec(num, suffix='B', sep='', precision=2):
  499. return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'], power=1024)
  500. def sizeof_fmt_decimal(num, suffix='B', sep='', precision=2):
  501. return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], power=1000)
  502. def format_archive(archive):
  503. return '%-36s %s' % (archive.name, format_time(to_localtime(archive.ts)))
  504. def memoize(function):
  505. cache = {}
  506. def decorated_function(*args):
  507. try:
  508. return cache[args]
  509. except KeyError:
  510. val = function(*args)
  511. cache[args] = val
  512. return val
  513. return decorated_function
  514. @memoize
  515. def uid2user(uid, default=None):
  516. try:
  517. return pwd.getpwuid(uid).pw_name
  518. except KeyError:
  519. return default
  520. @memoize
  521. def user2uid(user, default=None):
  522. try:
  523. return user and pwd.getpwnam(user).pw_uid
  524. except KeyError:
  525. return default
  526. @memoize
  527. def gid2group(gid, default=None):
  528. try:
  529. return grp.getgrgid(gid).gr_name
  530. except KeyError:
  531. return default
  532. @memoize
  533. def group2gid(group, default=None):
  534. try:
  535. return group and grp.getgrnam(group).gr_gid
  536. except KeyError:
  537. return default
  538. def posix_acl_use_stored_uid_gid(acl):
  539. """Replace the user/group field with the stored uid/gid
  540. """
  541. entries = []
  542. for entry in safe_decode(acl).split('\n'):
  543. if entry:
  544. fields = entry.split(':')
  545. if len(fields) == 4:
  546. entries.append(':'.join([fields[0], fields[3], fields[2]]))
  547. else:
  548. entries.append(entry)
  549. return safe_encode('\n'.join(entries))
  550. def safe_decode(s, coding='utf-8', errors='surrogateescape'):
  551. """decode bytes to str, with round-tripping "invalid" bytes"""
  552. return s.decode(coding, errors)
  553. def safe_encode(s, coding='utf-8', errors='surrogateescape'):
  554. """encode str to bytes, with round-tripping "invalid" bytes"""
  555. return s.encode(coding, errors)
  556. class Location:
  557. """Object representing a repository / archive location
  558. """
  559. proto = user = host = port = path = archive = None
  560. # borg mount's FUSE filesystem creates one level of directories from
  561. # the archive names. Thus, we must not accept "/" in archive names.
  562. ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
  563. r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
  564. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  565. file_re = re.compile(r'(?P<proto>file)://'
  566. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  567. scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
  568. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  569. # get the repo from BORG_RE env and the optional archive from param.
  570. # if the syntax requires giving REPOSITORY (see "borg mount"),
  571. # use "::" to let it use the env var.
  572. # if REPOSITORY argument is optional, it'll automatically use the env.
  573. env_re = re.compile(r'(?:::(?P<archive>[^/]+)?)?$')
  574. def __init__(self, text=''):
  575. self.orig = text
  576. if not self.parse(self.orig):
  577. raise ValueError
  578. def parse(self, text):
  579. text = replace_placeholders(text)
  580. valid = self._parse(text)
  581. if valid:
  582. return True
  583. m = self.env_re.match(text)
  584. if not m:
  585. return False
  586. repo = os.environ.get('BORG_REPO')
  587. if repo is None:
  588. return False
  589. valid = self._parse(repo)
  590. if not valid:
  591. return False
  592. self.archive = m.group('archive')
  593. return True
  594. def _parse(self, text):
  595. m = self.ssh_re.match(text)
  596. if m:
  597. self.proto = m.group('proto')
  598. self.user = m.group('user')
  599. self.host = m.group('host')
  600. self.port = m.group('port') and int(m.group('port')) or None
  601. self.path = os.path.normpath(m.group('path'))
  602. self.archive = m.group('archive')
  603. return True
  604. m = self.file_re.match(text)
  605. if m:
  606. self.proto = m.group('proto')
  607. self.path = os.path.normpath(m.group('path'))
  608. self.archive = m.group('archive')
  609. return True
  610. m = self.scp_re.match(text)
  611. if m:
  612. self.user = m.group('user')
  613. self.host = m.group('host')
  614. self.path = os.path.normpath(m.group('path'))
  615. self.archive = m.group('archive')
  616. self.proto = self.host and 'ssh' or 'file'
  617. return True
  618. return False
  619. def __str__(self):
  620. items = [
  621. 'proto=%r' % self.proto,
  622. 'user=%r' % self.user,
  623. 'host=%r' % self.host,
  624. 'port=%r' % self.port,
  625. 'path=%r' % self.path,
  626. 'archive=%r' % self.archive,
  627. ]
  628. return ', '.join(items)
  629. def to_key_filename(self):
  630. name = re.sub('[^\w]', '_', self.path).strip('_')
  631. if self.proto != 'file':
  632. name = self.host + '__' + name
  633. return os.path.join(get_keys_dir(), name)
  634. def __repr__(self):
  635. return "Location(%s)" % self
  636. def canonical_path(self):
  637. if self.proto == 'file':
  638. return self.path
  639. else:
  640. if self.path and self.path.startswith('~'):
  641. path = '/' + self.path
  642. elif self.path and not self.path.startswith('/'):
  643. path = '/~/' + self.path
  644. else:
  645. path = self.path
  646. return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
  647. self.host,
  648. ':{}'.format(self.port) if self.port else '',
  649. path)
  650. def location_validator(archive=None):
  651. def validator(text):
  652. try:
  653. loc = Location(text)
  654. except ValueError:
  655. raise argparse.ArgumentTypeError('Invalid location format: "%s"' % text) from None
  656. if archive is True and not loc.archive:
  657. raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
  658. elif archive is False and loc.archive:
  659. raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
  660. return loc
  661. return validator
  662. def archivename_validator():
  663. def validator(text):
  664. if '/' in text or '::' in text or not text:
  665. raise argparse.ArgumentTypeError('Invalid repository name: "%s"' % text)
  666. return text
  667. return validator
  668. def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'):
  669. for key in keys:
  670. if isinstance(d.get(key), bytes):
  671. d[key] = d[key].decode(encoding, errors)
  672. return d
  673. def remove_surrogates(s, errors='replace'):
  674. """Replace surrogates generated by fsdecode with '?'
  675. """
  676. return s.encode('utf-8', errors).decode('utf-8')
  677. _safe_re = re.compile(r'^((\.\.)?/+)+')
  678. def make_path_safe(path):
  679. """Make path safe by making it relative and local
  680. """
  681. return _safe_re.sub('', path) or '.'
  682. def daemonize():
  683. """Detach process from controlling terminal and run in background
  684. """
  685. pid = os.fork()
  686. if pid:
  687. os._exit(0)
  688. os.setsid()
  689. pid = os.fork()
  690. if pid:
  691. os._exit(0)
  692. os.chdir('/')
  693. os.close(0)
  694. os.close(1)
  695. os.close(2)
  696. fd = os.open('/dev/null', os.O_RDWR)
  697. os.dup2(fd, 0)
  698. os.dup2(fd, 1)
  699. os.dup2(fd, 2)
  700. class StableDict(dict):
  701. """A dict subclass with stable items() ordering"""
  702. def items(self):
  703. return sorted(super().items())
  704. def bigint_to_int(mtime):
  705. """Convert bytearray to int
  706. """
  707. if isinstance(mtime, bytes):
  708. return int.from_bytes(mtime, 'little', signed=True)
  709. return mtime
  710. def int_to_bigint(value):
  711. """Convert integers larger than 64 bits to bytearray
  712. Smaller integers are left alone
  713. """
  714. if value.bit_length() > 63:
  715. return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True)
  716. return value
  717. def is_slow_msgpack():
  718. return msgpack.Packer is msgpack.fallback.Packer
  719. FALSISH = ('No', 'NO', 'no', 'N', 'n', '0', )
  720. TRUISH = ('Yes', 'YES', 'yes', 'Y', 'y', '1', )
  721. DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', )
  722. def yes(msg=None, false_msg=None, true_msg=None, default_msg=None,
  723. retry_msg=None, invalid_msg=None, env_msg=None,
  724. falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH,
  725. default=False, retry=True, env_var_override=None, ofile=None, input=input):
  726. """Output <msg> (usually a question) and let user input an answer.
  727. Qualifies the answer according to falsish, truish and defaultish as True, False or <default>.
  728. If it didn't qualify and retry_msg is None (no retries wanted),
  729. return the default [which defaults to False]. Otherwise let user retry
  730. answering until answer is qualified.
  731. If env_var_override is given and this var is present in the environment, do not ask
  732. the user, but just use the env var contents as answer as if it was typed in.
  733. Otherwise read input from stdin and proceed as normal.
  734. If EOF is received instead an input or an invalid input without retry possibility,
  735. return default.
  736. :param msg: introducing message to output on ofile, no \n is added [None]
  737. :param retry_msg: retry message to output on ofile, no \n is added [None]
  738. :param false_msg: message to output before returning False [None]
  739. :param true_msg: message to output before returning True [None]
  740. :param default_msg: message to output before returning a <default> [None]
  741. :param invalid_msg: message to output after a invalid answer was given [None]
  742. :param env_msg: message to output when using input from env_var_override [None],
  743. needs to have 2 placeholders for answer and env var name, e.g.: "{} (from {})"
  744. :param falsish: sequence of answers qualifying as False
  745. :param truish: sequence of answers qualifying as True
  746. :param defaultish: sequence of answers qualifying as <default>
  747. :param default: default return value (defaultish answer was given or no-answer condition) [False]
  748. :param retry: if True and input is incorrect, retry. Otherwise return default. [True]
  749. :param env_var_override: environment variable name [None]
  750. :param ofile: output stream [sys.stderr]
  751. :param input: input function [input from builtins]
  752. :return: boolean answer value, True or False
  753. """
  754. # note: we do not assign sys.stderr as default above, so it is
  755. # really evaluated NOW, not at function definition time.
  756. if ofile is None:
  757. ofile = sys.stderr
  758. if default not in (True, False):
  759. raise ValueError("invalid default value, must be True or False")
  760. if msg:
  761. print(msg, file=ofile, end='', flush=True)
  762. while True:
  763. answer = None
  764. if env_var_override:
  765. answer = os.environ.get(env_var_override)
  766. if answer is not None and env_msg:
  767. print(env_msg.format(answer, env_var_override), file=ofile)
  768. if answer is None:
  769. try:
  770. answer = input()
  771. except EOFError:
  772. # avoid defaultish[0], defaultish could be empty
  773. answer = truish[0] if default else falsish[0]
  774. if answer in defaultish:
  775. if default_msg:
  776. print(default_msg, file=ofile)
  777. return default
  778. if answer in truish:
  779. if true_msg:
  780. print(true_msg, file=ofile)
  781. return True
  782. if answer in falsish:
  783. if false_msg:
  784. print(false_msg, file=ofile)
  785. return False
  786. # if we get here, the answer was invalid
  787. if invalid_msg:
  788. print(invalid_msg, file=ofile)
  789. if not retry:
  790. return default
  791. if retry_msg:
  792. print(retry_msg, file=ofile, end='', flush=True)
  793. # in case we used an environment variable and it gave an invalid answer, do not use it again:
  794. env_var_override = None
  795. class ProgressIndicatorPercent:
  796. def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%", file=None):
  797. """
  798. Percentage-based progress indicator
  799. :param total: total amount of items
  800. :param step: step size in percent
  801. :param start: at which percent value to start
  802. :param same_line: if True, emit output always on same line
  803. :param msg: output message, must contain one %f placeholder for the percentage
  804. :param file: output file, default: sys.stderr
  805. """
  806. self.counter = 0 # 0 .. (total-1)
  807. self.total = total
  808. self.trigger_at = start # output next percentage value when reaching (at least) this
  809. self.step = step
  810. if file is None:
  811. file = sys.stderr
  812. self.file = file
  813. self.msg = msg
  814. self.same_line = same_line
  815. def progress(self, current=None):
  816. if current is not None:
  817. self.counter = current
  818. pct = self.counter * 100 / self.total
  819. self.counter += 1
  820. if pct >= self.trigger_at:
  821. self.trigger_at += self.step
  822. return pct
  823. def show(self, current=None):
  824. pct = self.progress(current)
  825. if pct is not None:
  826. return self.output(pct)
  827. def output(self, percent):
  828. print(self.msg % percent, file=self.file, end='\r' if self.same_line else '\n', flush=True)
  829. def finish(self):
  830. if self.same_line:
  831. print(" " * len(self.msg % 100.0), file=self.file, end='\r')
  832. class ProgressIndicatorEndless:
  833. def __init__(self, step=10, file=None):
  834. """
  835. Progress indicator (long row of dots)
  836. :param step: every Nth call, call the func
  837. :param file: output file, default: sys.stderr
  838. """
  839. self.counter = 0 # call counter
  840. self.triggered = 0 # increases 1 per trigger event
  841. self.step = step # trigger every <step> calls
  842. if file is None:
  843. file = sys.stderr
  844. self.file = file
  845. def progress(self):
  846. self.counter += 1
  847. trigger = self.counter % self.step == 0
  848. if trigger:
  849. self.triggered += 1
  850. return trigger
  851. def show(self):
  852. trigger = self.progress()
  853. if trigger:
  854. return self.output(self.triggered)
  855. def output(self, triggered):
  856. print('.', end='', file=self.file, flush=True)
  857. def finish(self):
  858. print(file=self.file)
  859. def sysinfo():
  860. info = []
  861. info.append('Platform: %s' % (' '.join(platform.uname()), ))
  862. if sys.platform.startswith('linux'):
  863. info.append('Linux: %s %s %s' % platform.linux_distribution())
  864. info.append('Borg: %s Python: %s %s' % (borg_version, platform.python_implementation(), platform.python_version()))
  865. info.append('PID: %d CWD: %s' % (os.getpid(), os.getcwd()))
  866. info.append('sys.argv: %r' % sys.argv)
  867. info.append('SSH_ORIGINAL_COMMAND: %r' % os.environ.get('SSH_ORIGINAL_COMMAND'))
  868. info.append('')
  869. return '\n'.join(info)
  870. def log_multi(*msgs, level=logging.INFO):
  871. """
  872. log multiple lines of text, each line by a separate logging call for cosmetic reasons
  873. each positional argument may be a single or multiple lines (separated by newlines) of text.
  874. """
  875. lines = []
  876. for msg in msgs:
  877. lines.extend(msg.splitlines())
  878. for line in lines:
  879. logger.log(level, line)
  880. class ErrorIgnoringTextIOWrapper(io.TextIOWrapper):
  881. def read(self, n):
  882. if not self.closed:
  883. try:
  884. return super().read(n)
  885. except BrokenPipeError:
  886. try:
  887. super().close()
  888. except OSError:
  889. pass
  890. return ''
  891. def write(self, s):
  892. if not self.closed:
  893. try:
  894. return super().write(s)
  895. except BrokenPipeError:
  896. try:
  897. super().close()
  898. except OSError:
  899. pass
  900. return len(s)