mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-15 07:13:18 +00:00
* Feature: Allow configuring the compression type and compression levels during export Building on the zip export improvements, this now allows users to further configure the zip to fit their needs. A simple stored zip for speed, or a high compression zstd for the smallest archive. Full validation of the method and levels at the command line Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
import zipfile
|
|
|
|
# ZIP_ZSTANDARD exists only on Python 3.14+ (PEP 784). None elsewhere.
|
|
ZSTD: int | None = getattr(zipfile, "ZIP_ZSTANDARD", None)
|
|
|
|
# CLI choices are fixed across runtimes so argparse never hides zstd; runtime
|
|
# availability is enforced separately in compression_available().
|
|
COMPRESSION_CHOICES: tuple[str, ...] = (
|
|
"stored",
|
|
"deflated",
|
|
"bzip2",
|
|
"lzma",
|
|
"zstd",
|
|
)
|
|
|
|
# Method name -> zipfile compression constant (zstd only when supported).
|
|
COMPRESSION_METHODS: dict[str, int] = {
|
|
"stored": zipfile.ZIP_STORED,
|
|
"deflated": zipfile.ZIP_DEFLATED,
|
|
"bzip2": zipfile.ZIP_BZIP2,
|
|
"lzma": zipfile.ZIP_LZMA,
|
|
}
|
|
if ZSTD is not None:
|
|
COMPRESSION_METHODS["zstd"] = ZSTD
|
|
|
|
# Inclusive (min, max) level bounds per method; None => level not applicable.
|
|
# Verified on CPython 3.14.3.
|
|
#
|
|
# zstd's raw library bounds are (-131072, 22)
|
|
# (compression.zstd.CompressionParameter.compression_level.bounds()) — the
|
|
# minimum is an internal implementation constant (-ZSTD_TARGETLENGTH_MAX),
|
|
# not a meaningful distinct "level"; deeper negative values than -22 buy
|
|
# nothing over -22 in practice. We expose the conventional zstd CLI range
|
|
# instead of the raw library bounds.
|
|
LEVEL_BOUNDS: dict[str, tuple[int, int] | None] = {
|
|
"stored": None,
|
|
"deflated": (0, 9),
|
|
"bzip2": (1, 9),
|
|
"lzma": None,
|
|
"zstd": (-22, 22),
|
|
}
|
|
|
|
# zipfile compress_type id -> method name.
|
|
_COMPRESS_TYPE_TO_METHOD: dict[int, str] = {
|
|
zipfile.ZIP_STORED: "stored",
|
|
zipfile.ZIP_DEFLATED: "deflated",
|
|
zipfile.ZIP_BZIP2: "bzip2",
|
|
zipfile.ZIP_LZMA: "lzma",
|
|
93: "zstd",
|
|
}
|
|
|
|
|
|
def compression_available(method: str) -> bool:
|
|
"""Whether the running interpreter can actually use the given method."""
|
|
if method in ("stored", "deflated"):
|
|
# zlib is a hard CPython dependency; stored needs nothing.
|
|
return True
|
|
if method == "bzip2":
|
|
return _module_importable("bz2")
|
|
if method == "lzma":
|
|
return _module_importable("lzma")
|
|
if method == "zstd":
|
|
return ZSTD is not None and _module_importable("compression.zstd")
|
|
return False # pragma: no cover -- method is always one of COMPRESSION_CHOICES
|
|
|
|
|
|
def _module_importable(name: str) -> bool:
|
|
try:
|
|
importlib.import_module(name)
|
|
except ImportError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def level_error(method: str, level: int | None) -> str | None:
|
|
"""Return a human message if (method, level) is invalid, else None."""
|
|
if level is None:
|
|
return None
|
|
bounds = LEVEL_BOUNDS[method]
|
|
if bounds is None:
|
|
return f"--zip-compression-level has no effect for '{method}'"
|
|
low, high = bounds
|
|
if not (low <= level <= high):
|
|
return (
|
|
f"--zip-compression-level for '{method}' must be between {low} and {high}"
|
|
)
|
|
return None
|
|
|
|
|
|
def compress_type_readable(compress_type: int) -> bool:
|
|
"""Whether this interpreter can decompress an entry of the given type."""
|
|
method = _COMPRESS_TYPE_TO_METHOD.get(compress_type)
|
|
if method is None:
|
|
return False
|
|
return compression_available(method)
|
|
|
|
|
|
def unreadable_method_names(compress_types: set[int]) -> set[str]:
|
|
"""Map a set of compress_type ids to human method names for error messages."""
|
|
names: set[str] = set()
|
|
for ct in compress_types:
|
|
names.add(_COMPRESS_TYPE_TO_METHOD.get(ct, f"method {ct}"))
|
|
return names
|