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