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