]> arthur.barton.de Git - bup.git/blob - cmd/save-cmd.py
save-cmd: progress meter wouldn't count identical files correctly.
[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
29 refname = opt.name and 'refs/heads/%s' % opt.name or None
30 if opt.remote:
31     cli = client.Client(opt.remote)
32     oldref = refname and cli.read_ref(refname) or None
33     w = cli.new_packwriter()
34 else:
35     cli = None
36     oldref = refname and git.read_ref(refname) or None
37     w = git.PackWriter()
38
39
40 def eatslash(dir):
41     if dir.endswith('/'):
42         return dir[:-1]
43     else:
44         return dir
45
46
47 parts = ['']
48 shalists = [[]]
49
50 def _push(part):
51     assert(part)
52     parts.append(part)
53     shalists.append([])
54
55 def _pop(force_tree):
56     assert(len(parts) >= 1)
57     part = parts.pop()
58     shalist = shalists.pop()
59     tree = force_tree or w.new_tree(shalist)
60     if shalists:
61         shalists[-1].append(('40000', part, tree))
62     else:  # this was the toplevel, so put it back for sanity
63         shalists.append(shalist)
64     return tree
65
66 lastremain = None
67 def progress_report(n):
68     global count, subcount, lastremain
69     subcount += n
70     cc = count + subcount
71     pct = total and (cc*100.0/total) or 0
72     now = time.time()
73     elapsed = now - tstart
74     kps = elapsed and int(cc/1024./elapsed)
75     kps_frac = 10 ** int(math.log(kps+1, 10) - 1)
76     kps = int(kps/kps_frac)*kps_frac
77     if cc:
78         remain = elapsed*1.0/cc * (total-cc)
79     else:
80         remain = 0.0
81     if (lastremain and (remain > lastremain)
82           and ((remain - lastremain)/lastremain < 0.05)):
83         remain = lastremain
84     else:
85         lastremain = remain
86     hours = int(remain/60/60)
87     mins = int(remain/60 - hours*60)
88     secs = int(remain - hours*60*60 - mins*60)
89     if elapsed < 30:
90         remainstr = ''
91         kpsstr = ''
92     else:
93         kpsstr = '%dk/s' % kps
94         if hours:
95             remainstr = '%dh%dm' % (hours, mins)
96         elif mins:
97             remainstr = '%dm%d' % (mins, secs)
98         else:
99             remainstr = '%ds' % secs
100     progress('Saving: %.2f%% (%d/%dk, %d/%d files) %s %s\r'
101              % (pct, cc/1024, total/1024, fcount, ftotal,
102                 remainstr, kpsstr))
103
104
105 r = index.Reader(git.repo('bupindex'))
106
107 def already_saved(ent):
108     return ent.is_valid() and w.exists(ent.sha) and ent.sha
109
110 def wantrecurse_pre(ent):
111     return not already_saved(ent)
112
113 def wantrecurse_during(ent):
114     return not already_saved(ent) or ent.sha_missing()
115
116 total = ftotal = 0
117 if opt.progress or 1:
118     for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse_pre):
119         if not (ftotal % 10024):
120             progress('Reading index: %d\r' % ftotal)
121         exists = ent.exists()
122         hashvalid = already_saved(ent)
123         ent.set_sha_missing(not hashvalid)
124         if exists and not hashvalid:
125             total += ent.size
126         ftotal += 1
127     progress('Reading index: %d, done.\n' % ftotal)
128     hashsplit.progress_callback = progress_report
129
130 tstart = time.time()
131 count = subcount = fcount = 0
132 for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse_during):
133     (dir, file) = os.path.split(ent.name)
134     exists = (ent.flags & index.IX_EXISTS)
135     hashvalid = already_saved(ent)
136     oldsize = ent.size
137     if opt.verbose:
138         if not exists:
139             status = 'D'
140         elif not hashvalid:
141             if ent.sha == index.EMPTY_SHA:
142                 status = 'A'
143             else:
144                 status = 'M'
145         else:
146             status = ' '
147         if opt.verbose >= 2 or stat.S_ISDIR(ent.mode):
148             log('%s %-70s\n' % (status, ent.name))
149
150     if opt.progress:
151         progress_report(0)
152     fcount += 1
153     
154     if not exists:
155         continue
156
157     assert(dir.startswith('/'))
158     dirp = dir.split('/')
159     while parts > dirp:
160         _pop(force_tree = None)
161     if dir != '/':
162         for part in dirp[len(parts):]:
163             _push(part)
164
165     if not file:
166         # sub/parentdirectories already handled in the pop/push() part above.
167         oldtree = already_saved(ent) # may be None
168         newtree = _pop(force_tree = oldtree)
169         if not oldtree:
170             ent.validate(040000, newtree)
171             ent.repack()
172         if exists and ent.sha_missing():
173             count += oldsize
174         continue
175
176     # it's not a directory
177     id = None
178     if hashvalid:
179         mode = '%o' % ent.gitmode
180         id = ent.sha
181         shalists[-1].append((mode, file, id))
182     elif opt.smaller and ent.size >= opt.smaller:
183         add_error('skipping large file "%s"' % ent.name)
184     else:
185         if stat.S_ISREG(ent.mode):
186             try:
187                 f = open(ent.name)
188             except IOError, e:
189                 add_error(e)
190             except OSError, e:
191                 add_error(e)
192             else:
193                 (mode, id) = hashsplit.split_to_blob_or_tree(w, [f])
194         else:
195             if stat.S_ISDIR(ent.mode):
196                 assert(0)  # handled above
197             elif stat.S_ISLNK(ent.mode):
198                 try:
199                     rl = os.readlink(ent.name)
200                 except OSError, e:
201                     add_error(e)
202                 except IOError, e:
203                     add_error(e)
204                 else:
205                     (mode, id) = ('120000', w.new_blob(rl))
206             else:
207                 add_error(Exception('skipping special file "%s"' % ent.name))
208         if id:
209             ent.validate(int(mode, 8), id)
210             ent.repack()
211             shalists[-1].append((mode, file, id))
212     if exists and ent.sha_missing():
213         count += oldsize
214         subcount = 0
215
216
217 if opt.progress:
218     pct = total and count*100.0/total or 100
219     progress('Saving: %.2f%% (%d/%dk, %d/%d files), done.    \n'
220              % (pct, count/1024, total/1024, fcount, ftotal))
221
222 while len(parts) > 1:
223     _pop(force_tree = None)
224 assert(len(shalists) == 1)
225 tree = w.new_tree(shalists[-1])
226 if opt.tree:
227     print tree.encode('hex')
228 if opt.commit or opt.name:
229     msg = 'bup save\n\nGenerated by command:\n%r' % sys.argv
230     ref = opt.name and ('refs/heads/%s' % opt.name) or None
231     commit = w.new_commit(oldref, tree, msg)
232     if opt.commit:
233         print commit.encode('hex')
234
235 w.close()  # must close before we can update the ref
236         
237 if opt.name:
238     if cli:
239         cli.update_ref(refname, commit, oldref)
240     else:
241         git.update_ref(refname, commit, oldref)
242
243 if cli:
244     cli.close()
245
246 if saved_errors:
247     log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))