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