]> arthur.barton.de Git - bup.git/blob - cmd/restore-cmd.py
Change index; include atime; pack time as xstat timespec; use ns in memory.
[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 do_root(n):
120     # Very similar to do_node(), except that this function doesn't
121     # create a path for n's destination directory (and so ignores
122     # n.fullname).  It assumes the destination is '.', and restores
123     # n's metadata and content there.
124     global total_restored, opt
125     meta_stream = None
126     try:
127         # Directory metadata is the first entry in any .bupm file in
128         # the directory.  Get it.
129         mfile = n.metadata_file() # VFS file -- cannot close().
130         if mfile:
131             meta_stream = mfile.open()
132             meta = metadata.Metadata.read(meta_stream)
133         print_info(n, '.')
134         total_restored += 1
135         plog('Restoring: %d\r' % total_restored)
136         for sub in n:
137             m = None
138             # Don't get metadata if this is a dir -- handled in sub do_node().
139             if meta_stream and not stat.S_ISDIR(sub.mode):
140                 m = metadata.Metadata.read(meta_stream)
141             do_node(n, sub, m)
142         if meta:
143             meta.apply_to_path('.', restore_numeric_ids = opt.numeric_ids)
144     finally:
145         if meta_stream:
146             meta_stream.close()
147
148
149 def do_node(top, n, meta=None):
150     # Create n.fullname(), relative to the current directory, and
151     # restore all of its metadata, when available.  The meta argument
152     # will be None for dirs, or when there is no .bupm (i.e. no
153     # metadata).
154     global total_restored, opt
155     meta_stream = None
156     try:
157         fullname = n.fullname(stop_at=top)
158         # If this is a directory, its metadata is the first entry in
159         # any .bupm file inside the directory.  Get it.
160         if(stat.S_ISDIR(n.mode)):
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, fullname)
166
167         created_hardlink = False
168         if meta and meta.hardlink_target:
169             created_hardlink = hardlink_if_possible(fullname, n, meta)
170
171         if not created_hardlink:
172             create_path(n, fullname, meta)
173             if meta:
174                 if stat.S_ISREG(meta.mode):
175                     write_file_content(fullname, n)
176             elif stat.S_ISREG(n.mode):
177                 write_file_content(fullname, n)
178
179         total_restored += 1
180         plog('Restoring: %d\r' % total_restored)
181         for sub in n:
182             m = None
183             # Don't get metadata if this is a dir -- handled in sub do_node().
184             if meta_stream and not stat.S_ISDIR(sub.mode):
185                 m = metadata.Metadata.read(meta_stream)
186             do_node(top, sub, m)
187         if meta and not created_hardlink:
188             meta.apply_to_path(fullname, restore_numeric_ids = opt.numeric_ids)
189     finally:
190         if meta_stream:
191             meta_stream.close()
192
193 handle_ctrl_c()
194
195 o = options.Options(optspec)
196 (opt, flags, extra) = o.parse(sys.argv[1:])
197
198 git.check_repo_or_die()
199 top = vfs.RefList(None)
200
201 if not extra:
202     o.fatal('must specify at least one filename to restore')
203     
204 if opt.outdir:
205     mkdirp(opt.outdir)
206     os.chdir(opt.outdir)
207
208 ret = 0
209 for d in extra:
210     path,name = os.path.split(d)
211     try:
212         n = top.lresolve(d)
213     except vfs.NodeError, e:
214         add_error(e)
215         continue
216     isdir = stat.S_ISDIR(n.mode)
217     if not name or name == '.':
218         # Source is /foo/what/ever/ or /foo/what/ever/. -- extract
219         # what/ever/* to the current directory, and if name == '.'
220         # (i.e. /foo/what/ever/.), then also restore what/ever's
221         # metadata to the current directory.
222         if not isdir:
223             add_error('%r: not a directory' % d)
224         else:
225             if name == '.':
226                 do_root(n)
227             else:
228                 for sub in n:
229                     do_node(n, sub)
230     else:
231         # Source is /foo/what/ever -- extract ./ever to cwd.
232         if isinstance(n, vfs.FakeSymlink):
233             # Source is actually /foo/what, i.e. a top-level commit
234             # like /foo/latest, which is a symlink to ../.commit/SHA.
235             # So dereference it, and restore ../.commit/SHA/. to
236             # "./what/.".
237             target = n.dereference()
238             mkdirp(n.name)
239             os.chdir(n.name)
240             do_root(target)
241         else:
242             do_node(n.parent, n)
243
244 if not opt.quiet:
245     progress('Restoring: %d, done.\n' % total_restored)
246
247 if saved_errors:
248     log('WARNING: %d errors encountered while restoring.\n' % len(saved_errors))
249     sys.exit(1)