From cbe44faa1bfa97a013025b07d4628d8319285550 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 16 Dec 2025 11:11:20 -0800 Subject: [PATCH] Update web_server.py --- web_server.py | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/web_server.py b/web_server.py index 4c4ecb3b..68e27c6f 100644 --- a/web_server.py +++ b/web_server.py @@ -7334,6 +7334,47 @@ def _post_process_matched_download_with_verification(context_key, context, file_ _on_download_completed(batch_id, task_id, success=False) +def _safe_move_file(src, dst): + """ + Safely move a file across different filesystems/volumes. + Handles Docker volume mount issues where shutil.move() fails on metadata preservation. + + Args: + src: Source file path (str or Path) + dst: Destination file path (str or Path) + """ + import shutil + from pathlib import Path + + src = Path(src) + dst = Path(dst) + + try: + # Try standard move first (works if same filesystem) + shutil.move(str(src), str(dst)) + return + except (OSError, PermissionError) as e: + # If it's a cross-device link error or permission error, do manual copy + if "cross-device" in str(e).lower() or "operation not permitted" in str(e).lower(): + logger.warning(f"⚠️ Cross-device move detected, using fallback copy method: {e}") + try: + # Simple copy without metadata preservation (avoids permission errors) + with open(src, 'rb') as f_src: + with open(dst, 'wb') as f_dst: + shutil.copyfileobj(f_src, f_dst) + + # Delete source after successful copy + src.unlink() + logger.info(f"✅ Successfully moved file using fallback method: {src} -> {dst}") + return + except Exception as fallback_error: + logger.error(f"❌ Fallback copy also failed: {fallback_error}") + raise + else: + # Re-raise if it's a different error + raise + + def _post_process_matched_download(context_key, context, file_path): """ This is the final, corrected post-processing function. It now mirrors the @@ -7393,7 +7434,7 @@ def _post_process_matched_download(context_key, context, file_path): filename = Path(file_path).name destination = album_folder / filename - shutil.move(str(file_path), str(destination)) + _safe_move_file(file_path, destination) logger.info(f"✅ Moved simple download to: {destination}") # Clean up context @@ -7444,7 +7485,7 @@ def _post_process_matched_download(context_key, context, file_path): print(f"⚠️ File already exists, overwriting: {os.path.basename(final_path)}") os.remove(final_path) - shutil.move(file_path, final_path) + _safe_move_file(file_path, final_path) # Clean up empty directories in downloads folder downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) @@ -7653,8 +7694,8 @@ def _post_process_matched_download(context_key, context, file_path): except Exception as check_error: print(f"⚠️ [Protection] Error checking existing file metadata, proceeding with overwrite: {check_error}") os.remove(final_path) - - shutil.move(file_path, final_path) + + _safe_move_file(file_path, final_path) _download_cover_art(album_info, os.path.dirname(final_path))