]> arthur.barton.de Git - bup.git/blob - cmd/save-cmd.py
rbackup-cmd: we can now backup a *remote* machine to a *local* server.
[bup.git] / cmd / save-cmd.py
1 #!/usr/bin/env python
2 import sys, re, errno, stat, time, math
3 from bup import hashsplit, git, options, index, client
4 from bup.helpers import *
5
6
7 optspec = """
8 bup save [-tc] [-n name] <filenames...>
9 --
10 r,remote=  remote repository path
11 t,tree     output a tree id
12 c,commit   output a commit id
13 n,name=    name of backup set to update (if any)
14 v,verbose  increase log output (can be used more than once)
15 q,quiet    don't show progress meter
16 smaller=   only back up files smaller than n bytes
17 """
18 o = options.Options('bup save', optspec)
19 (opt, flags, extra) = o.parse(sys.argv[1:])
20
21 git.check_repo_or_die()
22 if not (opt.tree or opt.commit or opt.name):
23     o.fatal("use one or more of -t, -c, -n")
24 if not extra:
25     o.fatal("no filenames given")
26
27 opt.progress = (istty and not opt.quiet)
28 opt.smaller = parse_num(opt.smaller or 0)
29
30 is_reverse = os.environ.get('BUP_SERVER_REVERSE')
31 if is_reverse and opt.remote:
32     o.fatal("don't use -r in reverse mode; it's automatic")
33
34 refname = opt.name and 'refs/heads/%s' % opt.name or None
35 if opt.remote or is_reverse:
36     cli = client.Client(opt.remote)
37     oldref = refname and cli.read_ref(refname) or None
38     w = cli.new_packwriter()
39 else:
40     cli = None
41     oldref = refname and git.read_ref(refname) or None
42     w = git.PackWriter()
43
44 handle_ctrl_c()
45
46
47 def eatslash(dir):
48     if dir.endswith('/'):
49         return dir[:-1]
50     else:
51         return dir
52
53
54 parts = ['']
55 shalists = [[]]
56
57 def _push(part):
58     assert(part)
59     parts.append(part)
60     shalists.append([])
61
62 def _pop(force_tree):
63     assert(len(parts) >= 1)
64     part = parts.pop()
65     shalist = shalists.pop()
66     tree = force_tree or w.new_tree(shalist)
67     if shalists:
68         shalists[-1].append(('40000', part, tree))
69     else:  # this was the toplevel, so put it back for sanity
70         shalists.append(shalist)
71     return tree
72
73 lastremain = None
74 def progress_report(n):
75     global count, subcount, lastremain
76     subcount += n
77     cc = count + subcount
78     pct = total and (cc*100.0/total) or 0
79     now = time.time()
80     elapsed = now - tstart
81     kps = elapsed and int(cc/1024./elapsed)
82     kps_frac = 10 ** int(math.log(kps+1, 10) - 1)
83     kps = int(kps/kps_frac)*kps_frac
84     if cc:
85         remain = elapsed*1.0/cc * (total-cc)
86     else:
87         remain = 0.0
88     if (lastremain and (remain > lastremain)
89           and ((remain - lastremain)/lastremain < 0.05)):
90         remain = lastremain
91     else:
92         lastremain = remain
93     hours = int(remain/60/60)
94     mins = int(remain/60 - hours*60)
95     secs = int(remain - hours*60*60 - mins*60)
96     if elapsed < 30:
97         remainstr = ''
98         kpsstr = ''
99     else:
100         kpsstr = '%dk/s' % kps
101         if hours:
102             remainstr = '%dh%dm' % (hours, mins)
103         elif mins:
104             remainstr = '%dm%d' % (mins, secs)
105         else:
106             remainstr = '%ds' % secs
107     progress('Saving: %.2f%% (%d/%dk, %d/%d files) %s %s\r'
108              % (pct, cc/1024, total/1024, fcount, ftotal,
109                 remainstr, kpsstr))
110
111
112 r = index.Reader(git.repo('bupindex'))
113
114 def already_saved(ent):
115     return ent.is_valid() and w.exists(ent.sha) and ent.sha
116
117 def wantrecurse_pre(ent):
118     return not already_saved(ent)
119
120 def wantrecurse_during(ent):
121     return not already_saved(ent) or ent.sha_missing()
122
123 total = ftotal = 0
124 if opt.progress:
125     for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse_pre):
126         if not (ftotal % 10024):
127             progress('Reading index: %d\r' % ftotal)
128         exists = ent.exists()
129         hashvalid = already_saved(ent)
130         ent.set_sha_missing(not hashvalid)
131         if not opt.smaller or ent.size < opt.smaller:
132             if exists and not hashvalid:
133                 total += ent.size
134         ftotal += 1
135     progress('Reading index: %d, done.\n' % ftotal)
136     hashsplit.progress_callback = progress_report
137
138 tstart = time.time()
139 count = subcount = fcount = 0
140 lastskip_name = None
141 lastdir = ''
142 for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse_during):
143     (dir, file) = os.path.split(ent.name)
144     exists = (ent.flags & index.IX_EXISTS)
145     hashvalid = already_saved(ent)
146     wasmissing = ent.sha_missing()
147     oldsize = ent.size
148     if opt.verbose:
149         if not exists:
150             status = 'D'
151         elif not hashvalid:
152             if ent.sha == index.EMPTY_SHA:
153                 status = 'A'
154             else:
155                 status = 'M'
156         else:
157             status = ' '
158         if opt.verbose >= 2:
159             log('%s %-70s\n' % (status, ent.name))
160         elif not stat.S_ISDIR(ent.mode) and lastdir != dir:
161             if not lastdir.startswith(dir):
162                 log('%s %-70s\n' % (status, os.path.join(dir, '')))
163             lastdir = dir
164
165     if opt.progress:
166         progress_report(0)
167     fcount += 1
168     
169     if not exists:
170         continue
171     if opt.smaller and ent.size >= opt.smaller:
172         if exists and not hashvalid:
173             add_error('skipping large file "%s"' % ent.name)
174             lastskip_name = ent.name
175         continue
176
177     assert(dir.startswith('/'))
178     dirp = dir.split('/')
179     while parts > dirp:
180         _pop(force_tree = None)
181     if dir != '/':
182         for part in dirp[len(parts):]:
183             _push(part)
184
185     if not file:
186         # sub/parentdirectories already handled in the pop/push() part above.
187         oldtree = already_saved(ent) # may be None
188         newtree = _pop(force_tree = oldtree)
189         if not oldtree:
190             if lastskip_name and lastskip_name.startswith(ent.name):
191                 ent.invalidate()
192             else:
193                 ent.validate(040000, newtree)
194             ent.repack()
195         if exists and wasmissing:
196             count += oldsize
197         continue
198
199     # it's not a directory
200     id = None
201     if hashvalid:
202         mode = '%o' % ent.gitmode
203         id = ent.sha
204         shalists[-1].append((mode, file, id))
205     else:
206         if stat.S_ISREG(ent.mode):
207             try:
208                 f = hashsplit.open_noatime(ent.name)
209             except IOError, e:
210                 add_error(e)
211                 lastskip_name = ent.name
212             except OSError, e:
213                 add_error(e)
214                 lastskip_name = ent.name
215             else:
216                 (mode, id) = hashsplit.split_to_blob_or_tree(w, [f])
217         else:
218             if stat.S_ISDIR(ent.mode):
219                 assert(0)  # handled above
220             elif stat.S_ISLNK(ent.mode):
221                 try:
222                     rl = os.readlink(ent.name)
223                 except OSError, e:
224                     add_error(e)
225                     lastskip_name = ent.name
226                 except IOError, e:
227                     add_error(e)
228                     lastskip_name = ent.name
229                 else:
230                     (mode, id) = ('120000', w.new_blob(rl))
231             else:
232                 add_error(Exception('skipping special file "%s"' % ent.name))
233                 lastskip_name = ent.name
234         if id:
235             ent.validate(int(mode, 8), id)
236             ent.repack()
237             shalists[-1].append((mode, file, id))
238     if exists and wasmissing:
239         count += oldsize
240         subcount = 0
241
242
243 if opt.progress:
244     pct = total and count*100.0/total or 100
245     progress('Saving: %.2f%% (%d/%dk, %d/%d files), done.    \n'
246              % (pct, count/1024, total/1024, fcount, ftotal))
247
248 while len(parts) > 1:
249     _pop(force_tree = None)
250 assert(len(shalists) == 1)
251 tree = w.new_tree(shalists[-1])
252 if opt.tree:
253     print tree.encode('hex')
254 if opt.commit or opt.name:
255     msg = 'bup save\n\nGenerated by command:\n%r' % sys.argv
256     ref = opt.name and ('refs/heads/%s' % opt.name) or None
257     commit = w.new_commit(oldref, tree, msg)
258     if opt.commit:
259         print commit.encode('hex')
260
261 w.close()  # must close before we can update the ref
262         
263 if opt.name:
264     if cli:
265         cli.update_ref(refname, commit, oldref)
266     else:
267         git.update_ref(refname, commit, oldref)
268
269 if cli:
270     cli.close()
271
272 if saved_errors:
273     log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))
274     sys.exit(1)