archival.py
295 lines
| 9.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / archival.py
Vadim Gelfer
|
r2112 | # archival.py - revision archival for mercurial | ||
# | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Vadim Gelfer
|
r2112 | |||
Matt Mackall
|
r3891 | from i18n import _ | ||
Joel Rosdahl
|
r6211 | from node import hex | ||
Matt Harbison
|
r17108 | import match as matchmod | ||
Gilles Moris
|
r9614 | import cmdutil | ||
Adrian Buehlmann
|
r13970 | import scmutil, util, encoding | ||
Markus F.X.J. Oberhumer
|
r13668 | import cStringIO, os, tarfile, time, zipfile | ||
csaba.henk@creo.hu
|
r4652 | import zlib, gzip | ||
Vadim Gelfer
|
r2112 | |||
Mads Kiilerich
|
r17429 | # from unzip source code: | ||
_UNX_IFREG = 0x8000 | ||||
_UNX_IFLNK = 0xa000 | ||||
Martin Geisler
|
r11558 | def tidyprefix(dest, kind, prefix): | ||
Vadim Gelfer
|
r2112 | '''choose prefix to use for names in archive. make sure prefix is | ||
safe for consumers.''' | ||||
if prefix: | ||||
Shun-ichi GOTO
|
r5842 | prefix = util.normpath(prefix) | ||
Vadim Gelfer
|
r2112 | else: | ||
if not isinstance(dest, str): | ||||
raise ValueError('dest must be string if no prefix') | ||||
prefix = os.path.basename(dest) | ||||
lower = prefix.lower() | ||||
Martin Geisler
|
r11558 | for sfx in exts.get(kind, []): | ||
Vadim Gelfer
|
r2112 | if lower.endswith(sfx): | ||
prefix = prefix[:-len(sfx)] | ||||
break | ||||
lpfx = os.path.normpath(util.localpath(prefix)) | ||||
prefix = util.pconvert(lpfx) | ||||
if not prefix.endswith('/'): | ||||
prefix += '/' | ||||
if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix: | ||||
raise util.Abort(_('archive prefix contains illegal components')) | ||||
return prefix | ||||
Martin Geisler
|
r11557 | exts = { | ||
'tar': ['.tar'], | ||||
'tbz2': ['.tbz2', '.tar.bz2'], | ||||
'tgz': ['.tgz', '.tar.gz'], | ||||
'zip': ['.zip'], | ||||
} | ||||
def guesskind(dest): | ||||
for kind, extensions in exts.iteritems(): | ||||
if util.any(dest.endswith(ext) for ext in extensions): | ||||
return kind | ||||
return None | ||||
Benoit Boissinot
|
r8778 | class tarit(object): | ||
Vadim Gelfer
|
r2112 | '''write archive to tar file or stream. can write uncompressed, | ||
or compress with gzip or bzip2.''' | ||||
csaba.henk@creo.hu
|
r4652 | class GzipFileWithTime(gzip.GzipFile): | ||
def __init__(self, *args, **kw): | ||||
timestamp = None | ||||
if 'timestamp' in kw: | ||||
timestamp = kw.pop('timestamp') | ||||
Martin Geisler
|
r8527 | if timestamp is None: | ||
csaba.henk@creo.hu
|
r4652 | self.timestamp = time.time() | ||
else: | ||||
self.timestamp = timestamp | ||||
gzip.GzipFile.__init__(self, *args, **kw) | ||||
def _write_gzip_header(self): | ||||
self.fileobj.write('\037\213') # magic header | ||||
self.fileobj.write('\010') # compression method | ||||
Dirkjan Ochtman
|
r6495 | # Python 2.6 deprecates self.filename | ||
Thomas Arendsen Hein
|
r6498 | fname = getattr(self, 'name', None) or self.filename | ||
Brodie Rao
|
r13102 | if fname and fname.endswith('.gz'): | ||
fname = fname[:-3] | ||||
csaba.henk@creo.hu
|
r4652 | flags = 0 | ||
if fname: | ||||
flags = gzip.FNAME | ||||
self.fileobj.write(chr(flags)) | ||||
gzip.write32u(self.fileobj, long(self.timestamp)) | ||||
self.fileobj.write('\002') | ||||
self.fileobj.write('\377') | ||||
if fname: | ||||
self.fileobj.write(fname + '\000') | ||||
Martin Geisler
|
r11558 | def __init__(self, dest, mtime, kind=''): | ||
Vadim Gelfer
|
r2477 | self.mtime = mtime | ||
Dan Villiom Podlaski Christiansen
|
r13400 | self.fileobj = None | ||
csaba.henk@creo.hu
|
r4652 | |||
def taropen(name, mode, fileobj=None): | ||||
if kind == 'gz': | ||||
mode = mode[0] | ||||
if not fileobj: | ||||
csaba.henk@creo.hu
|
r4731 | fileobj = open(name, mode + 'b') | ||
csaba.henk@creo.hu
|
r4652 | gzfileobj = self.GzipFileWithTime(name, mode + 'b', | ||
zlib.Z_BEST_COMPRESSION, | ||||
fileobj, timestamp=mtime) | ||||
Dan Villiom Podlaski Christiansen
|
r13400 | self.fileobj = gzfileobj | ||
csaba.henk@creo.hu
|
r4652 | return tarfile.TarFile.taropen(name, mode, gzfileobj) | ||
else: | ||||
Dan Villiom Podlaski Christiansen
|
r13400 | self.fileobj = fileobj | ||
csaba.henk@creo.hu
|
r4652 | return tarfile.open(name, mode + kind, fileobj) | ||
Vadim Gelfer
|
r2112 | if isinstance(dest, str): | ||
csaba.henk@creo.hu
|
r4652 | self.z = taropen(dest, mode='w:') | ||
Vadim Gelfer
|
r2112 | else: | ||
Brendan Cully
|
r4357 | # Python 2.5-2.5.1 have a regression that requires a name arg | ||
csaba.henk@creo.hu
|
r4652 | self.z = taropen(name='', mode='w|', fileobj=dest) | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
Martin Geisler
|
r11558 | i = tarfile.TarInfo(name) | ||
Vadim Gelfer
|
r2112 | i.mtime = self.mtime | ||
i.size = len(data) | ||||
Alexis S. L. Carvalho
|
r4831 | if islink: | ||
i.type = tarfile.SYMTYPE | ||||
i.mode = 0777 | ||||
i.linkname = data | ||||
data = None | ||||
Peter van Dijk
|
r7770 | i.size = 0 | ||
Alexis S. L. Carvalho
|
r4831 | else: | ||
i.mode = mode | ||||
data = cStringIO.StringIO(data) | ||||
self.z.addfile(i, data) | ||||
Vadim Gelfer
|
r2112 | |||
def done(self): | ||||
self.z.close() | ||||
Dan Villiom Podlaski Christiansen
|
r13400 | if self.fileobj: | ||
self.fileobj.close() | ||||
Vadim Gelfer
|
r2112 | |||
Benoit Boissinot
|
r8778 | class tellable(object): | ||
Vadim Gelfer
|
r2112 | '''provide tell method for zipfile.ZipFile when writing to http | ||
response file object.''' | ||||
def __init__(self, fp): | ||||
self.fp = fp | ||||
self.offset = 0 | ||||
def __getattr__(self, key): | ||||
return getattr(self.fp, key) | ||||
def write(self, s): | ||||
self.fp.write(s) | ||||
self.offset += len(s) | ||||
def tell(self): | ||||
return self.offset | ||||
Benoit Boissinot
|
r8778 | class zipit(object): | ||
Vadim Gelfer
|
r2112 | '''write archive to zip file or stream. can write uncompressed, | ||
or compressed with deflate.''' | ||||
Martin Geisler
|
r11558 | def __init__(self, dest, mtime, compress=True): | ||
Colin McMillen
|
r2168 | if not isinstance(dest, str): | ||
try: | ||||
dest.tell() | ||||
Thomas Arendsen Hein
|
r2169 | except (AttributeError, IOError): | ||
Colin McMillen
|
r2168 | dest = tellable(dest) | ||
Vadim Gelfer
|
r2112 | self.z = zipfile.ZipFile(dest, 'w', | ||
compress and zipfile.ZIP_DEFLATED or | ||||
zipfile.ZIP_STORED) | ||||
Martin Geisler
|
r12319 | |||
# Python's zipfile module emits deprecation warnings if we try | ||||
# to store files with a date before 1980. | ||||
epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0)) | ||||
if mtime < epoch: | ||||
mtime = epoch | ||||
Vadim Gelfer
|
r2477 | self.date_time = time.gmtime(mtime)[:6] | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
Martin Geisler
|
r11558 | i = zipfile.ZipInfo(name, self.date_time) | ||
Vadim Gelfer
|
r2112 | i.compress_type = self.z.compression | ||
# unzip will not honor unix file modes unless file creator is | ||||
# set to unix (id 3). | ||||
i.create_system = 3 | ||||
Mads Kiilerich
|
r17429 | ftype = _UNX_IFREG | ||
Alexis S. L. Carvalho
|
r4831 | if islink: | ||
mode = 0777 | ||||
Mads Kiilerich
|
r17429 | ftype = _UNX_IFLNK | ||
Alexis S. L. Carvalho
|
r4831 | i.external_attr = (mode | ftype) << 16L | ||
Vadim Gelfer
|
r2112 | self.z.writestr(i, data) | ||
def done(self): | ||||
self.z.close() | ||||
Benoit Boissinot
|
r8778 | class fileit(object): | ||
Vadim Gelfer
|
r2112 | '''write archive as files in directory.''' | ||
Martin Geisler
|
r11558 | def __init__(self, name, mtime): | ||
Vadim Gelfer
|
r2112 | self.basedir = name | ||
Adrian Buehlmann
|
r13970 | self.opener = scmutil.opener(self.basedir) | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
if islink: | ||||
self.opener.symlink(data, name) | ||||
return | ||||
Alexis S. L. Carvalho
|
r4830 | f = self.opener(name, "w", atomictemp=True) | ||
f.write(data) | ||||
Greg Ward
|
r15057 | f.close() | ||
Vadim Gelfer
|
r2112 | destfile = os.path.join(self.basedir, name) | ||
Alexis S. L. Carvalho
|
r4830 | os.chmod(destfile, mode) | ||
Vadim Gelfer
|
r2112 | |||
def done(self): | ||||
pass | ||||
archivers = { | ||||
'files': fileit, | ||||
'tar': tarit, | ||||
Martin Geisler
|
r11558 | 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'), | ||
'tgz': lambda name, mtime: tarit(name, mtime, 'gz'), | ||||
'uzip': lambda name, mtime: zipit(name, mtime, False), | ||||
Vadim Gelfer
|
r2112 | 'zip': zipit, | ||
} | ||||
def archive(repo, dest, node, kind, decode=True, matchfn=None, | ||||
Martin Geisler
|
r12323 | prefix=None, mtime=None, subrepos=False): | ||
Vadim Gelfer
|
r2112 | '''create archive of repo as it was at node. | ||
dest can be name of directory, name of archive file, or file | ||||
object to write archive to. | ||||
kind is type of archive to create. | ||||
decode tells whether to put files through decode filters from | ||||
hgrc. | ||||
matchfn is function to filter names of files to write to archive. | ||||
prefix is name of path to put before every archive member.''' | ||||
Martin Geisler
|
r11558 | if kind == 'files': | ||
if prefix: | ||||
raise util.Abort(_('cannot give prefix when archiving to files')) | ||||
else: | ||||
prefix = tidyprefix(dest, kind, prefix) | ||||
Alexis S. L. Carvalho
|
r4951 | def write(name, mode, islink, getdata): | ||
data = getdata() | ||||
Vadim Gelfer
|
r2112 | if decode: | ||
Matt Mackall
|
r4005 | data = repo.wwritedata(name, data) | ||
Martin Geisler
|
r11558 | archiver.addfile(prefix + name, mode, islink, data) | ||
Vadim Gelfer
|
r2112 | |||
Dirkjan Ochtman
|
r6019 | if kind not in archivers: | ||
Martin Geisler
|
r6913 | raise util.Abort(_("unknown archive type '%s'") % kind) | ||
Matt Mackall
|
r6749 | |||
ctx = repo[node] | ||||
Martin Geisler
|
r11558 | archiver = archivers[kind](dest, mtime or ctx.date()[0]) | ||
Matt Mackall
|
r6749 | |||
Thomas Arendsen Hein
|
r6183 | if repo.ui.configbool("ui", "archivemeta", True): | ||
Gilles Moris
|
r9614 | def metadata(): | ||
base = 'repo: %s\nnode: %s\nbranch: %s\n' % ( | ||||
Matt Mackall
|
r13047 | repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch())) | ||
Gilles Moris
|
r9614 | |||
tags = ''.join('tag: %s\n' % t for t in ctx.tags() | ||||
if repo.tagtype(t) == 'global') | ||||
if not tags: | ||||
repo.ui.pushbuffer() | ||||
opts = {'template': '{latesttag}\n{latesttagdistance}', | ||||
'style': '', 'patch': None, 'git': None} | ||||
cmdutil.show_changeset(repo.ui, repo, opts).show(ctx) | ||||
ltags, dist = repo.ui.popbuffer().split('\n') | ||||
tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':')) | ||||
tags += 'latesttagdistance: %s\n' % dist | ||||
return base + tags | ||||
Thomas Arendsen Hein
|
r16919 | name = '.hg_archival.txt' | ||
if not matchfn or matchfn(name): | ||||
write(name, 0644, False, metadata) | ||||
Gilles Moris
|
r9614 | |||
Thomas Arendsen Hein
|
r16919 | if matchfn: | ||
files = [f for f in ctx.manifest().keys() if matchfn(f)] | ||||
else: | ||||
files = ctx.manifest().keys() | ||||
files.sort() | ||||
total = len(files) | ||||
Martin Geisler
|
r13143 | repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total) | ||
Thomas Arendsen Hein
|
r16919 | for i, f in enumerate(files): | ||
Matt Mackall
|
r6749 | ff = ctx.flags(f) | ||
write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data) | ||||
Martin Geisler
|
r13143 | repo.ui.progress(_('archiving'), i + 1, item=f, | ||
unit=_('files'), total=total) | ||||
repo.ui.progress(_('archiving'), None) | ||||
Martin Geisler
|
r12323 | |||
if subrepos: | ||||
for subpath in ctx.substate: | ||||
sub = ctx.sub(subpath) | ||||
Matt Harbison
|
r17108 | submatch = matchmod.narrowmatcher(subpath, matchfn) | ||
sub.archive(repo.ui, archiver, prefix, submatch) | ||||
Martin Geisler
|
r12323 | |||
Vadim Gelfer
|
r2112 | archiver.done() | ||