From 581fbab8f3a8c44474492c93e8415399df62b279 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:59:27 -0700 Subject: [PATCH] Polish: atomic zip commit via Path.replace, widen sink params to ExportSink Path.rename() raises FileExistsError on Windows when the destination already exists; Path.replace() is atomic cross-platform. Also widen document_exporter's sink parameters from the concrete DirectoryExportSink | ZipExportSink union to the ExportSink ABC, so a future sink implementation is a pure addition rather than requiring every call site's annotation to change. --- src/documents/export/sinks.py | 2 +- .../management/commands/document_exporter.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/documents/export/sinks.py b/src/documents/export/sinks.py index 9f83d743f..ef940ffca 100644 --- a/src/documents/export/sinks.py +++ b/src/documents/export/sinks.py @@ -320,7 +320,7 @@ class ZipExportSink(ExportSink): self._zip = None if self._delete: self._wipe_destination() - self._tmp_path.rename(self._zip_path) + self._tmp_path.replace(self._zip_path) def _wipe_destination(self) -> None: skip = {self._zip_path.resolve(), self._tmp_path.resolve()} diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index fad9cbf2e..cc0aa4d1b 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -30,6 +30,7 @@ if settings.AUDIT_LOG_ENABLED: from auditlog.models import LogEntry from documents.export.sinks import DirectoryExportSink +from documents.export.sinks import ExportSink from documents.export.sinks import StreamingManifestWriter from documents.export.sinks import ZipExportSink from documents.file_handling import generate_filename @@ -245,7 +246,7 @@ class Command(CryptMixin, PaperlessCommand): if not os.access(self.target, os.W_OK): raise CommandError("That path doesn't appear to be writable") - sink: DirectoryExportSink | ZipExportSink + sink: ExportSink if self.zip_export: sink = ZipExportSink( self.target, @@ -264,7 +265,7 @@ class Command(CryptMixin, PaperlessCommand): with FileLock(settings.MEDIA_LOCK), sink: self.dump(sink) - def dump(self, sink: DirectoryExportSink | ZipExportSink) -> None: + def dump(self, sink: ExportSink) -> None: # 1. Create manifest, containing all correspondents, types, tags, storage # paths, note, documents and ui_settings _excluded_usernames = ["consumer", "AnonymousUser"] @@ -487,7 +488,7 @@ class Command(CryptMixin, PaperlessCommand): def copy_document_files( self, document: Document, - sink: DirectoryExportSink | ZipExportSink, + sink: ExportSink, original_arc: str, thumbnail_arc: str | None, archive_arc: str | None, @@ -536,7 +537,7 @@ class Command(CryptMixin, PaperlessCommand): def copy_share_link_bundle_file( self, bundle: ShareLinkBundle, - sink: DirectoryExportSink | ZipExportSink, + sink: ExportSink, bundle_arc: str, ) -> None: """ @@ -562,7 +563,7 @@ class Command(CryptMixin, PaperlessCommand): def _write_split_manifest( self, - sink: DirectoryExportSink | ZipExportSink, + sink: ExportSink, document_dict: dict, document: Document, base_name: Path,