2026-02-19 18:06:34 +01:00

229 lines
7.7 KiB
Python

# ##### 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Copyright (C) 2014-2018 Blender Foundation
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
__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()