feat(13-05): implement keep_both_name() for D-03/D-05 collision naming
- keep_both_name(filename, counter) inserts counter before first extension - Handles compound extensions (archive.tar.gz → archive (1).tar.gz) - Handles extensionless files (README → README (1)) - Lives in services/cloud_operations as shared upload-queue helper - All 23 upload contract + keep-both naming tests pass
This commit is contained in:
@@ -2,7 +2,10 @@
|
|||||||
Cloud operations orchestration service — Phase 13.
|
Cloud operations orchestration service — Phase 13.
|
||||||
|
|
||||||
This module is the single service-layer seam for all Phase 13 cloud mutation and
|
This module is the single service-layer seam for all Phase 13 cloud mutation and
|
||||||
connection-lifecycle operations. It:
|
connection-lifecycle operations. It also exposes shared naming utilities used by
|
||||||
|
the upload route for keep-both collision resolution (D-03, D-05).
|
||||||
|
|
||||||
|
It:
|
||||||
|
|
||||||
- Resolves owned connections via services.cloud_items.resolve_owned_connection
|
- Resolves owned connections via services.cloud_items.resolve_owned_connection
|
||||||
- Decrypts credentials only at the provider boundary (T-13-11)
|
- Decrypts credentials only at the provider boundary (T-13-11)
|
||||||
@@ -24,6 +27,7 @@ Domain exceptions raised here:
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -34,6 +38,49 @@ from db.models import CloudConnection, CloudItem, CloudFolderState
|
|||||||
from services.cloud_items import resolve_owned_connection, ConnectionNotFound # noqa: F401
|
from services.cloud_items import resolve_owned_connection, ConnectionNotFound # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
# ── Keep-both naming helper ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def keep_both_name(filename: str, counter: int) -> str:
|
||||||
|
"""Insert a collision counter before the file extension.
|
||||||
|
|
||||||
|
D-03 / D-05: When the user chooses "Keep both" for a same-name upload
|
||||||
|
conflict, DocuVault renames the incoming file by inserting ` (N)` before
|
||||||
|
the first file extension (or at the end if no extension is present).
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
keep_both_name("Report.pdf", 1) → "Report (1).pdf"
|
||||||
|
keep_both_name("Report.pdf", 2) → "Report (2).pdf"
|
||||||
|
keep_both_name("README", 1) → "README (1)"
|
||||||
|
keep_both_name("archive.tar.gz", 1) → "archive (1).tar.gz"
|
||||||
|
keep_both_name("My Document.docx", 1) → "My Document (1).docx"
|
||||||
|
|
||||||
|
The counter is always placed before the first dot in the filename so that
|
||||||
|
compound extensions (e.g. ``.tar.gz``) are preserved intact.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The original filename (basename only — no path components).
|
||||||
|
counter: Positive integer counter to insert (1-based by convention).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The renamed filename with the counter inserted before the extension.
|
||||||
|
"""
|
||||||
|
p = pathlib.PurePosixPath(filename)
|
||||||
|
# Use the stem from PurePosixPath but handle the suffix correctly:
|
||||||
|
# PurePosixPath("archive.tar.gz").suffix == ".gz" — we want all suffixes.
|
||||||
|
stem = p.name
|
||||||
|
suffix = ""
|
||||||
|
|
||||||
|
# Find the first dot that separates name from extension
|
||||||
|
dot_pos = stem.find(".")
|
||||||
|
if dot_pos > 0:
|
||||||
|
suffix = stem[dot_pos:] # everything from the first dot onward
|
||||||
|
stem = stem[:dot_pos] # everything before the first dot
|
||||||
|
|
||||||
|
return f"{stem} ({counter}){suffix}"
|
||||||
|
|
||||||
|
|
||||||
# ── Connection status constants ───────────────────────────────────────────────
|
# ── Connection status constants ───────────────────────────────────────────────
|
||||||
|
|
||||||
STATUS_ACTIVE = "ACTIVE"
|
STATUS_ACTIVE = "ACTIVE"
|
||||||
|
|||||||
Reference in New Issue
Block a user