]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: support import of zipped export (#10073)
authorKilian <70379879+kaerbr@users.noreply.github.com>
Fri, 13 Jun 2025 17:06:37 +0000 (19:06 +0200)
committerGitHub <noreply@github.com>
Fri, 13 Jun 2025 17:06:37 +0000 (10:06 -0700)
docs/administration.md
src/documents/management/commands/document_importer.py
src/documents/tests/test_management_importer.py

index bb70551418aabf80b5ec1cfdef2f2ef1456d3755..0b9974deff9cd5295bfe5bbebe874504177da4bb 100644 (file)
@@ -333,7 +333,7 @@ must be provided to import. If this value is lost, the export cannot be imported
 The document importer takes the export produced by the [Document
 exporter](#exporter) and imports it into paperless.
 
-The importer works just like the exporter. You point it at a directory,
+The importer works just like the exporter. You point it at a directory or the generated .zip file,
 and the script does the rest of the work:
 
 ```shell
@@ -351,9 +351,6 @@ When you use the provided docker compose script, put the export inside
 the `export` folder in your paperless source directory. Specify
 `../export` as the `source`.
 
-Note that .zip files (as can be generated from the exporter) are not supported. You must unzip them into
-the target directory first.
-
 !!! note
 
     Importing from a previous version of Paperless may work, but for best
index 9e3af47e772af020951d1fc70fa476a2f6bf11c1..282f5c48ed171caa3a772af93f82bcdb39ebea30 100644 (file)
@@ -1,9 +1,12 @@
 import json
 import logging
 import os
+import tempfile
 from collections.abc import Generator
 from contextlib import contextmanager
 from pathlib import Path
+from zipfile import ZipFile
+from zipfile import is_zipfile
 
 import tqdm
 from django.conf import settings
@@ -234,14 +237,19 @@ class Command(CryptMixin, BaseCommand):
         self.manifest_paths = []
         self.manifest = []
 
-        self.pre_check()
+        # Create a temporary directory for extracting a zip file into it, even if supplied source is no zip file to keep code cleaner.
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            if is_zipfile(self.source):
+                with ZipFile(self.source) as zf:
+                    zf.extractall(tmp_dir)
+                self.source = Path(tmp_dir)
+            self._run_import()
 
+    def _run_import(self):
+        self.pre_check()
         self.load_metadata()
-
         self.load_manifest_files()
-
         self.check_manifest_validity()
-
         self.decrypt_secret_fields()
 
         # see /src/documents/signals/handlers.py
index 5cee9ae478b56dae2920eb90005f92a4fea9a68b..e700ecdc98b24d7c81011bf63794a8bb34b20e61 100644 (file)
@@ -2,6 +2,7 @@ import json
 import tempfile
 from io import StringIO
 from pathlib import Path
+from zipfile import ZipFile
 
 from django.contrib.auth.models import User
 from django.core.management import call_command
@@ -335,3 +336,42 @@ class TestCommandImport(
 
         self.assertIn("Version mismatch:", stdout_str)
         self.assertIn("importing 2.8.1", stdout_str)
+
+    def test_import_zipped_export(self):
+        """
+        GIVEN:
+            - A zip file with correct content (manifest.json and version.json inside)
+        WHEN:
+            - An import is attempted using the zip file as the source
+        THEN:
+            - The command reads from the zip without warnings or errors
+        """
+
+        stdout = StringIO()
+        zip_path = self.dirs.scratch_dir / "export.zip"
+
+        # Create manifest.json and version.json in a temp dir
+        with tempfile.TemporaryDirectory() as temp_dir:
+            temp_dir_path = Path(temp_dir)
+
+            (temp_dir_path / "manifest.json").touch()
+            (temp_dir_path / "version.json").touch()
+
+            # Create the zip file
+            with ZipFile(zip_path, "w") as zf:
+                zf.write(temp_dir_path / "manifest.json", arcname="manifest.json")
+                zf.write(temp_dir_path / "version.json", arcname="version.json")
+
+        # Try to import from the zip file
+        with self.assertRaises(json.decoder.JSONDecodeError):
+            call_command(
+                "document_importer",
+                "--no-progress-bar",
+                str(zip_path),
+                stdout=stdout,
+            )
+        stdout.seek(0)
+        stdout_str = str(stdout.read())
+
+        # There should be no error or warnings. Therefore the output should be empty.
+        self.assertEqual(stdout_str, "")