2 from contextlib import ExitStack
5 from bup.helpers import atomically_replaced_file, unlink
8 def pickle_load(filename):
10 f = open(filename, 'rb')
11 except FileNotFoundError:
14 return pickle.load(f, encoding='bytes')
17 class Error(Exception):
21 def __init__(self, filename):
23 self._cleanup = ExitStack()
24 self._filename = filename
25 self._pending_save = None
26 # Map a "dev:ino" node to a list of paths associated with that node.
27 self._node_paths = pickle_load(filename) or {}
28 # Map a path to a "dev:ino" node (a reverse hard link index).
30 for node, paths in self._node_paths.items():
32 self._path_node[path] = node
34 def prepare_save(self):
35 """ Commit all of the relevant data to disk. Do as much work
36 as possible without actually making the changes visible."""
37 if self._pending_save:
38 raise Error('save of %r already in progress' % self._filename)
41 dir, name = os.path.split(self._filename)
42 self._pending_save = atomically_replaced_file(self._filename,
45 with self._cleanup.enter_context(self._pending_save) as f:
46 pickle.dump(self._node_paths, f, 2)
48 self._cleanup.callback(lambda: unlink(self._filename))
49 self._cleanup = self._cleanup.pop_all()
51 def commit_save(self):
53 if self._node_paths and not self._pending_save:
54 raise Error('cannot commit save of %r; no save prepared'
57 self._pending_save = None
62 if self._pending_save:
63 self._pending_save.cancel()
64 self._pending_save = None
69 def __exit__(self, type, value, traceback):
75 def add_path(self, path, dev, ino):
77 node = b'%d:%d' % (dev, ino)
78 self._path_node[path] = node
79 link_paths = self._node_paths.get(node)
80 if link_paths and path not in link_paths:
81 link_paths.append(path)
83 self._node_paths[node] = [path]
85 def _del_node_path(self, node, path):
86 link_paths = self._node_paths[node]
87 link_paths.remove(path)
89 del self._node_paths[node]
91 def change_path(self, path, new_dev, new_ino):
92 prev_node = self._path_node.get(path)
94 self._del_node_path(prev_node, path)
95 self.add_path(new_dev, new_ino, path)
97 def del_path(self, path):
98 # Path may not be in db (if updating a pre-hardlink support index).
99 node = self._path_node.get(path)
101 self._del_node_path(node, path)
102 del self._path_node[path]
104 def node_paths(self, dev, ino):
105 node = b'%d:%d' % (dev, ino)
106 return self._node_paths[node]