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