##// END OF EJS Templates
archival: tar file modes need to be sysstrs...
Augie Fackler -
r36725:abf252a1 default
parent child Browse files
Show More
@@ -1,362 +1,364 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 '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 161 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
162 162 zlib.Z_BEST_COMPRESSION,
163 163 fileobj, timestamp=mtime)
164 164 self.fileobj = gzfileobj
165 return tarfile.TarFile.taropen(name, mode, gzfileobj)
165 return tarfile.TarFile.taropen(
166 name, pycompat.sysstr(mode), gzfileobj)
166 167 else:
167 return tarfile.open(name, mode + kind, fileobj)
168 return tarfile.open(
169 name, pycompat.sysstr(mode + kind), fileobj)
168 170
169 171 if isinstance(dest, str):
170 172 self.z = taropen('w:', name=dest)
171 173 else:
172 174 self.z = taropen('w|', fileobj=dest)
173 175
174 176 def addfile(self, name, mode, islink, data):
175 177 name = pycompat.fsdecode(name)
176 178 i = tarfile.TarInfo(name)
177 179 i.mtime = self.mtime
178 180 i.size = len(data)
179 181 if islink:
180 182 i.type = tarfile.SYMTYPE
181 183 i.mode = 0o777
182 184 i.linkname = pycompat.fsdecode(data)
183 185 data = None
184 186 i.size = 0
185 187 else:
186 188 i.mode = mode
187 189 data = stringio(data)
188 190 self.z.addfile(i, data)
189 191
190 192 def done(self):
191 193 self.z.close()
192 194 if self.fileobj:
193 195 self.fileobj.close()
194 196
195 197 class tellable(object):
196 198 '''provide tell method for zipfile.ZipFile when writing to http
197 199 response file object.'''
198 200
199 201 def __init__(self, fp):
200 202 self.fp = fp
201 203 self.offset = 0
202 204
203 205 def __getattr__(self, key):
204 206 return getattr(self.fp, key)
205 207
206 208 def write(self, s):
207 209 self.fp.write(s)
208 210 self.offset += len(s)
209 211
210 212 def tell(self):
211 213 return self.offset
212 214
213 215 class zipit(object):
214 216 '''write archive to zip file or stream. can write uncompressed,
215 217 or compressed with deflate.'''
216 218
217 219 def __init__(self, dest, mtime, compress=True):
218 220 if not isinstance(dest, str):
219 221 try:
220 222 dest.tell()
221 223 except (AttributeError, IOError):
222 224 dest = tellable(dest)
223 225 self.z = zipfile.ZipFile(pycompat.fsdecode(dest), r'w',
224 226 compress and zipfile.ZIP_DEFLATED or
225 227 zipfile.ZIP_STORED)
226 228
227 229 # Python's zipfile module emits deprecation warnings if we try
228 230 # to store files with a date before 1980.
229 231 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
230 232 if mtime < epoch:
231 233 mtime = epoch
232 234
233 235 self.mtime = mtime
234 236 self.date_time = time.gmtime(mtime)[:6]
235 237
236 238 def addfile(self, name, mode, islink, data):
237 239 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
238 240 i.compress_type = self.z.compression
239 241 # unzip will not honor unix file modes unless file creator is
240 242 # set to unix (id 3).
241 243 i.create_system = 3
242 244 ftype = _UNX_IFREG
243 245 if islink:
244 246 mode = 0o777
245 247 ftype = _UNX_IFLNK
246 248 i.external_attr = (mode | ftype) << 16
247 249 # add "extended-timestamp" extra block, because zip archives
248 250 # without this will be extracted with unexpected timestamp,
249 251 # if TZ is not configured as GMT
250 252 i.extra += struct.pack('<hhBl',
251 253 0x5455, # block type: "extended-timestamp"
252 254 1 + 4, # size of this block
253 255 1, # "modification time is present"
254 256 int(self.mtime)) # last modification (UTC)
255 257 self.z.writestr(i, data)
256 258
257 259 def done(self):
258 260 self.z.close()
259 261
260 262 class fileit(object):
261 263 '''write archive as files in directory.'''
262 264
263 265 def __init__(self, name, mtime):
264 266 self.basedir = name
265 267 self.opener = vfsmod.vfs(self.basedir)
266 268 self.mtime = mtime
267 269
268 270 def addfile(self, name, mode, islink, data):
269 271 if islink:
270 272 self.opener.symlink(data, name)
271 273 return
272 274 f = self.opener(name, "w", atomictemp=True)
273 275 f.write(data)
274 276 f.close()
275 277 destfile = os.path.join(self.basedir, name)
276 278 os.chmod(destfile, mode)
277 279 if self.mtime is not None:
278 280 os.utime(destfile, (self.mtime, self.mtime))
279 281
280 282 def done(self):
281 283 pass
282 284
283 285 archivers = {
284 286 'files': fileit,
285 287 'tar': tarit,
286 288 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
287 289 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
288 290 'uzip': lambda name, mtime: zipit(name, mtime, False),
289 291 'zip': zipit,
290 292 }
291 293
292 294 def archive(repo, dest, node, kind, decode=True, matchfn=None,
293 295 prefix='', mtime=None, subrepos=False):
294 296 '''create archive of repo as it was at node.
295 297
296 298 dest can be name of directory, name of archive file, or file
297 299 object to write archive to.
298 300
299 301 kind is type of archive to create.
300 302
301 303 decode tells whether to put files through decode filters from
302 304 hgrc.
303 305
304 306 matchfn is function to filter names of files to write to archive.
305 307
306 308 prefix is name of path to put before every archive member.
307 309
308 310 mtime is the modified time, in seconds, or None to use the changeset time.
309 311
310 312 subrepos tells whether to include subrepos.
311 313 '''
312 314
313 315 if kind == 'files':
314 316 if prefix:
315 317 raise error.Abort(_('cannot give prefix when archiving to files'))
316 318 else:
317 319 prefix = tidyprefix(dest, kind, prefix)
318 320
319 321 def write(name, mode, islink, getdata):
320 322 data = getdata()
321 323 if decode:
322 324 data = repo.wwritedata(name, data)
323 325 archiver.addfile(prefix + name, mode, islink, data)
324 326
325 327 if kind not in archivers:
326 328 raise error.Abort(_("unknown archive type '%s'") % kind)
327 329
328 330 ctx = repo[node]
329 331 archiver = archivers[kind](dest, mtime or ctx.date()[0])
330 332
331 333 if repo.ui.configbool("ui", "archivemeta"):
332 334 name = '.hg_archival.txt'
333 335 if not matchfn or matchfn(name):
334 336 write(name, 0o644, False, lambda: buildmetadata(ctx))
335 337
336 338 if matchfn:
337 339 files = [f for f in ctx.manifest().keys() if matchfn(f)]
338 340 else:
339 341 files = ctx.manifest().keys()
340 342 total = len(files)
341 343 if total:
342 344 files.sort()
343 345 scmutil.fileprefetchhooks(repo, ctx, files)
344 346 repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total)
345 347 for i, f in enumerate(files):
346 348 ff = ctx.flags(f)
347 349 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, ctx[f].data)
348 350 repo.ui.progress(_('archiving'), i + 1, item=f,
349 351 unit=_('files'), total=total)
350 352 repo.ui.progress(_('archiving'), None)
351 353
352 354 if subrepos:
353 355 for subpath in sorted(ctx.substate):
354 356 sub = ctx.workingsub(subpath)
355 357 submatch = matchmod.subdirmatcher(subpath, matchfn)
356 358 total += sub.archive(archiver, prefix, submatch, decode)
357 359
358 360 if total == 0:
359 361 raise error.Abort(_('no files match the archive pattern'))
360 362
361 363 archiver.done()
362 364 return total
General Comments 0
You need to be logged in to leave comments. Login now