61 lines
1.8 KiB
Python
61 lines
1.8 KiB
Python
"""Shared, presentation-independent helpers for document ZIP exports."""
|
|
|
|
import posixpath
|
|
from zipfile import ZipFile
|
|
|
|
|
|
def safe_archive_part(value: object, fallback: str) -> str:
|
|
"""Sanitize one ZIP path component while preserving Unicode names."""
|
|
raw = str(value or fallback)
|
|
cleaned = "".join(
|
|
"_" if char in {"/", "\\"} or ord(char) < 32 else char for char in raw
|
|
).strip(" .")
|
|
return cleaned or fallback
|
|
|
|
|
|
def unique_archive_path(archive_path: str, used_paths: set[str]) -> str:
|
|
"""Reserve a deterministic, collision-free path inside an archive."""
|
|
if archive_path not in used_paths:
|
|
used_paths.add(archive_path)
|
|
return archive_path
|
|
|
|
directory = posixpath.dirname(archive_path)
|
|
filename = posixpath.basename(archive_path)
|
|
stem, extension = posixpath.splitext(filename)
|
|
counter = 2
|
|
while True:
|
|
candidate_name = f"{stem} ({counter}){extension}"
|
|
candidate = (
|
|
posixpath.join(directory, candidate_name) if directory else candidate_name
|
|
)
|
|
if candidate not in used_paths:
|
|
used_paths.add(candidate)
|
|
return candidate
|
|
counter += 1
|
|
|
|
|
|
def document_archive_filename(document) -> str:
|
|
"""Return the best available filename for a document asset."""
|
|
return safe_archive_part(
|
|
getattr(document.file, "original_filename", "")
|
|
or getattr(document.file, "name", "")
|
|
or document.title
|
|
or f"document-{document.id}",
|
|
f"document-{document.id}",
|
|
)
|
|
|
|
|
|
def write_document_to_archive(
|
|
archive: ZipFile,
|
|
*,
|
|
document,
|
|
archive_path: str,
|
|
) -> None:
|
|
"""Read a filer asset safely and write it to an open ZIP archive."""
|
|
file_obj = document.file.file
|
|
try:
|
|
file_obj.open("rb")
|
|
archive.writestr(archive_path, file_obj.read())
|
|
finally:
|
|
file_obj.close()
|