If extracting creates a single top-level directory, remove it.
[rox-archive.git] / formats.py
blob05456821f614d5230194c8e4fd1a34903bdda6bb
1 import os, sys
2 from support import pipe_through_command, escape, Tmp
3 import rox
5 operations = []
6 class Operation:
7 add_extension = 0
9 def __init__(self, extension):
10 operations.append(self)
11 self.extension = extension
13 def can_handle(self, data):
14 return isinstance(data, FileData)
16 def save_to_stream(self, data, stream):
17 pipe_through_command(self.command, data.source, stream)
19 class Compress(Operation):
20 "Compress a stream into another stream."
21 add_extension = 1
23 def __init__(self, extension, command, type):
24 Operation.__init__(self, extension)
25 self.command = command
26 self.type = type
28 def __str__(self):
29 return 'Compress as .%s' % self.extension
31 class Decompress(Operation):
32 "Decompress a stream into another stream."
33 type = 'text/plain'
35 def __init__(self, extension, command):
36 Operation.__init__(self, extension)
37 self.command = command
39 def __str__(self):
40 return 'Decompress .%s' % self.extension
42 class Extract(Operation):
43 "Extract an archive to a directory."
44 type = 'inode/directory'
46 def __init__(self, extension, command):
47 "If command has a %s then the source path is inserted, else uses stdin."
48 Operation.__init__(self, extension)
49 self.command = command
51 def __str__(self):
52 return 'Extract from a .%s' % self.extension
54 def save_to_stream(self, data, stream):
55 raise Exception('This operation creates a directory, so you have '
56 'to drag to a filer window on the local machine')
58 def save_to_file(self, data, path):
59 if os.path.exists(path):
60 if not os.path.isdir(path):
61 raise Exception("'%s' already exists and is not a directory!" %
62 path)
63 if not rox.confirm('Directory already exists; extract into it?',
64 g.STOCK_YES):
65 raise Exception('Extraction aborted... try somewhere else')
66 if not os.path.exists(path):
67 os.mkdir(path)
68 os.chdir(path)
69 command = self.command
70 source = data.source
71 if command.find("'%s'") != -1:
72 command = command % escape(source.name)
73 source = None
74 try:
75 pipe_through_command(command, source, None)
76 finally:
77 try:
78 os.rmdir(path) # Will only succeed if it's empty
79 except:
80 pass
81 # If we created only a single subdirectory, move it up.
82 dirs = os.listdir(path)
83 if len(dirs) != 1:
84 return
85 dir = dirs[0]
86 unneeded_path = os.path.join(path, dir)
87 if not os.path.isdir(unneeded_path):
88 return
89 import random
90 tmp_path = os.path.join(path, 'tmp-' + `random.randint(0, 100000)`)
91 os.rename(unneeded_path, tmp_path)
92 for file in os.listdir(tmp_path):
93 os.rename(os.path.join(tmp_path, file), os.path.join(path, file))
94 os.rmdir(tmp_path)
96 class Archive(Operation):
97 "Create an archive from a directory."
98 add_extension = 1
100 def __init__(self, extension, command, type):
101 assert command.find("'%s'") != -1
103 Operation.__init__(self, extension)
104 self.command = command
105 self.type = type
107 def __str__(self):
108 return 'Create .%s archive' % self.extension
110 def can_handle(self, data):
111 return isinstance(data, DirData)
113 def save_to_stream(self, data, stream):
114 os.chdir(os.path.dirname(data.path))
115 command = self.command % escape(os.path.basename(data.path))
116 pipe_through_command(command, None, stream)
118 tgz = Extract('tgz', "gunzip -c - | tar xf -")
119 tbz = Extract('tar.bz2', "bunzip2 -c - | tar xf -")
120 jar = Extract('jar', "unzip -q -")
121 rar = Extract('rar', "rar x -")
122 tar = Extract('tar', "tar xf -")
123 rpm = Extract('rpm', "rpm2cpio - | cpio -id --quiet")
124 cpio = Extract('cpio', "cpio -id --quiet")
125 deb = Extract('deb', "ar x '%s'")
126 zip = Extract('zip', "unzip -q '%s'")
128 make_tgz = Archive('tgz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
129 Archive('tar.gz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
130 Archive('tar.bz', "tar cf - '%s' | bzip2", 'application/x-bzip-compressed-tar')
131 Archive('tar.bz2', "tar cf - '%s' | bzip2", 'application/x-bzip-compressed-tar')
132 Archive('zip', "zip -qr - '%s'", 'application/zip'),
133 Archive('jar', "zip -qr - '%s'", 'application/x-jar')
134 Archive('tar', "tar cf - '%s'", 'application/x-tar')
136 # Note: these go afterwards so that .tar.gz matches before .gz
137 make_gz = Compress('gz', "gzip -c -", 'application/x-gzip')
138 Compress('bz2', "bzip2 -c -", 'application/x-bzip')
140 gz = Decompress('gz', "gunzip -c -")
141 bz2 = Decompress('bz2', "bunzip2 -ck -")
144 # Can bzip2 read bzip files?
146 aliases = {
147 'tar.gz': 'tgz',
148 'tar.bz': 'tar.bz2',
149 'bz': 'bz2'
152 known_extensions = {}
153 for x in operations:
154 try:
155 known_extensions[x.extension] = None
156 except AttributeError:
157 pass
159 class FileData:
160 "A file on the local filesystem."
161 def __init__(self, path):
162 self.path = path
164 if path == '-':
165 source = sys.stdin
166 else:
167 try:
168 source = file(path)
169 except:
170 rox.report_exception()
171 sys.exit(1)
173 self.path = path
174 start = source.read(300)
175 try:
176 source.seek(0)
177 self.source = source
178 except:
179 # Input is not a regular, local, seekable file, so copy it
180 # to a local temp file.
181 import shutil
182 tmp = Tmp()
183 tmp.write(start)
184 tmp.flush()
185 shutil.copyfileobj(source, tmp)
186 self.source = tmp
187 self.source.seek(0)
188 self.default = self.guess_format(start)
190 if path == '-':
191 name = 'Data'
192 else:
193 name = path
194 for ext in known_extensions:
195 if path.endswith('.' + ext):
196 new = path[:-len(ext)-1]
197 if len(new) < len(name):
198 name = new
199 if self.default.add_extension:
200 name += '.' + self.default.extension
201 self.default_name = name
203 def guess_format(self, data):
204 "Return a good default Operation, judging by the first 300 bytes or so."
205 l = len(data)
206 def string(offset, match):
207 return data[offset:offset + len(match)] == match
208 def short(offset, match):
209 if l > offset + 1:
210 a = data[offset]
211 b = data[offset + 1]
212 return ((a == match & 0xff) and (b == (match >> 8))) or \
213 (b == match & 0xff) and (a == (match >> 8))
214 return 0
216 # Archives
217 if string(257, 'ustar\0') or string(257, 'ustar\040\040\0'):
218 return tar
219 if short(0, 070707) or short(0, 0143561) or string(0, '070707') or \
220 string(0, '070701') or string(0, '070702'):
221 return cpio
222 if string(0, '!<arch>') or string(0, '\\<ar>') or string(0, '<ar>'):
223 if string(7, '\ndebian'):
224 return deb
225 if string(0, 'Rar!'): return rar
226 if string(0, 'PK\003\004'): return zip
228 # Compressed streams
229 if string(0, '\037\213'):
230 if self.path.endswith('.tar.gz') or self.path.endswith('.tgz'):
231 return tgz
232 return gz
233 if string(0, 'BZh') or string(0, 'BZ'):
234 if self.path.endswith('.tar.bz') or self.path.endswith('.tar.bz2') or \
235 self.path.endswith('.tbz') or self.path.endswith('.tbz2'):
236 return tbz
237 return bz2
239 return make_gz
241 class DirData:
242 def __init__(self, path):
243 self.path = path
244 self.default = make_tgz
245 self.default_name = path + '.' + self.default.extension