]> arthur.barton.de Git - bup.git/blob - lib/bup/hlinkdb.py
29d575eec3f8aae567b4656140e9185226e31b47
[bup.git] / lib / bup / hlinkdb.py
1
2 from __future__ import absolute_import
3 import errno, os, tempfile
4
5 from bup import compat
6 from bup.compat import pending_raise
7
8 if compat.py_maj > 2:
9     import pickle
10     def pickle_load(f):
11         return pickle.load(f, encoding='bytes')
12 else:
13     import cPickle as pickle
14     pickle_load = pickle.load
15
16
17 class Error(Exception):
18     pass
19
20 class HLinkDB:
21     def __init__(self, filename):
22         self.closed = False
23         # Map a "dev:ino" node to a list of paths associated with that node.
24         self._node_paths = {}
25         # Map a path to a "dev:ino" node.
26         self._path_node = {}
27         self._filename = filename
28         self._save_prepared = None
29         self._tmpname = None
30         f = None
31         try:
32             f = open(filename, 'rb')
33         except IOError as e:
34             if e.errno == errno.ENOENT:
35                 pass
36             else:
37                 raise
38         if f:
39             try:
40                 self._node_paths = pickle_load(f)
41             finally:
42                 f.close()
43                 f = None
44         # Set up the reverse hard link index.
45         for node, paths in compat.items(self._node_paths):
46             for path in paths:
47                 self._path_node[path] = node
48
49     def prepare_save(self):
50         """ Commit all of the relevant data to disk.  Do as much work
51         as possible without actually making the changes visible."""
52         if self._save_prepared:
53             raise Error('save of %r already in progress' % self._filename)
54         if self._node_paths:
55             (dir, name) = os.path.split(self._filename)
56             (ffd, self._tmpname) = tempfile.mkstemp(b'.tmp', name, dir)
57             try:
58                 try:
59                     f = os.fdopen(ffd, 'wb', 65536)
60                 except:
61                     os.close(ffd)
62                     raise
63                 try:
64                     pickle.dump(self._node_paths, f, 2)
65                 finally:
66                     f.close()
67                     f = None
68             except:
69                 tmpname = self._tmpname
70                 self._tmpname = None
71                 os.unlink(tmpname)
72                 raise
73         self._save_prepared = True
74
75     def commit_save(self):
76         self.closed = True
77         if not self._save_prepared:
78             raise Error('cannot commit save of %r; no save prepared'
79                         % self._filename)
80         if self._tmpname:
81             os.rename(self._tmpname, self._filename)
82             self._tmpname = None
83         else: # No data -- delete _filename if it exists.
84             try:
85                 os.unlink(self._filename)
86             except OSError as e:
87                 if e.errno == errno.ENOENT:
88                     pass
89                 else:
90                     raise
91         self._save_prepared = None
92
93     def abort_save(self):
94         self.closed = True
95         if self._tmpname:
96             os.unlink(self._tmpname)
97             self._tmpname = None
98
99     def __enter__(self):
100         return self
101
102     def __exit__(self, type, value, traceback):
103         with pending_raise(value, rethrow=True):
104             self.abort_save()
105
106     def __del__(self):
107         assert self.closed
108
109     def add_path(self, path, dev, ino):
110         # Assume path is new.
111         node = b'%d:%d' % (dev, ino)
112         self._path_node[path] = node
113         link_paths = self._node_paths.get(node)
114         if link_paths and path not in link_paths:
115             link_paths.append(path)
116         else:
117             self._node_paths[node] = [path]
118
119     def _del_node_path(self, node, path):
120         link_paths = self._node_paths[node]
121         link_paths.remove(path)
122         if not link_paths:
123             del self._node_paths[node]
124
125     def change_path(self, path, new_dev, new_ino):
126         prev_node = self._path_node.get(path)
127         if prev_node:
128             self._del_node_path(prev_node, path)
129         self.add_path(new_dev, new_ino, path)
130
131     def del_path(self, path):
132         # Path may not be in db (if updating a pre-hardlink support index).
133         node = self._path_node.get(path)
134         if node:
135             self._del_node_path(node, path)
136             del self._path_node[path]
137
138     def node_paths(self, dev, ino):
139         node = b'%d:%d' % (dev, ino)
140         return self._node_paths[node]