|
@@ -1,6 +1,6 @@
|
|
|
import enum
|
|
|
import re
|
|
|
-from collections import abc, namedtuple
|
|
|
+from collections import namedtuple
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
from operator import attrgetter
|
|
|
from collections.abc import Sequence
|
|
@@ -68,11 +68,13 @@ def filter_archives_by_date(archives, older=None, newer=None, oldest=None, newes
|
|
|
return archives
|
|
|
|
|
|
|
|
|
-class Archives(abc.MutableMapping):
|
|
|
+class Archives:
|
|
|
"""
|
|
|
- Nice wrapper around the archives dict, making sure only valid types/values get in
|
|
|
- and we can deal with str keys (and it internally encodes to byte keys) and either
|
|
|
- str timestamps or datetime timestamps.
|
|
|
+ Manage the list of archives.
|
|
|
+
|
|
|
+ We still need to support the borg 1.x manifest-with-list-of-archives,
|
|
|
+ so borg transfer can work.
|
|
|
+ borg2 has separate items archives/* in the borgstore.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, repository):
|
|
@@ -88,20 +90,20 @@ class Archives(abc.MutableMapping):
|
|
|
def prepare(self, manifest, m):
|
|
|
self.manifest = manifest
|
|
|
if not self.legacy:
|
|
|
- self.load()
|
|
|
+ self._load()
|
|
|
else:
|
|
|
- self.set_raw_dict(m.archives)
|
|
|
+ self._set_raw_dict(m.archives)
|
|
|
|
|
|
def finish(self, manifest):
|
|
|
self.manifest = manifest # note: .prepare is not always called
|
|
|
if not self.legacy:
|
|
|
- self.save()
|
|
|
+ self._save()
|
|
|
manifest_archives = {}
|
|
|
else:
|
|
|
- manifest_archives = StableDict(self.get_raw_dict())
|
|
|
+ manifest_archives = StableDict(self._get_raw_dict())
|
|
|
return manifest_archives
|
|
|
|
|
|
- def load(self):
|
|
|
+ def _load(self):
|
|
|
# load archives list from store
|
|
|
from .helpers import msgpack
|
|
|
|
|
@@ -116,12 +118,12 @@ class Archives(abc.MutableMapping):
|
|
|
_, value = self.manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST)
|
|
|
archive = msgpack.unpackb(value)
|
|
|
archives[archive["name"]] = dict(id=archive["id"], time=archive["time"])
|
|
|
- self.set_raw_dict(archives)
|
|
|
+ self._set_raw_dict(archives)
|
|
|
|
|
|
- def save(self):
|
|
|
+ def _save(self):
|
|
|
# save archives list to store
|
|
|
valid_keys = set()
|
|
|
- for name, info in self.get_raw_dict().items():
|
|
|
+ for name, info in self._get_raw_dict().items():
|
|
|
archive = dict(name=name, id=info["id"], time=info["time"])
|
|
|
value = self.manifest.key.pack_metadata(archive) #
|
|
|
id = self.manifest.repo_objs.id_hash(value)
|
|
@@ -141,33 +143,44 @@ class Archives(abc.MutableMapping):
|
|
|
if info.name not in valid_keys:
|
|
|
self.repository.store_delete(f"archives/{info.name}")
|
|
|
|
|
|
- def __len__(self):
|
|
|
+ def count(self):
|
|
|
+ # return the count of archives in the repo
|
|
|
return len(self._archives)
|
|
|
|
|
|
- def __iter__(self):
|
|
|
- return iter(self._archives)
|
|
|
+ def exists(self, name):
|
|
|
+ # check if an archive with this name exists
|
|
|
+ assert isinstance(name, str)
|
|
|
+ return name in self._archives
|
|
|
+
|
|
|
+ def names(self):
|
|
|
+ # yield the names of all archives
|
|
|
+ yield from self._archives
|
|
|
|
|
|
- def __getitem__(self, name):
|
|
|
+ def get(self, name, raw=False):
|
|
|
assert isinstance(name, str)
|
|
|
values = self._archives.get(name)
|
|
|
if values is None:
|
|
|
raise KeyError
|
|
|
- ts = parse_timestamp(values["time"])
|
|
|
- return ArchiveInfo(name=name, id=values["id"], ts=ts)
|
|
|
+ if not raw:
|
|
|
+ ts = parse_timestamp(values["time"])
|
|
|
+ return ArchiveInfo(name=name, id=values["id"], ts=ts)
|
|
|
+ else:
|
|
|
+ return dict(name=name, id=values["id"], time=values["time"])
|
|
|
|
|
|
- def __setitem__(self, name, info):
|
|
|
+ def create(self, name, id, ts, *, overwrite=False):
|
|
|
assert isinstance(name, str)
|
|
|
- assert isinstance(info, tuple)
|
|
|
- id, ts = info
|
|
|
assert isinstance(id, bytes)
|
|
|
if isinstance(ts, datetime):
|
|
|
ts = ts.isoformat(timespec="microseconds")
|
|
|
assert isinstance(ts, str)
|
|
|
+ if name in self._archives and not overwrite:
|
|
|
+ raise KeyError("archive already exists")
|
|
|
self._archives[name] = {"id": id, "time": ts}
|
|
|
|
|
|
- def __delitem__(self, name):
|
|
|
+ def delete(self, name):
|
|
|
+ # delete an archive
|
|
|
assert isinstance(name, str)
|
|
|
- del self._archives[name]
|
|
|
+ self._archives.pop(name)
|
|
|
|
|
|
def list(
|
|
|
self,
|
|
@@ -203,7 +216,7 @@ class Archives(abc.MutableMapping):
|
|
|
if isinstance(sort_by, (str, bytes)):
|
|
|
raise TypeError("sort_by must be a sequence of str")
|
|
|
|
|
|
- archives = self.values()
|
|
|
+ archives = [self.get(name) for name in self.names()]
|
|
|
regex = get_regex_from_pattern(match or "re:.*")
|
|
|
regex = re.compile(regex + match_end)
|
|
|
archives = [x for x in archives if regex.match(x.name) is not None]
|
|
@@ -240,14 +253,14 @@ class Archives(abc.MutableMapping):
|
|
|
newest=getattr(args, "newest", None),
|
|
|
)
|
|
|
|
|
|
- def set_raw_dict(self, d):
|
|
|
+ def _set_raw_dict(self, d):
|
|
|
"""set the dict we get from the msgpack unpacker"""
|
|
|
for k, v in d.items():
|
|
|
assert isinstance(k, str)
|
|
|
assert isinstance(v, dict) and "id" in v and "time" in v
|
|
|
self._archives[k] = v
|
|
|
|
|
|
- def get_raw_dict(self):
|
|
|
+ def _get_raw_dict(self):
|
|
|
"""get the dict we can give to the msgpack packer"""
|
|
|
return self._archives
|
|
|
|
|
@@ -362,8 +375,8 @@ class Manifest:
|
|
|
max_ts = max(incremented_ts, now_ts)
|
|
|
self.timestamp = max_ts.isoformat(timespec="microseconds")
|
|
|
# include checks for limits as enforced by limited unpacker (used by load())
|
|
|
- assert len(self.archives) <= MAX_ARCHIVES
|
|
|
- assert all(len(name) <= 255 for name in self.archives)
|
|
|
+ assert self.archives.count() <= MAX_ARCHIVES
|
|
|
+ assert all(len(name) <= 255 for name in self.archives.names())
|
|
|
assert len(self.item_keys) <= 100
|
|
|
self.config["item_keys"] = tuple(sorted(self.item_keys))
|
|
|
manifest_archives = self.archives.finish(self)
|