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