3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 import copy, errno, os, sys, stat, re
10 from bup import options, git, metadata, vfs
11 from bup._helpers import write_sparsely
12 from bup.helpers import (add_error, chunkyreader, handle_ctrl_c, log, mkdirp,
13 parse_rx_excludes, progress, qprogress, saved_errors,
14 should_rx_exclude_path, unlink)
18 bup restore [-C outdir] </branch/revision/path/to/dir ...>
20 C,outdir= change to given outdir before extracting files
21 numeric-ids restore numeric IDs (user, group, etc.) rather than names
22 exclude-rx= skip paths matching the unanchored regex (may be repeated)
23 exclude-rx-from= skip --exclude-rx patterns in file (may be repeated)
24 sparse create sparse files
25 v,verbose increase log output (can be used more than once)
26 map-user= given OLD=NEW, restore OLD user as NEW user
27 map-group= given OLD=NEW, restore OLD group as NEW group
28 map-uid= given OLD=NEW, restore OLD uid as NEW uid
29 map-gid= given OLD=NEW, restore OLD gid as NEW gid
30 q,quiet don't show progress meter
35 # stdout should be flushed after each line, even when not connected to a tty
37 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
55 def valid_restore_path(path):
56 path = os.path.normpath(path)
57 if path.startswith('/'):
63 def print_info(n, fullname):
64 if stat.S_ISDIR(n.mode):
65 verbose1('%s/' % fullname)
66 elif stat.S_ISLNK(n.mode):
67 verbose2('%s@ -> %s' % (fullname, n.readlink()))
72 def create_path(n, fullname, meta):
74 meta.create_path(fullname)
76 # These fallbacks are important -- meta could be null if, for
77 # example, save created a "fake" item, i.e. a new strip/graft
78 # path element, etc. You can find cases like that by
79 # searching for "Metadata()".
81 if stat.S_ISDIR(n.mode):
83 elif stat.S_ISLNK(n.mode):
84 os.symlink(n.readlink(), fullname)
87 def parse_owner_mappings(type, options, fatal):
88 """Traverse the options and parse all --map-TYPEs, or call Option.fatal()."""
89 opt_name = '--map-' + type
90 value_rx = r'^([^=]+)=([^=]*)$'
91 if type in ('uid', 'gid'):
92 value_rx = r'^(-?[0-9]+)=(-?[0-9]+)$'
95 (option, parameter) = flag
96 if option != opt_name:
98 match = re.match(value_rx, parameter)
100 raise fatal("couldn't parse %s as %s mapping" % (parameter, type))
101 old_id, new_id = match.groups()
102 if type in ('uid', 'gid'):
105 owner_map[old_id] = new_id
109 def apply_metadata(meta, name, restore_numeric_ids, owner_map):
110 m = copy.deepcopy(meta)
111 m.user = owner_map['user'].get(m.user, m.user)
112 m.group = owner_map['group'].get(m.group, m.group)
113 m.uid = owner_map['uid'].get(m.uid, m.uid)
114 m.gid = owner_map['gid'].get(m.gid, m.gid)
115 m.apply_to_path(name, restore_numeric_ids = restore_numeric_ids)
118 # Track a list of (restore_path, vfs_path, meta) triples for each path
119 # we've written for a given hardlink_target. This allows us to handle
120 # the case where we restore a set of hardlinks out of order (with
121 # respect to the original save call(s)) -- i.e. when we don't restore
122 # the hardlink_target path first. This data also allows us to attempt
123 # to handle other situations like hardlink sets that change on disk
124 # during a save, or between index and save.
127 def hardlink_compatible(target_path, target_vfs_path, target_meta,
130 if not os.path.exists(target_path):
132 target_node = top.lresolve(target_vfs_path)
133 if src_node.mode != target_node.mode \
134 or src_node.mtime != target_node.mtime \
135 or src_node.ctime != target_node.ctime \
136 or src_node.hash != target_node.hash:
138 if not src_meta.same_file(target_meta):
143 def hardlink_if_possible(fullname, node, meta):
144 """Find a suitable hardlink target, link to it, and return true,
145 otherwise return false."""
146 # Expect the caller to handle restoring the metadata if
147 # hardlinking isn't possible.
148 global targets_written
149 target = meta.hardlink_target
150 target_versions = targets_written.get(target)
152 # Check every path in the set that we've written so far for a match.
153 for (target_path, target_vfs_path, target_meta) in target_versions:
154 if hardlink_compatible(target_path, target_vfs_path, target_meta,
157 os.link(target_path, fullname)
160 if e.errno != errno.EXDEV:
164 targets_written[target] = target_versions
165 full_vfs_path = node.fullname()
166 target_versions.append((fullname, full_vfs_path, meta))
170 def write_file_content(fullname, n):
171 outf = open(fullname, 'wb')
173 for b in chunkyreader(n.open()):
179 def write_file_content_sparsely(fullname, n):
180 outfd = os.open(fullname, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
183 for b in chunkyreader(n.open()):
184 trailing_zeros = write_sparsely(outfd, b, 512, trailing_zeros)
185 pos = os.lseek(outfd, trailing_zeros, os.SEEK_END)
186 os.ftruncate(outfd, pos)
191 def find_dir_item_metadata_by_name(dir, name):
192 """Find metadata in dir (a node) for an item with the given name,
193 or for the directory itself if the name is ''."""
196 mfile = dir.metadata_file() # VFS file -- cannot close().
198 meta_stream = mfile.open()
199 # First entry is for the dir itself.
200 meta = metadata.Metadata.read(meta_stream)
204 if stat.S_ISDIR(sub.mode):
205 meta = find_dir_item_metadata_by_name(sub, '')
207 meta = metadata.Metadata.read(meta_stream)
215 def do_root(n, sparse, owner_map, restore_root_meta = True):
216 # Very similar to do_node(), except that this function doesn't
217 # create a path for n's destination directory (and so ignores
218 # n.fullname). It assumes the destination is '.', and restores
219 # n's metadata and content there.
220 global total_restored, opt
223 # Directory metadata is the first entry in any .bupm file in
224 # the directory. Get it.
225 mfile = n.metadata_file() # VFS file -- cannot close().
228 meta_stream = mfile.open()
229 root_meta = metadata.Metadata.read(meta_stream)
232 plog('Restoring: %d\r' % total_restored)
235 # Don't get metadata if this is a dir -- handled in sub do_node().
236 if meta_stream and not stat.S_ISDIR(sub.mode):
237 m = metadata.Metadata.read(meta_stream)
238 do_node(n, sub, sparse, owner_map, meta = m)
239 if root_meta and restore_root_meta:
240 apply_metadata(root_meta, '.', opt.numeric_ids, owner_map)
245 def do_node(top, n, sparse, owner_map, meta = None):
246 # Create n.fullname(), relative to the current directory, and
247 # restore all of its metadata, when available. The meta argument
248 # will be None for dirs, or when there is no .bupm (i.e. no
250 global total_restored, opt
252 write_content = sparse and write_file_content_sparsely or write_file_content
254 fullname = n.fullname(stop_at=top)
255 # Match behavior of index --exclude-rx with respect to paths.
256 exclude_candidate = '/' + fullname
257 if(stat.S_ISDIR(n.mode)):
258 exclude_candidate += '/'
259 if should_rx_exclude_path(exclude_candidate, exclude_rxs):
261 # If this is a directory, its metadata is the first entry in
262 # any .bupm file inside the directory. Get it.
263 if(stat.S_ISDIR(n.mode)):
264 mfile = n.metadata_file() # VFS file -- cannot close().
266 meta_stream = mfile.open()
267 meta = metadata.Metadata.read(meta_stream)
268 print_info(n, fullname)
270 created_hardlink = False
271 if meta and meta.hardlink_target:
272 created_hardlink = hardlink_if_possible(fullname, n, meta)
274 if not created_hardlink:
275 create_path(n, fullname, meta)
277 if stat.S_ISREG(meta.mode):
278 write_content(fullname, n)
279 elif stat.S_ISREG(n.mode):
280 write_content(fullname, n)
283 plog('Restoring: %d\r' % total_restored)
286 # Don't get metadata if this is a dir -- handled in sub do_node().
287 if meta_stream and not stat.S_ISDIR(sub.mode):
288 m = metadata.Metadata.read(meta_stream)
289 do_node(top, sub, sparse, owner_map, meta = m)
290 if meta and not created_hardlink:
291 apply_metadata(meta, fullname, opt.numeric_ids, owner_map)
300 o = options.Options(optspec)
301 (opt, flags, extra) = o.parse(sys.argv[1:])
303 git.check_repo_or_die()
304 top = vfs.RefList(None)
307 o.fatal('must specify at least one filename to restore')
309 exclude_rxs = parse_rx_excludes(flags, o.fatal)
312 for map_type in ('user', 'group', 'uid', 'gid'):
313 owner_map[map_type] = parse_owner_mappings(map_type, flags, o.fatal)
321 if not valid_restore_path(d):
322 add_error("ERROR: path %r doesn't include a branch and revision" % d)
324 path,name = os.path.split(d)
327 except vfs.NodeError as e:
330 isdir = stat.S_ISDIR(n.mode)
331 if not name or name == '.':
332 # Source is /foo/what/ever/ or /foo/what/ever/. -- extract
333 # what/ever/* to the current directory, and if name == '.'
334 # (i.e. /foo/what/ever/.), then also restore what/ever's
335 # metadata to the current directory.
337 add_error('%r: not a directory' % d)
339 do_root(n, opt.sparse, owner_map, restore_root_meta = (name == '.'))
341 # Source is /foo/what/ever -- extract ./ever to cwd.
342 if isinstance(n, vfs.FakeSymlink):
343 # Source is actually /foo/what, i.e. a top-level commit
344 # like /foo/latest, which is a symlink to ../.commit/SHA.
345 # So dereference it, and restore ../.commit/SHA/. to
347 target = n.dereference()
350 do_root(target, opt.sparse, owner_map)
351 else: # Not a directory or fake symlink.
352 meta = find_dir_item_metadata_by_name(n.parent, n.name)
353 do_node(n.parent, n, opt.sparse, owner_map, meta = meta)
356 progress('Restoring: %d, done.\n' % total_restored)
359 log('WARNING: %d errors encountered while restoring.\n' % len(saved_errors))