| # Copyright 2020 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Takes a set of input files and zips them up.""" |
| |
| import argparse |
| import pathlib |
| import sys |
| import zipfile |
| |
| from collections.abc import Iterable |
| |
| DEFAULT_DELIMITER = '>' |
| |
| |
| class ZipError(Exception): |
| """Raised when a pw_zip archive can't be built as specified.""" |
| |
| |
| def _parse_args(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| '--delimiter', |
| nargs='?', |
| default=DEFAULT_DELIMITER, |
| help='Symbol that separates the path and the zip path destination.') |
| parser.add_argument( |
| '--input_list', |
| nargs='+', |
| help='Paths to files and dirs to zip and their desired zip location.') |
| parser.add_argument('--out_filename', help='Zip file destination.') |
| |
| return parser.parse_args() |
| |
| |
| def zip_up(input_list: Iterable, |
| out_filename: str, |
| delimiter=DEFAULT_DELIMITER): |
| """Zips up all input files/dirs. |
| |
| Args: |
| input_list: List of strings consisting of file or directory, |
| the delimiter, and a path to the desired .zip destination. |
| out_filename: Path and name of the .zip file. |
| delimiter: string that separates the input source and the zip |
| destination. Defaults to '>'. Examples: |
| '/foo.txt > /' # /foo.txt zipped as /foo.txt |
| '/foo.txt > /bar.txt' # /foo.txt zipped as /bar.txt |
| 'foo.txt > /' # foo.txt from invokers dir zipped as /foo.txt |
| '/bar/ > /' # Whole bar dir zipped into / |
| """ |
| with zipfile.ZipFile(out_filename, 'w', zipfile.ZIP_DEFLATED) as zip_file: |
| for _input in input_list: |
| try: |
| source, destination = _input.split(delimiter) |
| source = source.strip() |
| destination = destination.strip() |
| except ValueError as value_error: |
| msg = ( |
| f'Input in the form of "[filename or dir] {delimiter} ' |
| f'/zip_destination/" expected. Instead got:\n {_input}') |
| raise ZipError(msg) from value_error |
| if not source: |
| raise ZipError( |
| f'Bad input:\n {_input}\nInput source ' |
| f'cannot be empty. Please specify the input in the form ' |
| f'of "[filename or dir] {delimiter} /zip_destination/".') |
| if not destination.startswith('/'): |
| raise ZipError( |
| f'Bad input:\n {_input}\nZip desination ' |
| f'"{destination}" must start with "/" to indicate the ' |
| f'zip file\'s root directory.') |
| source_path = pathlib.Path(source) |
| destination_path = pathlib.PurePath(destination) |
| |
| # Case: the input source path points to a file. |
| if source_path.is_file(): |
| # Case: "foo.txt > /mydir/"; destination is dir. Put foo.txt |
| # into mydir as /mydir/foo.txt |
| if destination.endswith('/'): |
| zip_file.write(source_path, |
| destination_path / source_path.name) |
| # Case: "foo.txt > /bar.txt"; destination is a file--rename the |
| # source file: put foo.txt into the zip as /bar.txt |
| else: |
| zip_file.write(source_path, destination_path) |
| continue |
| # Case: the input source path points to a directory. |
| if source_path.is_dir(): |
| zip_up_dir(source, source_path, destination, destination_path, |
| zip_file) |
| continue |
| raise ZipError(f'Unknown source path\n {source_path}') |
| |
| |
| def zip_up_dir(source: str, source_path: pathlib.Path, destination: str, |
| destination_path: pathlib.PurePath, zip_file: zipfile.ZipFile): |
| if not source.endswith('/'): |
| raise ZipError( |
| f'Source path:\n {source}\nis a directory, but is ' |
| f'missing a trailing "/". The / requirement helps prevent bugs. ' |
| f'To fix, add a trailing /:\n {source}/') |
| if not destination.endswith('/'): |
| raise ZipError( |
| f'Destination path:\n {destination}\nis a directory, ' |
| f'but is missing a trailing "/". The / requirement helps prevent ' |
| f'bugs. To fix, add a trailing /:\n {destination}/') |
| |
| # Walk the directory and add zip all of the files with the |
| # same structure as the source. |
| for file_path in source_path.glob('**/*'): |
| if file_path.is_file(): |
| rel_path = file_path.relative_to(source_path) |
| zip_file.write(file_path, destination_path / rel_path) |
| |
| |
| def main(): |
| zip_up(**vars(_parse_args())) |
| |
| |
| if __name__ == '__main__': |
| try: |
| main() |
| except ZipError as err: |
| print('ERROR:', str(err), file=sys.stderr) |
| sys.exit(1) |