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