diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index 7c15d513a..b1b8a35ba 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -44,14 +44,21 @@
Download
- @if (metadata?.has_archive_version) {
-
-
-
+
+
+
+ @if (metadata?.has_archive_version) {
-
+
+ }
+
- }
+
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
index 5c62bc2dd..799a08455 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
@@ -1813,7 +1813,13 @@ describe('DocumentDetailComponent', () => {
component.selectedVersionId = 10
component.download()
- expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(1, doc.id, false, null)
+ expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(
+ 1,
+ doc.id,
+ false,
+ null,
+ false
+ )
httpTestingController
.expectOne('download-latest')
.error(new ProgressEvent('failed'))
@@ -1826,7 +1832,13 @@ describe('DocumentDetailComponent', () => {
component.selectedVersionId = doc.id
component.download()
- expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(3, doc.id, false, doc.id)
+ expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(
+ 3,
+ doc.id,
+ false,
+ doc.id,
+ false
+ )
httpTestingController
.expectOne('download-non-latest')
.error(new ProgressEvent('failed'))
@@ -1849,7 +1861,13 @@ describe('DocumentDetailComponent', () => {
.mockReturnValueOnce('print-no-version')
component.download()
- expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(1, doc.id, false, null)
+ expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(
+ 1,
+ doc.id,
+ false,
+ null,
+ false
+ )
httpTestingController
.expectOne('download-no-version')
.error(new ProgressEvent('failed'))
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index cfb06b9a0..3a3cdae00 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -276,6 +276,7 @@ export class DocumentDetailComponent
customFields: CustomField[]
public downloading: boolean = false
+ public useFormattedFilename: boolean = false
public readonly CustomFieldDataType = CustomFieldDataType
@@ -1289,7 +1290,8 @@ export class DocumentDetailComponent
const downloadUrl = this.documentsService.getDownloadUrl(
this.documentId,
original,
- selectedVersionId
+ selectedVersionId,
+ this.useFormattedFilename
)
this.http
.get(downloadUrl, { observe: 'response', responseType: 'blob' })
diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts
index 48c40e4d5..78cef69cb 100644
--- a/src-ui/src/app/services/rest/document.service.spec.ts
+++ b/src-ui/src/app/services/rest/document.service.spec.ts
@@ -272,10 +272,10 @@ describe(`DocumentService`, () => {
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?version=123`
)
- url = service.getDownloadUrl(documents[0].id, true, 123)
- expect(url).toEqual(
- `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?original=true&version=123`
- )
+ url = service.getDownloadUrl(documents[0].id, true, 123, true)
+ expect(url).toContain('original=true')
+ expect(url).toContain('version=123')
+ expect(url).toContain('follow_formatting=true')
})
it('should pass optional get params for version and fields', () => {
diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts
index ace34cc00..4b4ed7072 100644
--- a/src-ui/src/app/services/rest/document.service.ts
+++ b/src-ui/src/app/services/rest/document.service.ts
@@ -202,7 +202,8 @@ export class DocumentService extends AbstractPaperlessService {
getDownloadUrl(
id: number,
original: boolean = false,
- versionID: number = null
+ versionID: number = null,
+ followFormatting: boolean = false
): string {
let url = new URL(this.getResourceUrl(id, 'download'))
if (original) {
@@ -211,6 +212,9 @@ export class DocumentService extends AbstractPaperlessService {
if (versionID) {
url.searchParams.append('version', versionID.toString())
}
+ if (followFormatting) {
+ url.searchParams.append('follow_formatting', 'true')
+ }
return url.toString()
}
diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py
index ea7240765..e8e3c407d 100644
--- a/src/documents/tests/test_api_documents.py
+++ b/src/documents/tests/test_api_documents.py
@@ -445,6 +445,40 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, content)
+ @override_settings(FILENAME_FORMAT="")
+ def test_download_follow_formatting(self) -> None:
+ content = b"This is a test"
+ content_archive = b"This is the same test but archived"
+
+ doc = Document.objects.create(
+ title="none",
+ filename="my_document.pdf",
+ archive_filename="archived.pdf",
+ mime_type="application/pdf",
+ )
+
+ with Path(doc.source_path).open("wb") as f:
+ f.write(content)
+
+ with Path(doc.archive_path).open("wb") as f:
+ f.write(content_archive)
+
+ # Without follow_formatting, should use public filename
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+ self.assertIn("none.pdf", response["Content-Disposition"])
+
+ # With follow_formatting, should use actual filename on disk
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/download/?follow_formatting=true",
+ )
+ self.assertIn("archived.pdf", response["Content-Disposition"])
+
+ # With follow_formatting and original, should use source filename
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/download/?original=true&follow_formatting=true",
+ )
+ self.assertIn("my_document.pdf", response["Content-Disposition"])
+
def test_document_actions_not_existing_file(self) -> None:
doc = Document.objects.create(
title="none",
diff --git a/src/documents/views.py b/src/documents/views.py
index e333d64af..546da6e83 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -10,6 +10,7 @@ from collections import deque
from datetime import datetime
from pathlib import Path
from time import mktime
+from typing import TYPE_CHECKING
from typing import Any
from typing import Literal
from unicodedata import normalize
@@ -616,6 +617,12 @@ class EmailDocumentDetailSchema(EmailSerializer):
type=OpenApiTypes.BOOL,
location=OpenApiParameter.QUERY,
),
+ OpenApiParameter(
+ name="follow_formatting",
+ description="Whether or not to use the filename on disk",
+ type=OpenApiTypes.BOOL,
+ location=OpenApiParameter.QUERY,
+ ),
],
responses={200: OpenApiTypes.BINARY},
),
@@ -1061,6 +1068,7 @@ class DocumentViewSet(
use_archive=not self.original_requested(request)
and file_doc.has_archive_version,
disposition=disposition,
+ follow_formatting=request.query_params.get("follow_formatting", False),
)
def get_metadata(self, file, mime_type):
@@ -3445,14 +3453,30 @@ class SharedLinkView(View):
return response
-def serve_file(*, doc: Document, use_archive: bool, disposition: str) -> HttpResponse:
+def serve_file(
+ *,
+ doc: Document,
+ use_archive: bool,
+ disposition: str,
+ follow_formatting: bool = False,
+) -> HttpResponse:
if use_archive:
+ if TYPE_CHECKING:
+ assert doc.archive_filename
+
file_handle = doc.archive_file
- filename = doc.get_public_filename(archive=True)
+ filename = (
+ doc.archive_filename
+ if follow_formatting
+ else doc.get_public_filename(archive=True)
+ )
mime_type = "application/pdf"
else:
+ if TYPE_CHECKING:
+ assert doc.filename
+
file_handle = doc.source_file
- filename = doc.get_public_filename()
+ filename = doc.filename if follow_formatting else doc.get_public_filename()
mime_type = doc.mime_type
# Support browser previewing csv files by using text mime type
if mime_type in {"application/csv", "text/csv"} and disposition == "inline":