feature: pack folder

This commit is contained in:
2026-09-10 13:38:47 +02:00
parent 6998242b58
commit 89bcad5cdc
9 changed files with 978 additions and 16 deletions
+1
View File
@@ -80,6 +80,7 @@ def cli_main():
blocks.add_parser(subparsers)
pack.add_parser(subparsers)
pack.add_sequence_parser(subparsers)
pack.add_folder_parser(subparsers)
list_deps.add_parser(subparsers)
version.add_parser(subparsers)
+143 -13
View File
@@ -26,6 +26,7 @@ import typing
import blender_asset_tracer.pack.transfer
from blender_asset_tracer import pack, bpathlib
from blender_asset_tracer.pack import folder as folder_scan
log = logging.getLogger(__name__)
@@ -166,6 +167,102 @@ def add_sequence_parser(subparsers):
)
def add_folder_parser(subparsers):
"""Add argparser for the pack-folder subcommand."""
parser = subparsers.add_parser(
"pack-folder",
help="Pack every blend file in a directory tree, together with their "
"dependencies, deduplicating shared assets.",
)
parser.set_defaults(func=cli_pack_folder)
parser.add_argument(
"folder",
type=pathlib.Path,
help="Directory to scan recursively for blend files.",
)
parser.add_argument(
"-t",
"--target",
type=str,
required=True,
help="Target directory or ZIP file.",
)
parser.add_argument(
"-p",
"--project",
type=pathlib.Path,
help="Root directory of your project. Defaults to the scanned folder, "
"so that the pack mirrors the folder's own structure and assets from "
"outside it end up in '_outside_project'.",
)
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 to exclude.",
)
parser.add_argument(
"-c",
"--compress",
default=False,
action="store_true",
help="Compress blend files while copying.",
)
parser.add_argument(
"-r",
"--relative-only",
default=False,
action="store_true",
help="Only pack assets referred to with a relative path.",
)
parser.add_argument(
"--keep-hierarchy",
default=False,
action="store_true",
help="Preserve the full filesystem directory hierarchy in the pack, "
"instead of packing relative to the scanned folder.",
)
parser.add_argument(
"--latest-only",
default=False,
action="store_true",
help="Of every 'name_vNNN.blend' version series in a directory, only "
"pack the highest version. Variants like 'name_vNNN_light.blend' form "
"their own series; files without a version are always packed.",
)
parser.add_argument(
"--follow-symlinks",
default=False,
action="store_true",
help="Descend into symlinked directories while scanning.",
)
parser.add_argument(
"--ignore-dir",
nargs="*",
default=list(folder_scan.DEFAULT_IGNORE_DIRS),
metavar="GLOB",
help="Globs of directory names to skip while scanning (default: %s)."
% " ".join(folder_scan.DEFAULT_IGNORE_DIRS),
)
parser.add_argument(
"--no-validate",
dest="validate",
default=True,
action="store_false",
help="Don't check that each '.blend' file really is a blend file. "
"Faster, but a non-blend file will abort the pack.",
)
def derive_common_project(bpaths: typing.List[pathlib.Path]) -> pathlib.Path:
"""Derive common project directory from multiple blend file paths.
@@ -202,25 +299,58 @@ def cli_pack_sequence(args):
log.critical("No target specified. Use -t/--target.")
sys.exit(3)
bpaths, ppath, tpath = paths_from_cli(args)
run_packer(args, *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 cli_pack_folder(args):
"""CLI entry point for pack-folder subcommand."""
fpath = args.folder
if not fpath.exists():
log.critical("Folder %s does not exist", fpath)
sys.exit(3)
if not fpath.is_dir():
log.critical("%s is not a directory", fpath)
sys.exit(3)
fpath = bpathlib.make_absolute(fpath)
log.info("Scanning %s for blend files", fpath)
scan = folder_scan.find_blend_files(
fpath,
follow_symlinks=args.follow_symlinks,
ignore_dirs=args.ignore_dir,
latest_only=args.latest_only,
validate=args.validate,
)
for dirpath, reason in scan.unreadable_dirs:
log.error("Could not scan %s: %s", dirpath, reason)
if scan.skipped:
log.warning("Skipped %d file(s) while scanning", len(scan.skipped))
if not scan.blendfiles:
log.critical("No blend files found in %s", fpath)
sys.exit(3)
log.warning("Found %d blend files in %s", len(scan.blendfiles), fpath)
# Synthesize args to reuse paths_from_cli and create_packer.
args.blendfile = None
args.sequence = scan.blendfiles
if args.project is None:
args.project = fpath
run_packer(args, *paths_from_cli(args))
def cli_pack(args):
if args.sequence:
log.warning("--sequence on 'pack' is deprecated. Use 'bat pack-sequence -t TARGET FILE...' instead.")
bpaths, ppath, tpath = paths_from_cli(args)
run_packer(args, *paths_from_cli(args))
def run_packer(
args, bpaths: typing.List[pathlib.Path], ppath: pathlib.Path, tpath: str
) -> None:
"""Create the packer for these paths, and run it."""
with create_packer(args, bpaths, ppath, tpath) as packer:
packer.strategise()