]> arthur.barton.de Git - bup.git/blob - cmd/restore-cmd.py
Include metadata when asked to restore individual non-directory paths.
[bup.git] / cmd / restore-cmd.py
1 #!/usr/bin/env python
2 import errno, sys, stat
3 from bup import options, git, metadata, vfs
4 from bup.helpers import *
5
6 optspec = """
7 bup restore [-C outdir] </branch/revision/path/to/dir ...>
8 --
9 C,outdir=   change to given outdir before extracting files
10 numeric-ids restore numeric IDs (user, group, etc.) rather than names
11 v,verbose   increase log output (can be used more than once)
12 q,quiet     don't show progress meter
13 """
14
15 total_restored = 0
16
17
18 def verbose1(s):
19     if opt.verbose >= 1:
20         print s
21
22
23 def verbose2(s):
24     if opt.verbose >= 2:
25         print s
26
27
28 def plog(s):
29     if opt.quiet:
30         return
31     qprogress(s)
32
33
34 def print_info(n, fullname):
35     if stat.S_ISDIR(n.mode):
36         verbose1('%s/' % fullname)
37     elif stat.S_ISLNK(n.mode):
38         verbose2('%s@ -> %s' % (fullname, n.readlink()))
39     else:
40         verbose2(fullname)
41
42
43 def create_path(n, fullname, meta):
44     if meta:
45         meta.create_path(fullname)
46     else:
47         # These fallbacks are important -- meta could be null if, for
48         # example, save created a "fake" item, i.e. a new strip/graft
49         # path element, etc.  You can find cases like that by
50         # searching for "Metadata()".
51         unlink(fullname)
52         if stat.S_ISDIR(n.mode):
53             mkdirp(fullname)
54         elif stat.S_ISLNK(n.mode):
55             os.symlink(n.readlink(), fullname)
56
57 # Track a list of (restore_path, vfs_path, meta) triples for each path
58 # we've written for a given hardlink_target.  This allows us to handle
59 # the case where we restore a set of hardlinks out of order (with
60 # respect to the original save call(s)) -- i.e. when we don't restore
61 # the hardlink_target path first.  This data also allows us to attempt
62 # to handle other situations like hardlink sets that change on disk
63 # during a save, or between index and save.
64 targets_written = {}
65
66 def hardlink_compatible(target_path, target_vfs_path, target_meta,
67                         src_node, src_meta):
68     global top
69     if not os.path.exists(target_path):
70         return False
71     target_node = top.lresolve(target_vfs_path)
72     if src_node.mode != target_node.mode \
73             or src_node.atime != target_node.atime \
74             or src_node.mtime != target_node.mtime \
75             or src_node.ctime != target_node.ctime \
76             or src_node.hash != target_node.hash:
77         return False
78     if not src_meta.same_file(target_meta):
79         return False
80     return True
81
82
83 def hardlink_if_possible(fullname, node, meta):
84     """Find a suitable hardlink target, link to it, and return true,
85     otherwise return false."""
86     # Expect the caller to handle restoring the metadata if
87     # hardlinking isn't possible.
88     global targets_written
89     target = meta.hardlink_target
90     target_versions = targets_written.get(target)
91     if target_versions:
92         # Check every path in the set that we've written so far for a match.
93         for (target_path, target_vfs_path, target_meta) in target_versions:
94             if hardlink_compatible(target_path, target_vfs_path, target_meta,
95                                    node, meta):
96                 try:
97                     os.link(target_path, fullname)
98                     return True
99                 except OSError, e:
100                     if e.errno != errno.EXDEV:
101                         raise
102     else:
103         target_versions = []
104         targets_written[target] = target_versions
105     full_vfs_path = node.fullname()
106     target_versions.append((fullname, full_vfs_path, meta))
107     return False
108
109
110 def write_file_content(fullname, n):
111     outf = open(fullname, 'wb')
112     try:
113         for b in chunkyreader(n.open()):
114             outf.write(b)
115     finally:
116         outf.close()
117
118
119 def find_dir_item_metadata_by_name(dir, name):
120     """Find metadata in dir (a node) for an item with the given name,
121     or for the directory itself if the name is ''."""
122     meta_stream = None
123     try:
124         mfile = dir.metadata_file() # VFS file -- cannot close().
125         if mfile:
126             meta_stream = mfile.open()
127             meta = metadata.Metadata.read(meta_stream)
128             if name == '':
129                 return meta
130             for sub in dir:
131                 if stat.S_ISDIR(sub.mode):
132                     return find_dir_item_metadata_by_name(sub, '')
133                 else:
134                     meta = metadata.Metadata.read(meta_stream)
135                 if sub.name == name:
136                     return meta
137     finally:
138         if meta_stream:
139             meta_stream.close()
140
141
142 def do_root(n):
143     # Very similar to do_node(), except that this function doesn't
144     # create a path for n's destination directory (and so ignores
145     # n.fullname).  It assumes the destination is '.', and restores
146     # n's metadata and content there.
147     global total_restored, opt
148     meta_stream = None
149     try:
150         # Directory metadata is the first entry in any .bupm file in
151         # the directory.  Get it.
152         mfile = n.metadata_file() # VFS file -- cannot close().
153         if mfile:
154             meta_stream = mfile.open()
155             meta = metadata.Metadata.read(meta_stream)
156         print_info(n, '.')
157         total_restored += 1
158         plog('Restoring: %d\r' % total_restored)
159         for sub in n:
160             m = None
161             # Don't get metadata if this is a dir -- handled in sub do_node().
162             if meta_stream and not stat.S_ISDIR(sub.mode):
163                 m = metadata.Metadata.read(meta_stream)
164             do_node(n, sub, m)
165         if meta:
166             meta.apply_to_path('.', restore_numeric_ids = opt.numeric_ids)
167     finally:
168         if meta_stream:
169             meta_stream.close()
170
171
172 def do_node(top, n, meta=None):
173     # Create n.fullname(), relative to the current directory, and
174     # restore all of its metadata, when available.  The meta argument
175     # will be None for dirs, or when there is no .bupm (i.e. no
176     # metadata).
177     global total_restored, opt
178     meta_stream = None
179     try:
180         fullname = n.fullname(stop_at=top)
181         # If this is a directory, its metadata is the first entry in
182         # any .bupm file inside the directory.  Get it.
183         if(stat.S_ISDIR(n.mode)):
184             mfile = n.metadata_file() # VFS file -- cannot close().
185             if mfile:
186                 meta_stream = mfile.open()
187                 meta = metadata.Metadata.read(meta_stream)
188         print_info(n, fullname)
189
190         created_hardlink = False
191         if meta and meta.hardlink_target:
192             created_hardlink = hardlink_if_possible(fullname, n, meta)
193
194         if not created_hardlink:
195             create_path(n, fullname, meta)
196             if meta:
197                 if stat.S_ISREG(meta.mode):
198                     write_file_content(fullname, n)
199             elif stat.S_ISREG(n.mode):
200                 write_file_content(fullname, n)
201
202         total_restored += 1
203         plog('Restoring: %d\r' % total_restored)
204         for sub in n:
205             m = None
206             # Don't get metadata if this is a dir -- handled in sub do_node().
207             if meta_stream and not stat.S_ISDIR(sub.mode):
208                 m = metadata.Metadata.read(meta_stream)
209             do_node(top, sub, m)
210         if meta and not created_hardlink:
211             meta.apply_to_path(fullname, restore_numeric_ids = opt.numeric_ids)
212     finally:
213         if meta_stream:
214             meta_stream.close()
215
216 handle_ctrl_c()
217
218 o = options.Options(optspec)
219 (opt, flags, extra) = o.parse(sys.argv[1:])
220
221 git.check_repo_or_die()
222 top = vfs.RefList(None)
223
224 if not extra:
225     o.fatal('must specify at least one filename to restore')
226     
227 if opt.outdir:
228     mkdirp(opt.outdir)
229     os.chdir(opt.outdir)
230
231 ret = 0
232 for d in extra:
233     path,name = os.path.split(d)
234     try:
235         n = top.lresolve(d)
236     except vfs.NodeError, e:
237         add_error(e)
238         continue
239     isdir = stat.S_ISDIR(n.mode)
240     if not name or name == '.':
241         # Source is /foo/what/ever/ or /foo/what/ever/. -- extract
242         # what/ever/* to the current directory, and if name == '.'
243         # (i.e. /foo/what/ever/.), then also restore what/ever's
244         # metadata to the current directory.
245         if not isdir:
246             add_error('%r: not a directory' % d)
247         else:
248             if name == '.':
249                 do_root(n)
250             else:
251                 for sub in n:
252                     do_node(n, sub)
253     else:
254         # Source is /foo/what/ever -- extract ./ever to cwd.
255         if isinstance(n, vfs.FakeSymlink):
256             # Source is actually /foo/what, i.e. a top-level commit
257             # like /foo/latest, which is a symlink to ../.commit/SHA.
258             # So dereference it, and restore ../.commit/SHA/. to
259             # "./what/.".
260             target = n.dereference()
261             mkdirp(n.name)
262             os.chdir(n.name)
263             do_root(target)
264         else: # Not a directory or fake symlink.
265             meta = find_dir_item_metadata_by_name(n.parent, n.name)
266             do_node(n.parent, n, meta=meta)
267
268 if not opt.quiet:
269     progress('Restoring: %d, done.\n' % total_restored)
270
271 if saved_errors:
272     log('WARNING: %d errors encountered while restoring.\n' % len(saved_errors))
273     sys.exit(1)