diff --git a/bam/__init__.py b/bam/__init__.py index 619a557..ecaf7f1 100644 --- a/bam/__init__.py +++ b/bam/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import sys -__version__ = "0.0.4.8" +__version__ = "0.0.5.1" def main(argv=sys.argv): from .cli import main diff --git a/bam/blend/blendfile_copy.py b/bam/blend/blendfile_copy.py new file mode 100644 index 0000000..595f2b0 --- /dev/null +++ b/bam/blend/blendfile_copy.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +# ***** 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 ***** + +""" +A simply utility to copy blend files and their deps to a new location. + +Similar to packing, but don't attempt any path remapping. +""" + +from bam.blend import blendfile_path_walker + +TIMEIT = False + +# ------------------ +# Ensure module path +import os +import sys +path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "modules")) +if path not in sys.path: + sys.path.append(path) +del os, sys, path +# -------- + + +def copy_paths( + paths, + output, + base, + + # load every libs dep, not just used deps. + all_deps=False, + # yield reports + report=None, + + # Filename filter, allow to exclude files from the pack, + # function takes a string returns True if the files should be included. + filename_filter=None, + ): + + import os + import shutil + + from bam.utils.system import colorize, is_subdir + + path_copy_files = set(paths) + + # Avoid walking over same libs many times + lib_visit = {} + + yield report("Reading %d blend file(s)\n" % len(paths)) + for blendfile_src in paths: + yield report(" %s: %r\n" % (colorize("blend", color='blue'), blendfile_src)) + for fp, (rootdir, fp_blend_basename) in blendfile_path_walker.FilePath.visit_from_blend( + blendfile_src, + readonly=True, + recursive=True, + recursive_all=all_deps, + lib_visit=lib_visit, + ): + + f_abs = os.path.normpath(fp.filepath_absolute) + path_copy_files.add(f_abs) + + # Source -> Dest Map + path_src_dst_map = {} + + for path_src in sorted(path_copy_files): + + if filename_filter and not filename_filter(path_src): + yield report(" %s: %r\n" % (colorize("exclude", color='yellow'), path_src)) + continue + + if not os.path.exists(path_src): + yield report(" %s: %r\n" % (colorize("missing path", color='red'), path_src)) + continue + + if not is_subdir(path_src, base): + yield report(" %s: %r\n" % (colorize("external path ignored", color='red'), path_src)) + continue + + path_rel = os.path.relpath(path_src, base) + path_dst = os.path.join(output, path_rel) + + path_src_dst_map[path_src] = path_dst + + # Create directories + path_dst_dir = {os.path.dirname(path_dst) for path_dst in path_src_dst_map.values()} + yield report("Creating %d directories in %r\n" % (len(path_dst_dir), output)) + for path_dir in sorted(path_dst_dir): + os.makedirs(path_dir, exist_ok=True) + del path_dst_dir + + # Copy files + yield report("Copying %d files to %r\n" % (len(path_src_dst_map), output)) + for path_src, path_dst in sorted(path_src_dst_map.items()): + yield report(" %s: %r -> %r\n" % (colorize("copying", color='blue'), path_src, path_dst)) + shutil.copy(path_src, path_dst) diff --git a/bam/cli.py b/bam/cli.py index 8d82d4e..c87e935 100755 --- a/bam/cli.py +++ b/bam/cli.py @@ -1386,6 +1386,63 @@ class bam_commands: ): pass + @staticmethod + def copy( + paths, + output, + base, + all_deps=False, + use_quiet=False, + filename_filter=None, + ): + # Local packing (don't use any project/session stuff) + from .blend import blendfile_copy + from bam.utils.system import is_subdir + + paths = [os.path.abspath(path) for path in paths] + base = os.path.abspath(base) + output = os.path.abspath(output) + + # check all blends are in the base path + for path in paths: + if not is_subdir(path, base): + fatal("Input blend file %r is not a sub directory of %r" % (path, base)) + + if use_quiet: + report = lambda msg: None + else: + report = lambda msg: print(msg, end="") + + # replace var with a pattern matching callback + if filename_filter: + # convert string into regex callback + # "*.txt;*.png;*.rst" --> r".*\.txt$|.*\.png$|.*\.rst$" + import re + import fnmatch + + compiled_pattern = re.compile( + b'|'.join(fnmatch.translate(f).encode('utf-8') + for f in filename_filter.split(";") if f), + re.IGNORECASE, + ) + + def filename_filter(f): + return (not filename_filter.compiled_pattern.match(f)) + filename_filter.compiled_pattern = compiled_pattern + + del compiled_pattern + del re, fnmatch + + for msg in blendfile_copy.copy_paths( + [path.encode('utf-8') for path in paths], + output.encode('utf-8'), + base.encode('utf-8'), + all_deps=all_deps, + report=report, + filename_filter=filename_filter, + ): + pass + @staticmethod def remap_start( paths, @@ -1466,6 +1523,7 @@ def init_argparse_common( use_all_deps=False, use_quiet=False, use_compress_level=False, + use_exclude=False, ): import argparse @@ -1495,6 +1553,20 @@ def init_argparse_common( choices=('default', 'fast', 'best', 'store'), help="Compression level for resulting archive", ) + if use_exclude: + subparse.add_argument( + "-e", "--exclude", dest="exclude", metavar='PATTERN(S)', required=False, + default="", + help=""" + Optionally exclude files from the pack. + + Using Unix shell-style wildcards *(case insensitive)*. + ``--exclude="*.png"`` + + Multiple patterns can be passed using the ``;`` separator. + ``--exclude="*.txt;*.avi;*.wav"`` + """ + ) def create_argparse_init(subparsers): @@ -1707,21 +1779,8 @@ def create_argparse_pack(subparsers): choices=('ZIP', 'FILE'), help="Output file or a directory when multiple inputs are passed", ) - subparse.add_argument( - "-e", "--exclude", dest="exclude", metavar='PATTERN(S)', required=False, - default="", - help=""" - Optionally exclude files from the pack. - Using Unix shell-style wildcards *(case insensitive)*. - ``--exclude="*.png"`` - - Multiple patterns can be passed using the ``;`` separator. - ``--exclude="*.txt;*.avi;*.wav"`` - """ - ) - - init_argparse_common(subparse, use_all_deps=True, use_quiet=True, use_compress_level=True) + init_argparse_common(subparse, use_all_deps=True, use_quiet=True, use_compress_level=True, use_exclude=True) subparse.set_defaults( func=lambda args: @@ -1739,6 +1798,54 @@ def create_argparse_pack(subparsers): ) +def create_argparse_copy(subparsers): + import argparse + subparse = subparsers.add_parser( + "copy", aliases=("cp",), + help="Copy blend file(s) and their dependencies to a new location (maintaining the directory structure).", + description= + """ + The line below will copy ``scene.blend`` to ``/destination/to/scene.blend``. + + .. code-block:: sh + + bam copy /path/to/scene.blend --base=/path --output=/destination + + .. code-block:: sh + + # you can also copy multiple files + bam copy /path/to/scene.blend /path/other/file.blend --base=/path --output /other/destination + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparse.add_argument( + dest="paths", nargs="+", + help="Path(s) to blend files to operate on", + ) + subparse.add_argument( + "-o", "--output", dest="output", metavar='DIR', required=True, + help="Output directory where where files will be copied to", + ) + subparse.add_argument( + "-b", "--base", dest="base", metavar='DIR', required=True, + help="Base directory for input paths (files outside this path will be omitted)", + ) + + init_argparse_common(subparse, use_all_deps=True, use_quiet=True, use_exclude=True) + + subparse.set_defaults( + func=lambda args: + bam_commands.copy( + args.paths, + args.output, + args.base, + all_deps=args.all_deps, + use_quiet=args.use_quiet, + filename_filter=args.exclude, + ), + ) + + def create_argparse_remap(subparsers): import argparse @@ -1874,6 +1981,7 @@ def create_argparse(): # non-bam project commands create_argparse_deps(subparsers) create_argparse_pack(subparsers) + create_argparse_copy(subparsers) create_argparse_remap(subparsers) return parser diff --git a/bam/utils/system.py b/bam/utils/system.py index 3aee356..2c2260d 100644 --- a/bam/utils/system.py +++ b/bam/utils/system.py @@ -123,3 +123,27 @@ def is_compressed_filetype(filepath): # '.gz', '.tgz', # '.zip', } + + +def is_subdir(path, directory): + """ + Returns true if *path* in a subdirectory of *directory*. + """ + import os + from os.path import normpath, normcase + path = normpath(normcase(path)) + directory = normpath(normcase(directory)) + + if isinstance(directory, bytes): + sep_i = ord(os.sep) + sep = os.sep.encode('ascii') + else: + sep_i = os.sep + sep = os.sep + + directory = directory.rstrip(sep) + if len(path) > len(directory): + if path.startswith(directory): + return (path[len(directory)] == sep_i) + return False +