2010-10-15 18:46:17 +00:00
|
|
|
import argparse
|
2013-06-26 20:00:23 +00:00
|
|
|
import binascii
|
2010-10-19 20:03:47 +00:00
|
|
|
import grp
|
2011-09-04 21:02:47 +00:00
|
|
|
import msgpack
|
2010-10-31 19:12:32 +00:00
|
|
|
import os
|
2010-10-19 20:03:47 +00:00
|
|
|
import pwd
|
2010-10-21 19:21:43 +00:00
|
|
|
import re
|
2011-08-07 10:37:17 +00:00
|
|
|
import sys
|
2011-06-16 19:55:54 +00:00
|
|
|
import time
|
2014-02-08 20:37:27 +00:00
|
|
|
from datetime import datetime, timezone, timedelta
|
2014-02-07 20:54:24 +00:00
|
|
|
from fnmatch import translate
|
2013-06-26 20:00:23 +00:00
|
|
|
from operator import attrgetter
|
2013-12-14 19:44:20 +00:00
|
|
|
import fcntl
|
|
|
|
|
2014-03-18 21:04:08 +00:00
|
|
|
import attic.hashindex
|
|
|
|
import attic.chunker
|
|
|
|
import attic.crypto
|
|
|
|
|
2013-12-14 19:44:20 +00:00
|
|
|
|
2013-12-15 19:35:29 +00:00
|
|
|
class Error(Exception):
|
|
|
|
"""Error base class"""
|
|
|
|
|
|
|
|
exit_code = 1
|
|
|
|
|
|
|
|
def get_message(self):
|
|
|
|
return 'Error: ' + type(self).__doc__.format(*self.args)
|
|
|
|
|
|
|
|
|
2014-03-18 21:04:08 +00:00
|
|
|
class ExtensionModuleError(Error):
|
2015-05-09 16:40:55 +00:00
|
|
|
"""The Borg binary extension modules do not seem to be properly installed"""
|
2014-03-18 21:04:08 +00:00
|
|
|
|
|
|
|
|
2013-12-14 19:44:20 +00:00
|
|
|
class UpgradableLock:
|
|
|
|
|
2014-05-31 13:39:51 +00:00
|
|
|
class ReadLockFailed(Error):
|
|
|
|
"""Failed to acquire read lock on {}"""
|
|
|
|
|
|
|
|
class WriteLockFailed(Error):
|
2013-12-15 19:35:29 +00:00
|
|
|
"""Failed to acquire write lock on {}"""
|
|
|
|
|
2013-12-14 19:44:20 +00:00
|
|
|
def __init__(self, path, exclusive=False):
|
2013-12-15 19:35:29 +00:00
|
|
|
self.path = path
|
2013-12-14 19:44:20 +00:00
|
|
|
try:
|
|
|
|
self.fd = open(path, 'r+')
|
|
|
|
except IOError:
|
|
|
|
self.fd = open(path, 'r')
|
2014-05-31 13:39:51 +00:00
|
|
|
try:
|
|
|
|
if exclusive:
|
|
|
|
fcntl.lockf(self.fd, fcntl.LOCK_EX)
|
|
|
|
else:
|
|
|
|
fcntl.lockf(self.fd, fcntl.LOCK_SH)
|
|
|
|
# Python 3.2 raises IOError, Python3.3+ raises OSError
|
|
|
|
except (IOError, OSError):
|
|
|
|
if exclusive:
|
|
|
|
raise self.WriteLockFailed(self.path)
|
|
|
|
else:
|
|
|
|
raise self.ReadLockFailed(self.path)
|
2013-12-14 19:44:20 +00:00
|
|
|
self.is_exclusive = exclusive
|
|
|
|
|
|
|
|
def upgrade(self):
|
2013-12-15 19:35:29 +00:00
|
|
|
try:
|
|
|
|
fcntl.lockf(self.fd, fcntl.LOCK_EX)
|
2014-05-31 13:13:49 +00:00
|
|
|
# Python 3.2 raises IOError, Python3.3+ raises OSError
|
2014-05-31 13:17:06 +00:00
|
|
|
except (IOError, OSError):
|
2014-05-31 13:39:51 +00:00
|
|
|
raise self.WriteLockFailed(self.path)
|
2013-12-14 19:44:20 +00:00
|
|
|
self.is_exclusive = True
|
|
|
|
|
|
|
|
def release(self):
|
|
|
|
fcntl.lockf(self.fd, fcntl.LOCK_UN)
|
|
|
|
self.fd.close()
|
2011-08-07 10:37:17 +00:00
|
|
|
|
2011-10-27 20:17:47 +00:00
|
|
|
|
2014-03-18 21:04:08 +00:00
|
|
|
def check_extension_modules():
|
2014-04-13 18:26:46 +00:00
|
|
|
import attic.platform
|
2014-07-10 13:32:12 +00:00
|
|
|
if (attic.hashindex.API_VERSION != 2 or
|
2014-08-03 13:04:41 +00:00
|
|
|
attic.chunker.API_VERSION != 2 or
|
2014-05-13 20:27:57 +00:00
|
|
|
attic.crypto.API_VERSION != 2 or
|
2014-08-01 13:50:18 +00:00
|
|
|
attic.platform.API_VERSION != 2):
|
2014-03-18 21:04:08 +00:00
|
|
|
raise ExtensionModuleError
|
|
|
|
|
|
|
|
|
2013-06-26 20:00:23 +00:00
|
|
|
class Manifest:
|
2011-09-04 21:02:47 +00:00
|
|
|
|
2013-06-03 11:45:48 +00:00
|
|
|
MANIFEST_ID = b'\0' * 32
|
2011-09-04 21:02:47 +00:00
|
|
|
|
2014-02-16 21:21:18 +00:00
|
|
|
def __init__(self, key, repository):
|
2011-09-04 21:02:47 +00:00
|
|
|
self.archives = {}
|
|
|
|
self.config = {}
|
2014-02-16 21:21:18 +00:00
|
|
|
self.key = key
|
|
|
|
self.repository = repository
|
2011-09-04 21:02:47 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
@classmethod
|
2014-02-17 17:25:25 +00:00
|
|
|
def load(cls, repository, key=None):
|
2012-12-04 22:02:10 +00:00
|
|
|
from .key import key_factory
|
2014-02-16 21:21:18 +00:00
|
|
|
cdata = repository.get(cls.MANIFEST_ID)
|
2014-02-17 17:25:25 +00:00
|
|
|
if not key:
|
|
|
|
key = key_factory(repository, cdata)
|
2014-02-16 21:21:18 +00:00
|
|
|
manifest = cls(key, repository)
|
2012-12-04 22:02:10 +00:00
|
|
|
data = key.decrypt(None, cdata)
|
|
|
|
manifest.id = key.id_hash(data)
|
|
|
|
m = msgpack.unpackb(data)
|
2013-06-03 11:45:48 +00:00
|
|
|
if not m.get(b'version') == 1:
|
2011-09-04 21:02:47 +00:00
|
|
|
raise ValueError('Invalid manifest version')
|
2015-03-17 22:47:21 +00:00
|
|
|
manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items())
|
2013-08-09 21:23:00 +00:00
|
|
|
manifest.timestamp = m.get(b'timestamp')
|
|
|
|
if manifest.timestamp:
|
|
|
|
manifest.timestamp = manifest.timestamp.decode('ascii')
|
2013-06-03 11:45:48 +00:00
|
|
|
manifest.config = m[b'config']
|
2012-12-04 22:02:10 +00:00
|
|
|
return manifest, key
|
2011-09-04 21:02:47 +00:00
|
|
|
|
|
|
|
def write(self):
|
2013-08-09 21:23:00 +00:00
|
|
|
self.timestamp = datetime.utcnow().isoformat()
|
2014-02-18 22:09:12 +00:00
|
|
|
data = msgpack.packb(StableDict({
|
2011-09-04 21:02:47 +00:00
|
|
|
'version': 1,
|
|
|
|
'archives': self.archives,
|
2013-08-09 21:23:00 +00:00
|
|
|
'timestamp': self.timestamp,
|
2011-09-04 21:02:47 +00:00
|
|
|
'config': self.config,
|
2014-02-18 22:09:12 +00:00
|
|
|
}))
|
2011-09-04 21:02:47 +00:00
|
|
|
self.id = self.key.id_hash(data)
|
2013-06-20 10:44:58 +00:00
|
|
|
self.repository.put(self.MANIFEST_ID, self.key.encrypt(data))
|
2011-09-04 21:02:47 +00:00
|
|
|
|
2011-08-07 12:04:14 +00:00
|
|
|
|
2014-02-08 20:37:27 +00:00
|
|
|
def prune_within(archives, within):
|
|
|
|
multiplier = {'H': 1, 'd': 24, 'w': 24*7, 'm': 24*31, 'y': 24*365}
|
|
|
|
try:
|
|
|
|
hours = int(within[:-1]) * multiplier[within[-1]]
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
# I don't like how this displays the original exception too:
|
|
|
|
raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within)
|
|
|
|
if hours <= 0:
|
|
|
|
raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
|
|
|
|
target = datetime.now(timezone.utc) - timedelta(seconds=hours*60*60)
|
|
|
|
return [a for a in archives if a.ts > target]
|
|
|
|
|
|
|
|
|
2011-11-22 20:47:17 +00:00
|
|
|
def prune_split(archives, pattern, n, skip=[]):
|
2014-02-03 04:45:53 +00:00
|
|
|
last = None
|
2011-08-12 06:49:01 +00:00
|
|
|
keep = []
|
2014-02-03 04:45:53 +00:00
|
|
|
if n == 0:
|
|
|
|
return keep
|
|
|
|
for a in sorted(archives, key=attrgetter('ts'), reverse=True):
|
2014-03-30 20:57:16 +00:00
|
|
|
period = to_localtime(a.ts).strftime(pattern)
|
2014-02-03 04:45:53 +00:00
|
|
|
if period != last:
|
|
|
|
last = period
|
|
|
|
if a not in skip:
|
|
|
|
keep.append(a)
|
2015-03-17 22:47:21 +00:00
|
|
|
if len(keep) == n:
|
|
|
|
break
|
2011-08-16 20:02:42 +00:00
|
|
|
return keep
|
2011-08-11 19:18:13 +00:00
|
|
|
|
|
|
|
|
2013-06-26 20:00:23 +00:00
|
|
|
class Statistics:
|
2011-08-07 15:10:21 +00:00
|
|
|
|
|
|
|
def __init__(self):
|
2011-08-07 18:00:18 +00:00
|
|
|
self.osize = self.csize = self.usize = self.nfiles = 0
|
2011-08-07 15:10:21 +00:00
|
|
|
|
|
|
|
def update(self, size, csize, unique):
|
|
|
|
self.osize += size
|
|
|
|
self.csize += csize
|
|
|
|
if unique:
|
|
|
|
self.usize += csize
|
|
|
|
|
2014-03-19 21:32:07 +00:00
|
|
|
def print_(self, label, cache):
|
2014-03-18 20:42:03 +00:00
|
|
|
total_size, total_csize, unique_size, unique_csize = cache.chunks.summarize()
|
|
|
|
print()
|
2014-03-19 20:52:49 +00:00
|
|
|
print(' Original size Compressed size Deduplicated size')
|
2014-03-19 21:32:07 +00:00
|
|
|
print('%-15s %20s %20s %20s' % (label, format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize)))
|
2014-03-19 20:52:49 +00:00
|
|
|
print('All archives: %20s %20s %20s' % (format_file_size(total_size), format_file_size(total_csize), format_file_size(unique_csize)))
|
2014-03-18 20:42:03 +00:00
|
|
|
|
2015-03-24 03:24:54 +00:00
|
|
|
def show_progress(self, item=None, final=False):
|
|
|
|
if not final:
|
|
|
|
path = remove_surrogates(item[b'path']) if item else ''
|
|
|
|
if len(path) > 43:
|
|
|
|
path = '%s...%s' % (path[:20], path[-20:])
|
|
|
|
msg = '%9s O %9s C %9s D %-43s' % (
|
|
|
|
format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize), path)
|
|
|
|
else:
|
|
|
|
msg = ' ' * 79
|
|
|
|
print(msg, end='\r')
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
2010-12-19 11:46:42 +00:00
|
|
|
|
2011-08-06 11:01:58 +00:00
|
|
|
def get_keys_dir():
|
2013-06-20 10:44:58 +00:00
|
|
|
"""Determine where to repository keys and cache"""
|
2015-05-09 16:40:55 +00:00
|
|
|
return os.environ.get('BORG_KEYS_DIR',
|
|
|
|
os.path.join(os.path.expanduser('~'), '.borg', 'keys'))
|
2011-08-06 11:01:58 +00:00
|
|
|
|
2011-08-07 12:04:14 +00:00
|
|
|
|
2011-08-06 11:01:58 +00:00
|
|
|
def get_cache_dir():
|
2013-06-20 10:44:58 +00:00
|
|
|
"""Determine where to repository keys and cache"""
|
2015-05-09 16:40:55 +00:00
|
|
|
return os.environ.get('BORG_CACHE_DIR',
|
|
|
|
os.path.join(os.path.expanduser('~'), '.cache', 'borg'))
|
2011-08-06 11:01:58 +00:00
|
|
|
|
|
|
|
|
2011-06-16 19:55:54 +00:00
|
|
|
def to_localtime(ts):
|
|
|
|
"""Convert datetime object from UTC to local time zone"""
|
2013-12-15 20:36:20 +00:00
|
|
|
return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])
|
2010-12-19 11:46:42 +00:00
|
|
|
|
2011-08-07 12:04:14 +00:00
|
|
|
|
2015-04-19 20:42:52 +00:00
|
|
|
def parse_timestamp(timestamp):
|
|
|
|
"""Parse a ISO 8601 timestamp string"""
|
|
|
|
if '.' in timestamp: # microseconds might not be pressent
|
|
|
|
return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=timezone.utc)
|
|
|
|
else:
|
|
|
|
return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
2014-02-08 17:44:48 +00:00
|
|
|
def update_excludes(args):
|
|
|
|
"""Merge exclude patterns from files with those on command line.
|
|
|
|
Empty lines and lines starting with '#' are ignored, but whitespace
|
|
|
|
is not stripped."""
|
|
|
|
if hasattr(args, 'exclude_files') and args.exclude_files:
|
|
|
|
if not hasattr(args, 'excludes') or args.excludes is None:
|
|
|
|
args.excludes = []
|
|
|
|
for file in args.exclude_files:
|
|
|
|
patterns = [line.rstrip('\r\n') for line in file if not line.startswith('#')]
|
|
|
|
args.excludes += [ExcludePattern(pattern) for pattern in patterns if pattern]
|
|
|
|
file.close()
|
|
|
|
|
|
|
|
|
2013-06-30 20:32:27 +00:00
|
|
|
def adjust_patterns(paths, excludes):
|
|
|
|
if paths:
|
|
|
|
return (excludes or []) + [IncludePattern(path) for path in paths] + [ExcludePattern('*')]
|
|
|
|
else:
|
|
|
|
return excludes
|
2012-10-17 09:40:23 +00:00
|
|
|
|
|
|
|
|
2010-11-02 21:47:39 +00:00
|
|
|
def exclude_path(path, patterns):
|
|
|
|
"""Used by create and extract sub-commands to determine
|
2014-02-07 20:54:24 +00:00
|
|
|
whether or not an item should be processed.
|
2010-11-02 21:47:39 +00:00
|
|
|
"""
|
|
|
|
for pattern in (patterns or []):
|
|
|
|
if pattern.match(path):
|
|
|
|
return isinstance(pattern, ExcludePattern)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2014-02-07 23:04:21 +00:00
|
|
|
# For both IncludePattern and ExcludePattern, we require that
|
|
|
|
# the pattern either match the whole path or an initial segment
|
|
|
|
# of the path up to but not including a path separator. To
|
|
|
|
# unify the two cases, we add a path separator to the end of
|
|
|
|
# the path before matching.
|
|
|
|
|
2013-06-26 20:00:23 +00:00
|
|
|
class IncludePattern:
|
2014-02-07 20:54:24 +00:00
|
|
|
"""Literal files or directories listed on the command line
|
2014-02-07 23:10:52 +00:00
|
|
|
for some operations (e.g. extract, but not create).
|
2014-02-07 20:54:24 +00:00
|
|
|
If a directory is specified, all paths that start with that
|
|
|
|
path match as well. A trailing slash makes no difference.
|
2010-11-02 21:47:39 +00:00
|
|
|
"""
|
|
|
|
def __init__(self, pattern):
|
2014-12-14 18:15:54 +00:00
|
|
|
self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep
|
2010-11-02 21:47:39 +00:00
|
|
|
|
|
|
|
def match(self, path):
|
2014-02-07 21:27:18 +00:00
|
|
|
return (path+os.path.sep).startswith(self.pattern)
|
2010-11-02 21:47:39 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '%s(%s)' % (type(self), self.pattern)
|
|
|
|
|
|
|
|
|
|
|
|
class ExcludePattern(IncludePattern):
|
2014-02-07 20:54:24 +00:00
|
|
|
"""Shell glob patterns to exclude. A trailing slash means to
|
|
|
|
exclude the contents of a directory, but not the directory itself.
|
2010-11-02 21:47:39 +00:00
|
|
|
"""
|
2013-06-30 20:32:27 +00:00
|
|
|
def __init__(self, pattern):
|
2014-02-07 20:54:24 +00:00
|
|
|
if pattern.endswith(os.path.sep):
|
2014-12-14 18:15:54 +00:00
|
|
|
self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep+'*'+os.path.sep
|
2014-02-07 20:54:24 +00:00
|
|
|
else:
|
2014-12-14 18:15:54 +00:00
|
|
|
self.pattern = os.path.normpath(pattern)+os.path.sep+'*'
|
2014-02-07 23:04:21 +00:00
|
|
|
# fnmatch and re.match both cache compiled regular expressions.
|
|
|
|
# Nevertheless, this is about 10 times faster.
|
|
|
|
self.regex = re.compile(translate(self.pattern))
|
2013-06-30 20:32:27 +00:00
|
|
|
|
|
|
|
def match(self, path):
|
2014-02-07 23:04:21 +00:00
|
|
|
return self.regex.match(path+os.path.sep) is not None
|
2013-06-30 20:32:27 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '%s(%s)' % (type(self), self.pattern)
|
2010-11-02 21:47:39 +00:00
|
|
|
|
2014-05-01 12:56:21 +00:00
|
|
|
|
2015-04-18 19:36:10 +00:00
|
|
|
def timestamp(s):
|
|
|
|
"""Convert a --timestamp=s argument to a datetime object"""
|
|
|
|
try:
|
|
|
|
# is it pointing to a file / directory?
|
|
|
|
ts = os.stat(s).st_mtime
|
|
|
|
return datetime.utcfromtimestamp(ts)
|
|
|
|
except OSError:
|
|
|
|
# didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support.
|
|
|
|
for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S+00:00',
|
|
|
|
'%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
|
|
|
|
'%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
|
|
|
|
'%Y-%m-%d', '%Y-%j',
|
|
|
|
):
|
|
|
|
try:
|
|
|
|
return datetime.strptime(s, format)
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
|
2014-04-30 21:27:04 +00:00
|
|
|
def is_cachedir(path):
|
|
|
|
"""Determines whether the specified path is a cache directory (and
|
|
|
|
therefore should potentially be excluded from the backup) according to
|
|
|
|
the CACHEDIR.TAG protocol
|
|
|
|
(http://www.brynosaurus.com/cachedir/spec.html).
|
|
|
|
"""
|
|
|
|
|
|
|
|
tag_contents = b'Signature: 8a477f597d28d172789f06886806bc55'
|
|
|
|
tag_path = os.path.join(path, 'CACHEDIR.TAG')
|
|
|
|
try:
|
|
|
|
if os.path.exists(tag_path):
|
|
|
|
with open(tag_path, 'rb') as tag_file:
|
|
|
|
tag_data = tag_file.read(len(tag_contents))
|
|
|
|
if tag_data == tag_contents:
|
|
|
|
return True
|
2014-05-01 12:56:21 +00:00
|
|
|
except OSError:
|
|
|
|
pass
|
2014-04-30 21:27:04 +00:00
|
|
|
return False
|
2010-11-02 21:47:39 +00:00
|
|
|
|
2014-05-01 12:56:21 +00:00
|
|
|
|
2010-10-27 17:30:21 +00:00
|
|
|
def format_time(t):
|
|
|
|
"""Format datetime suitable for fixed length list output
|
|
|
|
"""
|
2014-05-18 16:28:26 +00:00
|
|
|
if abs((datetime.now() - t).days) < 365:
|
2010-10-27 17:30:21 +00:00
|
|
|
return t.strftime('%b %d %H:%M')
|
|
|
|
else:
|
2010-10-27 18:40:48 +00:00
|
|
|
return t.strftime('%b %d %Y')
|
2010-10-27 17:30:21 +00:00
|
|
|
|
|
|
|
|
2011-08-07 15:10:21 +00:00
|
|
|
def format_timedelta(td):
|
2011-08-08 18:55:09 +00:00
|
|
|
"""Format timedelta in a human friendly format
|
|
|
|
"""
|
|
|
|
# Since td.total_seconds() requires python 2.7
|
2011-10-31 20:18:28 +00:00
|
|
|
ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
|
2011-08-07 15:10:21 +00:00
|
|
|
s = ts % 60
|
|
|
|
m = int(ts / 60) % 60
|
|
|
|
h = int(ts / 3600) % 24
|
|
|
|
txt = '%.2f seconds' % s
|
|
|
|
if m:
|
|
|
|
txt = '%d minutes %s' % (m, txt)
|
|
|
|
if h:
|
|
|
|
txt = '%d hours %s' % (h, txt)
|
|
|
|
if td.days:
|
|
|
|
txt = '%d days %s' % (td.days, txt)
|
|
|
|
return txt
|
|
|
|
|
|
|
|
|
2010-10-27 17:30:21 +00:00
|
|
|
def format_file_mode(mod):
|
|
|
|
"""Format file mode bits for list output
|
2010-10-23 19:38:42 +00:00
|
|
|
"""
|
2010-10-27 17:30:21 +00:00
|
|
|
def x(v):
|
|
|
|
return ''.join(v & m and s or '-'
|
|
|
|
for m, s in ((4, 'r'), (2, 'w'), (1, 'x')))
|
2013-06-03 11:45:48 +00:00
|
|
|
return '%s%s%s' % (x(mod // 64), x(mod // 8), x(mod))
|
2010-10-27 17:30:21 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-27 17:30:21 +00:00
|
|
|
def format_file_size(v):
|
|
|
|
"""Format file size into a human friendly format
|
2010-10-23 19:38:42 +00:00
|
|
|
"""
|
2014-03-19 21:32:07 +00:00
|
|
|
if abs(v) > 10**12:
|
2014-03-18 20:42:03 +00:00
|
|
|
return '%.2f TB' % (v / 10**12)
|
2014-03-19 21:32:07 +00:00
|
|
|
elif abs(v) > 10**9:
|
2014-03-18 20:42:03 +00:00
|
|
|
return '%.2f GB' % (v / 10**9)
|
2014-03-19 21:32:07 +00:00
|
|
|
elif abs(v) > 10**6:
|
2014-03-18 20:42:03 +00:00
|
|
|
return '%.2f MB' % (v / 10**6)
|
2014-03-19 21:32:07 +00:00
|
|
|
elif abs(v) > 10**3:
|
2014-03-18 20:42:03 +00:00
|
|
|
return '%.2f kB' % (v / 10**3)
|
2010-10-27 17:30:21 +00:00
|
|
|
else:
|
2014-03-19 21:32:07 +00:00
|
|
|
return '%d B' % v
|
2010-10-23 19:38:42 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2014-02-25 00:46:56 +00:00
|
|
|
def format_archive(archive):
|
|
|
|
return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
|
|
|
|
|
|
|
|
|
2014-02-18 20:33:06 +00:00
|
|
|
class IntegrityError(Error):
|
|
|
|
"""Data integrity error"""
|
2010-10-23 19:38:42 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
def memoize(function):
|
|
|
|
cache = {}
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
def decorated_function(*args):
|
|
|
|
try:
|
|
|
|
return cache[args]
|
|
|
|
except KeyError:
|
|
|
|
val = function(*args)
|
|
|
|
cache[args] = val
|
|
|
|
return val
|
|
|
|
return decorated_function
|
2010-10-15 18:46:17 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
@memoize
|
2014-04-13 18:26:46 +00:00
|
|
|
def uid2user(uid, default=None):
|
2010-10-19 20:03:47 +00:00
|
|
|
try:
|
|
|
|
return pwd.getpwuid(uid).pw_name
|
|
|
|
except KeyError:
|
2014-04-13 18:26:46 +00:00
|
|
|
return default
|
2010-10-19 20:03:47 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
@memoize
|
2014-04-13 18:26:46 +00:00
|
|
|
def user2uid(user, default=None):
|
2010-10-19 20:03:47 +00:00
|
|
|
try:
|
2012-03-03 13:02:22 +00:00
|
|
|
return user and pwd.getpwnam(user).pw_uid
|
2010-10-19 20:03:47 +00:00
|
|
|
except KeyError:
|
2014-04-13 18:26:46 +00:00
|
|
|
return default
|
2010-10-19 20:03:47 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
@memoize
|
2014-04-13 18:26:46 +00:00
|
|
|
def gid2group(gid, default=None):
|
2010-10-19 20:03:47 +00:00
|
|
|
try:
|
|
|
|
return grp.getgrgid(gid).gr_name
|
|
|
|
except KeyError:
|
2014-04-13 18:26:46 +00:00
|
|
|
return default
|
2010-10-19 19:12:12 +00:00
|
|
|
|
2011-10-31 20:18:28 +00:00
|
|
|
|
2010-10-19 20:03:47 +00:00
|
|
|
@memoize
|
2014-04-13 18:26:46 +00:00
|
|
|
def group2gid(group, default=None):
|
2010-10-19 20:03:47 +00:00
|
|
|
try:
|
2012-03-03 13:02:22 +00:00
|
|
|
return group and grp.getgrnam(group).gr_gid
|
2010-10-19 20:03:47 +00:00
|
|
|
except KeyError:
|
2014-04-13 18:26:46 +00:00
|
|
|
return default
|
|
|
|
|
|
|
|
|
2014-04-27 12:17:09 +00:00
|
|
|
def posix_acl_use_stored_uid_gid(acl):
|
2014-04-13 18:26:46 +00:00
|
|
|
"""Replace the user/group field with the stored uid/gid
|
|
|
|
"""
|
|
|
|
entries = []
|
|
|
|
for entry in acl.decode('ascii').split('\n'):
|
|
|
|
if entry:
|
|
|
|
fields = entry.split(':')
|
|
|
|
if len(fields) == 4:
|
|
|
|
entries.append(':'.join([fields[0], fields[3], fields[2]]))
|
|
|
|
else:
|
|
|
|
entries.append(entry)
|
|
|
|
return ('\n'.join(entries)).encode('ascii')
|
2010-10-19 20:03:47 +00:00
|
|
|
|
2010-10-27 17:30:21 +00:00
|
|
|
|
2013-06-26 11:41:42 +00:00
|
|
|
class Location:
|
2013-06-20 10:44:58 +00:00
|
|
|
"""Object representing a repository / archive location
|
2010-11-17 20:28:13 +00:00
|
|
|
"""
|
|
|
|
proto = user = host = port = path = archive = None
|
|
|
|
ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
|
|
|
|
r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
|
2014-08-08 21:34:27 +00:00
|
|
|
r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
|
2010-11-17 20:28:13 +00:00
|
|
|
file_re = re.compile(r'(?P<proto>file)://'
|
2014-08-08 21:34:27 +00:00
|
|
|
r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
|
2010-11-17 20:28:13 +00:00
|
|
|
scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
|
2014-08-08 21:34:27 +00:00
|
|
|
r'(?P<path>[^:]+)(?:::(?P<archive>.+))?$')
|
2010-10-15 18:46:17 +00:00
|
|
|
|
|
|
|
def __init__(self, text):
|
2011-10-27 20:17:47 +00:00
|
|
|
self.orig = text
|
2010-11-17 20:28:13 +00:00
|
|
|
if not self.parse(text):
|
2010-10-15 18:46:17 +00:00
|
|
|
raise ValueError
|
2010-11-17 20:28:13 +00:00
|
|
|
|
|
|
|
def parse(self, text):
|
|
|
|
m = self.ssh_re.match(text)
|
|
|
|
if m:
|
|
|
|
self.proto = m.group('proto')
|
|
|
|
self.user = m.group('user')
|
|
|
|
self.host = m.group('host')
|
2013-11-27 16:07:35 +00:00
|
|
|
self.port = m.group('port') and int(m.group('port')) or None
|
2010-11-17 20:28:13 +00:00
|
|
|
self.path = m.group('path')
|
|
|
|
self.archive = m.group('archive')
|
|
|
|
return True
|
|
|
|
m = self.file_re.match(text)
|
|
|
|
if m:
|
|
|
|
self.proto = m.group('proto')
|
|
|
|
self.path = m.group('path')
|
|
|
|
self.archive = m.group('archive')
|
|
|
|
return True
|
|
|
|
m = self.scp_re.match(text)
|
|
|
|
if m:
|
|
|
|
self.user = m.group('user')
|
|
|
|
self.host = m.group('host')
|
|
|
|
self.path = m.group('path')
|
|
|
|
self.archive = m.group('archive')
|
|
|
|
self.proto = self.host and 'ssh' or 'file'
|
|
|
|
return True
|
|
|
|
return False
|
2010-10-15 18:46:17 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
2010-11-17 20:28:13 +00:00
|
|
|
items = []
|
|
|
|
items.append('proto=%r' % self.proto)
|
|
|
|
items.append('user=%r' % self.user)
|
|
|
|
items.append('host=%r' % self.host)
|
|
|
|
items.append('port=%r' % self.port)
|
2012-07-31 12:43:32 +00:00
|
|
|
items.append('path=%r' % self.path)
|
2010-11-17 20:28:13 +00:00
|
|
|
items.append('archive=%r' % self.archive)
|
|
|
|
return ', '.join(items)
|
2010-10-15 18:46:17 +00:00
|
|
|
|
2011-08-04 13:27:52 +00:00
|
|
|
def to_key_filename(self):
|
|
|
|
name = re.sub('[^\w]', '_', self.path).strip('_')
|
|
|
|
if self.proto != 'file':
|
|
|
|
name = self.host + '__' + name
|
2011-08-06 11:01:58 +00:00
|
|
|
return os.path.join(get_keys_dir(), name)
|
2011-08-04 13:27:52 +00:00
|
|
|
|
2010-10-15 18:46:17 +00:00
|
|
|
def __repr__(self):
|
2010-11-17 20:28:13 +00:00
|
|
|
return "Location(%s)" % self
|
2010-10-15 18:46:17 +00:00
|
|
|
|
2015-04-13 20:35:09 +00:00
|
|
|
def canonical_path(self):
|
|
|
|
if self.proto == 'file':
|
|
|
|
return self.path
|
|
|
|
else:
|
|
|
|
if self.path and self.path.startswith('~'):
|
|
|
|
path = '/' + self.path
|
|
|
|
elif self.path and not self.path.startswith('/'):
|
|
|
|
path = '/~/' + self.path
|
|
|
|
else:
|
|
|
|
path = self.path
|
|
|
|
return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
|
|
|
|
self.host,
|
|
|
|
':{}'.format(self.port) if self.port else '',
|
|
|
|
path)
|
|
|
|
|
2010-10-15 18:46:17 +00:00
|
|
|
|
|
|
|
def location_validator(archive=None):
|
|
|
|
def validator(text):
|
|
|
|
try:
|
|
|
|
loc = Location(text)
|
|
|
|
except ValueError:
|
|
|
|
raise argparse.ArgumentTypeError('Invalid location format: "%s"' % text)
|
|
|
|
if archive is True and not loc.archive:
|
|
|
|
raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
|
|
|
|
elif archive is False and loc.archive:
|
|
|
|
raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
|
|
|
|
return loc
|
|
|
|
return validator
|
|
|
|
|
|
|
|
|
2011-09-04 21:02:47 +00:00
|
|
|
def read_msgpack(filename):
|
|
|
|
with open(filename, 'rb') as fd:
|
|
|
|
return msgpack.unpack(fd)
|
|
|
|
|
2012-07-31 12:43:32 +00:00
|
|
|
|
2011-09-04 21:02:47 +00:00
|
|
|
def write_msgpack(filename, d):
|
2012-07-31 12:43:32 +00:00
|
|
|
with open(filename + '.tmp', 'wb') as fd:
|
2011-09-04 21:02:47 +00:00
|
|
|
msgpack.pack(d, fd)
|
|
|
|
fd.flush()
|
2015-02-28 01:45:21 +00:00
|
|
|
os.fsync(fd.fileno())
|
2012-07-31 12:43:32 +00:00
|
|
|
os.rename(filename + '.tmp', filename)
|
2013-06-03 11:45:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'):
|
|
|
|
for key in keys:
|
|
|
|
if isinstance(d.get(key), bytes):
|
|
|
|
d[key] = d[key].decode(encoding, errors)
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
def remove_surrogates(s, errors='replace'):
|
2013-06-26 20:00:23 +00:00
|
|
|
"""Replace surrogates generated by fsdecode with '?'
|
|
|
|
"""
|
2013-06-15 18:56:27 +00:00
|
|
|
return s.encode('utf-8', errors).decode('utf-8')
|
|
|
|
|
|
|
|
|
2015-04-13 20:44:14 +00:00
|
|
|
_safe_re = re.compile(r'^((\.\.)?/+)+')
|
2013-08-03 11:34:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
def make_path_safe(path):
|
|
|
|
"""Make path safe by making it relative and local
|
|
|
|
"""
|
|
|
|
return _safe_re.sub('', path) or '.'
|
|
|
|
|
|
|
|
|
2013-07-24 11:23:51 +00:00
|
|
|
def daemonize():
|
|
|
|
"""Detach process from controlling terminal and run in background
|
|
|
|
"""
|
|
|
|
pid = os.fork()
|
|
|
|
if pid:
|
|
|
|
os._exit(0)
|
|
|
|
os.setsid()
|
|
|
|
pid = os.fork()
|
|
|
|
if pid:
|
|
|
|
os._exit(0)
|
|
|
|
os.chdir('/')
|
|
|
|
os.close(0)
|
|
|
|
os.close(1)
|
|
|
|
os.close(2)
|
|
|
|
fd = os.open('/dev/null', os.O_RDWR)
|
|
|
|
os.dup2(fd, 0)
|
|
|
|
os.dup2(fd, 1)
|
|
|
|
os.dup2(fd, 2)
|
|
|
|
|
|
|
|
|
2014-02-16 22:36:48 +00:00
|
|
|
class StableDict(dict):
|
|
|
|
"""A dict subclass with stable items() ordering"""
|
|
|
|
def items(self):
|
|
|
|
return sorted(super(StableDict, self).items())
|
|
|
|
|
|
|
|
|
2013-06-15 18:56:27 +00:00
|
|
|
if sys.version < '3.3':
|
2013-06-26 20:00:23 +00:00
|
|
|
# st_mtime_ns attribute only available in 3.3+
|
2013-06-15 18:56:27 +00:00
|
|
|
def st_mtime_ns(st):
|
2013-07-29 11:57:43 +00:00
|
|
|
return int(st.st_mtime * 1e9)
|
2013-06-26 20:00:23 +00:00
|
|
|
|
|
|
|
# unhexlify in < 3.3 incorrectly only accepts bytes input
|
|
|
|
def unhexlify(data):
|
|
|
|
if isinstance(data, str):
|
|
|
|
data = data.encode('ascii')
|
|
|
|
return binascii.unhexlify(data)
|
2013-06-15 18:56:27 +00:00
|
|
|
else:
|
|
|
|
def st_mtime_ns(st):
|
|
|
|
return st.st_mtime_ns
|
2013-06-26 20:00:23 +00:00
|
|
|
|
|
|
|
unhexlify = binascii.unhexlify
|
2014-05-18 16:28:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
def bigint_to_int(mtime):
|
|
|
|
"""Convert bytearray to int
|
|
|
|
"""
|
|
|
|
if isinstance(mtime, bytes):
|
|
|
|
return int.from_bytes(mtime, 'little', signed=True)
|
|
|
|
return mtime
|
|
|
|
|
|
|
|
|
|
|
|
def int_to_bigint(value):
|
|
|
|
"""Convert integers larger than 64 bits to bytearray
|
|
|
|
|
|
|
|
Smaller integers are left alone
|
|
|
|
"""
|
|
|
|
if value.bit_length() > 63:
|
|
|
|
return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True)
|
|
|
|
return value
|
2014-08-03 13:04:41 +00:00
|
|
|
|