diff --git a/CHANGES b/CHANGES index 6e64c5807..f703b9b12 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,14 @@ Attic Changelog Here you can see the full list of changes between each Attic release. +Version 0.16 +------------ + +(bugfix release, released on X) +- Fix "All archives" output for attic info. (#183) +- More user friendly error message when repository key file is not found (#236) +- Fix parsing of iso 8601 timestamps with zero microseconds (#282) + Version 0.15 ------------ diff --git a/attic/archive.py b/attic/archive.py index 94f560567..5d16c7809 100644 --- a/attic/archive.py +++ b/attic/archive.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime from getpass import getuser from itertools import groupby import errno @@ -17,7 +17,7 @@ from attic import xattr from attic.platform import acl_get, acl_set from attic.chunker import Chunker from attic.hashindex import ChunkIndex -from attic.helpers import Error, uid2user, user2uid, gid2group, group2gid, \ +from attic.helpers import parse_timestamp, Error, uid2user, user2uid, gid2group, group2gid, \ Manifest, Statistics, decode_dict, st_mtime_ns, make_path_safe, StableDict, int_to_bigint, bigint_to_int ITEMS_BUFFER = 1024 * 1024 @@ -172,11 +172,7 @@ class Archive: @property def ts(self): """Timestamp of archive creation in UTC""" - t = self.metadata[b'time'].split('.', 1) - dt = datetime.strptime(t[0], '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc) - if len(t) > 1: - dt += timedelta(seconds=float('.' + t[1])) - return dt + return parse_timestamp(self.metadata[b'time']) def __repr__(self): return 'Archive(%r)' % self.name @@ -225,9 +221,9 @@ class Archive: def calc_stats(self, cache): def add(id): - count, size, csize = self.cache.chunks[id] + count, size, csize = cache.chunks[id] stats.update(size, csize, count == 1) - self.cache.chunks[id] = count - 1, size, csize + cache.chunks[id] = count - 1, size, csize def add_file_chunks(chunks): for id, _, _ in chunks: add(id) diff --git a/attic/cache.py b/attic/cache.py index 0125aaab7..d856f0d01 100644 --- a/attic/cache.py +++ b/attic/cache.py @@ -103,11 +103,7 @@ class Cache: os.remove(os.path.join(self.path, 'config')) # kill config first shutil.rmtree(self.path) - def open(self): - if not os.path.isdir(self.path): - raise Exception('%s Does not look like an Attic cache' % self.path) - self.lock = UpgradableLock(os.path.join(self.path, 'config'), exclusive=True) - self.rollback() + def _do_open(self): self.config = RawConfigParser() self.config.read(os.path.join(self.path, 'config')) if self.config.getint('cache', 'version') != 1: @@ -120,6 +116,12 @@ class Cache: self.chunks = ChunkIndex.read(os.path.join(self.path, 'chunks').encode('utf-8')) self.files = None + def open(self): + if not os.path.isdir(self.path): + raise Exception('%s Does not look like an Attic cache' % self.path) + self.lock = UpgradableLock(os.path.join(self.path, 'config'), exclusive=True) + self.rollback() + def close(self): if self.lock: self.lock.release() @@ -192,6 +194,7 @@ class Cache: if os.path.exists(os.path.join(self.path, 'txn.tmp')): shutil.rmtree(os.path.join(self.path, 'txn.tmp')) self.txn_active = False + self._do_open() def sync(self): """Initializes cache by fetching and reading all archive indicies diff --git a/attic/helpers.py b/attic/helpers.py index 6b532f59e..26418e54e 100644 --- a/attic/helpers.py +++ b/attic/helpers.py @@ -197,6 +197,14 @@ def to_localtime(ts): return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6]) +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) + + def update_excludes(args): """Merge exclude patterns from files with those on command line. Empty lines and lines starting with '#' are ignored, but whitespace diff --git a/attic/key.py b/attic/key.py index 480fa38f8..7065042a7 100644 --- a/attic/key.py +++ b/attic/key.py @@ -17,6 +17,10 @@ class UnsupportedPayloadError(Error): """Unsupported payload type {}. A newer version is required to access this repository. """ +class KeyfileNotFoundError(Error): + """No key file for repository {} found in {}. + """ + class HMAC(hmac.HMAC): """Workaround a bug in Python < 3.4 Where HMAC does not accept memoryviews @@ -228,7 +232,7 @@ class KeyfileKey(AESKeyBase): line = fd.readline().strip() if line and line.startswith(cls.FILE_ID) and line[10:] == id: return filename - raise Exception('Key file for repository with ID %s not found' % id) + raise KeyfileNotFoundError(repository._location.canonical_path(), get_keys_dir()) def load(self, filename, passphrase): with open(filename, 'r') as fd: diff --git a/attic/platform.py b/attic/platform.py index 42ea65719..19f8072e6 100644 --- a/attic/platform.py +++ b/attic/platform.py @@ -1,12 +1,10 @@ -import os +import sys -platform = os.uname()[0] - -if platform == 'Linux': +if sys.platform.startswith('linux'): from attic.platform_linux import acl_get, acl_set, API_VERSION -elif platform == 'FreeBSD': +elif sys.platform.startswith('freebsd'): from attic.platform_freebsd import acl_get, acl_set, API_VERSION -elif platform == 'Darwin': +elif sys.platform == 'darwin': from attic.platform_darwin import acl_get, acl_set, API_VERSION else: API_VERSION = 2 diff --git a/attic/testsuite/helpers.py b/attic/testsuite/helpers.py index f47f8e603..9842390cb 100644 --- a/attic/testsuite/helpers.py +++ b/attic/testsuite/helpers.py @@ -4,8 +4,8 @@ from datetime import datetime, timezone, timedelta import os import tempfile import unittest -from attic.helpers import adjust_patterns, exclude_path, Location, format_timedelta, ExcludePattern, make_path_safe, UpgradableLock, prune_within, prune_split, \ - StableDict, int_to_bigint, bigint_to_int +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, parse_timestamp from attic.testsuite import AtticTestCase import msgpack @@ -209,3 +209,10 @@ class StableDictTestCase(AtticTestCase): d = StableDict(foo=1, bar=2, boo=3, baz=4) self.assert_equal(list(d.items()), [('bar', 2), ('baz', 4), ('boo', 3), ('foo', 1)]) self.assert_equal(hashlib.md5(msgpack.packb(d)).hexdigest(), 'fc78df42cd60691b3ac3dd2a2b39903f') + + +class TestParseTimestamp(AtticTestCase): + + def test(self): + self.assert_equal(parse_timestamp('2015-04-19T20:25:00.226410'), datetime(2015, 4, 19, 20, 25, 0, 226410, timezone.utc)) + self.assert_equal(parse_timestamp('2015-04-19T20:25:00'), datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc)) diff --git a/setup.py b/setup.py index 2623be324..6a166805d 100644 --- a/setup.py +++ b/setup.py @@ -9,8 +9,6 @@ versioneer.versionfile_build = 'attic/_version.py' versioneer.tag_prefix = '' versioneer.parentdir_prefix = 'Attic-' # dirname like 'myproject-1.2.0' -platform = os.uname()[0] - min_python = (3, 2) if sys.version_info < min_python: print("Attic requires Python %d.%d or later" % min_python) @@ -89,11 +87,11 @@ ext_modules = [ Extension('attic.chunker', [chunker_source]), Extension('attic.hashindex', [hashindex_source]) ] -if platform == 'Linux': +if sys.platform.startswith('linux'): ext_modules.append(Extension('attic.platform_linux', [platform_linux_source], libraries=['acl'])) -elif platform == 'FreeBSD': +elif sys.platform.startswith('freebsd'): ext_modules.append(Extension('attic.platform_freebsd', [platform_freebsd_source])) -elif platform == 'Darwin': +elif sys.platform == 'darwin': ext_modules.append(Extension('attic.platform_darwin', [platform_darwin_source])) setup(