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