Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Trenton Holmes 2025-04-09 14:53:31 -07:00
commit f036292b72
15 changed files with 136 additions and 82 deletions

View File

@ -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

View File

@ -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##*/}"

View File

@ -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

View File

@ -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

View File

@ -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",
]

View File

@ -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()

View File

@ -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<Blob>) => {
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() {

View File

@ -556,7 +556,7 @@
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<target state="translated">應用程式設定</target>
<target state="translated">系統配置</target>
</trans-unit>
<trans-unit id="8528041182664173532" datatype="html">
<source>Global app configuration options which apply to &lt;strong&gt;every&lt;/strong&gt; 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.</source>
@ -564,7 +564,7 @@
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="translated">全域應用程式設定選項適用於此安裝版本的&lt;strong&gt;每位&lt;/strong&gt;使用者。雖然也可以透過環境變數或設定檔來設定,但這裡的設定將始終優先於其他設定。</target>
<target state="translated">全域系統配置會套用至該系統的&lt;strong&gt;每一位&lt;/strong&gt;使用者。雖然環境變數或設定檔也可以調整相關設定,但此處的設定將優先於他處的設定。</target>
</trans-unit>
<trans-unit id="7991430199894172363" datatype="html">
<source>Read the documentation about this setting</source>
@ -864,7 +864,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="translated">自訂外觀、通知等選項。設定只適用於&lt;strong&gt;目前使用者&lt;/strong&gt;。</target>
<target state="translated">自訂外觀、通知等選項。這些設定只套用於&lt;strong&gt;目前的使用者&lt;/strong&gt;。</target>
</trans-unit>
<trans-unit id="1685061484835793745" datatype="html">
<source>Start tour</source>

View File

@ -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}"

View File

@ -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 = (

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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.

26
uv.lock generated
View File

@ -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]]