helpers.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. from .support import argparse # see support/__init__.py docstring
  2. # DEPRECATED - remove after requiring py 3.4
  3. import binascii
  4. from collections import namedtuple
  5. from functools import wraps
  6. import grp
  7. import os
  8. import pwd
  9. import re
  10. try:
  11. from shutil import get_terminal_size
  12. except ImportError:
  13. def get_terminal_size(fallback):
  14. return (os.environ.get('COLUMNS', fallback[0]), os.environ.get('LINES', fallback[1]))
  15. import sys
  16. import time
  17. import unicodedata
  18. from datetime import datetime, timezone, timedelta
  19. from fnmatch import translate
  20. from operator import attrgetter
  21. def have_cython():
  22. """allow for a way to disable Cython includes
  23. this is used during usage docs build, in setup.py. It is to avoid
  24. loading the Cython libraries which are built, but sometimes not in
  25. the search path (namely, during Tox runs).
  26. we simply check an environment variable (``BORG_CYTHON_DISABLE``)
  27. which, when set (to anything) will disable includes of Cython
  28. libraries in key places to enable usage docs to be built.
  29. :returns: True if Cython is available, False otherwise.
  30. """
  31. return not os.environ.get('BORG_CYTHON_DISABLE')
  32. if have_cython():
  33. from . import hashindex
  34. from . import chunker
  35. from . import crypto
  36. import msgpack
  37. class Error(Exception):
  38. """Error base class"""
  39. exit_code = 1
  40. def get_message(self):
  41. return 'Error: ' + type(self).__doc__.format(*self.args)
  42. class ExtensionModuleError(Error):
  43. """The Borg binary extension modules do not seem to be properly installed"""
  44. def check_extension_modules():
  45. from . import platform
  46. if hashindex.API_VERSION != 2:
  47. raise ExtensionModuleError
  48. if chunker.API_VERSION != 2:
  49. raise ExtensionModuleError
  50. if crypto.API_VERSION != 2:
  51. raise ExtensionModuleError
  52. if platform.API_VERSION != 2:
  53. raise ExtensionModuleError
  54. class Manifest:
  55. MANIFEST_ID = b'\0' * 32
  56. def __init__(self, key, repository):
  57. self.archives = {}
  58. self.config = {}
  59. self.key = key
  60. self.repository = repository
  61. @classmethod
  62. def load(cls, repository, key=None):
  63. from .key import key_factory
  64. cdata = repository.get(cls.MANIFEST_ID)
  65. if not key:
  66. key = key_factory(repository, cdata)
  67. manifest = cls(key, repository)
  68. data = key.decrypt(None, cdata)
  69. manifest.id = key.id_hash(data)
  70. m = msgpack.unpackb(data)
  71. if not m.get(b'version') == 1:
  72. raise ValueError('Invalid manifest version')
  73. manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items())
  74. manifest.timestamp = m.get(b'timestamp')
  75. if manifest.timestamp:
  76. manifest.timestamp = manifest.timestamp.decode('ascii')
  77. manifest.config = m[b'config']
  78. return manifest, key
  79. def write(self):
  80. self.timestamp = datetime.utcnow().isoformat()
  81. data = msgpack.packb(StableDict({
  82. 'version': 1,
  83. 'archives': self.archives,
  84. 'timestamp': self.timestamp,
  85. 'config': self.config,
  86. }))
  87. self.id = self.key.id_hash(data)
  88. self.repository.put(self.MANIFEST_ID, self.key.encrypt(data))
  89. def list_archive_infos(self, sort_by=None, reverse=False):
  90. # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts
  91. ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts')
  92. archives = []
  93. for name, values in self.archives.items():
  94. ts = parse_timestamp(values[b'time'].decode('utf-8'))
  95. id = values[b'id']
  96. archives.append(ArchiveInfo(name=name, id=id, ts=ts))
  97. if sort_by is not None:
  98. archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse)
  99. return archives
  100. def prune_within(archives, within):
  101. multiplier = {'H': 1, 'd': 24, 'w': 24*7, 'm': 24*31, 'y': 24*365}
  102. try:
  103. hours = int(within[:-1]) * multiplier[within[-1]]
  104. except (KeyError, ValueError):
  105. # I don't like how this displays the original exception too:
  106. raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within)
  107. if hours <= 0:
  108. raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
  109. target = datetime.now(timezone.utc) - timedelta(seconds=hours*60*60)
  110. return [a for a in archives if a.ts > target]
  111. def prune_split(archives, pattern, n, skip=[]):
  112. last = None
  113. keep = []
  114. if n == 0:
  115. return keep
  116. for a in sorted(archives, key=attrgetter('ts'), reverse=True):
  117. period = to_localtime(a.ts).strftime(pattern)
  118. if period != last:
  119. last = period
  120. if a not in skip:
  121. keep.append(a)
  122. if len(keep) == n:
  123. break
  124. return keep
  125. class Statistics:
  126. def __init__(self):
  127. self.osize = self.csize = self.usize = self.nfiles = 0
  128. def update(self, size, csize, unique):
  129. self.osize += size
  130. self.csize += csize
  131. if unique:
  132. self.usize += csize
  133. summary = """\
  134. Original size Compressed size Deduplicated size
  135. {label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}"""
  136. def __str__(self):
  137. return self.summary.format(stats=self, label='This archive:')
  138. def __repr__(self):
  139. fmt = "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>"
  140. return fmt.format(cls=type(self).__name__,
  141. hash=id(self),
  142. self=self)
  143. @property
  144. def osize_fmt(self):
  145. return format_file_size(self.osize)
  146. @property
  147. def usize_fmt(self):
  148. return format_file_size(self.usize)
  149. @property
  150. def csize_fmt(self):
  151. return format_file_size(self.csize)
  152. def show_progress(self, item=None, final=False, stream=None):
  153. (columns, lines) = get_terminal_size((80, 24))
  154. if not final:
  155. msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self)
  156. path = remove_surrogates(item[b'path']) if item else ''
  157. space = columns - len(msg)
  158. if space < len('...') + len(path):
  159. path = '%s...%s' % (path[:(space//2)-len('...')], path[-space//2:])
  160. msg += "{0:<{space}}".format(path, space=space)
  161. else:
  162. msg = ' ' * columns
  163. print(msg, file=stream or sys.stderr, end="\r")
  164. def get_keys_dir():
  165. """Determine where to repository keys and cache"""
  166. return os.environ.get('BORG_KEYS_DIR',
  167. os.path.join(os.path.expanduser('~'), '.borg', 'keys'))
  168. def get_cache_dir():
  169. """Determine where to repository keys and cache"""
  170. xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(os.path.expanduser('~'), '.cache'))
  171. return os.environ.get('BORG_CACHE_DIR', os.path.join(xdg_cache, 'borg'))
  172. def to_localtime(ts):
  173. """Convert datetime object from UTC to local time zone"""
  174. return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])
  175. def parse_timestamp(timestamp):
  176. """Parse a ISO 8601 timestamp string"""
  177. if '.' in timestamp: # microseconds might not be pressent
  178. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=timezone.utc)
  179. else:
  180. return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
  181. def update_excludes(args):
  182. """Merge exclude patterns from files with those on command line.
  183. Empty lines and lines starting with '#' are ignored, but whitespace
  184. is not stripped."""
  185. if hasattr(args, 'exclude_files') and args.exclude_files:
  186. if not hasattr(args, 'excludes') or args.excludes is None:
  187. args.excludes = []
  188. for file in args.exclude_files:
  189. patterns = [line.rstrip('\r\n') for line in file if not line.startswith('#')]
  190. args.excludes += [ExcludePattern(pattern) for pattern in patterns if pattern]
  191. file.close()
  192. def adjust_patterns(paths, excludes):
  193. if paths:
  194. return (excludes or []) + [IncludePattern(path) for path in paths] + [ExcludePattern('*')]
  195. else:
  196. return excludes
  197. def exclude_path(path, patterns):
  198. """Used by create and extract sub-commands to determine
  199. whether or not an item should be processed.
  200. """
  201. for pattern in (patterns or []):
  202. if pattern.match(path):
  203. return isinstance(pattern, ExcludePattern)
  204. return False
  205. # For both IncludePattern and ExcludePattern, we require that
  206. # the pattern either match the whole path or an initial segment
  207. # of the path up to but not including a path separator. To
  208. # unify the two cases, we add a path separator to the end of
  209. # the path before matching.
  210. def normalized(func):
  211. """ Decorator for the Pattern match methods, returning a wrapper that
  212. normalizes OSX paths to match the normalized pattern on OSX, and
  213. returning the original method on other platforms"""
  214. @wraps(func)
  215. def normalize_wrapper(self, path):
  216. return func(self, unicodedata.normalize("NFD", path))
  217. if sys.platform in ('darwin',):
  218. # HFS+ converts paths to a canonical form, so users shouldn't be
  219. # required to enter an exact match
  220. return normalize_wrapper
  221. else:
  222. # Windows and Unix filesystems allow different forms, so users
  223. # always have to enter an exact match
  224. return func
  225. class IncludePattern:
  226. """Literal files or directories listed on the command line
  227. for some operations (e.g. extract, but not create).
  228. If a directory is specified, all paths that start with that
  229. path match as well. A trailing slash makes no difference.
  230. """
  231. def __init__(self, pattern):
  232. self.pattern_orig = pattern
  233. self.match_count = 0
  234. if sys.platform in ('darwin',):
  235. pattern = unicodedata.normalize("NFD", pattern)
  236. self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep
  237. @normalized
  238. def match(self, path):
  239. matches = (path+os.path.sep).startswith(self.pattern)
  240. if matches:
  241. self.match_count += 1
  242. return matches
  243. def __repr__(self):
  244. return '%s(%s)' % (type(self), self.pattern)
  245. def __str__(self):
  246. return self.pattern_orig
  247. class ExcludePattern(IncludePattern):
  248. """Shell glob patterns to exclude. A trailing slash means to
  249. exclude the contents of a directory, but not the directory itself.
  250. """
  251. def __init__(self, pattern):
  252. self.pattern_orig = pattern
  253. self.match_count = 0
  254. if pattern.endswith(os.path.sep):
  255. self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep+'*'+os.path.sep
  256. else:
  257. self.pattern = os.path.normpath(pattern)+os.path.sep+'*'
  258. if sys.platform in ('darwin',):
  259. self.pattern = unicodedata.normalize("NFD", self.pattern)
  260. # fnmatch and re.match both cache compiled regular expressions.
  261. # Nevertheless, this is about 10 times faster.
  262. self.regex = re.compile(translate(self.pattern))
  263. @normalized
  264. def match(self, path):
  265. matches = self.regex.match(path+os.path.sep) is not None
  266. if matches:
  267. self.match_count += 1
  268. return matches
  269. def __repr__(self):
  270. return '%s(%s)' % (type(self), self.pattern)
  271. def __str__(self):
  272. return self.pattern_orig
  273. def timestamp(s):
  274. """Convert a --timestamp=s argument to a datetime object"""
  275. try:
  276. # is it pointing to a file / directory?
  277. ts = os.stat(s).st_mtime
  278. return datetime.utcfromtimestamp(ts)
  279. except OSError:
  280. # didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support.
  281. for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S+00:00',
  282. '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
  283. '%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
  284. '%Y-%m-%d', '%Y-%j',
  285. ):
  286. try:
  287. return datetime.strptime(s, format)
  288. except ValueError:
  289. continue
  290. raise ValueError
  291. def ChunkerParams(s):
  292. chunk_min, chunk_max, chunk_mask, window_size = s.split(',')
  293. if int(chunk_max) > 23:
  294. # do not go beyond 2**23 (8MB) chunk size now,
  295. # COMPR_BUFFER can only cope with up to this size
  296. raise ValueError('max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)')
  297. return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size)
  298. def CompressionSpec(s):
  299. values = s.split(',')
  300. count = len(values)
  301. if count < 1:
  302. raise ValueError
  303. compression = values[0]
  304. try:
  305. compression = int(compression)
  306. if count > 1:
  307. raise ValueError
  308. # DEPRECATED: it is just --compression N
  309. if 0 <= compression <= 9:
  310. return dict(name='zlib', level=compression)
  311. raise ValueError
  312. except ValueError:
  313. # --compression algo[,...]
  314. name = compression
  315. if name in ('none', 'lz4', ):
  316. return dict(name=name)
  317. if name in ('zlib', 'lzma', ):
  318. if count < 2:
  319. level = 6 # default compression level in py stdlib
  320. elif count == 2:
  321. level = int(values[1])
  322. if not 0 <= level <= 9:
  323. raise ValueError
  324. else:
  325. raise ValueError
  326. return dict(name=name, level=level)
  327. raise ValueError
  328. def is_cachedir(path):
  329. """Determines whether the specified path is a cache directory (and
  330. therefore should potentially be excluded from the backup) according to
  331. the CACHEDIR.TAG protocol
  332. (http://www.brynosaurus.com/cachedir/spec.html).
  333. """
  334. tag_contents = b'Signature: 8a477f597d28d172789f06886806bc55'
  335. tag_path = os.path.join(path, 'CACHEDIR.TAG')
  336. try:
  337. if os.path.exists(tag_path):
  338. with open(tag_path, 'rb') as tag_file:
  339. tag_data = tag_file.read(len(tag_contents))
  340. if tag_data == tag_contents:
  341. return True
  342. except OSError:
  343. pass
  344. return False
  345. def format_time(t):
  346. """Format datetime suitable for fixed length list output
  347. """
  348. if abs((datetime.now() - t).days) < 365:
  349. return t.strftime('%b %d %H:%M')
  350. else:
  351. return t.strftime('%b %d %Y')
  352. def format_timedelta(td):
  353. """Format timedelta in a human friendly format
  354. """
  355. # Since td.total_seconds() requires python 2.7
  356. ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
  357. s = ts % 60
  358. m = int(ts / 60) % 60
  359. h = int(ts / 3600) % 24
  360. txt = '%.2f seconds' % s
  361. if m:
  362. txt = '%d minutes %s' % (m, txt)
  363. if h:
  364. txt = '%d hours %s' % (h, txt)
  365. if td.days:
  366. txt = '%d days %s' % (td.days, txt)
  367. return txt
  368. def format_file_mode(mod):
  369. """Format file mode bits for list output
  370. """
  371. def x(v):
  372. return ''.join(v & m and s or '-'
  373. for m, s in ((4, 'r'), (2, 'w'), (1, 'x')))
  374. return '%s%s%s' % (x(mod // 64), x(mod // 8), x(mod))
  375. def format_file_size(v):
  376. """Format file size into a human friendly format
  377. """
  378. if abs(v) > 10**12:
  379. return '%.2f TB' % (v / 10**12)
  380. elif abs(v) > 10**9:
  381. return '%.2f GB' % (v / 10**9)
  382. elif abs(v) > 10**6:
  383. return '%.2f MB' % (v / 10**6)
  384. elif abs(v) > 10**3:
  385. return '%.2f kB' % (v / 10**3)
  386. else:
  387. return '%d B' % v
  388. def format_archive(archive):
  389. return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
  390. class IntegrityError(Error):
  391. """Data integrity error"""
  392. def memoize(function):
  393. cache = {}
  394. def decorated_function(*args):
  395. try:
  396. return cache[args]
  397. except KeyError:
  398. val = function(*args)
  399. cache[args] = val
  400. return val
  401. return decorated_function
  402. @memoize
  403. def uid2user(uid, default=None):
  404. try:
  405. return pwd.getpwuid(uid).pw_name
  406. except KeyError:
  407. return default
  408. @memoize
  409. def user2uid(user, default=None):
  410. try:
  411. return user and pwd.getpwnam(user).pw_uid
  412. except KeyError:
  413. return default
  414. @memoize
  415. def gid2group(gid, default=None):
  416. try:
  417. return grp.getgrgid(gid).gr_name
  418. except KeyError:
  419. return default
  420. @memoize
  421. def group2gid(group, default=None):
  422. try:
  423. return group and grp.getgrnam(group).gr_gid
  424. except KeyError:
  425. return default
  426. def posix_acl_use_stored_uid_gid(acl):
  427. """Replace the user/group field with the stored uid/gid
  428. """
  429. entries = []
  430. for entry in acl.decode('ascii').split('\n'):
  431. if entry:
  432. fields = entry.split(':')
  433. if len(fields) == 4:
  434. entries.append(':'.join([fields[0], fields[3], fields[2]]))
  435. else:
  436. entries.append(entry)
  437. return ('\n'.join(entries)).encode('ascii')
  438. class Location:
  439. """Object representing a repository / archive location
  440. """
  441. proto = user = host = port = path = archive = None
  442. # borg mount's FUSE filesystem creates one level of directories from
  443. # the archive names. Thus, we must not accept "/" in archive names.
  444. ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
  445. r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
  446. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  447. file_re = re.compile(r'(?P<proto>file)://'
  448. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  449. scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
  450. r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
  451. # get the repo from BORG_RE env and the optional archive from param.
  452. # if the syntax requires giving REPOSITORY (see "borg mount"),
  453. # use "::" to let it use the env var.
  454. # if REPOSITORY argument is optional, it'll automatically use the env.
  455. env_re = re.compile(r'(?:::(?P<archive>[^/]+)?)?$')
  456. def __init__(self, text=''):
  457. self.orig = text
  458. if not self.parse(self.orig):
  459. raise ValueError
  460. def parse(self, text):
  461. valid = self._parse(text)
  462. if valid:
  463. return True
  464. m = self.env_re.match(text)
  465. if not m:
  466. return False
  467. repo = os.environ.get('BORG_REPO')
  468. if repo is None:
  469. return False
  470. valid = self._parse(repo)
  471. if not valid:
  472. return False
  473. self.archive = m.group('archive')
  474. return True
  475. def _parse(self, text):
  476. m = self.ssh_re.match(text)
  477. if m:
  478. self.proto = m.group('proto')
  479. self.user = m.group('user')
  480. self.host = m.group('host')
  481. self.port = m.group('port') and int(m.group('port')) or None
  482. self.path = m.group('path')
  483. self.archive = m.group('archive')
  484. return True
  485. m = self.file_re.match(text)
  486. if m:
  487. self.proto = m.group('proto')
  488. self.path = m.group('path')
  489. self.archive = m.group('archive')
  490. return True
  491. m = self.scp_re.match(text)
  492. if m:
  493. self.user = m.group('user')
  494. self.host = m.group('host')
  495. self.path = m.group('path')
  496. self.archive = m.group('archive')
  497. self.proto = self.host and 'ssh' or 'file'
  498. return True
  499. return False
  500. def __str__(self):
  501. items = []
  502. items.append('proto=%r' % self.proto)
  503. items.append('user=%r' % self.user)
  504. items.append('host=%r' % self.host)
  505. items.append('port=%r' % self.port)
  506. items.append('path=%r' % self.path)
  507. items.append('archive=%r' % self.archive)
  508. return ', '.join(items)
  509. def to_key_filename(self):
  510. name = re.sub('[^\w]', '_', self.path).strip('_')
  511. if self.proto != 'file':
  512. name = self.host + '__' + name
  513. return os.path.join(get_keys_dir(), name)
  514. def __repr__(self):
  515. return "Location(%s)" % self
  516. def canonical_path(self):
  517. if self.proto == 'file':
  518. return self.path
  519. else:
  520. if self.path and self.path.startswith('~'):
  521. path = '/' + self.path
  522. elif self.path and not self.path.startswith('/'):
  523. path = '/~/' + self.path
  524. else:
  525. path = self.path
  526. return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
  527. self.host,
  528. ':{}'.format(self.port) if self.port else '',
  529. path)
  530. def location_validator(archive=None):
  531. def validator(text):
  532. try:
  533. loc = Location(text)
  534. except ValueError:
  535. raise argparse.ArgumentTypeError('Invalid location format: "%s"' % text)
  536. if archive is True and not loc.archive:
  537. raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
  538. elif archive is False and loc.archive:
  539. raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
  540. return loc
  541. return validator
  542. def read_msgpack(filename):
  543. with open(filename, 'rb') as fd:
  544. return msgpack.unpack(fd)
  545. def write_msgpack(filename, d):
  546. with open(filename + '.tmp', 'wb') as fd:
  547. msgpack.pack(d, fd)
  548. fd.flush()
  549. os.fsync(fd.fileno())
  550. os.rename(filename + '.tmp', filename)
  551. def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'):
  552. for key in keys:
  553. if isinstance(d.get(key), bytes):
  554. d[key] = d[key].decode(encoding, errors)
  555. return d
  556. def remove_surrogates(s, errors='replace'):
  557. """Replace surrogates generated by fsdecode with '?'
  558. """
  559. return s.encode('utf-8', errors).decode('utf-8')
  560. _safe_re = re.compile(r'^((\.\.)?/+)+')
  561. def make_path_safe(path):
  562. """Make path safe by making it relative and local
  563. """
  564. return _safe_re.sub('', path) or '.'
  565. def daemonize():
  566. """Detach process from controlling terminal and run in background
  567. """
  568. pid = os.fork()
  569. if pid:
  570. os._exit(0)
  571. os.setsid()
  572. pid = os.fork()
  573. if pid:
  574. os._exit(0)
  575. os.chdir('/')
  576. os.close(0)
  577. os.close(1)
  578. os.close(2)
  579. fd = os.open('/dev/null', os.O_RDWR)
  580. os.dup2(fd, 0)
  581. os.dup2(fd, 1)
  582. os.dup2(fd, 2)
  583. class StableDict(dict):
  584. """A dict subclass with stable items() ordering"""
  585. def items(self):
  586. return sorted(super().items())
  587. if sys.version < '3.3':
  588. # st_mtime_ns attribute only available in 3.3+
  589. def st_mtime_ns(st):
  590. return int(st.st_mtime * 1e9)
  591. # unhexlify in < 3.3 incorrectly only accepts bytes input
  592. def unhexlify(data):
  593. if isinstance(data, str):
  594. data = data.encode('ascii')
  595. return binascii.unhexlify(data)
  596. else:
  597. def st_mtime_ns(st):
  598. return st.st_mtime_ns
  599. unhexlify = binascii.unhexlify
  600. def bigint_to_int(mtime):
  601. """Convert bytearray to int
  602. """
  603. if isinstance(mtime, bytes):
  604. return int.from_bytes(mtime, 'little', signed=True)
  605. return mtime
  606. def int_to_bigint(value):
  607. """Convert integers larger than 64 bits to bytearray
  608. Smaller integers are left alone
  609. """
  610. if value.bit_length() > 63:
  611. return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True)
  612. return value