summaryrefslogtreecommitdiff
path: root/obnam
diff options
context:
space:
mode:
authorLars Wirzenius <liw@gytha>2008-04-20 15:59:58 +0300
committerLars Wirzenius <liw@gytha>2008-04-20 15:59:58 +0300
commit5f90ca5db557bcf5a6ae5387f2d527f7a4207411 (patch)
tree222c9bd9eee35086286d58958b9c0fdeae72de1c /obnam
parent323ee15aaa4144cb3a17257ff4707655000f5705 (diff)
parentb2b4b68d111ee60a34e003561c765beafce3b1d1 (diff)
downloadobnam-5f90ca5db557bcf5a6ae5387f2d527f7a4207411.tar.gz
Merged branch to create a Store abstraction.
Diffstat (limited to 'obnam')
-rw-r--r--obnam/__init__.py1
-rw-r--r--obnam/app.py100
-rw-r--r--obnam/appTests.py86
-rw-r--r--obnam/oper_backup.py4
-rw-r--r--obnam/oper_forget.py4
-rw-r--r--obnam/oper_restore.py4
-rw-r--r--obnam/oper_show_generations.py2
-rw-r--r--obnam/store.py228
-rw-r--r--obnam/storeTests.py301
9 files changed, 547 insertions, 183 deletions
diff --git a/obnam/__init__.py b/obnam/__init__.py
index 50a8ce4f..05d6936c 100644
--- a/obnam/__init__.py
+++ b/obnam/__init__.py
@@ -45,6 +45,7 @@ import walk
from app import Application
from oper import Operation, OperationFactory
+from store import Store
from oper_backup import Backup
from oper_forget import Forget
diff --git a/obnam/app.py b/obnam/app.py
index 834da3c1..3b9a3f95 100644
--- a/obnam/app.py
+++ b/obnam/app.py
@@ -41,7 +41,7 @@ class Application:
self._exclusion_strings = []
self._exclusion_regexps = []
self._filelist = None
- self._host = None
+ self._store = obnam.Store(self._context)
# When we traverse the file system tree while making a backup,
# we process children before the parent. This is necessary for
@@ -60,32 +60,14 @@ class Application:
"""Get the context for the backup application."""
return self._context
- def get_host(self):
- """Return currently active host object, or None if none is active."""
- return self._host
+ def get_store(self):
+ """Get the Store for the backup application."""
+ return self._store
def load_host(self):
"""Load the host block into memory."""
- if not self._host:
- host_block = obnam.io.get_host_block(self._context)
- if host_block:
- self._host = obnam.obj.create_host_from_block(host_block)
- else:
- id = self._context.config.get("backup", "host-id")
- self._host = obnam.obj.HostBlockObject(host_id=id)
- return self._host
-
- def load_maps(self):
- """Load non-content map blocks."""
- ids = self._host.get_map_block_ids()
- logging.info("Decoding %d mapping blocks" % len(ids))
- obnam.io.load_maps(self._context, self._context.map, ids)
-
- def load_content_maps(self):
- """Load content map blocks."""
- ids = self._host.get_contmap_block_ids()
- logging.info("Decoding %d content mapping blocks" % len(ids))
- obnam.io.load_maps(self._context, self._context.contmap, ids)
+ self.get_store().fetch_host_block()
+ return self.get_store().get_host_block()
def get_exclusion_regexps(self):
"""Return list of regexp to exclude things from backup."""
@@ -218,13 +200,6 @@ class Application:
return None
- def enqueue(self, objs):
- """Push objects to the object queue."""
- for obj in objs:
- obnam.io.enqueue_object(self._context, self._context.oq,
- self._context.map, obj.get_id(),
- obj.encode(), True)
-
def compute_signature(self, filename):
"""Compute rsync signature for a filename.
@@ -236,7 +211,7 @@ class Application:
sigdata = obnam.rsync.compute_signature(self._context, filename)
id = obnam.obj.object_id_new()
sig = obnam.obj.SignatureObject(id=id, sigdata=sigdata)
- self.enqueue([sig])
+ self.get_store().queue_object(sig)
return sig
def add_to_filegroup(self, fg, filename):
@@ -270,7 +245,7 @@ class Application:
list.append(obnam.obj.FileGroupObject(id=id))
self.add_to_filegroup(list[-1], filename)
- self.enqueue(list)
+ self.get_store().queue_objects(list)
return list
def _make_absolute(self, basename, relatives):
@@ -299,7 +274,7 @@ class Application:
filegrouprefs=filegrouprefs)
- self.enqueue([dir])
+ self.get_store().queue_object(dir)
return dir
def backup_one_root(self, root):
@@ -353,60 +328,5 @@ class Application:
gen = obnam.obj.GenerationObject(id=obnam.obj.object_id_new(),
dirrefs=dirrefs, start=start,
end=end)
- self.enqueue([gen])
+ self.get_store().queue_object(gen)
return gen
-
- def _update_map_helper(self, map):
- """Create new mapping blocks of a given kind, and upload them.
-
- Return list of block ids for the new blocks.
-
- """
-
- if obnam.map.get_new(map):
- id = self._context.be.generate_block_id()
- logging.debug("Creating mapping block %s" % id)
- block = obnam.map.encode_new_to_block(map, id)
- self._context.be.upload_block(id, block, True)
- return [id]
- else:
- logging.debug("No new mappings, no new mapping block")
- return []
-
- def update_maps(self):
- """Create new object mapping blocks and upload them."""
- logging.debug("Creating new mapping block for normal mappings")
- return self._update_map_helper(self._context.map)
-
- def update_content_maps(self):
- """Create new content object mapping blocks and upload them."""
- logging.debug("Creating new mapping block for content mappings")
- return self._update_map_helper(self._context.contmap)
-
- def finish(self, new_gens):
- """Finish a backup operation by updating maps and uploading host block.
-
- This also removes the host block that has been load. In other
- words, if you want to continue using the application for anything
- that requires the host block, you have to call load_host again.
-
- """
-
- obnam.io.flush_all_object_queues(self._context)
-
- logging.info("Creating new mapping blocks")
- host = self.get_host()
- map_ids = host.get_map_block_ids() + self.update_maps()
- contmap_ids = (host.get_contmap_block_ids() +
- self.update_content_maps())
-
- logging.info("Creating new host block")
- gen_ids = (host.get_generation_ids() +
- [gen.get_id() for gen in new_gens])
- host2 = obnam.obj.HostBlockObject(host_id=host.get_id(),
- gen_ids=gen_ids,
- map_block_ids=map_ids,
- contmap_block_ids=contmap_ids)
- obnam.io.upload_host_block(self._context, host2.encode())
-
- self._host = None
diff --git a/obnam/appTests.py b/obnam/appTests.py
index 72a54b7b..7cee6843 100644
--- a/obnam/appTests.py
+++ b/obnam/appTests.py
@@ -34,9 +34,6 @@ class ApplicationTests(unittest.TestCase):
context = obnam.context.Context()
self.app = obnam.Application(context)
- def testHasNoHostBlockInitially(self):
- self.failUnlessEqual(self.app.get_host(), None)
-
def testReturnsEmptyExclusionListInitially(self):
self.failUnlessEqual(self.app.get_exclusion_regexps(), [])
@@ -544,86 +541,3 @@ class ApplicationBackupTests(unittest.TestCase):
gen = self.app.backup([self.abs("pink"), self.abs("pretty")])
self.failIfEqual(gen.get_start_time(), None)
self.failIfEqual(gen.get_end_time(), None)
-
-
-class ApplicationMapTests(unittest.TestCase):
-
- def setUp(self):
- # First, set up two mappings.
-
- context = obnam.context.Context()
- context.cache = obnam.cache.Cache(context.config)
- context.be = obnam.backend.init(context.config, context.cache)
-
- obnam.map.add(context.map, "pink", "pretty")
- obnam.map.add(context.contmap, "black", "beautiful")
-
- map_id = context.be.generate_block_id()
- map_block = obnam.map.encode_new_to_block(context.map, map_id)
- context.be.upload_block(map_id, map_block, True)
-
- contmap_id = context.be.generate_block_id()
- contmap_block = obnam.map.encode_new_to_block(context.contmap,
- contmap_id)
- context.be.upload_block(contmap_id, contmap_block, True)
-
- host_id = context.config.get("backup", "host-id")
- host = obnam.obj.HostBlockObject(host_id=host_id,
- map_block_ids=[map_id],
- contmap_block_ids=[contmap_id])
- obnam.io.upload_host_block(context, host.encode())
-
- # Then set up the real context and app.
-
- self.context = obnam.context.Context()
- self.context.cache = obnam.cache.Cache(self.context.config)
- self.context.be = obnam.backend.init(self.context.config,
- self.context.cache)
- self.app = obnam.Application(self.context)
- self.app.load_host()
-
- def testHasNoMapsLoadedByDefault(self):
- self.failUnlessEqual(obnam.map.count(self.context.map), 0)
-
- def testHasNoContentMapsLoadedByDefault(self):
- self.failUnlessEqual(obnam.map.count(self.context.contmap), 0)
-
- def testLoadsMapsWhenRequested(self):
- self.app.load_maps()
- self.failUnlessEqual(obnam.map.count(self.context.map), 1)
-
- def testLoadsContentMapsWhenRequested(self):
- self.app.load_content_maps()
- self.failUnlessEqual(obnam.map.count(self.context.contmap), 1)
-
- def testAddsNoNewMapsWhenNothingHasChanged(self):
- self.app.update_maps()
- self.failUnlessEqual(obnam.map.count(self.context.map), 0)
-
- def testAddsANewMapsWhenSomethingHasChanged(self):
- obnam.map.add(self.context.map, "pink", "pretty")
- self.app.update_maps()
- self.failUnlessEqual(obnam.map.count(self.context.map), 1)
-
- def testAddsNoNewContentMapsWhenNothingHasChanged(self):
- self.app.update_content_maps()
- self.failUnlessEqual(obnam.map.count(self.context.contmap), 0)
-
- def testAddsANewContentMapsWhenSomethingHasChanged(self):
- obnam.map.add(self.context.contmap, "pink", "pretty")
- self.app.update_content_maps()
- self.failUnlessEqual(obnam.map.count(self.context.contmap), 1)
-
-
-class ApplicationFinishTests(unittest.TestCase):
-
- def testRemovesHostObject(self):
- self.context = obnam.context.Context()
- self.context.cache = obnam.cache.Cache(self.context.config)
- self.context.be = obnam.backend.init(self.context.config,
- self.context.cache)
- self.app = obnam.Application(self.context)
- self.app.load_host()
-
- self.app.finish([])
- self.failUnlessEqual(self.app.get_host(), None)
diff --git a/obnam/oper_backup.py b/obnam/oper_backup.py
index 7265c6ef..dd11690f 100644
--- a/obnam/oper_backup.py
+++ b/obnam/oper_backup.py
@@ -34,12 +34,12 @@ class Backup(obnam.Operation):
logging.info("Getting and decoding host block")
app = self.get_application()
host = app.load_host()
- app.load_maps()
+ app.get_store().load_maps()
# We don't need to load in file data, therefore we don't load
# the content map blocks.
gen = app.backup(roots)
- app.finish([gen])
+ app.get_store().commit_host_block([gen])
logging.info("Backup done")
diff --git a/obnam/oper_forget.py b/obnam/oper_forget.py
index b901da5c..40d98f41 100644
--- a/obnam/oper_forget.py
+++ b/obnam/oper_forget.py
@@ -40,8 +40,8 @@ class Forget(obnam.Operation):
map_block_ids = host.get_map_block_ids()
contmap_block_ids = host.get_contmap_block_ids()
- app.load_maps()
- app.load_content_maps()
+ app.get_store().load_maps()
+ app.get_store().load_content_maps()
logging.debug("forget: Forgetting each id")
for id in forgettable_ids:
diff --git a/obnam/oper_restore.py b/obnam/oper_restore.py
index 31129791..a4a161e1 100644
--- a/obnam/oper_restore.py
+++ b/obnam/oper_restore.py
@@ -197,8 +197,8 @@ class Restore(obnam.Operation):
context = app.get_context()
host = app.load_host()
- app.load_maps()
- app.load_content_maps()
+ app.get_store().load_maps()
+ app.get_store().load_content_maps()
logging.debug("Getting generation object")
gen = obnam.io.get_object(context, gen_id)
diff --git a/obnam/oper_show_generations.py b/obnam/oper_show_generations.py
index db20eb2f..e22d9510 100644
--- a/obnam/oper_show_generations.py
+++ b/obnam/oper_show_generations.py
@@ -90,7 +90,7 @@ class ShowGenerations(obnam.Operation):
app = self.get_application()
context = app.get_context()
host = app.load_host()
- app.load_maps()
+ app.get_store().load_maps()
for gen_id in gen_ids:
gen = obnam.io.get_object(context, gen_id)
diff --git a/obnam/store.py b/obnam/store.py
new file mode 100644
index 00000000..905e66f8
--- /dev/null
+++ b/obnam/store.py
@@ -0,0 +1,228 @@
+# Copyright (C) 2008 Lars Wirzenius <liw@iki.fi>
+#
+# 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 2 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+"""Abstraction for storing backup data, for Obnam."""
+
+
+import logging
+import os
+
+import obnam
+
+
+class ObjectNotFoundInStore(obnam.exception.ObnamException):
+
+ def __init__(self, id):
+ self._msg = "Object %s not found in store" % id
+
+
+class Store:
+
+ def __init__(self, context):
+ self._context = context
+ self._host = None
+
+ def get_host_block(self):
+ """Return current host block, or None if one is not known.
+
+ You must call fetch_host_block to fetch the host block first.
+
+ """
+
+ return self._host
+
+ def fetch_host_block(self):
+ """Fetch host block from store, if one exists.
+
+ If a host block does not exist, it is not an error. A new
+ host block is then created.
+
+ """
+
+ if not self._host:
+ host_block = obnam.io.get_host_block(self._context)
+ if host_block:
+ self._host = obnam.obj.create_host_from_block(host_block)
+ else:
+ id = self._context.config.get("backup", "host-id")
+ self._host = obnam.obj.HostBlockObject(host_id=id)
+ return self._host
+
+
+ def load_maps(self):
+ """Load non-content map blocks."""
+ ids = self._host.get_map_block_ids()
+ logging.info("Decoding %d mapping blocks" % len(ids))
+ obnam.io.load_maps(self._context, self._context.map, ids)
+
+ def load_content_maps(self):
+ """Load content map blocks."""
+ ids = self._host.get_contmap_block_ids()
+ logging.info("Decoding %d content mapping blocks" % len(ids))
+ obnam.io.load_maps(self._context, self._context.contmap, ids)
+
+ def _update_map_helper(self, map):
+ """Create new mapping blocks of a given kind, and upload them.
+
+ Return list of block ids for the new blocks.
+
+ """
+
+ if obnam.map.get_new(map):
+ id = self._context.be.generate_block_id()
+ logging.debug("Creating mapping block %s" % id)
+ block = obnam.map.encode_new_to_block(map, id)
+ self._context.be.upload_block(id, block, True)
+ return [id]
+ else:
+ logging.debug("No new mappings, no new mapping block")
+ return []
+
+ def update_maps(self):
+ """Create new object mapping blocks and upload them."""
+ logging.debug("Creating new mapping block for normal mappings")
+ return self._update_map_helper(self._context.map)
+
+ def update_content_maps(self):
+ """Create new content object mapping blocks and upload them."""
+ logging.debug("Creating new mapping block for content mappings")
+ return self._update_map_helper(self._context.contmap)
+
+ def commit_host_block(self, new_generations):
+ """Commit the current host block to the store.
+
+ If no host block exists, create one. If one already exists,
+ update it with new info.
+
+ NOTE that after this operation the host block has changed,
+ and you need to call get_host_block again.
+
+ """
+
+ obnam.io.flush_all_object_queues(self._context)
+
+ logging.info("Creating new mapping blocks")
+ host = self.get_host_block()
+ map_ids = host.get_map_block_ids() + self.update_maps()
+ contmap_ids = (host.get_contmap_block_ids() +
+ self.update_content_maps())
+
+ logging.info("Creating new host block")
+ gen_ids = (host.get_generation_ids() +
+ [gen.get_id() for gen in new_generations])
+ host2 = obnam.obj.HostBlockObject(host_id=host.get_id(),
+ gen_ids=gen_ids,
+ map_block_ids=map_ids,
+ contmap_block_ids=contmap_ids)
+ obnam.io.upload_host_block(self._context, host2.encode())
+
+ self._host = host2
+
+ def queue_object(self, object):
+ """Queue an object for upload to the store.
+
+ It won't necessarily be committed (i.e., uploaded, etc) until
+ you call commit_host_block. Until it is committed, you may not
+ call get_object on it.
+
+ """
+
+ obnam.io.enqueue_object(self._context, self._context.oq,
+ self._context.map, object.get_id(),
+ object.encode(), True)
+
+ def queue_objects(self, objects):
+ """Queue a list of objects for upload to the store.
+
+ See queue_object for information about what queuing means.
+
+ """
+
+ for object in objects:
+ self.queue_object(object)
+
+ def get_object(self, id):
+ """Get an object from the store.
+
+ If the object cannot be found, raise an exception.
+
+ """
+
+ object = obnam.io.get_object(self._context, id)
+ if object:
+ return object
+ raise ObjectNotFoundInStore(id)
+
+ def parse_pathname(self, pathname):
+ """Return list of components in pathname."""
+
+ list = []
+ while pathname:
+ dirname = os.path.dirname(pathname)
+ basename = os.path.basename(pathname)
+ if basename:
+ list.insert(0, basename)
+ elif dirname == os.sep:
+ list.insert(0, "/")
+ dirname = ""
+ pathname = dirname
+
+ return list
+
+ def _lookup_dir_from_refs(self, dirrefs, parts):
+ for ref in dirrefs:
+ dir = self.get_object(ref)
+ if dir.get_name() == parts[0]:
+ parts = parts[1:]
+ if parts:
+ dirrefs = dir.get_dirrefs()
+ return self._lookup_dir_from_refs(dirrefs, parts)
+ else:
+ return dir
+ return None
+
+ def lookup_dir(self, generation, pathname):
+ """Return a DirObject that corresponds to pathname in a generation.
+
+ Look up the directory in the generation. If it does not exist,
+ return None.
+
+ """
+
+ parts = self.parse_pathname(pathname)
+ return self._lookup_dir_from_refs(generation.get_dirrefs(), parts)
+
+ def lookup_file(self, generation, pathname):
+ """Find a non-directory thingy in a generation.
+
+ Return a FILE component that corresponds to the filesystem entity
+ in question. If not found, return None.
+
+ """
+
+ dirname = os.path.dirname(pathname)
+ if dirname:
+ dir = self.lookup_dir(generation, dirname)
+ if dir:
+ basename = os.path.basename(pathname)
+ for id in dir.get_filegrouprefs():
+ fg = self.get_object(id)
+ file = fg.get_file(basename)
+ if file:
+ return file
+
+ return None
diff --git a/obnam/storeTests.py b/obnam/storeTests.py
new file mode 100644
index 00000000..24113268
--- /dev/null
+++ b/obnam/storeTests.py
@@ -0,0 +1,301 @@
+# Copyright (C) 2008 Lars Wirzenius <liw@iki.fi>
+#
+# 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 2 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+"""Unit tests for abstraction for storing backup data, for Obnam."""
+
+
+import os
+import shutil
+import socket
+import tempfile
+import unittest
+
+import obnam
+
+
+class StoreTests(unittest.TestCase):
+
+ def setUp(self):
+ context = obnam.context.Context()
+ context.cache = obnam.cache.Cache(context.config)
+ context.be = obnam.backend.init(context.config, context.cache)
+ self.store = obnam.Store(context)
+
+ def tearDown(self):
+ shutil.rmtree(self.store._context.config.get("backup", "store"),
+ ignore_errors=True)
+ shutil.rmtree(self.store._context.config.get("backup", "cache"),
+ ignore_errors=True)
+
+ def testReturnsNoneWhenNoHostBlockExists(self):
+ self.failUnlessEqual(self.store.get_host_block(), None)
+
+ def testReturnsAnActualHostBlockAfterFetch(self):
+ self.store.fetch_host_block()
+ host = self.store.get_host_block()
+ self.failUnless(isinstance(host, obnam.obj.HostBlockObject))
+
+ def testReturnsActualHostBlockWhenOneExists(self):
+ self.store.fetch_host_block()
+ self.store.commit_host_block([])
+
+ context = obnam.context.Context()
+ context.be = obnam.backend.init(context.config, context.cache)
+ store = obnam.Store(context)
+ store.fetch_host_block()
+ host = store.get_host_block()
+ self.failUnless(isinstance(host, obnam.obj.HostBlockObject))
+
+ def testReplacesHostObjectInMemory(self):
+ self.store.fetch_host_block()
+ host = self.store.get_host_block()
+ self.store.commit_host_block([])
+ self.failIfEqual(self.store.get_host_block(), host)
+
+ def testCreatesNewHostBlockWhenNoneExists(self):
+ self.store.fetch_host_block()
+ host = self.store.get_host_block()
+ self.failUnlessEqual(host.get_id(), socket.gethostname())
+ self.failUnlessEqual(host.get_generation_ids(), [])
+ self.failUnlessEqual(host.get_map_block_ids(), [])
+ self.failUnlessEqual(host.get_contmap_block_ids(), [])
+
+ def testLoadsActualHostBlockWhenOneExists(self):
+ context = obnam.context.Context()
+ cache = obnam.cache.Cache(context.config)
+ context.be = obnam.backend.init(context.config, context.cache)
+ host_id = context.config.get("backup", "host-id")
+ temp = obnam.obj.HostBlockObject(host_id=host_id,
+ gen_ids=["pink", "pretty"])
+ obnam.io.upload_host_block(context, temp.encode())
+
+ self.store.fetch_host_block()
+ host = self.store.get_host_block()
+ self.failUnlessEqual(host.get_generation_ids(), ["pink", "pretty"])
+
+ def testGettingNonExistentObjectRaisesException(self):
+ self.failUnlessRaises(obnam.exception.ObnamException,
+ self.store.get_object, "pink")
+
+ def testAddsObjectToStore(self):
+ o = obnam.obj.GenerationObject(id="pink")
+ self.store.fetch_host_block()
+ self.store.queue_object(o)
+ self.store.commit_host_block([])
+
+ context2 = obnam.context.Context()
+ context2.cache = obnam.cache.Cache(context2.config)
+ context2.be = obnam.backend.init(context2.config, context2.cache)
+ store2 = obnam.Store(context2)
+ store2.fetch_host_block()
+ store2.load_maps()
+ self.failUnless(store2.get_object(o.get_id()))
+
+ def mock_queue_object(self, object):
+ self.queued_objects.append(object)
+
+ def testAddsSeveralObjectsToStore(self):
+ objs = [None, True, False]
+ self.queued_objects = []
+ self.store.queue_object = self.mock_queue_object
+ self.store.queue_objects(objs)
+ self.failUnlessEqual(objs, self.queued_objects)
+
+
+class StoreMapTests(unittest.TestCase):
+
+ def setUp(self):
+ # First, set up two mappings.
+
+ context = obnam.context.Context()
+ context.cache = obnam.cache.Cache(context.config)
+ context.be = obnam.backend.init(context.config, context.cache)
+
+ obnam.map.add(context.map, "pink", "pretty")
+ obnam.map.add(context.contmap, "black", "beautiful")
+
+ map_id = context.be.generate_block_id()
+ map_block = obnam.map.encode_new_to_block(context.map, map_id)
+ context.be.upload_block(map_id, map_block, True)
+
+ contmap_id = context.be.generate_block_id()
+ contmap_block = obnam.map.encode_new_to_block(context.contmap,
+ contmap_id)
+ context.be.upload_block(contmap_id, contmap_block, True)
+
+ host_id = context.config.get("backup", "host-id")
+ host = obnam.obj.HostBlockObject(host_id=host_id,
+ map_block_ids=[map_id],
+ contmap_block_ids=[contmap_id])
+ obnam.io.upload_host_block(context, host.encode())
+
+ # Then set up the real context and app.
+
+ self.context = obnam.context.Context()
+ self.context.cache = obnam.cache.Cache(self.context.config)
+ self.context.be = obnam.backend.init(self.context.config,
+ self.context.cache)
+ self.store = obnam.Store(self.context)
+ self.store.fetch_host_block()
+
+ def tearDown(self):
+ shutil.rmtree(self.store._context.config.get("backup", "store"),
+ ignore_errors=True)
+ shutil.rmtree(self.store._context.config.get("backup", "cache"),
+ ignore_errors=True)
+
+ def testHasNoMapsLoadedByDefault(self):
+ self.failUnlessEqual(obnam.map.count(self.context.map), 0)
+
+ def testHasNoContentMapsLoadedByDefault(self):
+ self.failUnlessEqual(obnam.map.count(self.context.contmap), 0)
+
+ def testLoadsMapsWhenRequested(self):
+ self.store.load_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.map), 1)
+
+ def testLoadsContentMapsWhenRequested(self):
+ self.store.load_content_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.contmap), 1)
+
+ def testAddsNoNewMapsWhenNothingHasChanged(self):
+ self.store.update_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.map), 0)
+
+ def testAddsANewMapsWhenSomethingHasChanged(self):
+ obnam.map.add(self.context.map, "pink", "pretty")
+ self.store.update_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.map), 1)
+
+ def testAddsNoNewContentMapsWhenNothingHasChanged(self):
+ self.store.update_content_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.contmap), 0)
+
+ def testAddsANewContentMapsWhenSomethingHasChanged(self):
+ obnam.map.add(self.context.contmap, "pink", "pretty")
+ self.store.update_content_maps()
+ self.failUnlessEqual(obnam.map.count(self.context.contmap), 1)
+
+
+class StorePathnameParserTests(unittest.TestCase):
+
+ def setUp(self):
+ context = obnam.context.Context()
+ self.store = obnam.Store(context)
+
+ def testReturnsRootForRoot(self):
+ self.failUnlessEqual(self.store.parse_pathname("/"), ["/"])
+
+ def testReturnsDotForDot(self):
+ self.failUnlessEqual(self.store.parse_pathname("."), ["."])
+
+ def testReturnsItselfForSingleElement(self):
+ self.failUnlessEqual(self.store.parse_pathname("foo"), ["foo"])
+
+ def testReturnsListOfPartsForMultipleElements(self):
+ self.failUnlessEqual(self.store.parse_pathname("foo/bar"),
+ ["foo", "bar"])
+
+ def testReturnsListOfPartsFromRootForAbsolutePathname(self):
+ self.failUnlessEqual(self.store.parse_pathname("/foo/bar"),
+ ["/", "foo", "bar"])
+
+ def testIgnoredTrailingSlashIfNotRoot(self):
+ self.failUnlessEqual(self.store.parse_pathname("foo/bar/"),
+ ["foo", "bar"])
+
+
+class StoreLookupTests(unittest.TestCase):
+
+ def create_data_dir(self):
+ dirname = tempfile.mkdtemp()
+ file(os.path.join(dirname, "file1"), "w").close()
+ os.mkdir(os.path.join(dirname, "dir1"))
+ os.mkdir(os.path.join(dirname, "dir1", "dir2"))
+ file(os.path.join(dirname, "dir1", "dir2", "file2"), "w").close()
+ return dirname
+
+ def create_context(self):
+ context = obnam.context.Context()
+ context.cache = obnam.cache.Cache(context.config)
+ context.be = obnam.backend.init(context.config, context.cache)
+ return context
+
+ def setUp(self):
+ self.datadir = self.create_data_dir()
+ self.dirbasename = os.path.basename(self.datadir)
+
+ app = obnam.Application(self.create_context())
+ app.load_host()
+ gen = app.backup([self.datadir])
+ app.get_store().commit_host_block([gen])
+
+ self.store = obnam.Store(self.create_context())
+ self.store.fetch_host_block()
+ self.store.load_maps()
+ gen_ids = self.store.get_host_block().get_generation_ids()
+ self.gen = self.store.get_object(gen_ids[0])
+
+ def tearDown(self):
+ shutil.rmtree(self.datadir)
+ shutil.rmtree(self.store._context.config.get("backup", "store"))
+
+ def testFindsBackupRoot(self):
+ dir = self.store.lookup_dir(self.gen, self.dirbasename)
+ self.failUnless(dir.get_name(), self.dirbasename)
+
+ def testFindsFirstSubdir(self):
+ pathname = os.path.join(self.dirbasename, "dir1")
+ dir = self.store.lookup_dir(self.gen, pathname)
+ self.failUnless(dir.get_name(), "dir1")
+
+ def testFindsSecondSubdir(self):
+ pathname = os.path.join(self.dirbasename, "dir1", "dir2")
+ dir = self.store.lookup_dir(self.gen, pathname)
+ self.failUnless(dir.get_name(), "dir2")
+
+ def testDoesNotFindNonExistentDir(self):
+ self.failUnlessEqual(self.store.lookup_dir(self.gen, "notexist"),
+ None)
+
+ def testDoesNotFindNonExistentFileInSubDirectory(self):
+ pathname = os.path.join(self.dirbasename, "dir1", "notexist")
+ file = self.store.lookup_file(self.gen, pathname)
+ self.failUnlessEqual(file, None)
+
+ def testDoesNotFindNonExistentFileInSubSubDirectory(self):
+ pathname = os.path.join(self.dirbasename, "dir1", "dir2", "notexist")
+ file = self.store.lookup_file(self.gen, pathname)
+ self.failUnlessEqual(file, None)
+
+ def testDoesNotFindNonExistentFileInRoot(self):
+ pathname = os.path.join(self.dirbasename, "notexist")
+ file = self.store.lookup_file(self.gen, pathname)
+ self.failUnlessEqual(file, None)
+
+ def filename(self, file):
+ return file.first_string_by_kind(obnam.cmp.FILENAME)
+
+ def testFindsFileInRootDirectory(self):
+ pathname = os.path.join(self.dirbasename, "file1")
+ file = self.store.lookup_file(self.gen, pathname)
+ self.failUnlessEqual(self.filename(file), "file1")
+
+ def testFindsFileInSubDirectory(self):
+ pathname = os.path.join(self.dirbasename, "dir1", "dir2", "file2")
+ file = self.store.lookup_file(self.gen, pathname)
+ self.failUnlessEqual(self.filename(file), "file2")