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