]> arthur.barton.de Git - bup.git/blob - lib/bup/cmd/split.py
6a646e7825871c7488cd068c92c96b2b69582d75
[bup.git] / lib / bup / cmd / split.py
1
2 from __future__ import absolute_import, division, print_function
3 from binascii import hexlify
4 import sys, time
5
6 from bup import compat, hashsplit, git, options, client
7 from bup.compat import argv_bytes, environ, nullcontext
8 from bup.helpers import (add_error, hostname, log, parse_num,
9                          qprogress, reprogress, saved_errors,
10                          valid_save_name,
11                          parse_date_or_fatal)
12 from bup.io import byte_stream
13 from bup.pwdgrp import userfullname, username
14
15
16 optspec = """
17 bup split [-t] [-c] [-n name] OPTIONS [--git-ids | filenames...]
18 bup split -b OPTIONS [--git-ids | filenames...]
19 bup split --copy OPTIONS [--git-ids | filenames...]
20 bup split --noop [-b|-t] OPTIONS [--git-ids | filenames...]
21 --
22  Modes:
23 b,blobs    output a series of blob ids.  Implies --fanout=0.
24 t,tree     output a tree id
25 c,commit   output a commit id
26 n,name=    save the result under the given name
27 noop       split the input, but throw away the result
28 copy       split the input, copy it to stdout, don't save to repo
29  Options:
30 r,remote=  remote repository path
31 d,date=    date for the commit (seconds since the epoch)
32 q,quiet    don't print progress messages
33 v,verbose  increase log output (can be used more than once)
34 git-ids    read a list of git object ids from stdin and split their contents
35 keep-boundaries  don't let one chunk span two input files
36 bench      print benchmark timings to stderr
37 max-pack-size=  maximum bytes in a single pack
38 max-pack-objects=  maximum number of objects in a single pack
39 fanout=    average number of blobs in a single tree
40 bwlimit=   maximum bytes/sec to transmit to server
41 #,compress=  set compression level to # (0-9, 9 is highest) [1]
42 """
43
44
45 class NoOpPackWriter:
46     def __init__(self):
47         self.closed = False
48     def __enter__(self):
49         return self
50     def __exit__(self, type, value, traceback):
51         self.close()
52     def close(self):
53         self.closed = True
54     def __del__(self):
55         assert self.closed
56     def new_blob(self, content):
57         return git.calc_hash(b'blob', content)
58     def new_tree(self, shalist):
59         return git.calc_hash(b'tree', git.tree_encode(shalist))
60
61 def opts_from_cmdline(argv):
62     o = options.Options(optspec)
63     opt, flags, extra = o.parse_bytes(argv[1:])
64     opt.sources = extra
65
66     if opt.name: opt.name = argv_bytes(opt.name)
67     if opt.remote: opt.remote = argv_bytes(opt.remote)
68     if opt.verbose is None: opt.verbose = 0
69
70     if not (opt.blobs or opt.tree or opt.commit or opt.name or
71             opt.noop or opt.copy):
72         o.fatal("use one or more of -b, -t, -c, -n, --noop, --copy")
73     if opt.copy and (opt.blobs or opt.tree):
74         o.fatal('--copy is incompatible with -b, -t')
75     if (opt.noop or opt.copy) and (opt.commit or opt.name):
76         o.fatal('--noop and --copy are incompatible with -c, -n')
77     if opt.blobs and (opt.tree or opt.commit or opt.name):
78         o.fatal('-b is incompatible with -t, -c, -n')
79     if extra and opt.git_ids:
80         o.fatal("don't provide filenames when using --git-ids")
81     if opt.verbose >= 2:
82         git.verbose = opt.verbose - 1
83         opt.bench = 1
84     if opt.max_pack_size:
85         opt.max_pack_size = parse_num(opt.max_pack_size)
86     if opt.max_pack_objects:
87         opt.max_pack_objects = parse_num(opt.max_pack_objects)
88     if opt.fanout:
89         opt.fanout = parse_num(opt.fanout)
90     if opt.bwlimit:
91         opt.bwlimit = parse_num(opt.bwlimit)
92     if opt.date:
93         opt.date = parse_date_or_fatal(opt.date, o.fatal)
94     else:
95         opt.date = time.time()
96
97     opt.is_reverse = environ.get(b'BUP_SERVER_REVERSE')
98     if opt.is_reverse and opt.remote:
99         o.fatal("don't use -r in reverse mode; it's automatic")
100
101     if opt.name and not valid_save_name(opt.name):
102         o.fatal("'%r' is not a valid branch name." % opt.name)
103
104     return opt
105
106 def split(opt, files, parent, out, pack_writer):
107     # Hack around lack of nonlocal vars in python 2
108     total_bytes = [0]
109     def prog(filenum, nbytes):
110         total_bytes[0] += nbytes
111         if filenum > 0:
112             qprogress('Splitting: file #%d, %d kbytes\r'
113                       % (filenum+1, total_bytes[0] // 1024))
114         else:
115             qprogress('Splitting: %d kbytes\r' % (total_bytes[0] // 1024))
116
117     new_blob = pack_writer.new_blob
118     new_tree = pack_writer.new_tree
119     if opt.blobs:
120         shalist = hashsplit.split_to_blobs(new_blob, files,
121                                            keep_boundaries=opt.keep_boundaries,
122                                            progress=prog)
123         for sha, size, level in shalist:
124             out.write(hexlify(sha) + b'\n')
125             reprogress()
126     elif opt.tree or opt.commit or opt.name:
127         if opt.name: # insert dummy_name which may be used as a restore target
128             mode, sha = \
129                 hashsplit.split_to_blob_or_tree(new_blob, new_tree, files,
130                                                 keep_boundaries=opt.keep_boundaries,
131                                                 progress=prog)
132             splitfile_name = git.mangle_name(b'data', hashsplit.GIT_MODE_FILE, mode)
133             shalist = [(mode, splitfile_name, sha)]
134         else:
135             shalist = \
136                 hashsplit.split_to_shalist(new_blob, new_tree, files,
137                                            keep_boundaries=opt.keep_boundaries,
138                                            progress=prog)
139         tree = new_tree(shalist)
140     else:
141         last = 0
142         it = hashsplit.hashsplit_iter(files,
143                                       keep_boundaries=opt.keep_boundaries,
144                                       progress=prog)
145         for blob, level in it:
146             hashsplit.total_split += len(blob)
147             if opt.copy:
148                 sys.stdout.write(str(blob))
149             megs = hashsplit.total_split // 1024 // 1024
150             if not opt.quiet and last != megs:
151                 last = megs
152
153     if opt.verbose:
154         log('\n')
155     if opt.tree:
156         out.write(hexlify(tree) + b'\n')
157
158     commit = None
159     if opt.commit or opt.name:
160         msg = b'bup split\n\nGenerated by command:\n%r\n' % compat.get_argvb()
161         userline = b'%s <%s@%s>' % (userfullname(), username(), hostname())
162         commit = pack_writer.new_commit(tree, parent, userline, opt.date,
163                                         None, userline, opt.date, None, msg)
164         if opt.commit:
165             out.write(hexlify(commit) + b'\n')
166
167     return commit
168
169 def main(argv):
170     opt = opts_from_cmdline(argv)
171     if opt.verbose >= 2:
172         git.verbose = opt.verbose - 1
173     if opt.fanout:
174         hashsplit.fanout = opt.fanout
175     if opt.blobs:
176         hashsplit.fanout = 0
177     if opt.bwlimit:
178         client.bwlimit = opt.bwlimit
179
180     start_time = time.time()
181
182     sys.stdout.flush()
183     out = byte_stream(sys.stdout)
184     stdin = byte_stream(sys.stdin)
185
186     if opt.git_ids:
187         # the input is actually a series of git object ids that we should retrieve
188         # and split.
189         #
190         # This is a bit messy, but basically it converts from a series of
191         # CatPipe.get() iterators into a series of file-type objects.
192         # It would be less ugly if either CatPipe.get() returned a file-like object
193         # (not very efficient), or split_to_shalist() expected an iterator instead
194         # of a file.
195         cp = git.CatPipe()
196         class IterToFile:
197             def __init__(self, it):
198                 self.it = iter(it)
199             def read(self, size):
200                 v = next(self.it, None)
201                 return v or b''
202         def read_ids():
203             while 1:
204                 line = stdin.readline()
205                 if not line:
206                     break
207                 if line:
208                     line = line.strip()
209                 try:
210                     it = cp.get(line.strip())
211                     next(it, None)  # skip the file info
212                 except KeyError as e:
213                     add_error('error: %s' % e)
214                     continue
215                 yield IterToFile(it)
216         files = read_ids()
217     else:
218         # the input either comes from a series of files or from stdin.
219         if opt.sources:
220             files = (open(argv_bytes(fn), 'rb') for fn in opt.sources)
221         else:
222             files = [stdin]
223
224     writing = not (opt.noop or opt.copy)
225     remote_dest = opt.remote or opt.is_reverse
226
227     if writing:
228         git.check_repo_or_die()
229
230     if remote_dest and writing:
231         cli = repo = client.Client(opt.remote)
232     else:
233         cli = nullcontext()
234         repo = git
235
236     # cli creation must be last nontrivial command in each if clause above
237     with cli:
238         if opt.name and writing:
239             refname = opt.name and b'refs/heads/%s' % opt.name
240             oldref = repo.read_ref(refname)
241         else:
242             refname = oldref = None
243
244         if not writing:
245             pack_writer = NoOpPackWriter()
246         elif not remote_dest:
247             pack_writer = git.PackWriter(compression_level=opt.compress,
248                                          max_pack_size=opt.max_pack_size,
249                                          max_pack_objects=opt.max_pack_objects)
250         else:
251             pack_writer = cli.new_packwriter(compression_level=opt.compress,
252                                              max_pack_size=opt.max_pack_size,
253                                              max_pack_objects=opt.max_pack_objects)
254
255         # packwriter creation must be last command in each if clause above
256         with pack_writer:
257             commit = split(opt, files, oldref, out, pack_writer)
258
259         # pack_writer must be closed before we can update the ref
260         if refname:
261             repo.update_ref(refname, commit, oldref)
262
263     secs = time.time() - start_time
264     size = hashsplit.total_split
265     if opt.bench:
266         log('bup: %.2f kbytes in %.2f secs = %.2f kbytes/sec\n'
267             % (size / 1024, secs, size / 1024 / secs))
268
269     if saved_errors:
270         log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))
271         sys.exit(1)