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