Enhancement: support import of zipped export (#10073)

This commit is contained in:
Kilian 2025-06-13 19:06:37 +02:00 committed by GitHub
parent 4313635b01
commit 246f17c6c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 8 deletions

View 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 The document importer takes the export produced by the [Document
exporter](#exporter) and imports it into paperless. 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: and the script does the rest of the work:
```shell ```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 the `export` folder in your paperless source directory. Specify
`../export` as the `source`. `../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 !!! note
Importing from a previous version of Paperless may work, but for best Importing from a previous version of Paperless may work, but for best

View File

@ -1,9 +1,12 @@
import json import json
import logging import logging
import os import os
import tempfile
from collections.abc import Generator from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from zipfile import ZipFile
from zipfile import is_zipfile
import tqdm import tqdm
from django.conf import settings from django.conf import settings
@ -234,14 +237,19 @@ class Command(CryptMixin, BaseCommand):
self.manifest_paths = [] self.manifest_paths = []
self.manifest = [] self.manifest = []
# 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.pre_check()
self.load_metadata() self.load_metadata()
self.load_manifest_files() self.load_manifest_files()
self.check_manifest_validity() self.check_manifest_validity()
self.decrypt_secret_fields() self.decrypt_secret_fields()
# see /src/documents/signals/handlers.py # see /src/documents/signals/handlers.py

View File

@ -2,6 +2,7 @@ import json
import tempfile import tempfile
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from zipfile import ZipFile
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management import call_command from django.core.management import call_command
@ -335,3 +336,42 @@ class TestCommandImport(
self.assertIn("Version mismatch:", stdout_str) self.assertIn("Version mismatch:", stdout_str)
self.assertIn("importing 2.8.1", 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, "")