More fixes for creating/extracting directories.
[rox-archive.git] / formats.py
blob71a649b46ee21d371e0c443d4ac0b55926ad6fde
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 def guess_format(data):
139 "Return a good default Operation, judging by the first 300 bytes or so."
140 l = len(data)
141 def string(offset, match):
142 return data[offset:offset + len(match)] == match
143 def short(offset, match):
144 if l > offset + 1:
145 a = data[offset]
146 b = data[offset + 1]
147 return ((a == match & 0xff) and (b == (match >> 8))) or \
148 (b == match & 0xff) and (a == (match >> 8))
149 return 0
151 # Archives
152 if string(257, 'ustar\0') or string(257, 'ustar\040\040\0'):
153 return tar
154 if short(0, 070707) or short(0, 0143561) or string(0, '070707') or \
155 string(0, '070701') or string(0, '070702'):
156 return cpio
157 if string(0, '!<arch>') or string(0, '\\<ar>') or string(0, '<ar>'):
158 if string(7, '\ndebian'):
159 return deb
160 if string(0, 'Rar!'): return rar
161 if string(0, 'PK\003\004'): return zip
163 # Compressed streams
164 if string(0, '\037\213'): return gz
165 if string(0, 'BZh'): return bz2
166 if string(0, 'BZ'): return bz2 # bzip, but maybe bzip2 can cope?
168 return make_gz
170 class FileData:
171 "A file on the local filesystem."
172 def __init__(self, path):
173 self.path = path
175 if path == '-':
176 source = sys.stdin
177 else:
178 try:
179 source = file(path)
180 except:
181 rox.report_exception()
182 sys.exit(1)
184 self.path = path
185 start = source.read(300)
186 try:
187 source.seek(0)
188 self.source = source
189 except:
190 print "(unseekable)"
191 # Input is not a regular, local, seekable file, so copy it
192 # to a local temp file.
193 import shutil
194 tmp = Tmp()
195 tmp.write(start)
196 shutil.copyfileobj(source, tmp)
197 self.source = tmp
198 self.default = guess_format(start)
200 if path == '-':
201 name = 'Data'
202 else:
203 name = path
204 for ext in known_extensions:
205 if path.endswith('.' + ext):
206 new = path[:-len(ext)-1]
207 if len(new) < len(name):
208 name = new
209 self.default_name = name
211 class DirData:
212 def __init__(self, path):
213 self.path = path
214 self.default = make_tgz
215 self.default_name = path + '.' + self.default.extension