]> arthur.barton.de Git - bup.git/blob - lib/bup/hlinkdb.py
f7e5d72153b6d72e7e5934296eb9169bfe4a12d8
[bup.git] / lib / bup / hlinkdb.py
1
2 from contextlib import ExitStack
3 import os, pickle
4
5 from bup.helpers import atomically_replaced_file, unlink
6
7
8 def pickle_load(filename):
9     try:
10         f = open(filename, 'rb')
11     except FileNotFoundError:
12         return None
13     with f:
14         return pickle.load(f, encoding='bytes')
15
16
17 class Error(Exception):
18     pass
19
20 class HLinkDB:
21     def __init__(self, filename):
22         self.closed = False
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).
29         self._path_node = {}
30         for node, paths in self._node_paths.items():
31             for path in paths:
32                 self._path_node[path] = node
33
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)
39         with self._cleanup:
40             if self._node_paths:
41                 dir, name = os.path.split(self._filename)
42                 self._pending_save = atomically_replaced_file(self._filename,
43                                                               mode='wb',
44                                                               buffering=65536)
45                 with self._cleanup.enter_context(self._pending_save) as f:
46                     pickle.dump(self._node_paths, f, 2)
47             else: # No data
48                 self._cleanup.callback(lambda: unlink(self._filename))
49             self._cleanup = self._cleanup.pop_all()
50
51     def commit_save(self):
52         self.closed = True
53         if self._node_paths and not self._pending_save:
54             raise Error('cannot commit save of %r; no save prepared'
55                         % self._filename)
56         self._cleanup.close()
57         self._pending_save = None
58
59     def abort_save(self):
60         self.closed = True
61         with self._cleanup:
62             if self._pending_save:
63                 self._pending_save.cancel()
64         self._pending_save = None
65
66     def __enter__(self):
67         return self
68
69     def __exit__(self, type, value, traceback):
70         self.abort_save()
71
72     def __del__(self):
73         assert self.closed
74
75     def add_path(self, path, dev, ino):
76         # Assume path is new.
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)
82         else:
83             self._node_paths[node] = [path]
84
85     def _del_node_path(self, node, path):
86         link_paths = self._node_paths[node]
87         link_paths.remove(path)
88         if not link_paths:
89             del self._node_paths[node]
90
91     def change_path(self, path, new_dev, new_ino):
92         prev_node = self._path_node.get(path)
93         if prev_node:
94             self._del_node_path(prev_node, path)
95         self.add_path(new_dev, new_ino, path)
96
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)
100         if node:
101             self._del_node_path(node, path)
102             del self._path_node[path]
103
104     def node_paths(self, dev, ino):
105         node = b'%d:%d' % (dev, ino)
106         return self._node_paths[node]