3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import print_function
9 from stat import S_ISDIR
10 import copy, errno, os, sys, stat, re
12 from bup import options, git, metadata, vfs2
13 from bup._helpers import write_sparsely
14 from bup.compat import wrap_main
15 from bup.helpers import (add_error, chunkyreader, die_if_errors, handle_ctrl_c,
16 log, mkdirp, parse_rx_excludes, progress, qprogress,
17 saved_errors, should_rx_exclude_path, unlink)
18 from bup.repo import LocalRepo
22 bup restore [-C outdir] </branch/revision/path/to/dir ...>
24 C,outdir= change to given outdir before extracting files
25 numeric-ids restore numeric IDs (user, group, etc.) rather than names
26 exclude-rx= skip paths matching the unanchored regex (may be repeated)
27 exclude-rx-from= skip --exclude-rx patterns in file (may be repeated)
28 sparse create sparse files
29 v,verbose increase log output (can be used more than once)
30 map-user= given OLD=NEW, restore OLD user as NEW user
31 map-group= given OLD=NEW, restore OLD group as NEW group
32 map-uid= given OLD=NEW, restore OLD uid as NEW uid
33 map-gid= given OLD=NEW, restore OLD gid as NEW gid
34 q,quiet don't show progress meter
39 # stdout should be flushed after each line, even when not connected to a tty
41 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
43 def valid_restore_path(path):
44 path = os.path.normpath(path)
45 if path.startswith('/'):
50 def parse_owner_mappings(type, options, fatal):
51 """Traverse the options and parse all --map-TYPEs, or call Option.fatal()."""
52 opt_name = '--map-' + type
53 value_rx = r'^([^=]+)=([^=]*)$'
54 if type in ('uid', 'gid'):
55 value_rx = r'^(-?[0-9]+)=(-?[0-9]+)$'
58 (option, parameter) = flag
59 if option != opt_name:
61 match = re.match(value_rx, parameter)
63 raise fatal("couldn't parse %s as %s mapping" % (parameter, type))
64 old_id, new_id = match.groups()
65 if type in ('uid', 'gid'):
68 owner_map[old_id] = new_id
71 def apply_metadata(meta, name, restore_numeric_ids, owner_map):
72 m = copy.deepcopy(meta)
73 m.user = owner_map['user'].get(m.user, m.user)
74 m.group = owner_map['group'].get(m.group, m.group)
75 m.uid = owner_map['uid'].get(m.uid, m.uid)
76 m.gid = owner_map['gid'].get(m.gid, m.gid)
77 m.apply_to_path(name, restore_numeric_ids = restore_numeric_ids)
79 def hardlink_compatible(prev_path, prev_item, new_item, top):
80 prev_candidate = top + prev_path
81 if not os.path.exists(prev_candidate):
83 prev_meta, new_meta = prev_item.meta, new_item.meta
84 if new_item.oid != prev_item.oid \
85 or new_meta.mtime != prev_meta.mtime \
86 or new_meta.ctime != prev_meta.ctime \
87 or new_meta.mode != prev_meta.mode:
89 # FIXME: should we be checking the path on disk, or the recorded metadata?
90 # The exists() above might seem to suggest the former.
91 if not new_meta.same_file(prev_meta):
95 def hardlink_if_possible(fullname, item, top, hardlinks):
96 """Find a suitable hardlink target, link to it, and return true,
97 otherwise return false."""
98 # The cwd will be dirname(fullname), and fullname will be
99 # absolute, i.e. /foo/bar, and the caller is expected to handle
100 # restoring the metadata if hardlinking isn't possible.
102 # FIXME: we can probably replace the target_vfs_path with the
105 # hardlinks tracks a list of (restore_path, vfs_path, meta)
106 # triples for each path we've written for a given hardlink_target.
107 # This allows us to handle the case where we restore a set of
108 # hardlinks out of order (with respect to the original save
109 # call(s)) -- i.e. when we don't restore the hardlink_target path
110 # first. This data also allows us to attempt to handle other
111 # situations like hardlink sets that change on disk during a save,
112 # or between index and save.
114 target = item.meta.hardlink_target
116 assert(fullname.startswith('/'))
117 target_versions = hardlinks.get(target)
119 # Check every path in the set that we've written so far for a match.
120 for prev_path, prev_item in target_versions:
121 if hardlink_compatible(prev_path, prev_item, item, top):
123 os.link(top + prev_path, top + fullname)
126 if e.errno != errno.EXDEV:
130 hardlinks[target] = target_versions
131 target_versions.append((fullname, item))
134 def write_file_content(repo, dest_path, vfs_file):
135 with vfs2.fopen(repo, vfs_file) as inf:
136 with open(dest_path, 'wb') as outf:
137 for b in chunkyreader(inf):
140 def write_file_content_sparsely(repo, dest_path, vfs_file):
141 with vfs2.fopen(repo, vfs_file) as inf:
142 outfd = os.open(dest_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
145 for b in chunkyreader(inf):
146 trailing_zeros = write_sparsely(outfd, b, 512, trailing_zeros)
147 pos = os.lseek(outfd, trailing_zeros, os.SEEK_END)
148 os.ftruncate(outfd, pos)
152 def restore(repo, parent_path, name, item, top, sparse, numeric_ids, owner_map,
153 exclude_rxs, verbosity, hardlinks):
154 global total_restored
155 mode = vfs2.item_mode(item)
156 treeish = S_ISDIR(mode)
157 fullname = parent_path + '/' + name
158 # Match behavior of index --exclude-rx with respect to paths.
159 if should_rx_exclude_path(fullname + ('/' if treeish else ''),
164 # Do this now so we'll have meta.symlink_target for verbose output
165 item = vfs2.augment_item_meta(repo, item, include_size=True)
167 assert(meta.mode == mode)
169 if stat.S_ISDIR(mode):
171 print('%s/' % fullname)
172 elif stat.S_ISLNK(mode):
173 assert(meta.symlink_target)
175 print('%s@ -> %s' % (fullname, meta.symlink_target))
180 orig_cwd = os.getcwd()
183 # Assumes contents() returns '.' with the full metadata first
184 sub_items = vfs2.contents(repo, item, want_meta=True)
185 dot, item = next(sub_items, None)
187 item = vfs2.augment_item_meta(repo, item, include_size=True)
189 meta.create_path(name)
193 qprogress('Restoring: %d\r' % total_restored)
194 for sub_name, sub_item in sub_items:
195 restore(repo, fullname, sub_name, sub_item, top, sparse,
196 numeric_ids, owner_map, exclude_rxs, verbosity,
199 apply_metadata(meta, name, numeric_ids, owner_map)
201 created_hardlink = False
202 if meta.hardlink_target:
203 created_hardlink = hardlink_if_possible(fullname, item, top,
205 if not created_hardlink:
206 meta.create_path(name)
207 if stat.S_ISREG(meta.mode):
209 write_file_content_sparsely(repo, name, item)
211 write_file_content(repo, name, item)
214 qprogress('Restoring: %d\r' % total_restored)
215 if not created_hardlink:
216 apply_metadata(meta, name, numeric_ids, owner_map)
221 o = options.Options(optspec)
222 opt, flags, extra = o.parse(sys.argv[1:])
223 verbosity = opt.verbose if not opt.quiet else -1
225 git.check_repo_or_die()
228 o.fatal('must specify at least one filename to restore')
230 exclude_rxs = parse_rx_excludes(flags, o.fatal)
233 for map_type in ('user', 'group', 'uid', 'gid'):
234 owner_map[map_type] = parse_owner_mappings(map_type, flags, o.fatal)
244 if not valid_restore_path(path):
245 add_error("path %r doesn't include a branch and revision" % path)
248 resolved = vfs2.lresolve(repo, path, want_meta=True)
249 except vfs2.IOError as e:
252 path_parent, path_name = os.path.split(path)
253 leaf_name, leaf_item = resolved[-1]
255 add_error('error: cannot access %r in %r'
256 % ('/'.join(name for name, item in resolved),
259 if not path_name or path_name == '.':
260 # Source is /foo/what/ever/ or /foo/what/ever/. -- extract
261 # what/ever/* to the current directory, and if name == '.'
262 # (i.e. /foo/what/ever/.), then also restore what/ever's
263 # metadata to the current directory.
264 treeish = vfs2.item_mode(leaf_item)
266 add_error('%r cannot be restored as a directory' % path)
268 items = vfs2.contents(repo, leaf_item, want_meta=True)
269 dot, leaf_item = next(items, None)
271 for sub_name, sub_item in items:
272 restore(repo, '', sub_name, sub_item, top,
273 opt.sparse, opt.numeric_ids, owner_map,
274 exclude_rxs, verbosity, hardlinks)
276 leaf_item = vfs2.augment_item_meta(repo, leaf_item,
278 apply_metadata(leaf_item.meta, '.',
279 opt.numeric_ids, owner_map)
281 restore(repo, '', leaf_name, leaf_item, top,
282 opt.sparse, opt.numeric_ids, owner_map,
283 exclude_rxs, verbosity, hardlinks)
286 progress('Restoring: %d, done.\n' % total_restored)