Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions attic/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,21 @@ def write_checkpoint(self):
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,
'items': self.items_buffer.chunks,
'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)
Expand Down
11 changes: 9 additions & 2 deletions attic/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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
Expand Down Expand Up @@ -127,7 +127,7 @@ def do_create(self, args):
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
Expand Down Expand Up @@ -551,6 +551,13 @@ def run(self, args=None):
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='TIMESTAMP',
help='use TIMESTAMP as the archive creation time. '
'TIMESTAMP can be either a string of the form YYYY-MM-DDThh:mm:ss[Z] '
'(or less precise versions without seconds, minutes, hours) '
'or a file/directory name from which the modification time is used.')
subparser.add_argument('archive', metavar='ARCHIVE',
type=location_validator(archive=True),
help='archive to create')
Expand Down
35 changes: 34 additions & 1 deletion attic/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import argparse
import binascii
import errno
import grp
import itertools
import msgpack
import os
import pwd
import re
import stat
import sys
import time
from datetime import datetime, timezone, timedelta
Expand Down Expand Up @@ -182,9 +183,16 @@ def get_cache_dir():

def to_localtime(ts):
"""Convert datetime object from UTC to local time zone"""
# note: this drops the microseconds
return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])


def from_localtime(ts):
"""Convert datetime object from local time zone to UTC"""
# note: this drops the microseconds
return datetime.utcfromtimestamp(time.mktime(ts.timetuple()))


def update_excludes(args):
"""Merge exclude patterns from files with those on command line.
Empty lines and lines starting with '#' are ignored, but whitespace
Expand Down Expand Up @@ -257,6 +265,31 @@ def __repr__(self):
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
ts = int(ts) # no fractions of a second
return datetime.utcfromtimestamp(ts)
except OSError as err:
if err.errno != errno.ENOENT:
raise argparse.ArgumentTypeError('Could not access timestamp file: %s' % err)
# file not found, try parsing as ISO-8601 timestamp.
date_formats = ['%Y-%m-%d', '%Y-%j']
time_formats = ['T%H:%M:%S', 'T%H:%M', 'T%H', '']
tz_formats = ['Z', ''] # UTC or localtime
for format in itertools.product(date_formats, time_formats, tz_formats):
try:
dt = datetime.strptime(s, ''.join(format))
return dt if format[2] == 'Z' else from_localtime(dt)
except ValueError:
pass
# nothing worked :(
raise argparse.ArgumentTypeError('Unsupported or invalid ISO-8601 timestamp. '
'Try: yyyy-mm-ddThh:mm:ss[Z] (or a less precise version).')


def is_cachedir(path):
"""Determines whether the specified path is a cache directory (and
therefore should potentially be excluded from the backup) according to
Expand Down
26 changes: 25 additions & 1 deletion attic/testsuite/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import argparse
import hashlib
from time import mktime, strptime
from datetime import datetime, timezone, timedelta
import os
import tempfile
import unittest
from attic.helpers import adjust_patterns, exclude_path, Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, UpgradableLock, prune_within, prune_split, to_localtime, \
StableDict, int_to_bigint, bigint_to_int
StableDict, int_to_bigint, bigint_to_int, timestamp
from attic.testsuite import AtticTestCase
import msgpack

Expand Down Expand Up @@ -71,6 +72,29 @@ def test(self):
)


class TimestampTestCase(AtticTestCase):

def test_str(self):
self.assert_equal(timestamp('2001-02-03T04:05:06Z'), datetime(2001, 2, 3, 4, 5, 6))
self.assert_equal(timestamp('2001-02-03T04:05Z'), datetime(2001, 2, 3, 4, 5, 0))
self.assert_equal(timestamp('2001-02-03T04Z'), datetime(2001, 2, 3, 4, 0, 0))
self.assert_equal(timestamp('2001-02-03Z'), datetime(2001, 2, 3, 0, 0, 0))
self.assert_equal(timestamp('2001-034Z'), datetime(2001, 2, 3, 0, 0, 0))

def test_file(self):
with tempfile.NamedTemporaryFile() as f:
mtime = os.stat(f.name).st_mtime
mtime = int(mtime) # no fractions of a second
mtime_dt = datetime.utcfromtimestamp(mtime)
self.assert_equal(timestamp(f.name), mtime_dt)

def test_invalid(self):
self.assert_raises(argparse.ArgumentTypeError, timestamp, '/file/not/found')
self.assert_raises(argparse.ArgumentTypeError, timestamp, '31.12.1999')
self.assert_raises(argparse.ArgumentTypeError, timestamp, '2015-12-32Z')
self.assert_raises(argparse.ArgumentTypeError, timestamp, '2015-12-31T24:23Z')


class PatternTestCase(AtticTestCase):

files = [
Expand Down