28
|
1 # archival.py - revision archival for mercurial
|
|
2 #
|
|
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
|
|
4 #
|
|
5 # This software may be used and distributed according to the terms of the
|
|
6 # GNU General Public License version 2, incorporated herein by reference.
|
|
7
|
|
8 from i18n import _
|
|
9 from node import hex
|
|
10 import util
|
|
11 import cStringIO, os, stat, tarfile, time, zipfile
|
|
12 import zlib, gzip
|
|
13
|
|
14 def tidyprefix(dest, prefix, suffixes):
|
|
15 '''choose prefix to use for names in archive. make sure prefix is
|
|
16 safe for consumers.'''
|
|
17
|
|
18 if prefix:
|
|
19 prefix = util.normpath(prefix)
|
|
20 else:
|
|
21 if not isinstance(dest, str):
|
|
22 raise ValueError('dest must be string if no prefix')
|
|
23 prefix = os.path.basename(dest)
|
|
24 lower = prefix.lower()
|
|
25 for sfx in suffixes:
|
|
26 if lower.endswith(sfx):
|
|
27 prefix = prefix[:-len(sfx)]
|
|
28 break
|
|
29 lpfx = os.path.normpath(util.localpath(prefix))
|
|
30 prefix = util.pconvert(lpfx)
|
|
31 if not prefix.endswith('/'):
|
|
32 prefix += '/'
|
|
33 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
|
|
34 raise util.Abort(_('archive prefix contains illegal components'))
|
|
35 return prefix
|
|
36
|
|
37 class tarit(object):
|
|
38 '''write archive to tar file or stream. can write uncompressed,
|
|
39 or compress with gzip or bzip2.'''
|
|
40
|
|
41 class GzipFileWithTime(gzip.GzipFile):
|
|
42
|
|
43 def __init__(self, *args, **kw):
|
|
44 timestamp = None
|
|
45 if 'timestamp' in kw:
|
|
46 timestamp = kw.pop('timestamp')
|
|
47 if timestamp is None:
|
|
48 self.timestamp = time.time()
|
|
49 else:
|
|
50 self.timestamp = timestamp
|
|
51 gzip.GzipFile.__init__(self, *args, **kw)
|
|
52
|
|
53 def _write_gzip_header(self):
|
|
54 self.fileobj.write('\037\213') # magic header
|
|
55 self.fileobj.write('\010') # compression method
|
|
56 # Python 2.6 deprecates self.filename
|
|
57 fname = getattr(self, 'name', None) or self.filename
|
|
58 flags = 0
|
|
59 if fname:
|
|
60 flags = gzip.FNAME
|
|
61 self.fileobj.write(chr(flags))
|
|
62 gzip.write32u(self.fileobj, long(self.timestamp))
|
|
63 self.fileobj.write('\002')
|
|
64 self.fileobj.write('\377')
|
|
65 if fname:
|
|
66 self.fileobj.write(fname + '\000')
|
|
67
|
|
68 def __init__(self, dest, prefix, mtime, kind=''):
|
|
69 self.prefix = tidyprefix(dest, prefix, ['.tar', '.tar.bz2', '.tar.gz',
|
|
70 '.tgz', '.tbz2'])
|
|
71 self.mtime = mtime
|
|
72
|
|
73 def taropen(name, mode, fileobj=None):
|
|
74 if kind == 'gz':
|
|
75 mode = mode[0]
|
|
76 if not fileobj:
|
|
77 fileobj = open(name, mode + 'b')
|
|
78 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
|
|
79 zlib.Z_BEST_COMPRESSION,
|
|
80 fileobj, timestamp=mtime)
|
|
81 return tarfile.TarFile.taropen(name, mode, gzfileobj)
|
|
82 else:
|
|
83 return tarfile.open(name, mode + kind, fileobj)
|
|
84
|
|
85 if isinstance(dest, str):
|
|
86 self.z = taropen(dest, mode='w:')
|
|
87 else:
|
|
88 # Python 2.5-2.5.1 have a regression that requires a name arg
|
|
89 self.z = taropen(name='', mode='w|', fileobj=dest)
|
|
90
|
|
91 def addfile(self, name, mode, islink, data):
|
|
92 i = tarfile.TarInfo(self.prefix + name)
|
|
93 i.mtime = self.mtime
|
|
94 i.size = len(data)
|
|
95 if islink:
|
|
96 i.type = tarfile.SYMTYPE
|
|
97 i.mode = 0777
|
|
98 i.linkname = data
|
|
99 data = None
|
|
100 i.size = 0
|
|
101 else:
|
|
102 i.mode = mode
|
|
103 data = cStringIO.StringIO(data)
|
|
104 self.z.addfile(i, data)
|
|
105
|
|
106 def done(self):
|
|
107 self.z.close()
|
|
108
|
|
109 class tellable(object):
|
|
110 '''provide tell method for zipfile.ZipFile when writing to http
|
|
111 response file object.'''
|
|
112
|
|
113 def __init__(self, fp):
|
|
114 self.fp = fp
|
|
115 self.offset = 0
|
|
116
|
|
117 def __getattr__(self, key):
|
|
118 return getattr(self.fp, key)
|
|
119
|
|
120 def write(self, s):
|
|
121 self.fp.write(s)
|
|
122 self.offset += len(s)
|
|
123
|
|
124 def tell(self):
|
|
125 return self.offset
|
|
126
|
|
127 class zipit(object):
|
|
128 '''write archive to zip file or stream. can write uncompressed,
|
|
129 or compressed with deflate.'''
|
|
130
|
|
131 def __init__(self, dest, prefix, mtime, compress=True):
|
|
132 self.prefix = tidyprefix(dest, prefix, ('.zip',))
|
|
133 if not isinstance(dest, str):
|
|
134 try:
|
|
135 dest.tell()
|
|
136 except (AttributeError, IOError):
|
|
137 dest = tellable(dest)
|
|
138 self.z = zipfile.ZipFile(dest, 'w',
|
|
139 compress and zipfile.ZIP_DEFLATED or
|
|
140 zipfile.ZIP_STORED)
|
|
141 self.date_time = time.gmtime(mtime)[:6]
|
|
142
|
|
143 def addfile(self, name, mode, islink, data):
|
|
144 i = zipfile.ZipInfo(self.prefix + name, self.date_time)
|
|
145 i.compress_type = self.z.compression
|
|
146 # unzip will not honor unix file modes unless file creator is
|
|
147 # set to unix (id 3).
|
|
148 i.create_system = 3
|
|
149 ftype = stat.S_IFREG
|
|
150 if islink:
|
|
151 mode = 0777
|
|
152 ftype = stat.S_IFLNK
|
|
153 i.external_attr = (mode | ftype) << 16L
|
|
154 self.z.writestr(i, data)
|
|
155
|
|
156 def done(self):
|
|
157 self.z.close()
|
|
158
|
|
159 class fileit(object):
|
|
160 '''write archive as files in directory.'''
|
|
161
|
|
162 def __init__(self, name, prefix, mtime):
|
|
163 if prefix:
|
|
164 raise util.Abort(_('cannot give prefix when archiving to files'))
|
|
165 self.basedir = name
|
|
166 self.opener = util.opener(self.basedir)
|
|
167
|
|
168 def addfile(self, name, mode, islink, data):
|
|
169 if islink:
|
|
170 self.opener.symlink(data, name)
|
|
171 return
|
|
172 f = self.opener(name, "w", atomictemp=True)
|
|
173 f.write(data)
|
|
174 f.rename()
|
|
175 destfile = os.path.join(self.basedir, name)
|
|
176 os.chmod(destfile, mode)
|
|
177
|
|
178 def done(self):
|
|
179 pass
|
|
180
|
|
181 archivers = {
|
|
182 'files': fileit,
|
|
183 'tar': tarit,
|
|
184 'tbz2': lambda name, prefix, mtime: tarit(name, prefix, mtime, 'bz2'),
|
|
185 'tgz': lambda name, prefix, mtime: tarit(name, prefix, mtime, 'gz'),
|
|
186 'uzip': lambda name, prefix, mtime: zipit(name, prefix, mtime, False),
|
|
187 'zip': zipit,
|
|
188 }
|
|
189
|
|
190 def archive(repo, dest, node, kind, decode=True, matchfn=None,
|
|
191 prefix=None, mtime=None):
|
|
192 '''create archive of repo as it was at node.
|
|
193
|
|
194 dest can be name of directory, name of archive file, or file
|
|
195 object to write archive to.
|
|
196
|
|
197 kind is type of archive to create.
|
|
198
|
|
199 decode tells whether to put files through decode filters from
|
|
200 hgrc.
|
|
201
|
|
202 matchfn is function to filter names of files to write to archive.
|
|
203
|
|
204 prefix is name of path to put before every archive member.'''
|
|
205
|
|
206 def write(name, mode, islink, getdata):
|
|
207 if matchfn and not matchfn(name): return
|
|
208 data = getdata()
|
|
209 if decode:
|
|
210 data = repo.wwritedata(name, data)
|
|
211 archiver.addfile(name, mode, islink, data)
|
|
212
|
|
213 if kind not in archivers:
|
|
214 raise util.Abort(_("unknown archive type '%s'") % kind)
|
|
215
|
|
216 ctx = repo[node]
|
|
217 archiver = archivers[kind](dest, prefix, mtime or ctx.date()[0])
|
|
218
|
|
219 if repo.ui.configbool("ui", "archivemeta", True):
|
|
220 write('.hg_archival.txt', 0644, False,
|
|
221 lambda: 'repo: %s\nnode: %s\n' % (
|
|
222 hex(repo.changelog.node(0)), hex(node)))
|
|
223 for f in ctx:
|
|
224 ff = ctx.flags(f)
|
|
225 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data)
|
|
226 archiver.done()
|