| # Copyright (c) 2015, Google Inc. | |
| # | |
| # Permission to use, copy, modify, and/or distribute this software for any | |
| # purpose with or without fee is hereby granted, provided that the above | |
| # copyright notice and this permission notice appear in all copies. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
| # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
| # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | |
| # SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
| # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION | |
| # OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |
| # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
| """Extracts archives.""" | |
| import hashlib | |
| import optparse | |
| import os | |
| import os.path | |
| import tarfile | |
| import shutil | |
| import sys | |
| import zipfile | |
| def CheckedJoin(output, path): | |
| """ | |
| CheckedJoin returns os.path.join(output, path). It checks that the resulting | |
| path is under output, but shouldn't be used on untrusted input. | |
| """ | |
| path = os.path.normpath(path) | |
| if os.path.isabs(path) or path.startswith("."): | |
| raise ValueError(path) | |
| return os.path.join(output, path) | |
| class FileEntry(object): | |
| def __init__(self, path, mode, fileobj): | |
| self.path = path | |
| self.mode = mode | |
| self.fileobj = fileobj | |
| class SymlinkEntry(object): | |
| def __init__(self, path, mode, target): | |
| self.path = path | |
| self.mode = mode | |
| self.target = target | |
| def IterateZip(path): | |
| """ | |
| IterateZip opens the zip file at path and returns a generator of entry objects | |
| for each file in it. | |
| """ | |
| with zipfile.ZipFile(path, "r") as zip_file: | |
| for info in zip_file.infolist(): | |
| if info.filename.endswith("/"): | |
| continue | |
| yield FileEntry(info.filename, None, zip_file.open(info)) | |
| def IterateTar(path, compression): | |
| """ | |
| IterateTar opens the tar.gz or tar.bz2 file at path and returns a generator of | |
| entry objects for each file in it. | |
| """ | |
| with tarfile.open(path, "r:" + compression) as tar_file: | |
| for info in tar_file: | |
| if info.isdir(): | |
| pass | |
| elif info.issym(): | |
| yield SymlinkEntry(info.name, None, info.linkname) | |
| elif info.isfile(): | |
| yield FileEntry(info.name, info.mode, | |
| tar_file.extractfile(info)) | |
| else: | |
| raise ValueError('Unknown entry type "%s"' % (info.name, )) | |
| def main(args): | |
| parser = optparse.OptionParser(usage="Usage: %prog ARCHIVE OUTPUT") | |
| parser.add_option( | |
| "--no-prefix", | |
| dest="no_prefix", | |
| action="store_true", | |
| help="Do not remove a prefix from paths in the archive.", | |
| ) | |
| options, args = parser.parse_args(args) | |
| if len(args) != 2: | |
| parser.print_help() | |
| return 1 | |
| archive, output = args | |
| if not os.path.exists(archive): | |
| # Skip archives that weren't downloaded. | |
| return 0 | |
| with open(archive, "rb") as f: | |
| sha256 = hashlib.sha256() | |
| while True: | |
| chunk = f.read(1024 * 1024) | |
| if not chunk: | |
| break | |
| sha256.update(chunk) | |
| digest = sha256.hexdigest() | |
| stamp_path = os.path.join(output, ".dawn_archive_digest") | |
| if os.path.exists(stamp_path): | |
| with open(stamp_path) as f: | |
| if f.read().strip() == digest: | |
| print("Already up-to-date.") | |
| return 0 | |
| if archive.endswith(".zip"): | |
| entries = IterateZip(archive) | |
| elif archive.endswith(".tar.gz"): | |
| entries = IterateTar(archive, "gz") | |
| elif archive.endswith(".tar.bz2"): | |
| entries = IterateTar(archive, "bz2") | |
| else: | |
| raise ValueError(archive) | |
| try: | |
| if os.path.exists(output): | |
| print("Removing %s" % (output, )) | |
| shutil.rmtree(output) | |
| print("Extracting %s to %s" % (archive, output)) | |
| prefix = None | |
| num_extracted = 0 | |
| for entry in entries: | |
| # Even on Windows, zip files must always use forward slashes. | |
| if "\\" in entry.path or entry.path.startswith("/"): | |
| raise ValueError(entry.path) | |
| if not options.no_prefix: | |
| new_prefix, rest = entry.path.split("/", 1) | |
| # Ensure the archive is consistent. | |
| if prefix is None: | |
| prefix = new_prefix | |
| if prefix != new_prefix: | |
| raise ValueError((prefix, new_prefix)) | |
| else: | |
| rest = entry.path | |
| # Extract the file into the output directory. | |
| fixed_path = CheckedJoin(output, rest) | |
| if not os.path.isdir(os.path.dirname(fixed_path)): | |
| os.makedirs(os.path.dirname(fixed_path)) | |
| if isinstance(entry, FileEntry): | |
| with open(fixed_path, "wb") as out: | |
| shutil.copyfileobj(entry.fileobj, out) | |
| elif isinstance(entry, SymlinkEntry): | |
| os.symlink(entry.target, fixed_path) | |
| else: | |
| raise TypeError("unknown entry type") | |
| # Fix up permissions if needbe. | |
| # TODO(davidben): To be extra tidy, this should only track the execute bit | |
| # as in git. | |
| if entry.mode is not None: | |
| os.chmod(fixed_path, entry.mode) | |
| # Print every 100 files, so bots do not time out on large archives. | |
| num_extracted += 1 | |
| if num_extracted % 100 == 0: | |
| print("Extracted %d files..." % (num_extracted, )) | |
| finally: | |
| entries.close() | |
| with open(stamp_path, "w") as f: | |
| f.write(digest) | |
| print("Done. Extracted %d files." % (num_extracted, )) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main(sys.argv[1:])) |