]> arthur.barton.de Git - bup.git/blob - cmd/restore-cmd.py
Avoid partial writes of config/config.h.
[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_node(top, n, meta=None):
120     # meta will be None for dirs, and when there is no .bupm (i.e. no metadata)
121     global total_restored, opt
122     meta_stream = None
123     try:
124         fullname = n.fullname(stop_at=top)
125         # If this is a directory, its metadata is the first entry in
126         # any .bupm file inside the directory.  Get it.
127         if(stat.S_ISDIR(n.mode)):
128             mfile = n.metadata_file() # VFS file -- cannot close().
129             if mfile:
130                 meta_stream = mfile.open()
131                 meta = metadata.Metadata.read(meta_stream)
132         print_info(n, fullname)
133
134         created_hardlink = False
135         if meta and meta.hardlink_target:
136             created_hardlink = hardlink_if_possible(fullname, n, meta)
137
138         if not created_hardlink:
139             create_path(n, fullname, meta)
140             if meta:
141                 if stat.S_ISREG(meta.mode):
142                     write_file_content(fullname, n)
143             elif stat.S_ISREG(n.mode):
144                 write_file_content(fullname, n)
145
146         total_restored += 1
147         plog('Restoring: %d\r' % total_restored)
148         for sub in n:
149             m = None
150             # Don't get metadata if this is a dir -- handled in sub do_node().
151             if meta_stream and not stat.S_ISDIR(sub.mode):
152                 m = metadata.Metadata.read(meta_stream)
153             do_node(top, sub, m)
154         if meta and not created_hardlink:
155             meta.apply_to_path(fullname,
156                                restore_numeric_ids=opt.numeric_ids)
157     finally:
158         if meta_stream:
159             meta_stream.close()
160
161 handle_ctrl_c()
162
163 o = options.Options(optspec)
164 (opt, flags, extra) = o.parse(sys.argv[1:])
165
166 git.check_repo_or_die()
167 top = vfs.RefList(None)
168
169 if not extra:
170     o.fatal('must specify at least one filename to restore')
171     
172 if opt.outdir:
173     mkdirp(opt.outdir)
174     os.chdir(opt.outdir)
175
176 ret = 0
177 for d in extra:
178     path,name = os.path.split(d)
179     try:
180         n = top.lresolve(d)
181     except vfs.NodeError, e:
182         add_error(e)
183         continue
184     isdir = stat.S_ISDIR(n.mode)
185     if not name or name == '.':
186         # trailing slash: extract children to cwd
187         if not isdir:
188             add_error('%r: not a directory' % d)
189         else:
190             for sub in n:
191                 do_node(n, sub)
192     else:
193         # no trailing slash: extract node and its children to cwd
194         do_node(n.parent, n)
195
196 if not opt.quiet:
197     progress('Restoring: %d, done.\n' % total_restored)
198
199 if saved_errors:
200     log('WARNING: %d errors encountered while restoring.\n' % len(saved_errors))
201     sys.exit(1)