diff --git a/Dockerfile b/Dockerfile index bdc2a1632..8b87ede8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ RUN set -eux \ # Purpose: Installs s6-overlay and rootfs # Comments: # - Don't leave anything extra in here either -FROM ghcr.io/astral-sh/uv:0.6.11-python3.12-bookworm-slim AS s6-overlay-base +FROM ghcr.io/astral-sh/uv:0.6.13-python3.12-bookworm-slim AS s6-overlay-base WORKDIR /usr/src/s6 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run index 08b7635f8..9b3e2964f 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run @@ -17,6 +17,9 @@ if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; the if [[ -f ${SECRETFILE} ]]; then # Trim off trailing _FILE FILESTRIP=${FILENAME//_FILE/} + if [[ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]]; then + echo "${log_prefix} Your secret: ${FILENAME##*/} contains a trailing newline and may not work as expected" + fi # Set environment variable cat "${SECRETFILE}" > "${FILESTRIP}" echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh new file mode 100755 index 000000000..93b45fd06 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh @@ -0,0 +1,7 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash +declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" + +# shellcheck disable=SC2164 +cd "${PAPERLESS_SRC_DIR}" +exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run index db0dc26d3..777724886 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run @@ -1,20 +1,12 @@ #!/command/with-contenv /usr/bin/bash # shellcheck shell=bash declare -r log_prefix="[init-migrations]" -declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" -( - # flock is in place to prevent multiple containers from doing migrations - # simultaneously. This also ensures that the db is ready when the command - # of the current container starts. - flock 200 - echo "${log_prefix} Apply database migrations..." - cd "${PAPERLESS_SRC_DIR}" +echo "${log_prefix} Apply database migrations..." - if [[ -n "${USER_IS_NON_ROOT}" ]]; then - exec python3 manage.py migrate --skip-checks --no-input - else - exec s6-setuidgid paperless python3 manage.py migrate --skip-checks --no-input - fi - -) 200>"${data_dir}/migration_lock" +# The whole migrate, with flock, needs to run as the right user +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec /etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh +else + exec s6-setuidgid paperless /etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh +fi diff --git a/pyproject.toml b/pyproject.toml index b1e0285d0..884782826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "tqdm~=4.67.1", "watchdog~=6.0", "whitenoise~=6.9", - "whoosh~=2.7", + "whoosh-reloaded>=2.7.5", "zxing-cpp~=2.3.0", ] 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 b85a7eaf4..76517d336 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 @@ -1,5 +1,10 @@ import { DatePipe } from '@angular/common' -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { + HttpHeaders, + HttpResponse, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' import { HttpTestingController, provideHttpClientTesting, @@ -1331,6 +1336,34 @@ describe('DocumentDetailComponent', () => { expect(urlRevokeSpy).toHaveBeenCalled() }) + it('should download a file with the correct filename', () => { + const mockBlob = new Blob(['test content'], { type: 'text/plain' }) + const mockResponse = new HttpResponse({ + body: mockBlob, + headers: new HttpHeaders({ + 'Content-Disposition': 'attachment; filename="test-file.txt"', + }), + }) + + const downloadUrl = 'http://example.com/download' + component.documentId = 123 + jest.spyOn(documentService, 'getDownloadUrl').mockReturnValue(downloadUrl) + + const createSpy = jest.spyOn(document, 'createElement') + const anchor: HTMLAnchorElement = {} as HTMLAnchorElement + createSpy.mockReturnValueOnce(anchor) + + component.download(false) + + httpTestingController + .expectOne(downloadUrl) + .flush(mockBlob, { headers: mockResponse.headers }) + + expect(createSpy).toHaveBeenCalledWith('a') + expect(anchor.download).toBe('test-file.txt') + createSpy.mockClear() + }) + it('should get email enabled status from settings', () => { jest.spyOn(settingsService, 'get').mockReturnValue(true) expect(component.emailEnabled).toBeTruthy() 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 130acbd05..d9d78206f 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 @@ -1,5 +1,5 @@ import { AsyncPipe, NgTemplateOutlet } from '@angular/common' -import { HttpClient } from '@angular/common/http' +import { HttpClient, HttpResponse } from '@angular/common/http' import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' import { FormArray, @@ -995,44 +995,48 @@ export class DocumentDetailComponent this.documentId, original ) - this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({ - next: (blob) => { - this.downloading = false - const blobParts = [blob] - const file = new File( - blobParts, - original - ? this.document.original_file_name - : this.document.archived_file_name, - { - type: original ? this.document.mime_type : 'application/pdf', - } - ) - if ( - !this.deviceDetectorService.isDesktop() && - navigator.canShare && - navigator.canShare({ files: [file] }) - ) { - navigator.share({ - files: [file], + this.http + .get(downloadUrl, { observe: 'response', responseType: 'blob' }) + .subscribe({ + next: (response: HttpResponse) => { + const filename = response.headers + .get('Content-Disposition') + ?.split(';') + ?.find((part) => part.trim().startsWith('filename=')) + ?.split('=')[1] + ?.replace(/['"]/g, '') + const blob = new Blob([response.body], { + type: response.body.type, }) - } else { - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = this.document.title - a.click() - URL.revokeObjectURL(url) - } - }, - error: (error) => { - this.downloading = false - this.toastService.showError( - $localize`Error downloading document`, - error - ) - }, - }) + this.downloading = false + const file = new File([blob], filename, { + type: response.body.type, + }) + if ( + !this.deviceDetectorService.isDesktop() && + navigator.canShare && + navigator.canShare({ files: [file] }) + ) { + navigator.share({ + files: [file], + }) + } else { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + } + }, + error: (error) => { + this.downloading = false + this.toastService.showError( + $localize`Error downloading document`, + error + ) + }, + }) } hasNext() { diff --git a/src-ui/src/locale/messages.zh_TW.xlf b/src-ui/src/locale/messages.zh_TW.xlf index baf0fecaa..f27ff5837 100644 --- a/src-ui/src/locale/messages.zh_TW.xlf +++ b/src-ui/src/locale/messages.zh_TW.xlf @@ -556,7 +556,7 @@ src/app/components/admin/config/config.component.html 2 - 應用程式設定 + 系統配置 Global app configuration options which apply to <strong>every</strong> user of this install of Paperless-ngx. Options can also be set using environment variables or the configuration file but the value here will always take precedence. @@ -564,7 +564,7 @@ src/app/components/admin/config/config.component.html 4 - 全域應用程式設定選項適用於此安裝版本的<strong>每位</strong>使用者。雖然也可以透過環境變數或設定檔來設定,但這裡的設定將始終優先於其他設定。 + 全域系統配置會套用至該系統的<strong>每一位</strong>使用者。雖然環境變數或設定檔也可以調整相關設定,但此處的設定將優先於他處的設定。 Read the documentation about this setting @@ -864,7 +864,7 @@ src/app/components/admin/settings/settings.component.html 4 - 自訂外觀、通知等選項。設定只適用於<strong>目前使用者</strong>。 + 自訂外觀、通知等選項。這些設定只套用於<strong>目前的使用者</strong>。 Start tour diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 073026b19..ae15992ed 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -1225,14 +1225,7 @@ def run_workflows( document.refresh_from_db() doc_tag_ids = list(document.tags.values_list("pk", flat=True)) - # If a workflow is supplied, we don't need to check if it matches - matches = ( - matching.document_matches_workflow(document, workflow, trigger_type) - if workflow_to_run is None - else True - ) - - if matches: + if matching.document_matches_workflow(document, workflow, trigger_type): action: WorkflowAction for action in workflow.actions.all(): message = f"Applying {action} from {workflow}" diff --git a/src/documents/views.py b/src/documents/views.py index 7298391f2..27f8ed51f 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2376,9 +2376,13 @@ def serve_file(*, doc: Document, use_archive: bool, disposition: str): # RFC 5987 addresses this issue # see https://datatracker.ietf.org/doc/html/rfc5987#section-4.2 # Chromium cannot handle commas in the filename - filename_normalized = normalize("NFKD", filename.replace(",", "_")).encode( - "ascii", - "ignore", + filename_normalized = ( + normalize("NFKD", filename.replace(",", "_")) + .encode( + "ascii", + "ignore", + ) + .decode("ascii") ) filename_encoded = quote(filename) content_disposition = ( diff --git a/src/locale/fr_FR/LC_MESSAGES/django.po b/src/locale/fr_FR/LC_MESSAGES/django.po index e1feffab6..98bf2c19b 100644 --- a/src/locale/fr_FR/LC_MESSAGES/django.po +++ b/src/locale/fr_FR/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-03-26 21:04-0700\n" -"PO-Revision-Date: 2025-04-02 00:33\n" +"PO-Revision-Date: 2025-04-09 12:12\n" "Last-Translator: \n" "Language-Team: French\n" "Language: fr_FR\n" @@ -582,7 +582,7 @@ msgstr "Fichier à consommer" #: documents/models.py:540 msgid "Train Classifier" -msgstr "" +msgstr "Entrainer le classificateur" #: documents/models.py:541 msgid "Check Sanity" diff --git a/src/locale/nl_NL/LC_MESSAGES/django.po b/src/locale/nl_NL/LC_MESSAGES/django.po index 21e08d070..b7b2baf9d 100644 --- a/src/locale/nl_NL/LC_MESSAGES/django.po +++ b/src/locale/nl_NL/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-03-26 21:04-0700\n" -"PO-Revision-Date: 2025-03-29 17:14\n" +"PO-Revision-Date: 2025-04-09 21:44\n" "Last-Translator: \n" "Language-Team: Dutch\n" "Language: nl_NL\n" @@ -23,7 +23,7 @@ msgstr "Documenten" #: documents/filters.py:374 msgid "Value must be valid JSON." -msgstr "" +msgstr "Waarde moet een geldige JSON zijn." #: documents/filters.py:393 msgid "Invalid custom field query expression" @@ -766,7 +766,7 @@ msgstr "aangepaste velden" #: documents/models.py:766 msgid "custom fields" -msgstr "Aangepaste velden" +msgstr "aangepaste velden" #: documents/models.py:863 msgid "custom field instance" diff --git a/src/locale/zh_TW/LC_MESSAGES/django.po b/src/locale/zh_TW/LC_MESSAGES/django.po index efd252857..22095c04e 100644 --- a/src/locale/zh_TW/LC_MESSAGES/django.po +++ b/src/locale/zh_TW/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-03-26 21:04-0700\n" -"PO-Revision-Date: 2025-03-31 12:13\n" +"PO-Revision-Date: 2025-04-09 21:44\n" "Last-Translator: \n" "Language-Team: Chinese Traditional\n" "Language: zh_TW\n" diff --git a/src/paperless/settings.py b/src/paperless/settings.py index b161d7016..361854a93 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -565,6 +565,10 @@ if DEBUG: # Allow access from the angular development server during debugging CORS_ALLOWED_ORIGINS.append("http://localhost:4200") +CORS_EXPOSE_HEADERS = [ + "Content-Disposition", +] + ALLOWED_HOSTS = __get_list("PAPERLESS_ALLOWED_HOSTS", ["*"]) if ALLOWED_HOSTS != ["*"]: # always allow localhost. Necessary e.g. for healthcheck in docker. diff --git a/uv.lock b/uv.lock index 7b53f0816..9ba02e32b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" resolution-markers = [ "sys_platform == 'darwin'", @@ -197,6 +198,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, ] +[[package]] +name = "cached-property" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", size = 10574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428 }, +] + [[package]] name = "celery" version = "5.4.0" @@ -1907,7 +1917,7 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "watchdog", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "whoosh", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zxing-cpp", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, { name = "zxing-cpp", version = "2.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, { name = "zxing-cpp", version = "2.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -2043,11 +2053,12 @@ requires-dist = [ { name = "tqdm", specifier = "~=4.67.1" }, { name = "watchdog", specifier = "~=6.0" }, { name = "whitenoise", specifier = "~=6.9" }, - { name = "whoosh", specifier = "~=2.7" }, + { name = "whoosh-reloaded", specifier = ">=2.7.5" }, { name = "zxing-cpp", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64') or (python_full_version != '3.12.*' and platform_machine == 'x86_64') or (platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", specifier = "~=2.3.0" }, { name = "zxing-cpp", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl" }, { name = "zxing-cpp", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl" }, ] +provides-extras = ["mariadb", "postgres", "webserver"] [package.metadata.requires-dev] dev = [ @@ -3708,12 +3719,15 @@ wheels = [ ] [[package]] -name = "whoosh" -version = "2.7.4" +name = "whoosh-reloaded" +version = "2.7.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83", size = 968741 } +dependencies = [ + { name = "cached-property", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/51/3fb4b9fdeaaf96512514ccf2871186333ce41a0de2ea48236a4056a5f6af/Whoosh-Reloaded-2.7.5.tar.gz", hash = "sha256:39ed7dfbd1fec97af33933107bdf78110728375ed0f2abb25dec6dbfdcb279d8", size = 1061606 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/19/24d0f1f454a2c1eb689ca28d2f178db81e5024f42d82729a4ff6771155cf/Whoosh-2.7.4-py2.py3-none-any.whl", hash = "sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852", size = 468790 }, + { url = "https://files.pythonhosted.org/packages/69/90/866dfe421f188217ecd7339585e961034a7f4fdc96b62cec3b40a50dbdef/Whoosh_Reloaded-2.7.5-py2.py3-none-any.whl", hash = "sha256:2ab6aeeafb359fbff4beb3c704b960fd88240354f3363f1c5bdb5c2325cae80e", size = 551793 }, ] [[package]]