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