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