2 import errno, sys, stat, re
3 from bup import options, git, metadata, vfs
4 from bup.helpers import *
7 bup restore [-C outdir] </branch/revision/path/to/dir ...>
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
35 def valid_restore_path(path):
36 path = os.path.normpath(path)
37 if path.startswith('/'):
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()))
52 def create_path(n, fullname, meta):
54 meta.create_path(fullname)
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()".
61 if stat.S_ISDIR(n.mode):
63 elif stat.S_ISLNK(n.mode):
64 os.symlink(n.readlink(), fullname)
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.
75 def hardlink_compatible(target_path, target_vfs_path, target_meta,
78 if not os.path.exists(target_path):
80 target_node = top.lresolve(target_vfs_path)
81 if src_node.mode != target_node.mode \
82 or src_node.mtime != target_node.mtime \
83 or src_node.ctime != target_node.ctime \
84 or src_node.hash != target_node.hash:
86 if not src_meta.same_file(target_meta):
91 def hardlink_if_possible(fullname, node, meta):
92 """Find a suitable hardlink target, link to it, and return true,
93 otherwise return false."""
94 # Expect the caller to handle restoring the metadata if
95 # hardlinking isn't possible.
96 global targets_written
97 target = meta.hardlink_target
98 target_versions = targets_written.get(target)
100 # Check every path in the set that we've written so far for a match.
101 for (target_path, target_vfs_path, target_meta) in target_versions:
102 if hardlink_compatible(target_path, target_vfs_path, target_meta,
105 os.link(target_path, fullname)
108 if e.errno != errno.EXDEV:
112 targets_written[target] = target_versions
113 full_vfs_path = node.fullname()
114 target_versions.append((fullname, full_vfs_path, meta))
118 def write_file_content(fullname, n):
119 outf = open(fullname, 'wb')
121 for b in chunkyreader(n.open()):
127 def find_dir_item_metadata_by_name(dir, name):
128 """Find metadata in dir (a node) for an item with the given name,
129 or for the directory itself if the name is ''."""
132 mfile = dir.metadata_file() # VFS file -- cannot close().
134 meta_stream = mfile.open()
135 # First entry is for the dir itself.
136 meta = metadata.Metadata.read(meta_stream)
140 if stat.S_ISDIR(sub.mode):
141 meta = find_dir_item_metadata_by_name(sub, '')
143 meta = metadata.Metadata.read(meta_stream)
151 def do_root(n, restore_root_meta=True):
152 # Very similar to do_node(), except that this function doesn't
153 # create a path for n's destination directory (and so ignores
154 # n.fullname). It assumes the destination is '.', and restores
155 # n's metadata and content there.
156 global total_restored, opt
159 # Directory metadata is the first entry in any .bupm file in
160 # the directory. Get it.
161 mfile = n.metadata_file() # VFS file -- cannot close().
163 meta_stream = mfile.open()
164 root_meta = metadata.Metadata.read(meta_stream)
167 plog('Restoring: %d\r' % total_restored)
170 # Don't get metadata if this is a dir -- handled in sub do_node().
171 if meta_stream and not stat.S_ISDIR(sub.mode):
172 m = metadata.Metadata.read(meta_stream)
174 if root_meta and restore_root_meta:
175 root_meta.apply_to_path('.', restore_numeric_ids = opt.numeric_ids)
181 def do_node(top, n, meta=None):
182 # Create n.fullname(), relative to the current directory, and
183 # restore all of its metadata, when available. The meta argument
184 # will be None for dirs, or when there is no .bupm (i.e. no
186 global total_restored, opt
189 fullname = n.fullname(stop_at=top)
190 # Match behavior of index --exclude-rx with respect to paths.
191 exclude_candidate = '/' + fullname
192 if(stat.S_ISDIR(n.mode)):
193 exclude_candidate += '/'
194 if should_rx_exclude_path(exclude_candidate, exclude_rxs):
196 # If this is a directory, its metadata is the first entry in
197 # any .bupm file inside the directory. Get it.
198 if(stat.S_ISDIR(n.mode)):
199 mfile = n.metadata_file() # VFS file -- cannot close().
201 meta_stream = mfile.open()
202 meta = metadata.Metadata.read(meta_stream)
203 print_info(n, fullname)
205 created_hardlink = False
206 if meta and meta.hardlink_target:
207 created_hardlink = hardlink_if_possible(fullname, n, meta)
209 if not created_hardlink:
210 create_path(n, fullname, meta)
212 if stat.S_ISREG(meta.mode):
213 write_file_content(fullname, n)
214 elif stat.S_ISREG(n.mode):
215 write_file_content(fullname, n)
218 plog('Restoring: %d\r' % total_restored)
221 # Don't get metadata if this is a dir -- handled in sub do_node().
222 if meta_stream and not stat.S_ISDIR(sub.mode):
223 m = metadata.Metadata.read(meta_stream)
225 if meta and not created_hardlink:
226 meta.apply_to_path(fullname, restore_numeric_ids = opt.numeric_ids)
234 o = options.Options(optspec)
235 (opt, flags, extra) = o.parse(sys.argv[1:])
237 git.check_repo_or_die()
238 top = vfs.RefList(None)
241 o.fatal('must specify at least one filename to restore')
243 exclude_rxs = parse_rx_excludes(flags, o.fatal)
251 if not valid_restore_path(d):
252 add_error("ERROR: path %r doesn't include a branch and revision" % d)
254 path,name = os.path.split(d)
257 except vfs.NodeError, e:
260 isdir = stat.S_ISDIR(n.mode)
261 if not name or name == '.':
262 # Source is /foo/what/ever/ or /foo/what/ever/. -- extract
263 # what/ever/* to the current directory, and if name == '.'
264 # (i.e. /foo/what/ever/.), then also restore what/ever's
265 # metadata to the current directory.
267 add_error('%r: not a directory' % d)
269 do_root(n, restore_root_meta = (name == '.'))
271 # Source is /foo/what/ever -- extract ./ever to cwd.
272 if isinstance(n, vfs.FakeSymlink):
273 # Source is actually /foo/what, i.e. a top-level commit
274 # like /foo/latest, which is a symlink to ../.commit/SHA.
275 # So dereference it, and restore ../.commit/SHA/. to
277 target = n.dereference()
281 else: # Not a directory or fake symlink.
282 meta = find_dir_item_metadata_by_name(n.parent, n.name)
283 do_node(n.parent, n, meta=meta)
286 progress('Restoring: %d, done.\n' % total_restored)
289 log('WARNING: %d errors encountered while restoring.\n' % len(saved_errors))