From 9e871dce25b1966972b85f7efa9ff26806cc8dde Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:19:42 -0700 Subject: [PATCH] Fix: Add directory marker entries to zip exports Without explicit directory entries, some zip viewers (simpler tools, web-based viewers) don't show the folder structure when browsing the archive. Add a _ensure_zip_dirs() helper that writes directory markers for all parent paths of each file entry, deduplicating via a set. Uses ZipFile.mkdir() (available since Python 3.11, the project minimum). Co-Authored-By: Claude Sonnet 4.6 --- .../management/commands/document_exporter.py | 16 ++++++++++++++++ src/documents/tests/test_management_exporter.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index af2ce4ff3..336ab7977 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -330,6 +330,7 @@ class Command(CryptMixin, PaperlessCommand): self.files_in_export_dir: set[Path] = set() self.exported_files: set[str] = set() self.zip_file: zipfile.ZipFile | None = None + self._zip_dirs: set[str] = set() if self.zip_export: zip_name = options["zip_name"] @@ -751,6 +752,19 @@ class Command(CryptMixin, PaperlessCommand): manifest_name.parent.mkdir(parents=True, exist_ok=True) self.check_and_write_json(content, manifest_name) + def _ensure_zip_dirs(self, arcname: str) -> None: + """Write directory marker entries for all parent directories of arcname. + + Some zip viewers only show folder structure when explicit directory + entries exist, so we add them to avoid confusing users. + """ + parts = Path(arcname).parts[:-1] + for i in range(len(parts)): + dir_arc = "/".join(parts[: i + 1]) + "/" + if dir_arc not in self._zip_dirs: + self._zip_dirs.add(dir_arc) + self.zip_file.mkdir(dir_arc) + def check_and_write_json( self, content: list[dict] | dict, @@ -765,6 +779,7 @@ class Command(CryptMixin, PaperlessCommand): if self.zip_export: arcname = str(target.resolve().relative_to(self.target)) + self._ensure_zip_dirs(arcname) self.zip_file.writestr( arcname, json.dumps( @@ -816,6 +831,7 @@ class Command(CryptMixin, PaperlessCommand): if self.zip_export: arcname = str(target.resolve().relative_to(self.target)) + self._ensure_zip_dirs(arcname) self.zip_file.write(source, arcname=arcname) return diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index ff644d800..315353b04 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -615,8 +615,8 @@ class TestExportImport( self.assertIsFile(expected_file) with ZipFile(expected_file) as zip: - # Direct ZipFile writing doesn't add directory entries (unlike shutil.make_archive) - self.assertEqual(len(zip.namelist()), 11) + # 11 files + 3 directory marker entries for the subdirectory structure + self.assertEqual(len(zip.namelist()), 14) self.assertIn("manifest.json", zip.namelist()) self.assertIn("metadata.json", zip.namelist())