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