|
1 | 1 | #!/usr/bin/env python |
2 | 2 |
|
3 | | -""" |
4 | | -USAGE: |
5 | | -
|
6 | | - sage-uncompress-spkg [-d DIR] PKG [FILE] |
7 | | -
|
8 | | -With a single argument, unpack the file PKG to the current directory. |
9 | | -
|
10 | | -If a directory is specified with the -d option the contents of |
11 | | -the archive are extracted into that directory using the following |
12 | | -rules: |
13 | | -
|
14 | | - 1. If the archive contains (like most) a top-level directory |
15 | | - which contains all other files in the archive, the contents |
16 | | - of that directory are extracted into DIR, ignoring the name |
17 | | - of the top-level directory in the archive. |
18 | | -
|
19 | | - 2. If the archive does not contain a single top-level directory |
20 | | - then all the contents of the archive are extracted into DIR. |
21 | | -
|
22 | | -The directory must not already exist. |
23 | | -
|
24 | | -If FILE is specified, extract FILE from PKG and send it to |
25 | | -stdout. (This option is present only for backwards compatibility: |
26 | | -printing the SPKG.txt file from an old-style spkg.) |
27 | | -""" |
28 | | - |
29 | | -from __future__ import print_function |
30 | | - |
31 | | -import argparse |
32 | | -import copy |
33 | | -import os |
34 | | -import stat |
35 | | -import sys |
36 | | -import tarfile |
37 | | -import zipfile |
38 | | - |
39 | | - |
40 | | -def filter_os_files(filenames): |
41 | | - """ |
42 | | - Given a list of filenames, returns a filtered list with OS-specific |
43 | | - special files removed. |
44 | | -
|
45 | | - Currently removes OSX .DS_Store files and AppleDouble format ._ files. |
46 | | - """ |
47 | | - |
48 | | - files_set = set(filenames) |
49 | | - |
50 | | - def is_os_file(path): |
51 | | - dirname, name = os.path.split(path) |
52 | | - |
53 | | - if name == '.DS_Store': |
54 | | - return True |
55 | | - |
56 | | - if name.startswith('._'): |
57 | | - name = os.path.join(dirname, name[2:]) |
58 | | - # These files store extended attributes on OSX |
59 | | - # In principle this could be a false positive but it's |
60 | | - # unlikely, and to be really sure we'd have to extract the file |
61 | | - # (or at least the first four bytes to check for the magic number |
62 | | - # documented in |
63 | | - # http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf) |
64 | | - if name in files_set or os.path.normpath(name) in files_set: |
65 | | - return True |
66 | | - |
67 | | - return False |
68 | | - |
69 | | - filenames = filter(lambda f: not is_os_file(f), filenames) |
70 | | - |
71 | | - if sys.version_info[0] == 2: |
72 | | - return filenames |
73 | | - else: |
74 | | - # Make sure to return a list on Python >= 3 |
75 | | - return list(filenames) |
76 | | - |
77 | | - |
78 | | -class SageTarFile(tarfile.TarFile): |
79 | | - """ |
80 | | - Sage as tarfile.TarFile, but applies the user's current umask to the |
81 | | - permissions of all extracted files and directories. |
82 | | -
|
83 | | - This mimics the default behavior of the ``tar`` utility. |
84 | | -
|
85 | | - See http://trac.sagemath.org/ticket/20218#comment:16 for more background. |
86 | | - """ |
87 | | - |
88 | | - def __new__(cls, *args, **kwargs): |
89 | | - # This is is that SageTarFile() is equivalent to TarFile.open() which |
90 | | - # is more flexible than the basic TarFile.__init__ |
91 | | - inst = tarfile.TarFile.open(*args, **kwargs) |
92 | | - inst.__class__ = cls |
93 | | - return inst |
94 | | - |
95 | | - def __init__(self, *args, **kwargs): |
96 | | - # Unfortunately the only way to get the current umask is to set it |
97 | | - # and then restore it |
98 | | - self.umask = os.umask(0o777) |
99 | | - os.umask(self.umask) |
100 | | - |
101 | | - @classmethod |
102 | | - def can_read(cls, filename): |
103 | | - """ |
104 | | - Given an archive filename, returns True if this class can read and |
105 | | - process the archive format of that file. |
106 | | - """ |
107 | | - |
108 | | - return tarfile.is_tarfile(filename) |
109 | | - |
110 | | - @property |
111 | | - def names(self): |
112 | | - """ |
113 | | - List of filenames in the archive. |
114 | | -
|
115 | | - Filters out names of OS-related files that shouldn't be in the |
116 | | - archive (.DS_Store, etc.) |
117 | | - """ |
118 | | - |
119 | | - return filter_os_files(self.getnames()) |
120 | | - |
121 | | - def chmod(self, tarinfo, target): |
122 | | - tarinfo = copy.copy(tarinfo) |
123 | | - tarinfo.mode &= ~self.umask |
124 | | - tarinfo.mode &= ~(stat.S_ISUID | stat.S_ISGID) |
125 | | - return super(SageTarFile, self).chmod(tarinfo, target) |
126 | | - |
127 | | - def extractall(self, path='.', members=None): |
128 | | - """ |
129 | | - Same as tarfile.TarFile.extractall but allows filenames for |
130 | | - the members argument (like zipfile.ZipFile). |
131 | | - """ |
132 | | - if members: |
133 | | - name_to_member = dict([member.name, member] for member in self.getmembers()) |
134 | | - members = [m if isinstance(m, tarfile.TarInfo) |
135 | | - else name_to_member[m] |
136 | | - for m in members] |
137 | | - return super(SageTarFile, self).extractall(path=path, members=members) |
138 | | - |
139 | | - def extractbytes(self, member): |
140 | | - """ |
141 | | - Return the contents of the specified archive member as bytes. |
142 | | -
|
143 | | - If the member does not exist, returns None. |
144 | | - """ |
145 | | - |
146 | | - if member in self.getnames(): |
147 | | - reader = self.extractfile(member) |
148 | | - return reader.read() |
149 | | - |
150 | | - |
151 | | -class SageZipFile(zipfile.ZipFile): |
152 | | - """ |
153 | | - Wrapper for zipfile.ZipFile to provide better API fidelity with |
154 | | - SageTarFile insofar as it's used by this script. |
155 | | - """ |
156 | | - |
157 | | - @classmethod |
158 | | - def can_read(cls, filename): |
159 | | - """ |
160 | | - Given an archive filename, returns True if this class can read and |
161 | | - process the archive format of that file. |
162 | | - """ |
163 | | - |
164 | | - return zipfile.is_zipfile(filename) |
165 | | - |
166 | | - @property |
167 | | - def names(self): |
168 | | - """ |
169 | | - List of filenames in the archive. |
170 | | -
|
171 | | - Filters out names of OS-related files that shouldn't be in the |
172 | | - archive (.DS_Store, etc.) |
173 | | - """ |
174 | | - |
175 | | - return filter_os_files(self.namelist()) |
176 | | - |
177 | | - def extractbytes(self, member): |
178 | | - """ |
179 | | - Return the contents of the specified archive member as bytes. |
180 | | -
|
181 | | - If the member does not exist, returns None. |
182 | | - """ |
183 | | - |
184 | | - if member in self.namelist(): |
185 | | - return self.read(member) |
186 | | - |
187 | | - |
188 | | -ARCHIVE_TYPES = [SageTarFile, SageZipFile] |
189 | | - |
190 | | - |
191 | | -def main(argv=None): |
192 | | - parser = argparse.ArgumentParser() |
193 | | - parser.add_argument('-d', dest='dir', metavar='DIR', |
194 | | - help='directory to extract archive contents into') |
195 | | - parser.add_argument('pkg', nargs=1, metavar='PKG', |
196 | | - help='the archive to extract') |
197 | | - parser.add_argument('file', nargs='?', metavar='FILE', |
198 | | - help='(deprecated) print the contents of the given ' |
199 | | - 'archive member to stdout') |
200 | | - |
201 | | - args = parser.parse_args(argv) |
202 | | - |
203 | | - filename = args.pkg[0] |
204 | | - dirname = args.dir |
205 | | - |
206 | | - for cls in ARCHIVE_TYPES: |
207 | | - if cls.can_read(filename): |
208 | | - break |
209 | | - else: |
210 | | - print('Error: Unknown file type: {}'.format(filename), |
211 | | - file=sys.stderr) |
212 | | - return 1 |
213 | | - |
214 | | - # For now ZipFile and TarFile both have default open modes that are |
215 | | - # acceptable |
216 | | - archive = cls(filename) |
217 | | - |
218 | | - if args.file: |
219 | | - contents = archive.extractbytes(args.file) |
220 | | - if contents: |
221 | | - print(contents, end='') |
222 | | - return 0 |
223 | | - else: |
224 | | - return 1 |
225 | | - |
226 | | - top_level = None |
227 | | - |
228 | | - if dirname: |
229 | | - if os.path.exists(dirname): |
230 | | - print('Error: Directory {} already exists'.format(dirname), |
231 | | - file=sys.stderr) |
232 | | - return 1 |
233 | | - |
234 | | - top_levels = set() |
235 | | - for member in archive.names: |
236 | | - # Zip and tar files all use forward slashes as separators |
237 | | - # internally |
238 | | - top_levels.add(member.split('/', 1)[0]) |
239 | | - |
240 | | - if len(top_levels) == 1: |
241 | | - top_level = top_levels.pop() |
242 | | - else: |
243 | | - os.makedirs(dirname) |
244 | | - |
245 | | - prev_cwd = os.getcwd() |
246 | | - |
247 | | - if dirname and not top_level: |
248 | | - # We want to extract content into dirname, but there is not |
249 | | - # a single top-level directory for the tarball, so we cd into |
250 | | - # the extraction target first |
251 | | - os.chdir(dirname) |
252 | | - |
253 | | - try: |
254 | | - archive.extractall(members=archive.names) |
255 | | - if dirname and top_level: |
256 | | - os.rename(top_level, dirname) |
257 | | - finally: |
258 | | - os.chdir(prev_cwd) |
259 | | - |
260 | | - return 0 |
261 | | - |
262 | | - |
263 | | -if __name__ == '__main__': |
264 | | - sys.exit(main()) |
| 3 | +# usage: sage-uncompress-spkg [-h] [-d DIR] PKG [FILE] |
| 4 | +# |
| 5 | +# positional arguments: |
| 6 | +# PKG the archive to extract |
| 7 | +# FILE (deprecated) print the contents of the given archive member to |
| 8 | +# stdout |
| 9 | +# |
| 10 | +# optional arguments: |
| 11 | +# -h, --help show this help message and exit |
| 12 | +# -d DIR directory to extract archive contents into |
| 13 | + |
| 14 | + |
| 15 | +try: |
| 16 | + import sage_bootstrap |
| 17 | +except ImportError: |
| 18 | + import os, sys |
| 19 | + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) |
| 20 | + import sage_bootstrap |
| 21 | + |
| 22 | +from sage_bootstrap.uncompress.cmdline import run |
| 23 | +run() |
0 commit comments