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