mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-11 10:00:48 -05:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
f036292b72
@ -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
|
||||
|
||||
|
@ -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##*/}"
|
||||
|
7
docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh
Executable file
7
docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/migrate.sh
Executable 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
|
@ -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
|
||||
|
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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 <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.</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">全域應用程式設定選項適用於此安裝版本的<strong>每位</strong>使用者。雖然也可以透過環境變數或設定檔來設定,但這裡的設定將始終優先於其他設定。</target>
|
||||
<target state="translated">全域系統配置會套用至該系統的<strong>每一位</strong>使用者。雖然環境變數或設定檔也可以調整相關設定,但此處的設定將優先於他處的設定。</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">自訂外觀、通知等選項。設定只適用於<strong>目前使用者</strong>。</target>
|
||||
<target state="translated">自訂外觀、通知等選項。這些設定只套用於<strong>目前的使用者</strong>。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1685061484835793745" datatype="html">
|
||||
<source>Start tour</source>
|
||||
|
@ -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}"
|
||||
|
@ -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 = (
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
26
uv.lock
generated
@ -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]]
|
||||
|
Loading…
x
Reference in New Issue
Block a user