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