mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-17 13:28:12 +00:00
Fix: keep counter and extension when shortening long public filenames (#14472)
get_public_filename relied on sanitize_filename to truncate, which cuts from the end and so dropped the counter and extension of long titles. Multi-byte titles could also exceed the 255 byte filesystem limit. Shorten the title by UTF-8 bytes before appending the counter and extension. The exporter reserves room for its longer derived thumbnail and archive suffixes.
This commit is contained in:
1 parent
30a24ee02e
commit
183cfd2da4
4 files changed
+178
-15
No files matched your search
+23
-12
@@ -157,6 +157,8 @@ class StoragePath(MatchingModel):
|
||||
|
||||
class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-missing]
|
||||
MAX_STORED_FILENAME_LENGTH: Final[int] = 1024
|
||||
# Per-component limit of common filesystems (ext4, XFS, btrfs, ZFS, APFS)
|
||||
MAX_PUBLIC_FILENAME_BYTES: Final[int] = 255
|
||||
|
||||
correspondent = models.ForeignKey(
|
||||
Correspondent,
|
||||
@@ -484,28 +486,37 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
||||
def archive_file(self):
|
||||
return Path(self.archive_path).open("rb")
|
||||
|
||||
def get_public_filename(self, *, archive=False, counter=0, suffix=None) -> str:
|
||||
def get_public_filename(
|
||||
self,
|
||||
*,
|
||||
archive: bool = False,
|
||||
counter: int = 0,
|
||||
max_bytes: int = MAX_PUBLIC_FILENAME_BYTES,
|
||||
) -> str:
|
||||
"""
|
||||
Returns a sanitized filename for the document, not including any paths.
|
||||
|
||||
The name fits in max_bytes (UTF-8). A long title is shortened before
|
||||
the counter and extension are appended, because sanitize_filename
|
||||
truncates from the end. The tail is cut only when max_bytes is
|
||||
smaller than the tail itself.
|
||||
"""
|
||||
# Root owns metadata for all versions
|
||||
context_document = (
|
||||
self.root_document if self.root_document_id is not None else self
|
||||
)
|
||||
result = str(context_document)
|
||||
|
||||
if counter:
|
||||
result += f"_{counter:02}"
|
||||
extension = ".pdf" if archive else self.file_type
|
||||
tail = f"_{counter:02}{extension}" if counter else extension
|
||||
|
||||
if suffix:
|
||||
result += suffix
|
||||
head_bytes = max(0, max_bytes - len(tail.encode()))
|
||||
head = str(context_document).encode()[:head_bytes].decode(errors="ignore")
|
||||
|
||||
if archive:
|
||||
result += ".pdf"
|
||||
else:
|
||||
result += self.file_type
|
||||
|
||||
return pathvalidate.sanitize_filename(result, replacement_text="-")
|
||||
return pathvalidate.sanitize_filename(
|
||||
head + tail,
|
||||
replacement_text="-",
|
||||
max_len=max_bytes,
|
||||
)
|
||||
|
||||
@property
|
||||
def file_type(self):
|
||||
|
||||
Reference in new issue
Block a user