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