Skip to content
This repository was archived by the owner on Jan 30, 2023. It is now read-only.

Commit 775a3d3

Browse files
committed
Refactor sage-uncompress-spkg, add tests
1 parent cadb3b6 commit 775a3d3

File tree

11 files changed

+498
-267
lines changed

11 files changed

+498
-267
lines changed

build/bin/sage-uncompress-spkg

Lines changed: 21 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -1,264 +1,23 @@
11
#!/usr/bin/env python
22

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()

build/sage_bootstrap/cmdline.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222
import logging
2323
log = logging.getLogger()
2424

25+
2526
# Note that argparse is not part of Python 2.6, so we bundle it
26-
from sage_bootstrap.compat import argparse
27+
try:
28+
import argparse
29+
except ImportError:
30+
from sage_bootstrap.compat import argparse
2731

2832
from sage_bootstrap.app import Application
2933

build/sage_bootstrap/download/cmdline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
log = logging.getLogger()
2323

2424
# Note that argparse is not part of Python 2.6, so we bundle it
25-
from sage_bootstrap.compat import argparse
25+
try:
26+
import argparse
27+
except ImportError:
28+
from sage_bootstrap.compat import argparse
2629

2730
from sage_bootstrap.download.app import Application
2831
from sage_bootstrap.env import SAGE_DISTFILES

build/sage_bootstrap/uncompress/__init__.py

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
"""
3+
4+
#*****************************************************************************
5+
# Copyright (C) 2016 Volker Braun <vbraun.name@gmail.com>
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation, either version 2 of the License, or
10+
# (at your option) any later version.
11+
# http://www.gnu.org/licenses/
12+
#*****************************************************************************
13+
14+
from __future__ import print_function
15+
16+
import os
17+
18+
from sage_bootstrap.uncompress.tar_file import SageTarFile
19+
from sage_bootstrap.uncompress.zip_file import SageZipFile
20+
21+
ARCHIVE_TYPES = [SageTarFile, SageZipFile]
22+
23+
24+
25+
def open_archive(filename):
26+
"""
27+
Automatically detect archive type
28+
"""
29+
for cls in ARCHIVE_TYPES:
30+
if cls.can_read(filename):
31+
break
32+
else:
33+
raise ValueError
34+
35+
# For now ZipFile and TarFile both have default open modes that are
36+
# acceptable
37+
return cls(filename)
38+
39+
40+
def unpack_archive(archive, dirname=None):
41+
"""
42+
Unpack archive
43+
"""
44+
top_level = None
45+
46+
if dirname:
47+
top_levels = set()
48+
for member in archive.names:
49+
# Zip and tar files all use forward slashes as separators
50+
# internally
51+
top_levels.add(member.split('/', 1)[0])
52+
53+
if len(top_levels) == 1:
54+
top_level = top_levels.pop()
55+
else:
56+
os.makedirs(dirname)
57+
58+
prev_cwd = os.getcwd()
59+
60+
if dirname and not top_level:
61+
# We want to extract content into dirname, but there is not
62+
# a single top-level directory for the tarball, so we cd into
63+
# the extraction target first
64+
os.chdir(dirname)
65+
66+
try:
67+
archive.extractall(members=archive.names)
68+
if dirname and top_level:
69+
os.rename(top_level, dirname)
70+
finally:
71+
os.chdir(prev_cwd)

0 commit comments

Comments
 (0)