Refactoring of services.py; tests
This commit is contained in:
447
server/app/storage/transfers.py
Normal file
447
server/app/storage/transfers.py
Normal file
@@ -0,0 +1,447 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Iterable
|
||||
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import FileAsset, FileFolder
|
||||
from app.storage.access import ensure_owner_access
|
||||
from app.storage.common import FileConflictResolution, FileStorageError, RenamePlanItem, utcnow
|
||||
from app.storage.files import (
|
||||
_active_asset_exists,
|
||||
_asset_owner_id,
|
||||
_asset_query_for_owner,
|
||||
_candidate_renamed_path,
|
||||
_copy_asset_to_path,
|
||||
_next_available_logical_path,
|
||||
_normalize_conflict_strategy,
|
||||
_resolution_by_path,
|
||||
_soft_delete_conflicting_asset,
|
||||
current_version_and_blob,
|
||||
get_asset_for_user,
|
||||
rename_asset,
|
||||
)
|
||||
from app.storage.folders import _active_folder_exists, _ensure_target_folder_hierarchy, _folder_query_for_owner
|
||||
from app.storage.paths import filename_from_path, join_folder_filename, normalize_folder, normalize_logical_path
|
||||
|
||||
|
||||
def transfer_selection(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
operation: str,
|
||||
file_ids: list[str],
|
||||
folder_paths: list[str],
|
||||
source_owner_type: str,
|
||||
source_owner_id: str,
|
||||
target_owner_type: str,
|
||||
target_owner_id: str,
|
||||
target_folder: str,
|
||||
conflict_strategy: str = "reject",
|
||||
conflict_resolutions: Iterable[FileConflictResolution] | None = None,
|
||||
is_admin: bool = False,
|
||||
) -> tuple[int, int]:
|
||||
"""Move or copy files/folders between user/group file spaces.
|
||||
|
||||
Folder transfers preserve the selected folder's basename below the target
|
||||
folder. File transfers place files directly in the target folder. Existing
|
||||
active target paths are handled by the requested conflict strategy. Copies
|
||||
create new file assets/versions that reference the existing immutable blob.
|
||||
"""
|
||||
|
||||
operation = operation.lower().strip()
|
||||
if operation not in {"move", "copy"}:
|
||||
raise FileStorageError("Unsupported transfer operation")
|
||||
source_owner_type = source_owner_type.lower().strip()
|
||||
target_owner_type = target_owner_type.lower().strip()
|
||||
source_folder_paths = [normalize_folder(path) for path in folder_paths if normalize_folder(path)]
|
||||
target_folder = normalize_folder(target_folder)
|
||||
conflict_strategy = _normalize_conflict_strategy(conflict_strategy)
|
||||
conflict_resolution_map = _resolution_by_path(conflict_resolutions)
|
||||
|
||||
ensure_owner_access(session, tenant_id=tenant_id, owner_type=source_owner_type, owner_id=source_owner_id, user_id=user_id, is_admin=is_admin)
|
||||
ensure_owner_access(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, user_id=user_id, is_admin=is_admin)
|
||||
|
||||
if operation == "move" and source_owner_type == target_owner_type and source_owner_id == target_owner_id:
|
||||
for folder_path in source_folder_paths:
|
||||
if target_folder == folder_path or target_folder.startswith(f"{folder_path}/"):
|
||||
raise FileStorageError("Cannot move a folder into itself or one of its child folders")
|
||||
|
||||
assets_by_id: dict[str, FileAsset] = {}
|
||||
for file_id in file_ids:
|
||||
asset = get_asset_for_user(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
asset_id=file_id,
|
||||
require_write=operation == "move",
|
||||
is_admin=is_admin,
|
||||
)
|
||||
assets_by_id[asset.id] = asset
|
||||
|
||||
folder_asset_targets: dict[str, str] = {}
|
||||
folder_target_paths: dict[str, str] = {}
|
||||
for folder_path in source_folder_paths:
|
||||
folder_basename = PurePosixPath(folder_path).name
|
||||
target_prefix = normalize_folder(f"{target_folder}/{folder_basename}" if target_folder else folder_basename)
|
||||
folder_target_paths[folder_path] = target_prefix
|
||||
if operation == "copy" or not (source_owner_type == target_owner_type and source_owner_id == target_owner_id and target_prefix == folder_path):
|
||||
if _active_folder_exists(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, path=target_prefix):
|
||||
resolution = conflict_resolution_map.get(target_prefix)
|
||||
action = resolution.action if resolution else conflict_strategy
|
||||
if action == "skip":
|
||||
folder_target_paths.pop(folder_path, None)
|
||||
continue
|
||||
if action == "rename":
|
||||
# Rename the selected folder root while preserving its contents below it.
|
||||
counter = 1
|
||||
candidate = target_prefix
|
||||
while _active_folder_exists(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, path=candidate):
|
||||
candidate = _candidate_renamed_path(target_prefix, counter)
|
||||
counter += 1
|
||||
target_prefix = normalize_folder(candidate)
|
||||
folder_target_paths[folder_path] = target_prefix
|
||||
elif action == "reject":
|
||||
raise FileStorageError(f"Target folder already exists: {target_prefix}")
|
||||
# overwrite on folders means merge into the existing folder; conflicting files are still handled below.
|
||||
source_assets = _asset_query_for_owner(session, tenant_id=tenant_id, owner_type=source_owner_type, owner_id=source_owner_id).filter(
|
||||
FileAsset.deleted_at.is_(None),
|
||||
FileAsset.display_path.like(f"{folder_path}/%"),
|
||||
).all()
|
||||
for asset in source_assets:
|
||||
relative = asset.display_path[len(folder_path) + 1 :]
|
||||
folder_asset_targets[asset.id] = normalize_logical_path(f"{target_prefix}/{relative}")
|
||||
assets_by_id[asset.id] = asset
|
||||
|
||||
direct_file_targets: dict[str, str] = {}
|
||||
for file_id, asset in assets_by_id.items():
|
||||
if file_id in folder_asset_targets:
|
||||
continue
|
||||
direct_file_targets[file_id] = normalize_logical_path(join_folder_filename(target_folder, filename_from_path(asset.display_path)))
|
||||
|
||||
all_targets = {**folder_asset_targets, **direct_file_targets}
|
||||
if not all_targets and not source_folder_paths:
|
||||
return 0, 0
|
||||
|
||||
resolved_targets: dict[str, str] = {}
|
||||
skipped_asset_ids: set[str] = set()
|
||||
reserved_targets: set[str] = set()
|
||||
for asset_id, target_path in all_targets.items():
|
||||
source_asset = assets_by_id[asset_id]
|
||||
same_owner = (source_asset.owner_type == target_owner_type and _asset_owner_id(source_asset) == target_owner_id)
|
||||
exclude = source_asset.id if operation == "move" and same_owner else None
|
||||
normalized_target = normalize_logical_path(target_path)
|
||||
resolution = conflict_resolution_map.get(normalized_target)
|
||||
action = resolution.action if resolution else conflict_strategy
|
||||
if resolution and resolution.new_path and action == "rename":
|
||||
normalized_target = normalize_logical_path(resolution.new_path)
|
||||
conflict = _active_asset_exists(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
owner_type=target_owner_type,
|
||||
owner_id=target_owner_id,
|
||||
path=normalized_target,
|
||||
exclude_asset_id=exclude,
|
||||
) or normalized_target in reserved_targets
|
||||
if conflict:
|
||||
if action == "reject":
|
||||
raise FileStorageError(f"Target file already exists: {normalized_target}")
|
||||
if action == "skip":
|
||||
skipped_asset_ids.add(asset_id)
|
||||
continue
|
||||
if action == "overwrite":
|
||||
_soft_delete_conflicting_asset(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
owner_type=target_owner_type,
|
||||
owner_id=target_owner_id,
|
||||
path=normalized_target,
|
||||
exclude_asset_id=exclude,
|
||||
)
|
||||
elif action == "rename":
|
||||
normalized_target = _next_available_logical_path(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
owner_type=target_owner_type,
|
||||
owner_id=target_owner_id,
|
||||
desired_path=normalized_target,
|
||||
reserved_paths=reserved_targets,
|
||||
exclude_asset_id=exclude,
|
||||
)
|
||||
elif action == "rename":
|
||||
normalized_target = _next_available_logical_path(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
owner_type=target_owner_type,
|
||||
owner_id=target_owner_id,
|
||||
desired_path=normalized_target,
|
||||
reserved_paths=reserved_targets,
|
||||
exclude_asset_id=exclude,
|
||||
)
|
||||
reserved_targets.add(normalized_target)
|
||||
resolved_targets[asset_id] = normalized_target
|
||||
all_targets = resolved_targets
|
||||
|
||||
copied_or_moved_files = 0
|
||||
copied_or_moved_folders = 0
|
||||
|
||||
# Create target folder hierarchy first, including selected folder roots and
|
||||
# any nested folders/files parents.
|
||||
same_owner_transfer = source_owner_type == target_owner_type and source_owner_id == target_owner_id
|
||||
for target_path in folder_target_paths.values():
|
||||
path_to_create = target_path
|
||||
if operation == "move" and same_owner_transfer:
|
||||
parent = str(PurePosixPath(target_path).parent)
|
||||
path_to_create = "" if parent == "." else parent
|
||||
if path_to_create:
|
||||
_ensure_target_folder_hierarchy(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, user_id=user_id, path=path_to_create)
|
||||
copied_or_moved_folders += 1
|
||||
for target_path in all_targets.values():
|
||||
parent = str(PurePosixPath(target_path).parent)
|
||||
if parent and parent != ".":
|
||||
_ensure_target_folder_hierarchy(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, user_id=user_id, path=parent)
|
||||
|
||||
if operation == "copy":
|
||||
for asset_id, target_path in all_targets.items():
|
||||
_copy_asset_to_path(session, assets_by_id[asset_id], tenant_id=tenant_id, target_owner_type=target_owner_type, target_owner_id=target_owner_id, target_path=target_path, user_id=user_id)
|
||||
copied_or_moved_files += 1
|
||||
return copied_or_moved_files, copied_or_moved_folders
|
||||
|
||||
now = utcnow()
|
||||
for asset_id, target_path in all_targets.items():
|
||||
asset = assets_by_id[asset_id]
|
||||
same_owner = asset.owner_type == target_owner_type and _asset_owner_id(asset) == target_owner_id
|
||||
if same_owner:
|
||||
if normalize_logical_path(asset.display_path) == target_path:
|
||||
continue
|
||||
rename_asset(asset, new_path=target_path)
|
||||
session.add(asset)
|
||||
else:
|
||||
_copy_asset_to_path(session, asset, tenant_id=tenant_id, target_owner_type=target_owner_type, target_owner_id=target_owner_id, target_path=target_path, user_id=user_id)
|
||||
asset.deleted_at = now
|
||||
session.add(asset)
|
||||
copied_or_moved_files += 1
|
||||
|
||||
# Move/copy persisted folder records after files. For cross-owner moves, create
|
||||
# target records and soft-delete source records. For same-owner moves, rename
|
||||
# them in place.
|
||||
for source_path, target_prefix in folder_target_paths.items():
|
||||
folder_rows = _folder_query_for_owner(session, tenant_id=tenant_id, owner_type=source_owner_type, owner_id=source_owner_id).filter(
|
||||
FileFolder.deleted_at.is_(None),
|
||||
or_(FileFolder.path == source_path, FileFolder.path.like(f"{source_path}/%")),
|
||||
).all()
|
||||
for folder in folder_rows:
|
||||
relative = "" if folder.path == source_path else folder.path[len(source_path) + 1 :]
|
||||
new_path = normalize_folder(f"{target_prefix}/{relative}" if relative else target_prefix)
|
||||
if operation == "move" and source_owner_type == target_owner_type and source_owner_id == target_owner_id:
|
||||
folder.path = new_path
|
||||
session.add(folder)
|
||||
else:
|
||||
_ensure_target_folder_hierarchy(session, tenant_id=tenant_id, owner_type=target_owner_type, owner_id=target_owner_id, user_id=user_id, path=new_path)
|
||||
if operation == "move":
|
||||
folder.deleted_at = now
|
||||
session.add(folder)
|
||||
return copied_or_moved_files, copied_or_moved_folders
|
||||
|
||||
|
||||
def _rename_basename(name: str, *, mode: str, new_name: str | None = None, find: str | None = None, replacement: str = "", prefix: str = "", suffix: str = "") -> str:
|
||||
if mode == "direct":
|
||||
cleaned = normalize_logical_path(new_name or "", fallback_filename="item")
|
||||
if "/" in cleaned:
|
||||
raise FileStorageError("Rename expects a name, not a path")
|
||||
return cleaned
|
||||
stem_path = PurePosixPath(name)
|
||||
suffixes = "".join(stem_path.suffixes)
|
||||
stem = name[: -len(suffixes)] if suffixes else name
|
||||
if mode == "prefix":
|
||||
return prefix + name
|
||||
if mode == "suffix":
|
||||
return f"{stem}{suffix}{suffixes}"
|
||||
if mode == "replace":
|
||||
return name.replace(find or "", replacement) if find else name
|
||||
raise FileStorageError("Unsupported rename mode")
|
||||
|
||||
|
||||
def _rename_path(path: str, *, mode: str, new_name: str | None = None, find: str | None = None, replacement: str = "", prefix: str = "", suffix: str = "") -> str:
|
||||
normalized = normalize_logical_path(path)
|
||||
logical = PurePosixPath(normalized)
|
||||
folder = "" if str(logical.parent) == "." else str(logical.parent)
|
||||
next_name = _rename_basename(logical.name, mode=mode, new_name=new_name, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
return normalize_logical_path(f"{folder}/{next_name}" if folder else next_name)
|
||||
|
||||
|
||||
def _rename_relative_recursive(relative_path: str, *, mode: str, new_name: str | None = None, find: str | None = None, replacement: str = "", prefix: str = "", suffix: str = "") -> str:
|
||||
parts = normalize_logical_path(relative_path).split("/")
|
||||
return normalize_logical_path(
|
||||
"/".join(
|
||||
_rename_basename(part, mode=mode, new_name=new_name if len(parts) == 1 else None, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
for part in parts
|
||||
if part
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_rename_preview(asset: FileAsset, *, mode: str, find: str | None = None, replacement: str = "", prefix: str = "", suffix: str = "") -> str:
|
||||
return _rename_path(asset.display_path, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
|
||||
|
||||
def _collapse_folder_roots(folder_paths: list[str]) -> list[str]:
|
||||
roots = sorted({normalize_folder(path) for path in folder_paths if normalize_folder(path)}, key=lambda item: (item.count("/"), item))
|
||||
collapsed: list[str] = []
|
||||
for path in roots:
|
||||
if any(path == root or path.startswith(f"{root}/") for root in collapsed):
|
||||
continue
|
||||
collapsed.append(path)
|
||||
return collapsed
|
||||
|
||||
|
||||
def _path_under_root(path: str, root: str) -> bool:
|
||||
normalized = normalize_logical_path(path)
|
||||
return normalized == root or normalized.startswith(f"{root}/")
|
||||
|
||||
|
||||
def _folder_new_path_for_root(path: str, root: str, new_root: str, *, recursive: bool, mode: str, find: str | None, replacement: str, prefix: str, suffix: str) -> str:
|
||||
if path == root:
|
||||
return normalize_folder(new_root)
|
||||
relative = path[len(root) + 1 :]
|
||||
if recursive:
|
||||
relative = _rename_relative_recursive(relative, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
return normalize_folder(f"{new_root}/{relative}")
|
||||
|
||||
|
||||
def _file_new_path_for_root(path: str, root: str, new_root: str, *, recursive: bool, mode: str, find: str | None, replacement: str, prefix: str, suffix: str) -> str:
|
||||
relative = path[len(root) + 1 :]
|
||||
if recursive:
|
||||
relative = _rename_relative_recursive(relative, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
return normalize_logical_path(f"{new_root}/{relative}")
|
||||
|
||||
|
||||
def rename_selection(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
file_ids: list[str],
|
||||
folder_paths: list[str],
|
||||
owner_type: str | None,
|
||||
owner_id: str | None,
|
||||
mode: str,
|
||||
new_name: str | None = None,
|
||||
find: str | None = None,
|
||||
replacement: str = "",
|
||||
prefix: str = "",
|
||||
suffix: str = "",
|
||||
recursive: bool = False,
|
||||
dry_run: bool = True,
|
||||
is_admin: bool = False,
|
||||
) -> list[RenamePlanItem]:
|
||||
mode = mode.lower().strip()
|
||||
if mode not in {"direct", "prefix", "suffix", "replace"}:
|
||||
raise FileStorageError("Unsupported rename mode")
|
||||
selected_file_ids = list(dict.fromkeys(file_ids))
|
||||
selected_folder_roots = _collapse_folder_roots(folder_paths)
|
||||
if not selected_file_ids and not selected_folder_roots:
|
||||
raise FileStorageError("No files or folders selected")
|
||||
if selected_folder_roots and (not owner_type or not owner_id):
|
||||
raise FileStorageError("Folder rename requires an owner file space")
|
||||
if mode == "direct" and (len(selected_file_ids) + len(selected_folder_roots)) != 1:
|
||||
raise FileStorageError("Direct rename requires exactly one selected item")
|
||||
|
||||
assets: dict[str, FileAsset] = {}
|
||||
for file_id in selected_file_ids:
|
||||
asset = get_asset_for_user(session, tenant_id=tenant_id, user_id=user_id, asset_id=file_id, require_write=True, is_admin=is_admin)
|
||||
assets[asset.id] = asset
|
||||
|
||||
owner_type_norm = owner_type.lower().strip() if owner_type else None
|
||||
owner_id_norm = owner_id
|
||||
folder_rows_by_path: dict[str, FileFolder] = {}
|
||||
affected_folder_paths: set[str] = set()
|
||||
if selected_folder_roots and owner_type_norm and owner_id_norm:
|
||||
ensure_owner_access(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm, user_id=user_id, is_admin=is_admin)
|
||||
for root in selected_folder_roots:
|
||||
rows = _folder_query_for_owner(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm).filter(
|
||||
FileFolder.deleted_at.is_(None),
|
||||
or_(FileFolder.path == root, FileFolder.path.like(f"{root}/%")),
|
||||
).all()
|
||||
if not rows and not _asset_query_for_owner(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm).filter(FileAsset.deleted_at.is_(None), FileAsset.display_path.like(f"{root}/%")).first():
|
||||
raise FileStorageError(f"Folder not found: {root}")
|
||||
for row in rows:
|
||||
folder_rows_by_path[row.path] = row
|
||||
affected_folder_paths.add(row.path)
|
||||
for asset in _asset_query_for_owner(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm).filter(FileAsset.deleted_at.is_(None), FileAsset.display_path.like(f"{root}/%")).all():
|
||||
assets[asset.id] = asset
|
||||
|
||||
folder_plan: dict[str, str] = {}
|
||||
root_target_paths: dict[str, str] = {}
|
||||
for root in selected_folder_roots:
|
||||
if mode == "direct":
|
||||
root_parent = "" if str(PurePosixPath(root).parent) == "." else str(PurePosixPath(root).parent)
|
||||
root_new_name = _rename_basename(PurePosixPath(root).name, mode=mode, new_name=new_name)
|
||||
new_root = normalize_folder(f"{root_parent}/{root_new_name}" if root_parent else root_new_name)
|
||||
else:
|
||||
new_root = normalize_folder(_rename_path(root, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix))
|
||||
root_target_paths[root] = new_root
|
||||
if root in affected_folder_paths:
|
||||
folder_plan[root] = new_root
|
||||
for path in sorted(affected_folder_paths, key=lambda item: item.count("/")):
|
||||
if path != root and _path_under_root(path, root):
|
||||
folder_plan[path] = _folder_new_path_for_root(path, root, new_root, recursive=recursive, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
|
||||
file_plan: dict[str, str] = {}
|
||||
for asset in assets.values():
|
||||
matched_root = next((root for root in sorted(selected_folder_roots, key=len, reverse=True) if _path_under_root(asset.display_path, root)), None)
|
||||
if matched_root:
|
||||
root_new = root_target_paths.get(matched_root)
|
||||
if not root_new:
|
||||
continue
|
||||
file_plan[asset.id] = _file_new_path_for_root(asset.display_path, matched_root, root_new, recursive=recursive, mode=mode, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
else:
|
||||
file_plan[asset.id] = _rename_path(asset.display_path, mode=mode, new_name=new_name, find=find, replacement=replacement, prefix=prefix, suffix=suffix)
|
||||
|
||||
# Detect duplicate targets and existing active target conflicts before applying.
|
||||
target_files: set[str] = set()
|
||||
target_folders: set[str] = set()
|
||||
affected_file_ids = set(file_plan)
|
||||
for asset_id, target in file_plan.items():
|
||||
normalized = normalize_logical_path(target)
|
||||
if normalized in target_files:
|
||||
raise FileStorageError(f"Rename would create duplicate file path: {normalized}")
|
||||
target_files.add(normalized)
|
||||
asset = assets[asset_id]
|
||||
if _active_asset_exists(session, tenant_id=tenant_id, owner_type=asset.owner_type, owner_id=_asset_owner_id(asset), path=normalized, exclude_asset_id=asset.id):
|
||||
raise FileStorageError(f"Target file already exists: {normalized}")
|
||||
if _active_folder_exists(session, tenant_id=tenant_id, owner_type=asset.owner_type, owner_id=_asset_owner_id(asset), path=normalized):
|
||||
raise FileStorageError(f"Target path is already a folder: {normalized}")
|
||||
if selected_folder_roots and owner_type_norm and owner_id_norm:
|
||||
for old_path, target in folder_plan.items():
|
||||
normalized = normalize_folder(target)
|
||||
if normalized in target_folders:
|
||||
raise FileStorageError(f"Rename would create duplicate folder path: {normalized}")
|
||||
target_folders.add(normalized)
|
||||
if _active_folder_exists(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm, path=normalized, exclude_paths=set(folder_plan.keys())):
|
||||
raise FileStorageError(f"Target folder already exists: {normalized}")
|
||||
if _active_asset_exists(session, tenant_id=tenant_id, owner_type=owner_type_norm, owner_id=owner_id_norm, path=normalized):
|
||||
raise FileStorageError(f"Target path is already a file: {normalized}")
|
||||
|
||||
plan: list[RenamePlanItem] = []
|
||||
for old_path, new_path in sorted(folder_plan.items()):
|
||||
plan.append(RenamePlanItem(kind="folder", id=old_path, old_path=old_path, new_path=new_path))
|
||||
for asset_id, new_path in file_plan.items():
|
||||
asset = assets[asset_id]
|
||||
plan.append(RenamePlanItem(kind="file", id=asset.id, old_path=asset.display_path, new_path=new_path))
|
||||
|
||||
if dry_run:
|
||||
return plan
|
||||
|
||||
for old_path, new_path in folder_plan.items():
|
||||
folder = folder_rows_by_path.get(old_path)
|
||||
if folder:
|
||||
folder.path = normalize_folder(new_path)
|
||||
session.add(folder)
|
||||
for asset_id, new_path in file_plan.items():
|
||||
rename_asset(assets[asset_id], new_path=new_path)
|
||||
session.add(assets[asset_id])
|
||||
return plan
|
||||
Reference in New Issue
Block a user