diff --git a/blender_asset_tracer/__init__.py b/blender_asset_tracer/__init__.py index 3856f32..f5a632b 100644 --- a/blender_asset_tracer/__init__.py +++ b/blender_asset_tracer/__init__.py @@ -20,4 +20,209 @@ # -__version__ = "1.21" +__version__ = '1.21' + +bl_info = { + "name": "Blender Asset Tracer", + "author": "Campbell Barton, Sybren A. St\u00fcvel, Lo\u00efc Charri\u00e8re, Cl\u00e9ment Ducarteron, Mario Hawat, Joseph Henry", + "version": (1, 21, 0), + "blender": (2, 80, 0), + "location": "File > External Data > BAT", + "description": "Utility for packing blend files", + "warning": "", + "wiki_url": "https://developer.blender.org/project/profile/79/", + "category": "Import-Export", +} + +# 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 + + # 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, + ) + + def register(): + preferences.register() + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.TOPBAR_MT_file_external_data.append(menu_func) + + def unregister(): + 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()