diff --git a/attic/archive.py b/attic/archive.py index d78a7fdb3..b4f9121b2 100644 --- a/attic/archive.py +++ b/attic/archive.py @@ -184,11 +184,13 @@ class Archive: del self.manifest.archives[self.checkpoint_name] self.cache.chunk_decref(self.id, self.stats) - def save(self, name=None): + def save(self, name=None, timestamp=None): name = name or self.name if name in self.manifest.archives: raise self.AlreadyExists(name) self.items_buffer.flush(flush=True) + if timestamp is None: + timestamp = datetime.utcnow() metadata = StableDict({ 'version': 1, 'name': name, @@ -196,7 +198,7 @@ class Archive: 'cmdline': sys.argv, 'hostname': socket.gethostname(), 'username': getuser(), - 'time': datetime.utcnow().isoformat(), + 'time': timestamp.isoformat(), }) data = msgpack.packb(metadata, unicode_errors='surrogateescape') self.id = self.key.id_hash(data) diff --git a/attic/archiver.py b/attic/archiver.py index 7295ac06e..febffb988 100644 --- a/attic/archiver.py +++ b/attic/archiver.py @@ -15,7 +15,7 @@ from attic.repository import Repository from attic.cache import Cache from attic.key import key_creator from attic.helpers import Error, location_validator, format_time, \ - format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, \ + format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \ get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \ Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ is_cachedir, bigint_to_int @@ -127,7 +127,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""") else: restrict_dev = None self._process(archive, cache, args.excludes, args.exclude_caches, skip_inodes, path, restrict_dev) - archive.save() + archive.save(timestamp=args.timestamp) if args.stats: t = datetime.now() diff = t - t0 @@ -551,6 +551,11 @@ Type "Yes I am sure" if you understand this and want to continue.\n""") subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only store numeric user and group identifiers') + subparser.add_argument('--timestamp', dest='timestamp', + type=timestamp, default=None, + metavar='yyyy-mm-ddThh:mm:ss', + help='manually specify the archive creation date/time (UTC). ' + 'alternatively, give a reference file/directory.') subparser.add_argument('archive', metavar='ARCHIVE', type=location_validator(archive=True), help='archive to create') diff --git a/attic/helpers.py b/attic/helpers.py index 646ba2571..c223dd049 100644 --- a/attic/helpers.py +++ b/attic/helpers.py @@ -257,6 +257,26 @@ class ExcludePattern(IncludePattern): return '%s(%s)' % (type(self), self.pattern) +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 + + def is_cachedir(path): """Determines whether the specified path is a cache directory (and therefore should potentially be excluded from the backup) according to