2026-03-11_4
This commit is contained in:
@@ -363,47 +363,246 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
|
||||
return filepath
|
||||
|
||||
def _convert_relpaths_to_absolute(self, context: bpy.types.Context) -> None:
|
||||
"""Convert all relative paths in the blend file to absolute paths.
|
||||
|
||||
This ensures that all libraries, images, and other assets are referenced
|
||||
by absolute paths, allowing the blend file to be sent as-is without BAT.
|
||||
"""
|
||||
# Convert library paths to absolute
|
||||
for library in bpy.data.libraries:
|
||||
if library.filepath:
|
||||
old_path = library.filepath
|
||||
abs_path = bpy.path.abspath(library.filepath)
|
||||
library.filepath = abs_path
|
||||
self.log.debug("Converted library path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert image paths to absolute
|
||||
for image in bpy.data.images:
|
||||
if image.filepath and not image.packed_file:
|
||||
old_path = image.filepath
|
||||
abs_path = bpy.path.abspath(image.filepath)
|
||||
image.filepath = abs_path
|
||||
self.log.debug("Converted image path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert movie paths to absolute
|
||||
for movie in bpy.data.movieclips:
|
||||
if movie.filepath:
|
||||
old_path = movie.filepath
|
||||
abs_path = bpy.path.abspath(movie.filepath)
|
||||
movie.filepath = abs_path
|
||||
self.log.debug("Converted movie path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert sound paths to absolute
|
||||
for sound in bpy.data.sounds:
|
||||
if sound.filepath:
|
||||
old_path = sound.filepath
|
||||
abs_path = bpy.path.abspath(sound.filepath)
|
||||
sound.filepath = abs_path
|
||||
self.log.debug("Converted sound path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert font paths to absolute (skip VectorFont - its filepath is read-only)
|
||||
for font in bpy.data.fonts:
|
||||
if font.filepath:
|
||||
try:
|
||||
old_path = font.filepath
|
||||
abs_path = bpy.path.abspath(font.filepath)
|
||||
font.filepath = abs_path
|
||||
self.log.debug("Converted font path: %s -> %s", old_path, abs_path)
|
||||
except (TypeError, AttributeError):
|
||||
self.log.debug("Skipping font %s (filepath is read-only)", font.name)
|
||||
|
||||
# Convert volume paths to absolute
|
||||
for volume in bpy.data.volumes:
|
||||
if volume.filepath:
|
||||
old_path = volume.filepath
|
||||
abs_path = bpy.path.abspath(volume.filepath)
|
||||
volume.filepath = abs_path
|
||||
self.log.debug("Converted volume path: %s -> %s", old_path, abs_path)
|
||||
|
||||
def _submit_files(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Ensure that the files are somewhere in the shared storage.
|
||||
|
||||
Bypasses BAT entirely. Converts all relative paths to absolute and sends
|
||||
the blend file as-is.
|
||||
|
||||
Returns True if a packing thread has been started, and False otherwise.
|
||||
"""
|
||||
|
||||
from .bat import interface as bat_interface
|
||||
|
||||
if bat_interface.is_packing():
|
||||
self.report({"ERROR"}, "Another packing operation is running")
|
||||
self._quit(context)
|
||||
return False
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
return False
|
||||
|
||||
# Convert all relative paths to absolute before saving
|
||||
self.log.info("Converting all relative paths to absolute")
|
||||
self._convert_relpaths_to_absolute(context)
|
||||
|
||||
# Save the blend file with absolute paths
|
||||
self.log.info("Saving blend file with absolute paths")
|
||||
blendfile = self._save_blendfile(context)
|
||||
blendfile = bpathlib.make_absolute(blendfile)
|
||||
|
||||
if manager.shared_storage.shaman_enabled:
|
||||
# self.blendfile_on_farm will be set when BAT created the checkout,
|
||||
# see _on_bat_pack_msg() below.
|
||||
self.blendfile_on_farm = None
|
||||
self._bat_pack_shaman(context, blendfile)
|
||||
# Upload blend file directly to Shaman without BAT
|
||||
self.log.info("Uploading blend file directly to Shaman (bypassing BAT)")
|
||||
self._upload_blendfile_to_shaman(context, blendfile)
|
||||
# Job is submitted synchronously, cleanup and finish
|
||||
self._quit(context)
|
||||
return False # No thread running, handled synchronously
|
||||
elif job_submission.is_file_inside_job_storage(context, blendfile):
|
||||
self.log.info(
|
||||
"File is already in job storage location, submitting it as-is"
|
||||
)
|
||||
self._use_blendfile_directly(context, blendfile)
|
||||
return False
|
||||
else:
|
||||
self.log.info(
|
||||
"File is not already in job storage location, copying it there"
|
||||
)
|
||||
try:
|
||||
self.blendfile_on_farm = self._bat_pack_filesystem(context, blendfile)
|
||||
self._copy_blendfile_to_storage(context, blendfile)
|
||||
except FileNotFoundError:
|
||||
self._quit(context)
|
||||
return False
|
||||
return False
|
||||
|
||||
wm = context.window_manager
|
||||
self.timer = wm.event_timer_add(self.TIMER_PERIOD, window=context.window)
|
||||
def _upload_blendfile_to_shaman(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Upload blend file directly to Shaman without BAT.
|
||||
|
||||
Creates a Shaman checkout with just the blend file, maintaining its
|
||||
relative path from the project root.
|
||||
"""
|
||||
from .bat import cache
|
||||
from .manager.apis import ShamanApi
|
||||
from .manager.models import (
|
||||
ShamanFileSpec,
|
||||
ShamanCheckout,
|
||||
)
|
||||
from .manager.exceptions import ApiException
|
||||
from . import preferences
|
||||
|
||||
api_client = self.get_api_client(context)
|
||||
shaman_api = ShamanApi(api_client)
|
||||
|
||||
# Get project root to calculate relative path
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
|
||||
|
||||
# Calculate relative path from project root
|
||||
try:
|
||||
blendfile_rel_path = blendfile.relative_to(project_path)
|
||||
# Convert to POSIX path for Shaman
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile_rel_path.as_posix())
|
||||
except ValueError:
|
||||
# Blend file is not under project root, use just the filename
|
||||
self.log.warning(
|
||||
"Blend file %s is not under project root %s, using filename only",
|
||||
blendfile,
|
||||
project_path,
|
||||
)
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile.name)
|
||||
|
||||
# Compute checksum and file size
|
||||
self.log.info("Computing checksum for %s", blendfile.name)
|
||||
checksum = cache.compute_cached_checksum(blendfile)
|
||||
filesize = blendfile.stat().st_size
|
||||
|
||||
# Upload the blend file to Shaman
|
||||
self.log.info("Uploading blend file to Shaman: %s", blendfile.name)
|
||||
try:
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=True,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
except ApiException as ex:
|
||||
if ex.status == 208:
|
||||
# File already known to Shaman
|
||||
self.log.info("Blend file already known to Shaman")
|
||||
elif ex.status == 425:
|
||||
# Defer upload - someone else is uploading
|
||||
self.log.info("Blend file is being uploaded by another client, deferring")
|
||||
# Retry after a short delay
|
||||
import time
|
||||
time.sleep(1)
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=False,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
else:
|
||||
self.log.error("Error uploading to Shaman: %s", ex)
|
||||
self.report({"ERROR"}, f"Error uploading to Shaman: {ex}")
|
||||
return
|
||||
|
||||
# Create checkout definition with just the blend file
|
||||
checkout_path = self._shaman_checkout_path()
|
||||
filespec = ShamanFileSpec(
|
||||
sha=checksum,
|
||||
size=filesize,
|
||||
path=str(blendfile_path_in_checkout), # Relative path from project root
|
||||
)
|
||||
|
||||
# Create the checkout
|
||||
self.log.info("Creating Shaman checkout: %s", checkout_path)
|
||||
self.log.info("Blend file path in checkout: %s", blendfile_path_in_checkout)
|
||||
checkout = ShamanCheckout(
|
||||
files=[filespec],
|
||||
checkout_path=str(checkout_path),
|
||||
)
|
||||
|
||||
try:
|
||||
result = shaman_api.shaman_checkout(checkout)
|
||||
self.actual_shaman_checkout_path = PurePosixPath(result.checkout_path)
|
||||
# The checkout itself is created in a unique subdirectory. The job's
|
||||
# blendfile must include that checkout path.
|
||||
self.blendfile_on_farm = (
|
||||
PurePosixPath("{jobs}")
|
||||
/ self.actual_shaman_checkout_path
|
||||
/ blendfile_path_in_checkout
|
||||
)
|
||||
self.log.info("Shaman checkout created: %s", self.actual_shaman_checkout_path)
|
||||
self._submit_job(context)
|
||||
except ApiException as ex:
|
||||
self.log.error("Error creating Shaman checkout: %s", ex)
|
||||
self.report({"ERROR"}, f"Error creating Shaman checkout: {ex}")
|
||||
return
|
||||
|
||||
return True
|
||||
def _copy_blendfile_to_storage(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Copy blend file to job storage without BAT."""
|
||||
import shutil
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
raise FileNotFoundError("Manager info not known")
|
||||
|
||||
unique_dir = "%s-%s" % (
|
||||
datetime.datetime.now().isoformat("-").replace(":", ""),
|
||||
self.job_name,
|
||||
)
|
||||
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
|
||||
pack_target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pack_target_file = pack_target_dir / blendfile.name
|
||||
self.log.info("Copying blend file to %s", pack_target_file)
|
||||
|
||||
shutil.copy2(blendfile, pack_target_file)
|
||||
|
||||
self.blendfile_on_farm = PurePosixPath(pack_target_file.as_posix())
|
||||
self.actual_shaman_checkout_path = None
|
||||
|
||||
self._submit_job(context)
|
||||
|
||||
def _bat_pack_filesystem(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
@@ -442,7 +641,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
project=project_path,
|
||||
target=str(pack_target_dir),
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
relative_only=True, # TODO: get from GUI.
|
||||
relative_only=True, # Only include files relative to project path.
|
||||
)
|
||||
|
||||
return PurePosixPath(pack_target_file.as_posix())
|
||||
@@ -481,7 +680,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
project=project_path,
|
||||
target="/", # Target directory irrelevant for Shaman transfers.
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
relative_only=True, # TODO: get from GUI.
|
||||
relative_only=True, # Only include files relative to project path.
|
||||
packer_class=bat_shaman.Packer,
|
||||
packer_kwargs=dict(
|
||||
api_client=self.get_api_client(context),
|
||||
|
||||
Reference in New Issue
Block a user