# ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ***** END GPL LICENCE BLOCK ***** # # (c) 2018, Blender Foundation - Sybren A. Stüvel """Create a BAT-pack for the given blend file.""" import logging import os import pathlib import sys import typing import blender_asset_tracer.pack.transfer from blender_asset_tracer import pack, bpathlib log = logging.getLogger(__name__) def add_parser(subparsers): """Add argparser for this subcommand.""" parser = subparsers.add_parser("pack", help=__doc__) parser.set_defaults(func=cli_pack) parser.add_argument("blendfile", nargs='?', type=pathlib.Path, default=None, help="The Blend file to pack (omit when using --sequence).") parser.add_argument( "target", type=str, help="The target can be a directory, a ZIP file (does not have to exist " "yet, just use 'something.zip' as target), " "or a URL of S3 storage (s3://endpoint/path) " "or Shaman storage (shaman://endpoint/#checkoutID).", ) parser.add_argument( "-p", "--project", type=pathlib.Path, help="Root directory of your project. Paths to below this directory are " "kept in the BAT Pack as well, whereas references to assets from " "outside this directory will have to be rewitten. The blend file MUST " "be inside the project directory. If this option is ommitted, the " "directory containing the blend file is taken as the project " "directoy.", ) parser.add_argument( "-n", "--noop", default=False, action="store_true", help="Don't copy files, just show what would be done.", ) parser.add_argument( "-e", "--exclude", nargs="*", default="", help="Space-separated list of glob patterns (like '*.abc *.vbo') to " "exclude.", ) parser.add_argument( "-c", "--compress", default=False, action="store_true", help="Compress blend files while copying. This option is only valid when " "packing into a directory (contrary to ZIP file or S3 upload). " "Note that files will NOT be compressed when the destination file " "already exists and has the same size as the original file.", ) parser.add_argument( "-r", "--relative-only", default=False, action="store_true", help="Only pack assets that are referred to with a relative path (e.g. " "starting with `//`.", ) parser.add_argument( "--keep-hierarchy", default=False, action="store_true", help="Preserve the full filesystem directory hierarchy in the pack. " "All files (including the blend file) are placed at their absolute " "path structure under the target directory. Paths in blend files are " "rewritten to relative paths within this structure.", ) parser.add_argument( "--sequence", nargs="+", type=pathlib.Path, metavar="BLENDFILE", help="Pack multiple blend files together, deduplicating shared dependencies. " "All listed blend files and their dependencies are packed into the target.", ) def cli_pack(args): bpaths, ppath, tpath = paths_from_cli(args) with create_packer(args, bpaths, ppath, tpath) as packer: packer.strategise() try: packer.execute() except blender_asset_tracer.pack.transfer.FileTransferError as ex: log.error( "%d files couldn't be copied, starting with %s", len(ex.files_remaining), ex.files_remaining[0], ) raise SystemExit(1) def create_packer( args, bpaths: typing.List[pathlib.Path], ppath: pathlib.Path, target: str ) -> pack.Packer: if target.startswith("s3:/"): if len(bpaths) > 1: raise ValueError("S3 uploader does not support --sequence") if args.noop: raise ValueError("S3 uploader does not support no-op.") if args.compress: raise ValueError("S3 uploader does not support on-the-fly compression") if args.relative_only: raise ValueError("S3 uploader does not support the --relative-only option") if args.keep_hierarchy: raise ValueError("S3 uploader does not support the --keep-hierarchy option") packer = create_s3packer(bpaths[0], ppath, pathlib.PurePosixPath(target)) elif ( target.startswith("shaman+http:/") or target.startswith("shaman+https:/") or target.startswith("shaman:/") ): if len(bpaths) > 1: raise ValueError("Shaman uploader does not support --sequence") if args.noop: raise ValueError("Shaman uploader does not support no-op.") if args.compress: raise ValueError("Shaman uploader does not support on-the-fly compression") if args.relative_only: raise ValueError( "Shaman uploader does not support the --relative-only option" ) if args.keep_hierarchy: raise ValueError( "Shaman uploader does not support the --keep-hierarchy option" ) packer = create_shamanpacker(bpaths[0], ppath, target) elif target.lower().endswith(".zip"): from blender_asset_tracer.pack import zipped if args.compress: raise ValueError("ZIP packer does not support on-the-fly compression") packer = zipped.ZipPacker( bpaths, ppath, target, noop=args.noop, relative_only=args.relative_only, keep_hierarchy=args.keep_hierarchy, ) else: packer = pack.Packer( bpaths, ppath, target, noop=args.noop, compress=args.compress, relative_only=args.relative_only, keep_hierarchy=args.keep_hierarchy, ) if args.exclude: # args.exclude is a list, due to nargs='*', so we have to split and flatten. globs = [glob for globs in args.exclude for glob in globs.split()] log.info("Excluding: %s", ", ".join(repr(g) for g in globs)) packer.exclude(*globs) return packer def create_s3packer(bpath, ppath, tpath) -> pack.Packer: from blender_asset_tracer.pack import s3 # Split the target path into 's3:/', hostname, and actual target path parts = tpath.parts endpoint = "https://%s/" % parts[1] tpath = pathlib.Path(*tpath.parts[2:]) log.info("Uploading to S3-compatible storage %s at %s", endpoint, tpath) return s3.S3Packer(bpath, ppath, tpath, endpoint=endpoint) def create_shamanpacker( bpath: pathlib.Path, ppath: pathlib.Path, tpath: str ) -> pack.Packer: """Creates a package for sending files to a Shaman server. URLs should have the form: shaman://hostname/base/url#jobID This uses HTTPS to connect to the server. To connect using HTTP, use: shaman+http://hostname/base-url#jobID """ from blender_asset_tracer.pack import shaman endpoint, checkout_id = shaman.parse_endpoint(tpath) if not checkout_id: log.warning( "No checkout ID given on the URL. Going to send BAT pack to Shaman, " "but NOT creating a checkout" ) log.info("Uploading to Shaman server %s with job %s", endpoint, checkout_id) return shaman.ShamanPacker( bpath, ppath, "/", endpoint=endpoint, checkout_id=checkout_id ) def paths_from_cli(args) -> typing.Tuple[typing.List[pathlib.Path], pathlib.Path, str]: """Return paths to blendfile(s), project, and pack target. Calls sys.exit() if anything is wrong. """ # Collect blend files from positional and --sequence arguments. bpaths = [] # type: typing.List[pathlib.Path] if args.blendfile is not None: bpaths.append(args.blendfile) if args.sequence: bpaths.extend(args.sequence) if not bpaths: log.critical("No blend file specified. Provide a positional blendfile or use --sequence.") sys.exit(3) # Deduplicate preserving order, in case the same file appears both as # positional arg and in --sequence. bpaths = list(dict.fromkeys(bpaths)) # Validate each blend file and make absolute. for i, bpath in enumerate(bpaths): if not bpath.exists(): log.critical("File %s does not exist", bpath) sys.exit(3) if bpath.is_dir(): log.critical("%s is a directory, should be a blend file", bpath) sys.exit(3) bpaths[i] = bpathlib.make_absolute(bpath) tpath = args.target if args.project is None: if len(bpaths) == 1: ppath = bpaths[0].parent else: ppath = pathlib.Path(os.path.commonpath([p.parent for p in bpaths])) log.warning("No project path given, using %s", ppath) else: ppath = bpathlib.make_absolute(args.project) if not ppath.exists(): log.critical("Project directory %s does not exist", ppath) sys.exit(5) if not ppath.is_dir(): log.warning( "Project path %s is not a directory; using the parent %s", ppath, ppath.parent, ) ppath = ppath.parent for bpath in bpaths: try: bpath.relative_to(ppath) except ValueError: log.critical( "Project directory %s does not contain blend file %s", ppath, bpath, ) sys.exit(5) for bpath in bpaths: log.info("Blend file to pack: %s", bpath) log.info("Project path: %s", ppath) log.info("Pack will be created in: %s", tpath) return bpaths, ppath, tpath