diff --git a/blender_asset_tracer/__init__.py b/blender_asset_tracer/__init__.py index f5a632b..6d27b73 100644 --- a/blender_asset_tracer/__init__.py +++ b/blender_asset_tracer/__init__.py @@ -20,7 +20,7 @@ # -__version__ = '1.21' +__version__ = "1.21" bl_info = { "name": "Blender Asset Tracer", @@ -37,192 +37,40 @@ bl_info = { # Reset root module name if folder has an unexpected name # (like "blender_asset_tracer-main" from zip-dl) import sys + if __name__ != "blender_asset_tracer": sys.modules["blender_asset_tracer"] = sys.modules[__name__] try: import bpy - from bpy.types import Operator - from bpy_extras.io_utils import ExportHelper + _HAS_BPY = True except ImportError: _HAS_BPY = False if _HAS_BPY: - import zipfile - import os - import re - import subprocess - import tempfile - from pathlib import Path, PurePath - - from blender_asset_tracer.pack import zipped from blender_asset_tracer import blendfile - from . import preferences + from . import preferences, operators # Match the CLI's default: skip dangling pointers gracefully instead of crashing. # Production blend files often have references to missing linked libraries. blendfile.set_strict_pointer_mode(False) - class ExportBatPack(Operator, ExportHelper): - bl_idname = "export_bat.pack" - bl_label = "Export to Archive using BAT" - filename_ext = ".zip" - - @classmethod - def poll(cls, context): - return bpy.data.is_saved - - def execute(self, context): - outfname = bpy.path.ensure_ext(self.filepath, ".zip") - self.report({'INFO'}, 'Executing ZipPacker ...') - - with zipped.ZipPacker( - Path(bpy.data.filepath), - Path(bpy.data.filepath).parent, - str(self.filepath)) as packer: - packer.strategise() - packer.execute() - self.report({'INFO'}, 'Packing successful!') - - with zipfile.ZipFile(str(self.filepath)) as inzip: - inzip.testzip() - - self.report({'INFO'}, 'Written to %s' % outfname) - return {'FINISHED'} - - def open_folder(folderpath): - """Open the folder at the path given with cmd relative to user's OS.""" - from shutil import which - my_os = sys.platform - if my_os.startswith(('linux', 'freebsd')): - cmd = 'xdg-open' - elif my_os.startswith('win'): - cmd = 'explorer' - if not folderpath: - return - else: - cmd = 'open' - - if not folderpath: - return - - folderpath = str(folderpath) - if os.path.isfile(folderpath): - select = False - if my_os.startswith('win'): - cmd = 'explorer /select,' - select = True - elif my_os.startswith(('linux', 'freebsd')): - if which('nemo'): - cmd = 'nemo --no-desktop' - select = True - elif which('nautilus'): - cmd = 'nautilus --no-desktop' - select = True - if not select: - folderpath = os.path.dirname(folderpath) - - folderpath = os.path.normpath(folderpath) - fullcmd = cmd.split() + [folderpath] - subprocess.Popen(fullcmd) - - class BAT_OT_export_zip(Operator, ExportHelper): - """Export current blendfile with hierarchy preservation""" - bl_label = "BAT Pack - keep hierarchy" - bl_idname = "bat.export_zip" - - filename_ext = '.zip' - - root_dir: bpy.props.StringProperty( - name="Root", - description='Top Level Folder of your project.' - '\nFor now Copy/Paste correct folder by hand if default is incorrect.' - '\n!!! Everything outside won\'t be zipped !!!', - ) - - use_zip: bpy.props.BoolProperty( - name="Output as ZIP", - description="If enabled, pack into a ZIP archive. If disabled, copy to a directory.", - default=True, - ) - - @classmethod - def poll(cls, context): - return bpy.data.is_saved - - def execute(self, context): - from blender_asset_tracer.pack import Packer - from blender_asset_tracer.pack.zipped import ZipPacker - - bfile = Path(bpy.data.filepath) - project = Path(self.root_dir) if self.root_dir else bfile.parent - target = str(self.filepath) - - if self.use_zip: - target = bpy.path.ensure_ext(target, ".zip") - packer_cls = ZipPacker - else: - if target.lower().endswith('.zip'): - target = target[:-4] - packer_cls = Packer - - self.report({'INFO'}, 'Packing with hierarchy...') - - with packer_cls(bfile, project, target, - keep_hierarchy=True) as packer: - packer.strategise() - packer.execute() - - if self.use_zip: - with zipfile.ZipFile(target) as inzip: - inzip.testzip() - log_output = Path(tempfile.gettempdir(), 'README.txt') - with open(log_output, 'w') as log: - log.write("Packed with BAT (keep-hierarchy mode)") - log.write(f"\nBlend file: {bpy.data.filepath}") - with zipfile.ZipFile(target, 'a') as zipObj: - zipObj.write(log_output, log_output.name) - - self.report({'INFO'}, 'Written to %s' % target) - open_folder(Path(target).parent) - return {'FINISHED'} - - def menu_func(self, context): - layout = self.layout - layout.separator() - layout.operator(ExportBatPack.bl_idname) - filepath = layout.operator(BAT_OT_export_zip.bl_idname) - - try: - prefs = preferences.get_addon_prefs() - root_dir_env = None - if prefs.use_env_root: - root_dir_env = os.getenv('ZIP_ROOT') - if not root_dir_env: - root_dir_env = os.getenv('PROJECT_ROOT') - if not root_dir_env: - root_dir_env = prefs.root_default - filepath.root_dir = '' if root_dir_env is None else root_dir_env - except Exception: - filepath.root_dir = os.getenv('ZIP_ROOT') or os.getenv('PROJECT_ROOT') or '' - classes = ( - ExportBatPack, - BAT_OT_export_zip, + preferences.BATPreferences, + operators.ExportBatPack, + operators.BAT_OT_export_zip, ) def register(): - preferences.register() for cls in classes: bpy.utils.register_class(cls) - bpy.types.TOPBAR_MT_file_external_data.append(menu_func) + bpy.types.TOPBAR_MT_file_external_data.append(operators.menu_func) def unregister(): + bpy.types.TOPBAR_MT_file_external_data.remove(operators.menu_func) for cls in classes: bpy.utils.unregister_class(cls) - preferences.unregister() - bpy.types.TOPBAR_MT_file_external_data.remove(menu_func) if __name__ == "__main__": register() diff --git a/blender_asset_tracer/cli/pack.py b/blender_asset_tracer/cli/pack.py index 19e713e..b8508b0 100755 --- a/blender_asset_tracer/cli/pack.py +++ b/blender_asset_tracer/cli/pack.py @@ -97,6 +97,14 @@ def add_parser(subparsers): "path structure under the target directory. Paths in blend files are " "rewritten to relative paths within this structure.", ) + parser.add_argument( + "-z", + "--zip", + default=False, + action="store_true", + help="Force output as a ZIP archive, even when the target path does " + "not end in '.zip'. The '.zip' extension is appended automatically.", + ) def cli_pack(args): @@ -156,12 +164,15 @@ def create_packer( packer = create_shamanpacker(bpath, ppath, target) - elif target.lower().endswith(".zip"): + elif args.zip: from blender_asset_tracer.pack import zipped if args.compress: raise ValueError("ZIP packer does not support on-the-fly compression") + if not target.lower().endswith(".zip"): + target += ".zip" + packer = zipped.ZipPacker( bpath, ppath, target, noop=args.noop, relative_only=args.relative_only, keep_hierarchy=args.keep_hierarchy, diff --git a/blender_asset_tracer/operators.py b/blender_asset_tracer/operators.py new file mode 100644 index 0000000..c8dc807 --- /dev/null +++ b/blender_asset_tracer/operators.py @@ -0,0 +1,163 @@ +import os +import sys +import subprocess +import tempfile +import zipfile +from pathlib import Path + +import bpy +from bpy.types import Operator +from bpy_extras.io_utils import ExportHelper + +from blender_asset_tracer.pack import zipped + + +class ExportBatPack(Operator, ExportHelper): + bl_idname = "export_bat.pack" + bl_label = "BAT - Zip pack (flat)" + filename_ext = ".zip" + + @classmethod + def poll(cls, context): + return bpy.data.is_saved + + def execute(self, context): + outfname = bpy.path.ensure_ext(self.filepath, ".zip") + self.report({"INFO"}, "Executing ZipPacker ...") + + with zipped.ZipPacker( + Path(bpy.data.filepath), + Path(bpy.data.filepath).parent, + str(self.filepath), + ) as packer: + packer.strategise() + packer.execute() + self.report({"INFO"}, "Packing successful!") + + with zipfile.ZipFile(str(self.filepath)) as inzip: + inzip.testzip() + + self.report({"INFO"}, "Written to %s" % outfname) + return {"FINISHED"} + + +def open_folder(folderpath): + """Open the folder at the path given with cmd relative to user's OS.""" + from shutil import which + + my_os = sys.platform + if my_os.startswith(("linux", "freebsd")): + cmd = "xdg-open" + elif my_os.startswith("win"): + cmd = "explorer" + if not folderpath: + return + else: + cmd = "open" + + if not folderpath: + return + + folderpath = str(folderpath) + if os.path.isfile(folderpath): + select = False + if my_os.startswith("win"): + cmd = "explorer /select," + select = True + elif my_os.startswith(("linux", "freebsd")): + if which("nemo"): + cmd = "nemo --no-desktop" + select = True + elif which("nautilus"): + cmd = "nautilus --no-desktop" + select = True + if not select: + folderpath = os.path.dirname(folderpath) + + folderpath = os.path.normpath(folderpath) + fullcmd = cmd.split() + [folderpath] + subprocess.Popen(fullcmd) + + +class BAT_OT_export_zip(Operator, ExportHelper): + """Export current blendfile with hierarchy preservation""" + + bl_label = "BAT - Zip pack (keep hierarchy)" + bl_idname = "bat.export_zip" + + filename_ext = ".zip" + + root_dir: bpy.props.StringProperty( + name="Root", + description="Top Level Folder of your project." + "\nFor now Copy/Paste correct folder by hand if default is incorrect." + "\n!!! Everything outside won't be zipped !!!", + ) + + use_zip: bpy.props.BoolProperty( + name="Output as ZIP", + description="If enabled, pack into a ZIP archive. If disabled, copy to a directory.", + default=True, + ) + + @classmethod + def poll(cls, context): + return bpy.data.is_saved + + def execute(self, context): + from blender_asset_tracer.pack import Packer + from blender_asset_tracer.pack.zipped import ZipPacker + + bfile = Path(bpy.data.filepath) + project = Path(self.root_dir) if self.root_dir else bfile.parent + target = str(self.filepath) + + if self.use_zip: + target = bpy.path.ensure_ext(target, ".zip") + packer_cls = ZipPacker + else: + if target.lower().endswith(".zip"): + target = target[:-4] + packer_cls = Packer + + self.report({"INFO"}, "Packing with hierarchy...") + + with packer_cls(bfile, project, target, keep_hierarchy=True) as packer: + packer.strategise() + packer.execute() + + if self.use_zip: + with zipfile.ZipFile(target) as inzip: + inzip.testzip() + log_output = Path(tempfile.gettempdir(), "README.txt") + with open(log_output, "w") as log: + log.write("Packed with BAT (keep-hierarchy mode)") + log.write(f"\nBlend file: {bpy.data.filepath}") + with zipfile.ZipFile(target, "a") as zipObj: + zipObj.write(log_output, log_output.name) + + self.report({"INFO"}, "Written to %s" % target) + open_folder(Path(target).parent) + return {"FINISHED"} + + +def menu_func(self, context): + layout = self.layout + layout.separator() + layout.operator(ExportBatPack.bl_idname) + filepath = layout.operator(BAT_OT_export_zip.bl_idname) + + try: + prefs = bpy.context.preferences.addons["blender_asset_tracer"].preferences + root_dir_env = None + if prefs.use_env_root: + root_dir_env = os.getenv("ZIP_ROOT") + if not root_dir_env: + root_dir_env = os.getenv("PROJECT_ROOT") + if not root_dir_env: + root_dir_env = prefs.root_default + filepath.root_dir = "" if root_dir_env is None else root_dir_env + except Exception: + filepath.root_dir = os.getenv("ZIP_ROOT") or os.getenv("PROJECT_ROOT") or "" + + diff --git a/blender_asset_tracer/preferences.py b/blender_asset_tracer/preferences.py new file mode 100644 index 0000000..5cfe624 --- /dev/null +++ b/blender_asset_tracer/preferences.py @@ -0,0 +1,27 @@ +import bpy +from bpy.types import AddonPreferences +from bpy.props import BoolProperty, StringProperty + + +class BATPreferences(AddonPreferences): + bl_idname = "blender_asset_tracer" + + use_env_root: BoolProperty( + name="Use Environment Variable for Root", + description="Read the project root from ZIP_ROOT or PROJECT_ROOT environment variables", + default=False, + ) + + root_default: StringProperty( + name="Default Root", + description="Fallback project root when the environment variable is not set", + default="", + subtype="DIR_PATH", + ) + + def draw(self, context): + layout = self.layout + layout.prop(self, "use_env_root") + layout.prop(self, "root_default") + +