summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2015-05-15 20:57:31 +0300
committerLars Wirzenius <liw@liw.fi>2015-05-16 11:45:06 +0300
commite1da1ce26effdaabf4f8fee77ec8c65b5c926e6f (patch)
tree2675c6d53d95f3e7a4c8d64b9a7b372d630cbb31
parent2bceeae41eef43b9142d7163a45229920be9067d (diff)
downloadobnam-e1da1ce26effdaabf4f8fee77ec8c65b5c926e6f.tar.gz
Add Green Albatross repository format
-rw-r--r--obnamlib/__init__.py12
-rw-r--r--obnamlib/app.py3
-rw-r--r--obnamlib/fmt_ga/__init__.py23
-rw-r--r--obnamlib/fmt_ga/chunk_store.py84
-rw-r--r--obnamlib/fmt_ga/client.py268
-rw-r--r--obnamlib/fmt_ga/client_list.py141
-rw-r--r--obnamlib/fmt_ga/format.py117
-rw-r--r--obnamlib/fmt_ga/format_tests.py38
-rw-r--r--obnamlib/fmt_ga/indexes.py120
-rw-r--r--obnamlib/repo_factory.py1
-rw-r--r--without-tests32
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