##// END OF EJS Templates
gzip: use the stdlib version with python 3 (issue6284)...
marmoute -
r44982:b7ca03df stable
parent child Browse files
Show More
@@ -1,388 +1,390 b''
1 # archival.py - revision archival for mercurial
1 # archival.py - revision archival for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import gzip
10 import gzip
11 import os
11 import os
12 import struct
12 import struct
13 import tarfile
13 import tarfile
14 import time
14 import time
15 import zipfile
15 import zipfile
16 import zlib
16 import zlib
17
17
18 from .i18n import _
18 from .i18n import _
19 from .node import nullrev
19 from .node import nullrev
20 from .pycompat import open
20 from .pycompat import open
21
21
22 from . import (
22 from . import (
23 error,
23 error,
24 formatter,
24 formatter,
25 match as matchmod,
25 match as matchmod,
26 pycompat,
26 pycompat,
27 scmutil,
27 scmutil,
28 util,
28 util,
29 vfs as vfsmod,
29 vfs as vfsmod,
30 )
30 )
31
31
32 stringio = util.stringio
32 stringio = util.stringio
33
33
34 # from unzip source code:
34 # from unzip source code:
35 _UNX_IFREG = 0x8000
35 _UNX_IFREG = 0x8000
36 _UNX_IFLNK = 0xA000
36 _UNX_IFLNK = 0xA000
37
37
38
38
39 def tidyprefix(dest, kind, prefix):
39 def tidyprefix(dest, kind, prefix):
40 '''choose prefix to use for names in archive. make sure prefix is
40 '''choose prefix to use for names in archive. make sure prefix is
41 safe for consumers.'''
41 safe for consumers.'''
42
42
43 if prefix:
43 if prefix:
44 prefix = util.normpath(prefix)
44 prefix = util.normpath(prefix)
45 else:
45 else:
46 if not isinstance(dest, bytes):
46 if not isinstance(dest, bytes):
47 raise ValueError(b'dest must be string if no prefix')
47 raise ValueError(b'dest must be string if no prefix')
48 prefix = os.path.basename(dest)
48 prefix = os.path.basename(dest)
49 lower = prefix.lower()
49 lower = prefix.lower()
50 for sfx in exts.get(kind, []):
50 for sfx in exts.get(kind, []):
51 if lower.endswith(sfx):
51 if lower.endswith(sfx):
52 prefix = prefix[: -len(sfx)]
52 prefix = prefix[: -len(sfx)]
53 break
53 break
54 lpfx = os.path.normpath(util.localpath(prefix))
54 lpfx = os.path.normpath(util.localpath(prefix))
55 prefix = util.pconvert(lpfx)
55 prefix = util.pconvert(lpfx)
56 if not prefix.endswith(b'/'):
56 if not prefix.endswith(b'/'):
57 prefix += b'/'
57 prefix += b'/'
58 # Drop the leading '.' path component if present, so Windows can read the
58 # Drop the leading '.' path component if present, so Windows can read the
59 # zip files (issue4634)
59 # zip files (issue4634)
60 if prefix.startswith(b'./'):
60 if prefix.startswith(b'./'):
61 prefix = prefix[2:]
61 prefix = prefix[2:]
62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
63 raise error.Abort(_(b'archive prefix contains illegal components'))
63 raise error.Abort(_(b'archive prefix contains illegal components'))
64 return prefix
64 return prefix
65
65
66
66
67 exts = {
67 exts = {
68 b'tar': [b'.tar'],
68 b'tar': [b'.tar'],
69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
70 b'tgz': [b'.tgz', b'.tar.gz'],
70 b'tgz': [b'.tgz', b'.tar.gz'],
71 b'zip': [b'.zip'],
71 b'zip': [b'.zip'],
72 b'txz': [b'.txz', b'.tar.xz'],
72 b'txz': [b'.txz', b'.tar.xz'],
73 }
73 }
74
74
75
75
76 def guesskind(dest):
76 def guesskind(dest):
77 for kind, extensions in pycompat.iteritems(exts):
77 for kind, extensions in pycompat.iteritems(exts):
78 if any(dest.endswith(ext) for ext in extensions):
78 if any(dest.endswith(ext) for ext in extensions):
79 return kind
79 return kind
80 return None
80 return None
81
81
82
82
83 def _rootctx(repo):
83 def _rootctx(repo):
84 # repo[0] may be hidden
84 # repo[0] may be hidden
85 for rev in repo:
85 for rev in repo:
86 return repo[rev]
86 return repo[rev]
87 return repo[nullrev]
87 return repo[nullrev]
88
88
89
89
90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
91 # that to global tags. Therefore, use {latesttag} as a substitute when
91 # that to global tags. Therefore, use {latesttag} as a substitute when
92 # the distance is 0, since that will be the list of global tags on ctx.
92 # the distance is 0, since that will be the list of global tags on ctx.
93 _defaultmetatemplate = br'''
93 _defaultmetatemplate = br'''
94 repo: {root}
94 repo: {root}
95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
96 branch: {branch|utf8}
96 branch: {branch|utf8}
97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
98 separate("\n",
98 separate("\n",
99 join(latesttag % "latesttag: {tag}", "\n"),
99 join(latesttag % "latesttag: {tag}", "\n"),
100 "latesttagdistance: {latesttagdistance}",
100 "latesttagdistance: {latesttagdistance}",
101 "changessincelatesttag: {changessincelatesttag}"))}
101 "changessincelatesttag: {changessincelatesttag}"))}
102 '''[
102 '''[
103 1:
103 1:
104 ] # drop leading '\n'
104 ] # drop leading '\n'
105
105
106
106
107 def buildmetadata(ctx):
107 def buildmetadata(ctx):
108 '''build content of .hg_archival.txt'''
108 '''build content of .hg_archival.txt'''
109 repo = ctx.repo()
109 repo = ctx.repo()
110
110
111 opts = {
111 opts = {
112 b'template': repo.ui.config(
112 b'template': repo.ui.config(
113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
114 )
114 )
115 }
115 }
116
116
117 out = util.stringio()
117 out = util.stringio()
118
118
119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
120 fm.startitem()
120 fm.startitem()
121 fm.context(ctx=ctx)
121 fm.context(ctx=ctx)
122 fm.data(root=_rootctx(repo).hex())
122 fm.data(root=_rootctx(repo).hex())
123
123
124 if ctx.rev() is None:
124 if ctx.rev() is None:
125 dirty = b''
125 dirty = b''
126 if ctx.dirty(missing=True):
126 if ctx.dirty(missing=True):
127 dirty = b'+'
127 dirty = b'+'
128 fm.data(dirty=dirty)
128 fm.data(dirty=dirty)
129 fm.end()
129 fm.end()
130
130
131 return out.getvalue()
131 return out.getvalue()
132
132
133
133
134 class tarit(object):
134 class tarit(object):
135 '''write archive to tar file or stream. can write uncompressed,
135 '''write archive to tar file or stream. can write uncompressed,
136 or compress with gzip or bzip2.'''
136 or compress with gzip or bzip2.'''
137
137
138 if True:
138 if pycompat.ispy3:
139 GzipFileWithTime = gzip.GzipFile # camelcase-required
140 else:
139
141
140 class GzipFileWithTime(gzip.GzipFile):
142 class GzipFileWithTime(gzip.GzipFile):
141 def __init__(self, *args, **kw):
143 def __init__(self, *args, **kw):
142 timestamp = None
144 timestamp = None
143 if 'mtime' in kw:
145 if 'mtime' in kw:
144 timestamp = kw.pop('mtime')
146 timestamp = kw.pop('mtime')
145 if timestamp is None:
147 if timestamp is None:
146 self.timestamp = time.time()
148 self.timestamp = time.time()
147 else:
149 else:
148 self.timestamp = timestamp
150 self.timestamp = timestamp
149 gzip.GzipFile.__init__(self, *args, **kw)
151 gzip.GzipFile.__init__(self, *args, **kw)
150
152
151 def _write_gzip_header(self):
153 def _write_gzip_header(self):
152 self.fileobj.write(b'\037\213') # magic header
154 self.fileobj.write(b'\037\213') # magic header
153 self.fileobj.write(b'\010') # compression method
155 self.fileobj.write(b'\010') # compression method
154 fname = self.name
156 fname = self.name
155 if fname and fname.endswith(b'.gz'):
157 if fname and fname.endswith(b'.gz'):
156 fname = fname[:-3]
158 fname = fname[:-3]
157 flags = 0
159 flags = 0
158 if fname:
160 if fname:
159 flags = gzip.FNAME # pytype: disable=module-attr
161 flags = gzip.FNAME # pytype: disable=module-attr
160 self.fileobj.write(pycompat.bytechr(flags))
162 self.fileobj.write(pycompat.bytechr(flags))
161 gzip.write32u( # pytype: disable=module-attr
163 gzip.write32u( # pytype: disable=module-attr
162 self.fileobj, int(self.timestamp)
164 self.fileobj, int(self.timestamp)
163 )
165 )
164 self.fileobj.write(b'\002')
166 self.fileobj.write(b'\002')
165 self.fileobj.write(b'\377')
167 self.fileobj.write(b'\377')
166 if fname:
168 if fname:
167 self.fileobj.write(fname + b'\000')
169 self.fileobj.write(fname + b'\000')
168
170
169 def __init__(self, dest, mtime, kind=b''):
171 def __init__(self, dest, mtime, kind=b''):
170 self.mtime = mtime
172 self.mtime = mtime
171 self.fileobj = None
173 self.fileobj = None
172
174
173 def taropen(mode, name=b'', fileobj=None):
175 def taropen(mode, name=b'', fileobj=None):
174 if kind == b'gz':
176 if kind == b'gz':
175 mode = mode[0:1]
177 mode = mode[0:1]
176 if not fileobj:
178 if not fileobj:
177 fileobj = open(name, mode + b'b')
179 fileobj = open(name, mode + b'b')
178 gzfileobj = self.GzipFileWithTime(
180 gzfileobj = self.GzipFileWithTime(
179 name,
181 name,
180 pycompat.sysstr(mode + b'b'),
182 pycompat.sysstr(mode + b'b'),
181 zlib.Z_BEST_COMPRESSION,
183 zlib.Z_BEST_COMPRESSION,
182 fileobj,
184 fileobj,
183 mtime=mtime,
185 mtime=mtime,
184 )
186 )
185 self.fileobj = gzfileobj
187 self.fileobj = gzfileobj
186 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
188 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
187 name, pycompat.sysstr(mode), gzfileobj
189 name, pycompat.sysstr(mode), gzfileobj
188 )
190 )
189 else:
191 else:
190 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
192 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
191
193
192 if isinstance(dest, bytes):
194 if isinstance(dest, bytes):
193 self.z = taropen(b'w:', name=dest)
195 self.z = taropen(b'w:', name=dest)
194 else:
196 else:
195 self.z = taropen(b'w|', fileobj=dest)
197 self.z = taropen(b'w|', fileobj=dest)
196
198
197 def addfile(self, name, mode, islink, data):
199 def addfile(self, name, mode, islink, data):
198 name = pycompat.fsdecode(name)
200 name = pycompat.fsdecode(name)
199 i = tarfile.TarInfo(name)
201 i = tarfile.TarInfo(name)
200 i.mtime = self.mtime
202 i.mtime = self.mtime
201 i.size = len(data)
203 i.size = len(data)
202 if islink:
204 if islink:
203 i.type = tarfile.SYMTYPE
205 i.type = tarfile.SYMTYPE
204 i.mode = 0o777
206 i.mode = 0o777
205 i.linkname = pycompat.fsdecode(data)
207 i.linkname = pycompat.fsdecode(data)
206 data = None
208 data = None
207 i.size = 0
209 i.size = 0
208 else:
210 else:
209 i.mode = mode
211 i.mode = mode
210 data = stringio(data)
212 data = stringio(data)
211 self.z.addfile(i, data)
213 self.z.addfile(i, data)
212
214
213 def done(self):
215 def done(self):
214 self.z.close()
216 self.z.close()
215 if self.fileobj:
217 if self.fileobj:
216 self.fileobj.close()
218 self.fileobj.close()
217
219
218
220
219 class zipit(object):
221 class zipit(object):
220 '''write archive to zip file or stream. can write uncompressed,
222 '''write archive to zip file or stream. can write uncompressed,
221 or compressed with deflate.'''
223 or compressed with deflate.'''
222
224
223 def __init__(self, dest, mtime, compress=True):
225 def __init__(self, dest, mtime, compress=True):
224 if isinstance(dest, bytes):
226 if isinstance(dest, bytes):
225 dest = pycompat.fsdecode(dest)
227 dest = pycompat.fsdecode(dest)
226 self.z = zipfile.ZipFile(
228 self.z = zipfile.ZipFile(
227 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
229 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
228 )
230 )
229
231
230 # Python's zipfile module emits deprecation warnings if we try
232 # Python's zipfile module emits deprecation warnings if we try
231 # to store files with a date before 1980.
233 # to store files with a date before 1980.
232 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
234 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
233 if mtime < epoch:
235 if mtime < epoch:
234 mtime = epoch
236 mtime = epoch
235
237
236 self.mtime = mtime
238 self.mtime = mtime
237 self.date_time = time.gmtime(mtime)[:6]
239 self.date_time = time.gmtime(mtime)[:6]
238
240
239 def addfile(self, name, mode, islink, data):
241 def addfile(self, name, mode, islink, data):
240 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
242 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
241 i.compress_type = self.z.compression # pytype: disable=attribute-error
243 i.compress_type = self.z.compression # pytype: disable=attribute-error
242 # unzip will not honor unix file modes unless file creator is
244 # unzip will not honor unix file modes unless file creator is
243 # set to unix (id 3).
245 # set to unix (id 3).
244 i.create_system = 3
246 i.create_system = 3
245 ftype = _UNX_IFREG
247 ftype = _UNX_IFREG
246 if islink:
248 if islink:
247 mode = 0o777
249 mode = 0o777
248 ftype = _UNX_IFLNK
250 ftype = _UNX_IFLNK
249 i.external_attr = (mode | ftype) << 16
251 i.external_attr = (mode | ftype) << 16
250 # add "extended-timestamp" extra block, because zip archives
252 # add "extended-timestamp" extra block, because zip archives
251 # without this will be extracted with unexpected timestamp,
253 # without this will be extracted with unexpected timestamp,
252 # if TZ is not configured as GMT
254 # if TZ is not configured as GMT
253 i.extra += struct.pack(
255 i.extra += struct.pack(
254 b'<hhBl',
256 b'<hhBl',
255 0x5455, # block type: "extended-timestamp"
257 0x5455, # block type: "extended-timestamp"
256 1 + 4, # size of this block
258 1 + 4, # size of this block
257 1, # "modification time is present"
259 1, # "modification time is present"
258 int(self.mtime),
260 int(self.mtime),
259 ) # last modification (UTC)
261 ) # last modification (UTC)
260 self.z.writestr(i, data)
262 self.z.writestr(i, data)
261
263
262 def done(self):
264 def done(self):
263 self.z.close()
265 self.z.close()
264
266
265
267
266 class fileit(object):
268 class fileit(object):
267 '''write archive as files in directory.'''
269 '''write archive as files in directory.'''
268
270
269 def __init__(self, name, mtime):
271 def __init__(self, name, mtime):
270 self.basedir = name
272 self.basedir = name
271 self.opener = vfsmod.vfs(self.basedir)
273 self.opener = vfsmod.vfs(self.basedir)
272 self.mtime = mtime
274 self.mtime = mtime
273
275
274 def addfile(self, name, mode, islink, data):
276 def addfile(self, name, mode, islink, data):
275 if islink:
277 if islink:
276 self.opener.symlink(data, name)
278 self.opener.symlink(data, name)
277 return
279 return
278 f = self.opener(name, b"w", atomictemp=False)
280 f = self.opener(name, b"w", atomictemp=False)
279 f.write(data)
281 f.write(data)
280 f.close()
282 f.close()
281 destfile = os.path.join(self.basedir, name)
283 destfile = os.path.join(self.basedir, name)
282 os.chmod(destfile, mode)
284 os.chmod(destfile, mode)
283 if self.mtime is not None:
285 if self.mtime is not None:
284 os.utime(destfile, (self.mtime, self.mtime))
286 os.utime(destfile, (self.mtime, self.mtime))
285
287
286 def done(self):
288 def done(self):
287 pass
289 pass
288
290
289
291
290 archivers = {
292 archivers = {
291 b'files': fileit,
293 b'files': fileit,
292 b'tar': tarit,
294 b'tar': tarit,
293 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
295 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
294 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
296 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
295 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
297 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
296 b'uzip': lambda name, mtime: zipit(name, mtime, False),
298 b'uzip': lambda name, mtime: zipit(name, mtime, False),
297 b'zip': zipit,
299 b'zip': zipit,
298 }
300 }
299
301
300
302
301 def archive(
303 def archive(
302 repo,
304 repo,
303 dest,
305 dest,
304 node,
306 node,
305 kind,
307 kind,
306 decode=True,
308 decode=True,
307 match=None,
309 match=None,
308 prefix=b'',
310 prefix=b'',
309 mtime=None,
311 mtime=None,
310 subrepos=False,
312 subrepos=False,
311 ):
313 ):
312 '''create archive of repo as it was at node.
314 '''create archive of repo as it was at node.
313
315
314 dest can be name of directory, name of archive file, or file
316 dest can be name of directory, name of archive file, or file
315 object to write archive to.
317 object to write archive to.
316
318
317 kind is type of archive to create.
319 kind is type of archive to create.
318
320
319 decode tells whether to put files through decode filters from
321 decode tells whether to put files through decode filters from
320 hgrc.
322 hgrc.
321
323
322 match is a matcher to filter names of files to write to archive.
324 match is a matcher to filter names of files to write to archive.
323
325
324 prefix is name of path to put before every archive member.
326 prefix is name of path to put before every archive member.
325
327
326 mtime is the modified time, in seconds, or None to use the changeset time.
328 mtime is the modified time, in seconds, or None to use the changeset time.
327
329
328 subrepos tells whether to include subrepos.
330 subrepos tells whether to include subrepos.
329 '''
331 '''
330
332
331 if kind == b'txz' and not pycompat.ispy3:
333 if kind == b'txz' and not pycompat.ispy3:
332 raise error.Abort(_(b'xz compression is only available in Python 3'))
334 raise error.Abort(_(b'xz compression is only available in Python 3'))
333
335
334 if kind == b'files':
336 if kind == b'files':
335 if prefix:
337 if prefix:
336 raise error.Abort(_(b'cannot give prefix when archiving to files'))
338 raise error.Abort(_(b'cannot give prefix when archiving to files'))
337 else:
339 else:
338 prefix = tidyprefix(dest, kind, prefix)
340 prefix = tidyprefix(dest, kind, prefix)
339
341
340 def write(name, mode, islink, getdata):
342 def write(name, mode, islink, getdata):
341 data = getdata()
343 data = getdata()
342 if decode:
344 if decode:
343 data = repo.wwritedata(name, data)
345 data = repo.wwritedata(name, data)
344 archiver.addfile(prefix + name, mode, islink, data)
346 archiver.addfile(prefix + name, mode, islink, data)
345
347
346 if kind not in archivers:
348 if kind not in archivers:
347 raise error.Abort(_(b"unknown archive type '%s'") % kind)
349 raise error.Abort(_(b"unknown archive type '%s'") % kind)
348
350
349 ctx = repo[node]
351 ctx = repo[node]
350 archiver = archivers[kind](dest, mtime or ctx.date()[0])
352 archiver = archivers[kind](dest, mtime or ctx.date()[0])
351
353
352 if not match:
354 if not match:
353 match = scmutil.matchall(repo)
355 match = scmutil.matchall(repo)
354
356
355 if repo.ui.configbool(b"ui", b"archivemeta"):
357 if repo.ui.configbool(b"ui", b"archivemeta"):
356 name = b'.hg_archival.txt'
358 name = b'.hg_archival.txt'
357 if match(name):
359 if match(name):
358 write(name, 0o644, False, lambda: buildmetadata(ctx))
360 write(name, 0o644, False, lambda: buildmetadata(ctx))
359
361
360 files = [f for f in ctx.manifest().matches(match)]
362 files = [f for f in ctx.manifest().matches(match)]
361 total = len(files)
363 total = len(files)
362 if total:
364 if total:
363 files.sort()
365 files.sort()
364 scmutil.prefetchfiles(
366 scmutil.prefetchfiles(
365 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
367 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
366 )
368 )
367 progress = repo.ui.makeprogress(
369 progress = repo.ui.makeprogress(
368 _(b'archiving'), unit=_(b'files'), total=total
370 _(b'archiving'), unit=_(b'files'), total=total
369 )
371 )
370 progress.update(0)
372 progress.update(0)
371 for f in files:
373 for f in files:
372 ff = ctx.flags(f)
374 ff = ctx.flags(f)
373 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
375 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
374 progress.increment(item=f)
376 progress.increment(item=f)
375 progress.complete()
377 progress.complete()
376
378
377 if subrepos:
379 if subrepos:
378 for subpath in sorted(ctx.substate):
380 for subpath in sorted(ctx.substate):
379 sub = ctx.workingsub(subpath)
381 sub = ctx.workingsub(subpath)
380 submatch = matchmod.subdirmatcher(subpath, match)
382 submatch = matchmod.subdirmatcher(subpath, match)
381 subprefix = prefix + subpath + b'/'
383 subprefix = prefix + subpath + b'/'
382 total += sub.archive(archiver, subprefix, submatch, decode)
384 total += sub.archive(archiver, subprefix, submatch, decode)
383
385
384 if total == 0:
386 if total == 0:
385 raise error.Abort(_(b'no files match the archive pattern'))
387 raise error.Abort(_(b'no files match the archive pattern'))
386
388
387 archiver.done()
389 archiver.done()
388 return total
390 return total
General Comments 0
You need to be logged in to leave comments. Login now