]> arthur.barton.de Git - bup.git/blob - cmd/save-cmd.py
save-cmd: byte count was missing a few files.
[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(ent):
111     return not already_saved(ent)
112
113 total = ftotal = 0
114 if opt.progress:
115     for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse):
116         if not (ftotal % 10024):
117             progress('Reading index: %d\r' % ftotal)
118         exists = (ent.flags & index.IX_EXISTS)
119         hashvalid = already_saved(ent)
120         if exists and not hashvalid:
121             total += ent.size
122         ftotal += 1
123     progress('Reading index: %d, done.\n' % ftotal)
124     hashsplit.progress_callback = progress_report
125
126 tstart = time.time()
127 count = subcount = fcount = 0
128 for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse):
129     (dir, file) = os.path.split(ent.name)
130     exists = (ent.flags & index.IX_EXISTS)
131     hashvalid = already_saved(ent)
132     if opt.verbose:
133         if not exists:
134             status = 'D'
135         elif not hashvalid:
136             if ent.sha == index.EMPTY_SHA:
137                 status = 'A'
138             else:
139                 status = 'M'
140         else:
141             status = ' '
142         if opt.verbose >= 2 or stat.S_ISDIR(ent.mode):
143             log('%s %-70s\n' % (status, ent.name))
144
145     if opt.progress:
146         progress_report(0)
147     fcount += 1
148     
149     if not exists:
150         continue
151
152     assert(dir.startswith('/'))
153     dirp = dir.split('/')
154     while parts > dirp:
155         _pop(force_tree = None)
156     if dir != '/':
157         for part in dirp[len(parts):]:
158             _push(part)
159
160     if not file:
161         # sub/parentdirectories already handled in the pop/push() part above.
162         oldtree = already_saved(ent) # may be None
163         newtree = _pop(force_tree = oldtree)
164         if not oldtree:
165             ent.validate(040000, newtree)
166             ent.repack()
167         if exists and not hashvalid:
168             count += ent.size
169         continue
170
171     # it's not a directory
172     id = None
173     if hashvalid:
174         mode = '%o' % ent.gitmode
175         id = ent.sha
176         shalists[-1].append((mode, file, id))
177     elif opt.smaller and ent.size >= opt.smaller:
178         add_error('skipping large file "%s"' % ent.name)
179     else:
180         if stat.S_ISREG(ent.mode):
181             try:
182                 f = open(ent.name)
183             except IOError, e:
184                 add_error(e)
185             except OSError, e:
186                 add_error(e)
187             else:
188                 (mode, id) = hashsplit.split_to_blob_or_tree(w, [f])
189         else:
190             if stat.S_ISDIR(ent.mode):
191                 assert(0)  # handled above
192             elif stat.S_ISLNK(ent.mode):
193                 try:
194                     rl = os.readlink(ent.name)
195                 except OSError, e:
196                     add_error(e)
197                 except IOError, e:
198                     add_error(e)
199                 else:
200                     (mode, id) = ('120000', w.new_blob(rl))
201             else:
202                 add_error(Exception('skipping special file "%s"' % ent.name))
203         if id:
204             ent.validate(int(mode, 8), id)
205             ent.repack()
206             shalists[-1].append((mode, file, id))
207     if exists and not hashvalid:
208         count += ent.size
209         subcount = 0
210
211
212 if opt.progress:
213     pct = total and count*100.0/total or 100
214     progress('Saving: %.2f%% (%d/%dk, %d/%d files), done.    \n'
215              % (pct, count/1024, total/1024, fcount, ftotal))
216
217 while len(parts) > 1:
218     _pop(force_tree = None)
219 assert(len(shalists) == 1)
220 tree = w.new_tree(shalists[-1])
221 if opt.tree:
222     print tree.encode('hex')
223 if opt.commit or opt.name:
224     msg = 'bup save\n\nGenerated by command:\n%r' % sys.argv
225     ref = opt.name and ('refs/heads/%s' % opt.name) or None
226     commit = w.new_commit(oldref, tree, msg)
227     if opt.commit:
228         print commit.encode('hex')
229
230 w.close()  # must close before we can update the ref
231         
232 if opt.name:
233     if cli:
234         cli.update_ref(refname, commit, oldref)
235     else:
236         git.update_ref(refname, commit, oldref)
237
238 if cli:
239     cli.close()
240
241 if saved_errors:
242     log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))