Start development series 2.3-post
[rox-archive.git] / formats.py
blob45bd4cd9770070aa74a6c3c6a7052e650707ad7c
1 if __name__ == '__main__':
2 import findrox; findrox.version(1, 99, 11)
3 import os, sys
4 from support import shell_escape, Tmp
5 import rox
6 from rox.processes import PipeThroughCommand
8 current_command = None
10 def pipe_through_command(command, src, dst):
11 global current_command
12 assert not current_command
13 try:
14 src.seek(0)
15 except:
16 pass
17 current_command = PipeThroughCommand(command, src, dst)
18 try:
19 current_command.wait()
20 finally:
21 current_command = None
23 operations = []
24 class Operation:
25 add_extension = False
27 def __init__(self, extension):
28 operations.append(self)
29 self.extension = extension
31 def can_handle(self, data):
32 return isinstance(data, FileData)
34 def save_to_stream(self, data, stream):
35 pipe_through_command(self.command, data.source, stream)
37 class Compress(Operation):
38 "Compress a stream into another stream."
39 add_extension = True
41 def __init__(self, extension, command, type):
42 Operation.__init__(self, extension)
43 self.command = command
44 self.type = type
46 def __str__(self):
47 return _('Compress as .%s') % self.extension
49 class Decompress(Operation):
50 "Decompress a stream into another stream."
51 type = 'text/plain'
53 def __init__(self, extension, command):
54 Operation.__init__(self, extension)
55 self.command = command
57 def __str__(self):
58 return _('Decompress .%s') % self.extension
60 class Extract(Operation):
61 "Extract an archive to a directory."
62 type = 'inode/directory'
64 def __init__(self, extension, command):
65 "If command has a %s then the source path is inserted, else uses stdin."
66 Operation.__init__(self, extension)
67 self.command = command
69 def __str__(self):
70 return _('Extract from a .%s') % self.extension
72 def save_to_stream(self, data, stream):
73 raise Exception(_('This operation creates a directory, so you have '
74 'to drag to a filer window on the local machine'))
76 def save_to_file(self, data, path):
77 if os.path.exists(path):
78 if not os.path.isdir(path):
79 raise Exception(_("'%s' already exists and is not a directory!") %
80 path)
81 if not os.path.exists(path):
82 os.mkdir(path)
83 os.chdir(path)
84 command = self.command
85 source = data.source
86 if command.find("'%s'") != -1:
87 command = command % shell_escape(source.name)
88 source = None
89 try:
90 pipe_through_command(command, source, None)
91 finally:
92 try:
93 os.rmdir(path) # Will only succeed if it's empty
94 except:
95 pass
96 if os.path.exists(path):
97 self.pull_up(path)
99 def pull_up(self, path):
100 # If we created only a single subdirectory, move it up.
101 dirs = os.listdir(path)
102 if len(dirs) != 1:
103 return
104 dir = dirs[0]
105 unneeded_path = os.path.join(path, dir)
106 if not os.path.isdir(unneeded_path):
107 return
108 import random
109 tmp_path = os.path.join(path, 'tmp-' + `random.randint(0, 100000)`)
110 os.rename(unneeded_path, tmp_path)
111 for file in os.listdir(tmp_path):
112 os.rename(os.path.join(tmp_path, file), os.path.join(path, file))
113 os.rmdir(tmp_path)
115 class Archive(Operation):
116 "Create an archive from a directory."
117 add_extension = True
119 def __init__(self, extension, command, type):
120 assert command.find("'%s'") != -1
122 Operation.__init__(self, extension)
123 self.command = command
124 self.type = type
126 def __str__(self):
127 return _('Create .%s archive') % self.extension
129 def can_handle(self, data):
130 return isinstance(data, DirData)
132 def save_to_stream(self, data, stream):
133 os.chdir(os.path.dirname(data.path))
134 command = self.command % shell_escape(os.path.basename(data.path))
135 pipe_through_command(command, None, stream)
137 tgz = Extract('tgz', "gunzip -c - | tar xf -")
138 tbz = Extract('tar.bz2', "bunzip2 -c - | tar xf -")
139 tarz = Extract('tar.Z', "uncompress -c - | tar xf -")
140 tlz = Extract('tlz', "unlzma -c - | tar xf -")
141 txz = Extract('txz', "unxz -c - | tar xf -")
142 rar = Extract('rar', "unrar x '%s'")
143 ace = Extract('ace', "unace x '%s'")
144 tar = Extract('tar', "tar xf -")
145 rpm = Extract('rpm', "rpm2cpio - | cpio -id --quiet")
146 cpio = Extract('cpio', "cpio -id --quiet")
147 deb = Extract('deb', "ar x '%s'")
148 zip = Extract('zip', "unzip -q '%s'")
149 jar = Extract('jar', "unzip -q '%s'")
150 lha = Extract('lha', "lha x '%s'")
152 make_tgz = Archive('tgz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
153 Archive('tar.gz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
154 Archive('tar.bz2', "tar cf - '%s' | bzip2", 'application/x-bzip-compressed-tar')
155 Archive('tlz', "tar cf - '%s' | lzma", 'application/x-lzma-compressed-tar')
156 Archive('tar.lzma',"tar cf - '%s' | lzma", 'application/x-lzma-compressed-tar')
157 Archive('txz', "tar cf - '%s' | xz", 'application/x-xz-compressed-tar')
158 Archive('tar.xz', "tar cf - '%s' | xz", 'application/x-xz-compressed-tar')
159 Archive('zip', "zip -qr - '%s'", 'application/zip'),
160 Archive('jar', "zip -qr - '%s'", 'application/x-jar')
161 Archive('tar', "tar cf - '%s'", 'application/x-tar')
162 Archive('lha', "lha c - '%s'", 'application/x-lha'),
164 # Note: these go afterwards so that .tar.gz matches before .gz
165 make_gz = Compress('gz', "gzip -c -", 'application/x-gzip')
166 Compress('bz2', "bzip2 -c -", 'application/x-bzip')
167 Compress('lzma', "lzma -c -", 'application/x-lzma')
168 Compress('xz', "xz -c -", 'application/x-xz')
169 Compress('uue', "uuencode /dev/stdout", 'application/x-uuencoded')
171 gz = Decompress('gz', "gunzip -c -")
172 bz2 = Decompress('bz2', "bunzip2 -ck -")
173 uue = Decompress('uue', "uudecode -o /dev/stdout")
174 z = Decompress('Z', "uncompress -c -")
175 lzma= Decompress('lzma', "unlzma -c -")
176 xz = Decompress('xz', "unxz -c -")
179 # Can bzip2 read bzip files?
181 aliases = {
182 'tar.gz': 'tgz',
183 'tar.bz': 'tar.bz2',
184 'tbz': 'tar.bz2',
185 'tar.lzma': 'tlz',
186 'tar.xz': 'txz',
187 'bz': 'bz2'
190 known_extensions = {}
191 for x in operations:
192 try:
193 known_extensions[x.extension] = None
194 except AttributeError:
195 pass
197 class FileData:
198 "A file on the local filesystem."
199 mode = None
200 def __init__(self, path):
201 self.path = path
203 if path == '-':
204 source = sys.stdin
205 else:
206 try:
207 source = file(path)
208 self.mode = os.stat(path).st_mode
209 except:
210 rox.report_exception()
211 sys.exit(1)
213 self.path = path
214 start = source.read(300)
215 try:
216 if source is sys.stdin:
217 raise Exception("Always copy stdin!")
218 source.seek(0)
219 self.source = source
220 except:
221 # Input is not a regular, local, seekable file, so copy it
222 # to a local temp file.
223 import shutil
224 tmp = Tmp()
225 tmp.write(start)
226 tmp.flush()
227 shutil.copyfileobj(source, tmp)
228 tmp.seek(0)
229 tmp.flush()
230 self.source = tmp
231 self.default = self.guess_format(start)
233 if path == '-':
234 name = 'Data'
235 else:
236 name = path
237 for ext in known_extensions:
238 if path.endswith('.' + ext):
239 new = path[:-len(ext)-1]
240 if len(new) < len(name):
241 name = new
242 if self.default.add_extension:
243 name += '.' + self.default.extension
245 if name == path:
246 # Default name is same as input. Change it somehow...
247 if '.' in os.path.basename(name):
248 name = name[:name.rindex('.')]
249 else:
250 name += '.unpacked'
252 self.default_name = name
254 def guess_format(self, data):
255 "Return a good default Operation, judging by the first 300 bytes or so."
256 l = len(data)
257 def string(offset, match):
258 return data[offset:offset + len(match)] == match
259 def short(offset, match):
260 if l > offset + 1:
261 a = data[offset]
262 b = data[offset + 1]
263 return ((a == match & 0xff) and (b == (match >> 8))) or \
264 (b == match & 0xff) and (a == (match >> 8))
265 return 0
267 # Archives
268 if string(257, 'ustar\0') or string(257, 'ustar\040\040\0'):
269 return tar
270 if short(0, 070707) or short(0, 0143561) or string(0, '070707') or \
271 string(0, '070701') or string(0, '070702'):
272 return cpio
273 if string(0, '!<arch>') or string(0, '\\<ar>') or string(0, '<ar>'):
274 if string(7, '\ndebian'):
275 return deb
276 if string(0, 'Rar!'): return rar
277 if string(7, '**ACE**'): return ace
278 if string(0, 'PK\003\004'): return zip
279 if string(0, 'PK00'): return zip
280 if string(0, '\xed\xab\xee\xdb'): return rpm
281 if (string(2, '-lz') or string(2, '-lh')) and data[6] == '-':
282 return lha
284 # Compressed streams
285 if string(0, '\037\213'):
286 if self.path.endswith('.tar.gz') or self.path.endswith('.tgz'):
287 return tgz
288 return gz
289 if string(0, 'BZh') or string(0, 'BZ'):
290 if self.path.endswith('.tar.bz') or self.path.endswith('.tar.bz2') or \
291 self.path.endswith('.tbz') or self.path.endswith('.tbz2'):
292 return tbz
293 return bz2
294 if string(0, ']\0\0') and (0 == ord(data[3]) & 0x7f):
295 if self.path.endswith('.tar.lzma') or self.path.endswith('.tlz'):
296 return tlz
297 return lzma
298 if string(0, '\3757zXZ\0'):
299 if self.path.endswith('.tar.xz') or self.path.endswith('.txz'):
300 return txz
301 return xz
302 if string(0, 'begin '):
303 return uue
304 if string(0, '\037\235'):
305 if self.path.endswith('.tar.Z'):
306 return tarz
307 return z
309 return make_gz
311 class DirData:
312 mode = None
313 def __init__(self, path):
314 self.path = path
315 self.default = make_tgz
316 self.default_name = path + '.' + self.default.extension