generated from john/python-template
Delete Document & Delete Source buttons now delete the underlying files
This commit is contained in:
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -119,6 +120,7 @@ class DocumentService(ServiceBase):
|
||||
|
||||
async def delete_document(self, document: Document, *, session: AsyncSession | None = None) -> None:
|
||||
"""Delete a document from the database."""
|
||||
document_id = document.id
|
||||
async with self._session_scope(session) as _session:
|
||||
existing = await _session.get(
|
||||
Document,
|
||||
@@ -152,6 +154,20 @@ class DocumentService(ServiceBase):
|
||||
await _session.delete(existing)
|
||||
await self._finalize(session=_session, caller_session=session)
|
||||
|
||||
self._delete_document_storage_folder(document_id=document_id)
|
||||
|
||||
def _delete_document_storage_folder(self, *, document_id: UUID) -> None:
|
||||
"""Best-effort cleanup for document-scoped source storage."""
|
||||
document_dir = self.settings.upload_dir / "documents" / str(document_id)
|
||||
if not document_dir.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.rmtree(document_dir)
|
||||
logger.info("Deleted document storage folder: %s", document_dir)
|
||||
except OSError:
|
||||
logger.warning("Failed to delete document storage folder: %s", document_dir)
|
||||
|
||||
async def create_person(self, person: Person, *, session: AsyncSession | None = None) -> Person:
|
||||
"""Create a new person in the database."""
|
||||
async with self._session_scope(session) as _session:
|
||||
|
||||
@@ -113,10 +113,13 @@ class TranscriptionService(ServiceBase):
|
||||
|
||||
async def delete_source(self, source: Source, *, session: AsyncSession | None = None) -> None:
|
||||
"""Delete a source page record."""
|
||||
source_file_path = source.file_path
|
||||
async with self._session_scope(session) as _session:
|
||||
await _session.delete(source)
|
||||
await self._finalize(session=_session, caller_session=session)
|
||||
|
||||
self._delete_source_file(source_file_path=source_file_path)
|
||||
|
||||
async def delete_unlinked_source(self, *, source_id: UUID, session: AsyncSession | None = None) -> None:
|
||||
"""Delete a source only when no JobSource links exist."""
|
||||
async with self._session_scope(session) as _session:
|
||||
@@ -141,9 +144,12 @@ class TranscriptionService(ServiceBase):
|
||||
suggestion="Remove JobSource links first, then retry deletion.",
|
||||
)
|
||||
|
||||
source_file_path = source.file_path
|
||||
await _session.delete(source)
|
||||
await self._finalize(session=_session, caller_session=session)
|
||||
|
||||
self._delete_source_file(source_file_path=source_file_path)
|
||||
|
||||
async def list_sources(
|
||||
self,
|
||||
*,
|
||||
@@ -263,9 +269,26 @@ class TranscriptionService(ServiceBase):
|
||||
for job_source in matching_links:
|
||||
await _session.delete(job_source)
|
||||
|
||||
source_file_path = source.file_path
|
||||
await _session.delete(source)
|
||||
await self._finalize(session=_session, caller_session=session)
|
||||
|
||||
self._delete_source_file(source_file_path=source_file_path)
|
||||
|
||||
def _delete_source_file(self, *, source_file_path: str) -> None:
|
||||
"""Best-effort cleanup for source media files."""
|
||||
candidate_path = Path(source_file_path)
|
||||
resolved_path = candidate_path if candidate_path.is_absolute() else self.settings.upload_dir / candidate_path
|
||||
|
||||
if not resolved_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
resolved_path.unlink()
|
||||
logger.info("Deleted source file: %s", resolved_path)
|
||||
except OSError:
|
||||
logger.warning("Failed to delete source file: %s", resolved_path)
|
||||
|
||||
async def list_job_sources(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -23,6 +23,8 @@ class SourceTableRow:
|
||||
upload_name: str
|
||||
filename: str
|
||||
document_id: UUID
|
||||
job_source_status: str | None = None
|
||||
job_source_error_detail: str | None = None
|
||||
|
||||
|
||||
def _serialize_rows(rows: Sequence[SourceTableRow]) -> list[dict[str, Any]]:
|
||||
@@ -33,6 +35,8 @@ def _serialize_rows(rows: Sequence[SourceTableRow]) -> list[dict[str, Any]]:
|
||||
"upload_name": row.upload_name,
|
||||
"filename": row.filename,
|
||||
"document_id": str(row.document_id),
|
||||
"job_source_status": row.job_source_status or "-",
|
||||
"job_source_error_detail": row.job_source_error_detail or "-",
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
@@ -51,6 +55,20 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None:
|
||||
{"name": "page_number", "label": "Page", "field": "page_number", "sortable": True},
|
||||
{"name": "upload_name", "label": "Upload Title", "field": "upload_name", "sortable": True, "classes": "font-serif"},
|
||||
{"name": "filename", "label": "Stored Filename", "field": "filename", "sortable": True, "classes": "font-mono"},
|
||||
{
|
||||
"name": "job_source_status",
|
||||
"label": "Job Source Status",
|
||||
"field": "job_source_status",
|
||||
"sortable": True,
|
||||
"classes": "font-mono",
|
||||
},
|
||||
{
|
||||
"name": "job_source_error_detail",
|
||||
"label": "Job Source Error Detail",
|
||||
"field": "job_source_error_detail",
|
||||
"sortable": False,
|
||||
"classes": "font-mono text-xs",
|
||||
},
|
||||
{"name": "document_id", "label": "Document ID", "field": "document_id", "sortable": True, "classes": "font-mono"},
|
||||
],
|
||||
default_sort_by="page_number",
|
||||
|
||||
@@ -52,6 +52,7 @@ def register_page() -> None:
|
||||
job_label = None
|
||||
back_path = None
|
||||
sources: list[Source] = []
|
||||
job_source_by_source_id: dict[UUID, JobSource] = {}
|
||||
|
||||
try:
|
||||
if document_id is not None:
|
||||
@@ -64,6 +65,10 @@ def register_page() -> None:
|
||||
job_label = str(job.id)
|
||||
back_path = f"/jobs/{job.id}"
|
||||
job_sources = await sources_service.list_job_sources(job_id=job.id)
|
||||
job_source_by_source_id = {
|
||||
job_source.source_id: job_source
|
||||
for job_source in job_sources
|
||||
}
|
||||
sources = [job_source.source for job_source in job_sources if job_source.source is not None]
|
||||
sources.sort(key=lambda item: (item.page_number, item.upload_name.casefold()))
|
||||
else:
|
||||
@@ -103,6 +108,16 @@ def register_page() -> None:
|
||||
upload_name=source.upload_name,
|
||||
filename=source.filename,
|
||||
document_id=source.document_id,
|
||||
job_source_status=(
|
||||
job_source_by_source_id[source.id].status.value
|
||||
if source.id in job_source_by_source_id
|
||||
else None
|
||||
),
|
||||
job_source_error_detail=(
|
||||
job_source_by_source_id[source.id].error_detail
|
||||
if source.id in job_source_by_source_id
|
||||
else None
|
||||
),
|
||||
)
|
||||
for source in sources
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user