tgzs, etc extract archives by default, not streams.
[rox-archive.git] / formats.py
blob55311b6dba82230f521dd906293de90f02304e11
1 import os, sys
2 from support import pipe_through_command
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 if command.find('%s') != -1:
71 # TODO: Handle path being '-'
72 command = command % data.path
73 pipe_through_command(command, data.source, None)
75 class Archive(Operation):
76 "Create an archive from a directory."
77 add_extension = 1
79 def __init__(self, extension, command, type):
80 assert command.find("'%s'") != -1
82 Operation.__init__(self, extension)
83 self.command = command
84 self.type = type
86 def __str__(self):
87 return 'Create .%s archive' % self.extension
89 def can_handle(self, data):
90 return isinstance(data, DirData)
92 def save_to_stream(self, data, stream):
93 os.chdir(os.path.dirname(data.path))
94 command = self.command % os.path.basename(data.path)
95 pipe_through_command(command, None, stream)
97 tgz = Extract('tgz', "gunzip -c - | tar xf -")
98 tbz = Extract('tar.bz2', "bunzip2 -c - | tar xf -")
99 jar = Extract('jar', "unzip -q -")
100 rar = Extract('rar', "rar x -")
101 tar = Extract('tar', "tar xf -")
102 rpm = Extract('rpm', "rpm2cpio - | cpio -id --quiet")
103 cpio = Extract('cpio', "cpio -id --quiet")
104 deb = Extract('deb', "ar x '%s'")
105 zip = Extract('zip', "unzip -q '%s'")
107 make_tgz = Archive('tgz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
108 Archive('tar.gz', "tar cf - '%s' | gzip", 'application/x-compressed-tar')
109 Archive('tar.bz', "tar cf - '%s' | bzip2", 'application/x-bzip-compressed-tar')
110 Archive('tar.bz2', "tar cf - '%s' | bzip2", 'application/x-bzip-compressed-tar')
111 Archive('zip', "zip -qr - '%s'", 'application/zip'),
112 Archive('jar', "zip -qr - '%s'", 'application/x-jar')
113 Archive('tar', "tar cf - '%s'", 'application/x-tar')
115 # Note: these go afterwards so that .tar.gz matches before .gz
116 make_gz = Compress('gz', "gzip -c -", 'application/x-gzip')
117 Compress('bz2', "bzip2 -c -", 'application/x-bzip')
119 gz = Decompress('gz', "gunzip -c -")
120 bz2 = Decompress('bz2', "bunzip2 -ck -")
123 # Can bzip2 read bzip files?
125 aliases = {
126 'tar.gz': 'tgz',
127 'tar.bz': 'tar.bz2',
128 'bz': 'bz2'
131 known_extensions = {}
132 for x in operations:
133 try:
134 known_extensions[x.extension] = None
135 except AttributeError:
136 pass
138 class FileData:
139 "A file on the local filesystem."
140 def __init__(self, path):
141 self.path = path
143 if path == '-':
144 source = sys.stdin
145 else:
146 try:
147 source = file(path)
148 except:
149 rox.report_exception()
150 sys.exit(1)
152 self.path = path
153 start = source.read(300)
154 try:
155 source.seek(0)
156 self.source = source
157 except:
158 print "(unseekable)"
159 # Input is not a regular, local, seekable file, so copy it
160 # to a local temp file.
161 import shutil
162 tmp = Tmp()
163 tmp.write(start)
164 shutil.copyfileobj(source, tmp)
165 self.source = tmp
166 self.default = self.guess_format(start)
168 if path == '-':
169 name = 'Data'
170 else:
171 name = path
172 for ext in known_extensions:
173 if path.endswith('.' + ext):
174 new = path[:-len(ext)-1]
175 if len(new) < len(name):
176 name = new
177 self.default_name = name
179 def guess_format(self, data):
180 "Return a good default Operation, judging by the first 300 bytes or so."
181 l = len(data)
182 def string(offset, match):
183 return data[offset:offset + len(match)] == match
184 def short(offset, match):
185 if l > offset + 1:
186 a = data[offset]
187 b = data[offset + 1]
188 return ((a == match & 0xff) and (b == (match >> 8))) or \
189 (b == match & 0xff) and (a == (match >> 8))
190 return 0
192 # Archives
193 if string(257, 'ustar\0') or string(257, 'ustar\040\040\0'):
194 return tar
195 if short(0, 070707) or short(0, 0143561) or string(0, '070707') or \
196 string(0, '070701') or string(0, '070702'):
197 return cpio
198 if string(0, '!<arch>') or string(0, '\\<ar>') or string(0, '<ar>'):
199 if string(7, '\ndebian'):
200 return deb
201 if string(0, 'Rar!'): return rar
202 if string(0, 'PK\003\004'): return zip
204 # Compressed streams
205 if string(0, '\037\213'):
206 if self.path.endswith('.tar.gz') or self.path.endswith('.tgz'):
207 return tgz
208 return gz
209 if string(0, 'BZh') or string(0, 'BZ'):
210 if self.path.endswith('.tar.bz') or self.path.endswith('.tar.bz2') or \
211 self.path.endswith('.tbz') or self.path.endswith('.tbz2'):
212 return tbz
213 return bz2
215 return make_gz
218 class DirData:
219 def __init__(self, path):
220 self.path = path
221 self.default = make_tgz
222 self.default_name = path + '.' + self.default.extension