diff options
author | Lars Wirzenius <liw@liw.fi> | 2015-05-15 20:57:31 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2015-05-16 11:45:06 +0300 |
commit | e1da1ce26effdaabf4f8fee77ec8c65b5c926e6f (patch) | |
tree | 2675c6d53d95f3e7a4c8d64b9a7b372d630cbb31 | |
parent | 2bceeae41eef43b9142d7163a45229920be9067d (diff) | |
download | obnam-e1da1ce26effdaabf4f8fee77ec8c65b5c926e6f.tar.gz |
Add Green Albatross repository format
-rw-r--r-- | obnamlib/__init__.py | 12 | ||||
-rw-r--r-- | obnamlib/app.py | 3 | ||||
-rw-r--r-- | obnamlib/fmt_ga/__init__.py | 23 | ||||
-rw-r--r-- | obnamlib/fmt_ga/chunk_store.py | 84 | ||||
-rw-r--r-- | obnamlib/fmt_ga/client.py | 268 | ||||
-rw-r--r-- | obnamlib/fmt_ga/client_list.py | 141 | ||||
-rw-r--r-- | obnamlib/fmt_ga/format.py | 117 | ||||
-rw-r--r-- | obnamlib/fmt_ga/format_tests.py | 38 | ||||
-rw-r--r-- | obnamlib/fmt_ga/indexes.py | 120 | ||||
-rw-r--r-- | obnamlib/repo_factory.py | 1 | ||||
-rw-r--r-- | without-tests | 32 |
11 files changed, 825 insertions, 14 deletions
diff --git a/obnamlib/__init__.py b/obnamlib/__init__.py index f6bd428c..6418fe1c 100644 --- a/obnamlib/__init__.py +++ b/obnamlib/__init__.py @@ -180,6 +180,18 @@ from fmt_simple import RepositoryFormatSimple # +# Repository format green-albatross specific modules. +# + +from fmt_ga import ( + RepositoryFormatGA, + GAClientList, + GAClient, + GAChunkStore, + GAChunkIndexes) + + +# # Repository format 6 specific modules. # diff --git a/obnamlib/app.py b/obnamlib/app.py index 476259ae..170a1fa7 100644 --- a/obnamlib/app.py +++ b/obnamlib/app.py @@ -87,7 +87,7 @@ class App(cliapp.Application): self.settings.choice( ['repository-format'], - ['6', 'simple'], + ['6', 'simple', 'green-albatross'], 'what format to use for new repositories? one of "6", "simple"', metavar='FORMAT') @@ -264,6 +264,7 @@ class App(cliapp.Application): classes = { '6': obnamlib.RepositoryFormat6, 'simple': obnamlib.RepositoryFormatSimple, + 'green-albatross': obnamlib.RepositoryFormatGA, } return classes[self.settings['repository-format']] diff --git a/obnamlib/fmt_ga/__init__.py b/obnamlib/fmt_ga/__init__.py new file mode 100644 index 00000000..a32864a7 --- /dev/null +++ b/obnamlib/fmt_ga/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +from .client_list import GAClientList +from .chunk_store import GAChunkStore +from .indexes import GAChunkIndexes +from .client import GAClient +from .format import RepositoryFormatGA diff --git a/obnamlib/fmt_ga/chunk_store.py b/obnamlib/fmt_ga/chunk_store.py new file mode 100644 index 00000000..2a0562b9 --- /dev/null +++ b/obnamlib/fmt_ga/chunk_store.py @@ -0,0 +1,84 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import os +import random + +import obnamlib + + +class GAChunkStore(object): + + def __init__(self): + self._fs = None + self._dirname = 'chunk-store' + + def set_fs(self, fs): + self._fs = fs + + def put_chunk_content(self, content): + self._fs.create_and_init_toplevel(self._dirname) + while True: + chunk_id = self._random_chunk_id() + filename = self._chunk_filename(chunk_id) + try: + self._fs.write_file(filename, content) + except OSError, e: # pragma: no cover + if e.errno == errno.EEXIST: + continue + raise + else: + return chunk_id + + def get_chunk_content(self, chunk_id): + filename = self._chunk_filename(chunk_id) + if not self._fs.exists(filename): + raise obnamlib.RepositoryChunkDoesNotExist( + chunk_id=chunk_id, + filename=filename) + return self._fs.cat(filename) + + def has_chunk(self, chunk_id): + filename = self._chunk_filename(chunk_id) + return self._fs.exists(filename) + + def remove_chunk(self, chunk_id): + filename = self._chunk_filename(chunk_id) + if not self._fs.exists(filename): + raise obnamlib.RepositoryChunkDoesNotExist( + chunk_id=chunk_id, + filename=filename) + self._fs.remove(filename) + + def get_chunk_ids(self): + if not self._fs.exists(self._dirname): + return [] + basenames = self._fs.listdir(self._dirname) + return [ + self._parse_chunk_filename(x) + for x in basenames + if x.endswith('.chunk')] + + def _random_chunk_id(self): + return random.randint(0, obnamlib.MAX_ID) + + def _chunk_filename(self, chunk_id): + return os.path.join(self._dirname, '%d.chunk' % chunk_id) + + def _parse_chunk_filename(self, filename): + return int(filename[:-len('.chunk')]) diff --git a/obnamlib/fmt_ga/client.py b/obnamlib/fmt_ga/client.py new file mode 100644 index 00000000..8097a44f --- /dev/null +++ b/obnamlib/fmt_ga/client.py @@ -0,0 +1,268 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import copy +import os + +import obnamlib + + +class GAClient(object): + + def __init__(self, client_name): + self._fs = None + self.set_dirname(client_name) + self._client_name = client_name + self._current_time = None + self.clear() + + def set_current_time(self, current_time): + self._current_time = current_time + + def set_fs(self, fs): + self._fs = fs + + def set_dirname(self, dirname): + self._dirname = dirname + + def get_dirname(self): + return self._dirname + + def clear(self): + self._data = {} + self._data_is_loaded = False + + def commit(self): + self._load_data() + self._finish_current_generation_if_any() + self._save_data() + + def _finish_current_generation_if_any(self): + generations = self._data.get('generations', []) + if generations: + keys = generations[-1]['keys'] + key_name = obnamlib.repo_key_name(obnamlib.REPO_GENERATION_ENDED) + if keys[key_name] is None: + keys[key_name] = int(self._current_time()) + + def _save_data(self): + blob = obnamlib.serialise_object(self._data) + filename = self._get_filename() + self._fs.overwrite_file(filename, blob) + + def _get_filename(self): + return os.path.join(self.get_dirname(), 'data.bag') + + def get_client_generation_ids(self): + self._load_data() + generations = self._data.get('generations', []) + return [ + obnamlib.GenerationId(self._client_name, gen['id']) + for gen in generations] + + def _load_data(self): + if not self._data_is_loaded: + filename = self._get_filename() + if self._fs.exists(filename): + blob = self._fs.cat(filename) + self._data = obnamlib.deserialise_object(blob) + assert self._data is not None + else: + self._data = {} + self._data_is_loaded = True + + def create_generation(self): + self._load_data() + self._require_previous_generation_is_finished() + + generations = self._data.get('generations', []) + if generations: + previous = copy.deepcopy(generations[-1]) + else: + previous = { + 'keys': {}, + 'files': {}, + } + + new_generation = dict(previous) + new_generation['id'] = self._new_generation_number() + keys = new_generation['keys'] + keys[obnamlib.repo_key_name(obnamlib.REPO_GENERATION_STARTED)] = \ + int(self._current_time()) + keys[obnamlib.repo_key_name(obnamlib.REPO_GENERATION_ENDED)] = None + + self._data['generations'] = generations + [new_generation] + + return obnamlib.GenerationId(self._client_name, new_generation['id']) + + def _require_previous_generation_is_finished(self): + generations = self._data.get('generations', []) + if generations: + keys = generations[-1]['keys'] + key_name = obnamlib.repo_key_name(obnamlib.REPO_GENERATION_ENDED) + if keys[key_name] is None: + raise obnamlib.RepositoryClientGenerationUnfinished( + client_name=self._client_name) + + def _new_generation_number(self): + generations = self._data.get('generations', []) + ids = [int(gen['id']) for gen in generations] + if ids: + newest_id = ids[-1] + next_id = newest_id + 1 + else: + next_id = 1 + return str(next_id) + + def remove_generation(self, gen_number): + self._load_data() + generations = self._data.get('generations', []) + remaining = [] + removed = False + + for generation in generations: + if generation['id'] == gen_number: + removed = True + else: + remaining.append(generation) + + if not removed: + raise obnamlib.RepositoryGenerationDoesNotExist( + client_name=self._client_name, + gen_id=gen_number) + + self._data['generations'] = remaining + + def get_generation_key(self, gen_number, key): + self._load_data() + generation = self._lookup_generation_by_gen_number(gen_number) + key_name = obnamlib.repo_key_name(key) + if key in obnamlib.REPO_GENERATION_INTEGER_KEYS: + value = generation['keys'].get(key_name, None) + if value is None: + value = 0 + return int(value) + else: + return generation['keys'].get(key_name, '') + + def _lookup_generation_by_gen_number(self, gen_number): + if 'generations' in self._data: + generations = self._data['generations'] + for generation in generations: + if generation['id'] == gen_number: + return generation + raise obnamlib.RepositoryGenerationDoesNotExist( + gen_id=gen_number, client_name=self._client_name) + + def set_generation_key(self, gen_number, key, value): + self._load_data() + generation = self._lookup_generation_by_gen_number(gen_number) + generation['keys'][obnamlib.repo_key_name(key)] = value + + def file_exists(self, gen_number, filename): + self._load_data() + try: + generation = self._lookup_generation_by_gen_number(gen_number) + except obnamlib.RepositoryGenerationDoesNotExist: + return False + return filename in generation['files'] + + def add_file(self, gen_number, filename): + self._load_data() + generation = self._lookup_generation_by_gen_number(gen_number) + if filename not in generation['files']: + generation['files'][filename] = { + 'keys': {}, + 'chunks': [], + } + + def remove_file(self, gen_number, filename): + self._load_data() + generation = self._lookup_generation_by_gen_number(gen_number) + if filename in generation['files']: + del generation['files'][filename] + + def get_file_key(self, gen_number, filename, key): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + files = generation['files'] + key_name = obnamlib.repo_key_name(key) + + if key in obnamlib.REPO_FILE_INTEGER_KEYS: + default = 0 + else: + default = '' + + if key_name not in files[filename]['keys']: + return default + return files[filename]['keys'][key_name] or default + + def _require_file_exists(self, gen_number, filename): + generation = self._lookup_generation_by_gen_number(gen_number) + if filename not in generation['files']: + raise obnamlib.RepositoryFileDoesNotExistInGeneration( + client_name=self._client_name, + genspec=gen_number, + filename=filename) + + def set_file_key(self, gen_number, filename, key, value): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + files = generation['files'] + key_name = obnamlib.repo_key_name(key) + files[filename]['keys'][key_name] = value + + def get_file_chunk_ids(self, gen_number, filename): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + return generation['files'][filename]['chunks'] + + def append_file_chunk_id(self, gen_number, filename, chunk_id): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + generation['files'][filename]['chunks'].append(chunk_id) + + def clear_file_chunk_ids(self, gen_number, filename): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + generation['files'][filename]['chunks'] = [] + + def get_generation_chunk_ids(self, gen_number): + self._load_data() + chunk_ids = set() + generation = self._lookup_generation_by_gen_number(gen_number) + for filename in generation['files']: + file_chunk_ids = generation['files'][filename]['chunks'] + chunk_ids = chunk_ids.union(set(file_chunk_ids)) + return list(chunk_ids) + + def get_file_children(self, gen_number, filename): + self._load_data() + self._require_file_exists(gen_number, filename) + generation = self._lookup_generation_by_gen_number(gen_number) + return [ + x for x in generation['files'] + if self._is_direct_child_of(x, filename)] + + def _is_direct_child_of(self, child, parent): + return os.path.dirname(child) == parent and child != parent diff --git a/obnamlib/fmt_ga/client_list.py b/obnamlib/fmt_ga/client_list.py new file mode 100644 index 00000000..721be89d --- /dev/null +++ b/obnamlib/fmt_ga/client_list.py @@ -0,0 +1,141 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import os + +import obnamlib + + +class GAClientList(object): + + def __init__(self): + self._hooks = None + self._fs = None + self.set_dirname('client-list') + self.clear() + + def set_hooks(self, hooks): + self._hooks = hooks + + def set_fs(self, fs): + self._fs = fs + + def set_dirname(self, dirname): + self._dirname = dirname + + def get_dirname(self): + return self._dirname + + def clear(self): + self._data = None + self._data_is_loaded = False + self._added_clients = [] + + def commit(self): + self._load_data() + for client_name in self._added_clients: + self._hooks.call('repository-add-client', self, client_name) + self._save_data() + self._added_clients = [] + + def _save_data(self): + assert self._data is not None + blob = obnamlib.serialise_object(self._data) + filename = self._get_filename() + self._fs.overwrite_file(filename, blob) + + def _get_filename(self): + return os.path.join(self.get_dirname(), 'data.bag') + + def get_client_names(self): + self._load_data() + return self._data.get('clients', {}).keys() + + def _load_data(self): + if self._data_is_loaded: + assert self._data is not None + else: + assert self._data is None + if not self._data_is_loaded: + filename = self._get_filename() + if self._fs.exists(filename): + blob = self._fs.cat(filename) + self._data = obnamlib.deserialise_object(blob) + assert self._data is not None + else: + self._data = { + 'clients': {}, + } + self._data_is_loaded = True + + def add_client(self, client_name): + self._load_data() + self._require_client_does_not_exist(client_name) + + clients = self._data.get('clients', {}) + clients[client_name] = { + 'encryption-key': None, + } + self._data['clients'] = clients + + self._added_clients.append(client_name) + + def remove_client(self, client_name): + self._load_data() + self._require_client_exists(client_name) + + clients = self._data.get('clients', {}) + del clients[client_name] + self._data['clients'] = clients + + if client_name in self._added_clients: + self._added_clients.remove(client_name) + + def rename_client(self, old_client_name, new_client_name): + self._load_data() + self._require_client_exists(old_client_name) + self._require_client_does_not_exist(new_client_name) + + clients = self._data.get('clients', {}) + clients[new_client_name] = clients[old_client_name] + del clients[old_client_name] + self._data['clients'] = clients + + if old_client_name in self._added_clients: # pragma: no cover + self._added_clients.remove(old_client_name) + self._added_clients.append(new_client_name) + + def _require_client_exists(self, client_name): + if client_name not in self._data.get('clients', {}): + raise obnamlib.RepositoryClientDoesNotExist( + client_name=client_name) + + def _require_client_does_not_exist(self, client_name): + if client_name in self._data.get('clients', {}): + raise obnamlib.RepositoryClientAlreadyExists( + client_name=client_name) + + def get_client_encryption_key_id(self, client_name): + self._load_data() + self._require_client_exists(client_name) + return self._data['clients'][client_name]['encryption-key'] + + def set_client_encryption_key_id(self, client_name, encryption_key): + self._load_data() + self._require_client_exists(client_name) + self._data['clients'][client_name]['encryption-key'] = encryption_key diff --git a/obnamlib/fmt_ga/format.py b/obnamlib/fmt_ga/format.py new file mode 100644 index 00000000..088f563b --- /dev/null +++ b/obnamlib/fmt_ga/format.py @@ -0,0 +1,117 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import obnamlib + + +class RepositoryFormatGA(obnamlib.RepositoryDelegator): + + format = 'green-albatross' + + def __init__(self, **kwargs): + obnamlib.RepositoryDelegator.__init__(self, **kwargs) + self.set_client_list_object(obnamlib.GAClientList()) + self.set_chunk_store_object(obnamlib.GAChunkStore()) + self.set_chunk_indexes_object(obnamlib.GAChunkIndexes()) + self.set_client_factory(obnamlib.GAClient) + + def init_repo(self): + pass + + def close(self): + pass + + def get_fsck_work_items(self): + return [] + + def get_shared_directories(self): + return ['client-list', 'chunk-store', 'chunk-indexes'] + + # + # Per-client methods. + # + + def get_allowed_client_keys(self): + return [] + + def get_client_key(self, client_name, key): # pragma: no cover + raise obnamlib.RepositoryClientKeyNotAllowed( + format=self.format, + client_name=client_name, + key_name=obnamlib.repo_key_name(key)) + + def set_client_key(self, client_name, key, value): + raise obnamlib.RepositoryClientKeyNotAllowed( + format=self.format, + client_name=client_name, + key_name=obnamlib.repo_key_name(key)) + + def get_client_extra_data_directory(self, client_name): # pragma: no cover + if client_name not in self.get_client_names(): + raise obnamlib.RepositoryClientDoesNotExist( + client_name=client_name) + return client_name + + def get_allowed_generation_keys(self): + return [ + obnamlib.REPO_GENERATION_TEST_KEY, + obnamlib.REPO_GENERATION_STARTED, + obnamlib.REPO_GENERATION_ENDED, + obnamlib.REPO_GENERATION_IS_CHECKPOINT, + obnamlib.REPO_GENERATION_FILE_COUNT, + obnamlib.REPO_GENERATION_TOTAL_DATA, + ] + + def get_allowed_file_keys(self): + return [obnamlib.REPO_FILE_TEST_KEY, + obnamlib.REPO_FILE_MODE, + obnamlib.REPO_FILE_MTIME_SEC, + obnamlib.REPO_FILE_MTIME_NSEC, + obnamlib.REPO_FILE_ATIME_SEC, + obnamlib.REPO_FILE_ATIME_NSEC, + obnamlib.REPO_FILE_NLINK, + obnamlib.REPO_FILE_SIZE, + obnamlib.REPO_FILE_UID, + obnamlib.REPO_FILE_USERNAME, + obnamlib.REPO_FILE_GID, + obnamlib.REPO_FILE_GROUPNAME, + obnamlib.REPO_FILE_SYMLINK_TARGET, + obnamlib.REPO_FILE_XATTR_BLOB, + obnamlib.REPO_FILE_BLOCKS, + obnamlib.REPO_FILE_DEV, + obnamlib.REPO_FILE_INO, + obnamlib.REPO_FILE_MD5] + + def interpret_generation_spec(self, client_name, genspec): + ids = self.get_client_generation_ids(client_name) + if not ids: + raise obnamlib.RepositoryClientHasNoGenerations( + client_name=client_name) + + if genspec == 'latest': + return ids[-1] + + for gen_id in ids: + if self.make_generation_spec(gen_id) == genspec: + return gen_id + + raise obnamlib.RepositoryGenerationDoesNotExist( + client_name=client_name, gen_id=genspec) + + def make_generation_spec(self, generation_id): + return generation_id.gen_number diff --git a/obnamlib/fmt_ga/format_tests.py b/obnamlib/fmt_ga/format_tests.py new file mode 100644 index 00000000..b387ef50 --- /dev/null +++ b/obnamlib/fmt_ga/format_tests.py @@ -0,0 +1,38 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import tempfile +import time + +import obnamlib + + +class RepositoryFormatGATests(obnamlib.RepositoryInterfaceTests): + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + + # FIXME: The following must be format 6, for now. + obnamlib.RepositoryFormat6.setup_hooks(self.hooks) + + self.repo = obnamlib.RepositoryFormatGA( + hooks=self.hooks, + current_time=time.time) + self.repo.set_fs(fs) diff --git a/obnamlib/fmt_ga/indexes.py b/obnamlib/fmt_ga/indexes.py new file mode 100644 index 00000000..86c7648f --- /dev/null +++ b/obnamlib/fmt_ga/indexes.py @@ -0,0 +1,120 @@ +# Copyright 2015 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import hashlib +import os + +import obnamlib + + +class GAChunkIndexes(object): + + def __init__(self): + self._fs = None + self.set_dirname('chunk-indexes') + self.clear() + + def set_fs(self, fs): + self._fs = fs + + def set_dirname(self, dirname): + self._dirname = dirname + + def get_dirname(self): + return self._dirname + + def clear(self): + self._data = {} + self._data_is_loaded = False + + def commit(self): + self._load_data() + self._save_data() + + def _save_data(self): + blob = obnamlib.serialise_object(self._data) + filename = self._get_filename() + self._fs.overwrite_file(filename, blob) + + def _get_filename(self): + return os.path.join(self.get_dirname(), 'data.bag') + + def prepare_chunk_for_indexes(self, chunk_content): + return hashlib.sha512(chunk_content).hexdigest() + + def put_chunk_into_indexes(self, chunk_id, token, client_id): + self._load_data() + self._prepare_data() + self._data['index'].append({ + 'chunk-id': chunk_id, + 'sha512': token, + 'client-id': client_id, + }) + + def _load_data(self): + if not self._data_is_loaded: + filename = self._get_filename() + if self._fs.exists(filename): + blob = self._fs.cat(filename) + self._data = obnamlib.deserialise_object(blob) + assert self._data is not None + else: + self._data = {} + self._data_is_loaded = True + + def _prepare_data(self): + if 'index' not in self._data: + self._data['index'] = [] + + def find_chunk_ids_by_content(self, chunk_content): + self._load_data() + if 'index' in self._data: + token = self.prepare_chunk_for_indexes(chunk_content) + result = [ + record['chunk-id'] + for record in self._data['index'] + if record['sha512'] == token] + else: + result = [] + + if not result: + raise obnamlib.RepositoryChunkContentNotInIndexes() + return result + + def remove_chunk_from_indexes(self, chunk_id, client_id): + self._load_data() + self._prepare_data() + + self._data['index'] = self._filter_out( + self._data['index'], + lambda x: + x['chunk-id'] == chunk_id and x['client-id'] == client_id) + + def _filter_out(self, records, pred): + return [record for record in records if not pred(record)] + + def remove_chunk_from_indexes_for_all_clients(self, chunk_id): + self._load_data() + self._prepare_data() + + self._data['index'] = self._filter_out( + self._data['index'], + lambda x: x['chunk-id'] == chunk_id) + + def validate_chunk_content(self, chunk_id): + return None diff --git a/obnamlib/repo_factory.py b/obnamlib/repo_factory.py index 634895ef..6da6a05d 100644 --- a/obnamlib/repo_factory.py +++ b/obnamlib/repo_factory.py @@ -50,6 +50,7 @@ class RepositoryFactory(object): self._implementations = [ obnamlib.RepositoryFormat6, obnamlib.RepositoryFormatSimple, + obnamlib.RepositoryFormatGA, ] def get_implementation_classes(self): diff --git a/without-tests b/without-tests index 28ab10de..5315d076 100644 --- a/without-tests +++ b/without-tests @@ -1,35 +1,41 @@ setup.py -obnamlib/__init__.py + obnamlib/app.py obnamlib/delegator.py -obnamlib/humanise.py -obnamlib/fsck_work_item.py -obnamlib/repo_interface.py -obnamlib/vfs.py -obnamlib/repo_fs.py obnamlib/fmt_6/__init__.py obnamlib/fmt_6/repo_tree.py -obnamlib/plugins/__init__.py +obnamlib/fmt_ga/chunk_store.py +obnamlib/fmt_ga/client_list.py +obnamlib/fmt_ga/client.py +obnamlib/fmt_ga/indexes.py +obnamlib/fmt_ga/__init__.py +obnamlib/fmt_simple/__init__.py +obnamlib/fsck_work_item.py +obnamlib/humanise.py +obnamlib/__init__.py obnamlib/plugins/backup_plugin.py obnamlib/plugins/compression_plugin.py +obnamlib/plugins/dump_repo_plugin.py obnamlib/plugins/encryption_plugin.py +obnamlib/plugins/exclude_caches_plugin.py +obnamlib/plugins/exclude_pathnames_plugin.py obnamlib/plugins/force_lock_plugin.py obnamlib/plugins/forget_plugin.py obnamlib/plugins/fsck_plugin.py obnamlib/plugins/fuse_plugin.py +obnamlib/plugins/__init__.py obnamlib/plugins/list_formats_plugin.py +obnamlib/plugins/one_file_system_plugin.py obnamlib/plugins/restore_plugin.py obnamlib/plugins/sftp_plugin.py obnamlib/plugins/show_plugin.py obnamlib/plugins/verify_plugin.py obnamlib/plugins/vfs_local_plugin.py +obnamlib/repo_fs.py +obnamlib/repo_interface.py +obnamlib/vfs.py +test-plugins/aaa_hello_plugin.py test-plugins/hello_plugin.py test-plugins/oldhello_plugin.py -test-plugins/aaa_hello_plugin.py test-plugins/wrongversion_plugin.py -obnamlib/plugins/dump_repo_plugin.py -obnamlib/fmt_simple/__init__.py -obnamlib/plugins/exclude_pathnames_plugin.py -obnamlib/plugins/exclude_caches_plugin.py -obnamlib/plugins/one_file_system_plugin.py |