Compare commits

..

1 Commits

Author SHA1 Message Date
Trenton H
36ead3d08e Minor hand-selected updates to dependencies 2025-07-31 14:12:53 -07:00
28 changed files with 3003 additions and 4154 deletions

View File

@@ -159,23 +159,6 @@ Available options are `postgresql` and `mariadb`.
Defaults to unset, which uses Djangos built-in defaults.
#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
: Defines the maximum number of database connections to keep in the pool.
Only applies to PostgreSQL. This setting is ignored for other database engines.
The value must be greater than or equal to 1 to be used.
Defaults to unset, which disables connection pooling.
!!! note
A small pool is typically sufficient — for example, a size of 4.
Make sure your PostgreSQL server's max_connections setting is large enough to handle:
```(Paperless workers + Celery workers) × pool size + safety margin```
For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
(4 + 2) × 4 + 10 = 34 connections required.
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.

View File

@@ -30,9 +30,6 @@ Each document has data fields that you can assign to them:
- A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about.
- The document _storage path_ is the location where the document files
are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
more information.
- The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was

View File

@@ -52,7 +52,6 @@ dependencies = [
"ocrmypdf~=16.10.0",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
"psycopg-pool",
"python-dateutil~=2.9.0",
"python-dotenv~=1.1.0",
"python-gnupg~=0.5.4",
@@ -63,7 +62,7 @@ dependencies = [
"redis[hiredis]~=5.2.1",
"scikit-learn~=1.7.0",
"setproctitle~=1.3.4",
"tika-client~=0.10.0",
"tika-client~=0.9.0",
"tqdm~=4.67.1",
"watchdog~=6.0",
"whitenoise~=6.9",
@@ -75,13 +74,12 @@ optional-dependencies.mariadb = [
"mysqlclient~=2.2.7",
]
optional-dependencies.postgres = [
"psycopg[c,pool]==3.2.9",
"psycopg[c]==3.2.9",
# Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.9",
"psycopg-pool==3.2.6",
]
optional-dependencies.webserver = [
"granian[uvloop]~=2.5.0",
"granian[uvloop]~=2.4.1",
]
[dependency-groups]
@@ -241,7 +239,6 @@ testpaths = [
"src/paperless_mail/tests/",
"src/paperless_tesseract/tests/",
"src/paperless_tika/tests",
"src/paperless_text/tests/",
]
addopts = [
"--pythonwarnings=all",

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,17 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^20.1.4",
"@angular/common": "~20.1.4",
"@angular/compiler": "~20.1.4",
"@angular/core": "~20.1.4",
"@angular/forms": "~20.1.4",
"@angular/localize": "~20.1.4",
"@angular/platform-browser": "~20.1.4",
"@angular/platform-browser-dynamic": "~20.1.4",
"@angular/router": "~20.1.4",
"@angular/cdk": "^20.0.4",
"@angular/common": "~20.0.6",
"@angular/compiler": "~20.0.6",
"@angular/core": "~20.0.6",
"@angular/forms": "~20.0.6",
"@angular/localize": "~20.0.6",
"@angular/platform-browser": "~20.0.6",
"@angular/platform-browser-dynamic": "~20.0.6",
"@angular/router": "~20.0.6",
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@ng-select/ng-select": "^20.0.1",
"@ng-select/ng-select": "^15.1.3",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7",
@@ -32,7 +32,7 @@
"ngx-color": "^10.0.0",
"ngx-cookie-service": "^20.0.1",
"ngx-device-detector": "^10.0.2",
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
"ngx-ui-tour-ng-bootstrap": "^17.0.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
@@ -42,33 +42,33 @@
"devDependencies": {
"@angular-builders/custom-webpack": "^20.0.0",
"@angular-builders/jest": "^20.0.0",
"@angular-devkit/core": "^20.1.4",
"@angular-devkit/schematics": "^20.1.4",
"@angular-devkit/core": "^20.0.4",
"@angular-devkit/schematics": "^20.0.4",
"@angular-eslint/builder": "20.1.1",
"@angular-eslint/eslint-plugin": "20.1.1",
"@angular-eslint/eslint-plugin-template": "20.1.1",
"@angular-eslint/schematics": "20.1.1",
"@angular-eslint/template-parser": "20.1.1",
"@angular/build": "^20.1.4",
"@angular/cli": "~20.1.4",
"@angular/compiler-cli": "~20.1.4",
"@angular/build": "^20.0.4",
"@angular/cli": "~20.0.4",
"@angular/compiler-cli": "~20.0.6",
"@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.54.2",
"@types/jest": "^30.0.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@typescript-eslint/utils": "^8.38.0",
"eslint": "^9.32.0",
"jest": "30.0.5",
"jest-environment-jsdom": "^30.0.5",
"@playwright/test": "^1.53.2",
"@types/jest": "^29.5.14",
"@types/node": "^24.0.10",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"@typescript-eslint/utils": "^8.35.1",
"eslint": "^9.30.1",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^15.0.0",
"jest-preset-angular": "^14.5.5",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.2.0",
"prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1",
"typescript": "^5.8.3",
"webpack": "^5.101.0"
"webpack": "^5.99.9"
},
"pnpm": {
"onlyBuiltDependencies": [

5220
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,12 @@
import '@angular/localize/init'
import { jest } from '@jest/globals'
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
import { TextDecoder, TextEncoder } from 'node:util'
import { TextDecoder, TextEncoder } from 'util'
if (process.env.NODE_ENV === 'test') {
setupZoneTestEnv()
}
;(globalThis as any).TextEncoder = TextEncoder as unknown as {
new (): TextEncoder
}
;(globalThis as any).TextDecoder = TextDecoder as unknown as {
new (): TextDecoder
}
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
import { registerLocaleData } from '@angular/common'
import localeAf from '@angular/common/locales/af'
@@ -120,6 +116,10 @@ if (!URL.revokeObjectURL) {
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
}
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
})
HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext

View File

@@ -50,7 +50,7 @@
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2">
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
<button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>

View File

@@ -358,6 +358,6 @@
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
</form>

View File

@@ -36,7 +36,6 @@ import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
@@ -226,9 +225,6 @@ describe('SettingsComponent', () => {
})
it('should offer reload if settings changes require', () => {
const reloadSpy = jest
.spyOn(navUtils, 'locationReload')
.mockImplementation(() => {})
completeSetup()
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
@@ -245,7 +241,6 @@ describe('SettingsComponent', () => {
expect(toast.actionName).toEqual('Reload now')
toast.action()
expect(reloadSpy).toHaveBeenCalled()
})
it('should allow setting theme color, visually apply change immediately but not save', () => {
@@ -274,7 +269,7 @@ describe('SettingsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
@@ -286,7 +281,7 @@ describe('SettingsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should load system status on initialize, show errors if needed', () => {

View File

@@ -57,7 +57,6 @@ import {
} from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { locationReload } from 'src/app/utils/navigation'
import { CheckComponent } from '../../common/input/check/check.component'
import { ColorComponent } from '../../common/input/color/color.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
@@ -551,7 +550,7 @@ export class SettingsComponent
savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
savedToast.actionName = $localize`Reload now`
savedToast.action = () => {
locationReload()
location.reload()
}
}

View File

@@ -19,7 +19,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
@@ -108,7 +107,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
@@ -131,7 +130,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting user'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -143,18 +142,19 @@ describe('UsersAndGroupsComponent', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0])
const navSpy = jest
.spyOn(navUtils, 'setLocationHref')
.mockImplementation(() => {})
const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
tick(2600)
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
expect(window.location.href).toContain('logout')
}))
it('should support edit / create group, show error if needed', () => {
@@ -166,7 +166,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved group "${groups[0].name}".`
@@ -188,7 +188,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -210,7 +210,7 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
@@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
})
})

View File

@@ -10,7 +10,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { setLocationHref } from 'src/app/utils/navigation'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
@@ -94,9 +93,7 @@ export class UsersAndGroupsComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
setLocationHref(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
} else {
this.toastService.showInfo(

View File

@@ -30,7 +30,7 @@
}
<div class="list-group-item">
<div class="input-group input-group-sm">
<input class="form-control" type="text" spellcheck="false" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
@if (selectionModel.items) {

View File

@@ -18,7 +18,6 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation'
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
import { PasswordComponent } from '../input/password/password.component'
import { TextComponent } from '../input/text/text.component'
@@ -206,15 +205,16 @@ describe('ProfileEditDialogComponent', () => {
const updateSpy = jest.spyOn(profileService, 'update')
updateSpy.mockReturnValue(of(null))
const navSpy = jest
.spyOn(navUtils, 'setLocationHref')
.mockImplementation(() => {})
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
component.save()
expect(updateSpy).toHaveBeenCalled()
tick(2600)
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
expect(window.location.href).toContain('logout')
}))
it('should support auth token copy', fakeAsync(() => {

View File

@@ -21,7 +21,6 @@ import {
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { setLocationHref } from 'src/app/utils/navigation'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
import { PasswordComponent } from '../input/password/password.component'
@@ -195,9 +194,7 @@ export class ProfileEditDialogComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
setLocationHref(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
}
this.activeModal.close()

View File

@@ -188,7 +188,7 @@ describe('MailComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailAccounts[0] as any)
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved account "${mailAccounts[0].name}".`
@@ -211,7 +211,7 @@ describe('MailComponent', () => {
throwError(() => new Error('error deleting mail account'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -246,7 +246,7 @@ describe('MailComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailRules[0] as any)
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved rule "${mailRules[0].name}".`
@@ -280,7 +280,7 @@ describe('MailComponent', () => {
throwError(() => new Error('error deleting mail rule "rule1"'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()

View File

@@ -1,5 +1,4 @@
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions">
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>

View File

@@ -164,7 +164,7 @@ describe('ManagementListComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
const createButton = fixture.debugElement.queryAll(By.css('button'))[4]
const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@@ -188,7 +188,7 @@ describe('ManagementListComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
const editButton = fixture.debugElement.queryAll(By.css('button'))[6]
editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@@ -213,7 +213,7 @@ describe('ManagementListComponent', () => {
const deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@@ -233,7 +233,7 @@ describe('ManagementListComponent', () => {
it('should support quick filter for objects', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
const filterButton = fixture.debugElement.queryAll(By.css('button'))[8]
filterButton.triggerEventHandler('click')
expect(qfSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },

View File

@@ -70,6 +70,6 @@
}
</ul>
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
</form>

View File

@@ -1,8 +0,0 @@
/* istanbul ignore file */
export function setLocationHref(url: string) {
window.location.href = url
}
export function locationReload() {
window.location.reload()
}

View File

@@ -3,8 +3,7 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jest",
"node",
"jest"
],
"module": "commonjs",
"emitDecoratorMetadata": true,

View File

@@ -12,13 +12,11 @@ from celery.signals import before_task_publish
from celery.signals import task_failure
from celery.signals import task_postrun
from celery.signals import task_prerun
from celery.signals import worker_process_init
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db import DatabaseError
from django.db import close_old_connections
from django.db import connections
from django.db import models
from django.db.models import Q
from django.dispatch import receiver
@@ -1441,18 +1439,3 @@ def task_failure_handler(
task_instance.save()
except Exception: # pragma: no cover
logger.exception("Updating PaperlessTask failed")
@worker_process_init.connect
def close_connection_pool_on_worker_init(**kwargs):
"""
Close the DB connection pool for each Celery child process after it starts.
This is necessary because the parent process parse the Django configuration,
initializes connection pools then forks.
Closing these pools after forking ensures child processes have a valid connection.
"""
for conn in connections.all(initialized_only=True):
if conn.alias == "default" and hasattr(conn, "pool") and conn.pool:
conn.close_pool()

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-02 12:55+0000\n"
"POT-Creation-Date: 2025-07-08 21:14+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1645,147 +1645,147 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
#: paperless/settings.py:774
#: paperless/settings.py:762
msgid "English (US)"
msgstr ""
#: paperless/settings.py:775
#: paperless/settings.py:763
msgid "Arabic"
msgstr ""
#: paperless/settings.py:776
#: paperless/settings.py:764
msgid "Afrikaans"
msgstr ""
#: paperless/settings.py:777
#: paperless/settings.py:765
msgid "Belarusian"
msgstr ""
#: paperless/settings.py:778
#: paperless/settings.py:766
msgid "Bulgarian"
msgstr ""
#: paperless/settings.py:779
#: paperless/settings.py:767
msgid "Catalan"
msgstr ""
#: paperless/settings.py:780
#: paperless/settings.py:768
msgid "Czech"
msgstr ""
#: paperless/settings.py:781
#: paperless/settings.py:769
msgid "Danish"
msgstr ""
#: paperless/settings.py:782
#: paperless/settings.py:770
msgid "German"
msgstr ""
#: paperless/settings.py:783
#: paperless/settings.py:771
msgid "Greek"
msgstr ""
#: paperless/settings.py:784
#: paperless/settings.py:772
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:785
#: paperless/settings.py:773
msgid "Spanish"
msgstr ""
#: paperless/settings.py:786
#: paperless/settings.py:774
msgid "Persian"
msgstr ""
#: paperless/settings.py:787
#: paperless/settings.py:775
msgid "Finnish"
msgstr ""
#: paperless/settings.py:788
#: paperless/settings.py:776
msgid "French"
msgstr ""
#: paperless/settings.py:789
#: paperless/settings.py:777
msgid "Hungarian"
msgstr ""
#: paperless/settings.py:790
#: paperless/settings.py:778
msgid "Italian"
msgstr ""
#: paperless/settings.py:791
#: paperless/settings.py:779
msgid "Japanese"
msgstr ""
#: paperless/settings.py:792
#: paperless/settings.py:780
msgid "Korean"
msgstr ""
#: paperless/settings.py:793
#: paperless/settings.py:781
msgid "Luxembourgish"
msgstr ""
#: paperless/settings.py:794
#: paperless/settings.py:782
msgid "Norwegian"
msgstr ""
#: paperless/settings.py:795
#: paperless/settings.py:783
msgid "Dutch"
msgstr ""
#: paperless/settings.py:796
#: paperless/settings.py:784
msgid "Polish"
msgstr ""
#: paperless/settings.py:797
#: paperless/settings.py:785
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:798
#: paperless/settings.py:786
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:799
#: paperless/settings.py:787
msgid "Romanian"
msgstr ""
#: paperless/settings.py:800
#: paperless/settings.py:788
msgid "Russian"
msgstr ""
#: paperless/settings.py:801
#: paperless/settings.py:789
msgid "Slovak"
msgstr ""
#: paperless/settings.py:802
#: paperless/settings.py:790
msgid "Slovenian"
msgstr ""
#: paperless/settings.py:803
#: paperless/settings.py:791
msgid "Serbian"
msgstr ""
#: paperless/settings.py:804
#: paperless/settings.py:792
msgid "Swedish"
msgstr ""
#: paperless/settings.py:805
#: paperless/settings.py:793
msgid "Turkish"
msgstr ""
#: paperless/settings.py:806
#: paperless/settings.py:794
msgid "Ukrainian"
msgstr ""
#: paperless/settings.py:807
#: paperless/settings.py:795
msgid "Vietnamese"
msgstr ""
#: paperless/settings.py:808
#: paperless/settings.py:796
msgid "Chinese Simplified"
msgstr ""
#: paperless/settings.py:809
#: paperless/settings.py:797
msgid "Chinese Traditional"
msgstr ""

View File

@@ -703,9 +703,6 @@ def _parse_db_settings() -> dict:
# Leave room for future extensibility
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
engine = "django.db.backends.mysql"
# Contrary to Postgres, Django does not natively support connection pooling for MariaDB.
# However, since MariaDB uses threads instead of forks, establishing connections is significantly faster
# compared to PostgreSQL, so the lack of pooling is not an issue
options = {
"read_default_file": "/etc/mysql/my.cnf",
"charset": "utf8mb4",
@@ -725,15 +722,6 @@ def _parse_db_settings() -> dict:
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
"sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
}
if int(os.getenv("PAPERLESS_DB_POOLSIZE", 0)) > 0:
options.update(
{
"pool": {
"min_size": 1,
"max_size": int(os.getenv("PAPERLESS_DB_POOLSIZE")),
},
},
)
databases["default"]["ENGINE"] = engine
databases["default"]["OPTIONS"].update(options)

View File

@@ -16,15 +16,7 @@ class TextDocumentParser(DocumentParser):
logging_name = "paperless.parsing.text"
def get_thumbnail(self, document_path: Path, mime_type, file_name=None) -> Path:
# Avoid reading entire file into memory
max_chars = 100_000
file_size_limit = 50 * 1024 * 1024
if document_path.stat().st_size > file_size_limit:
text = "[File too large to preview]"
else:
with Path(document_path).open("r", encoding="utf-8", errors="replace") as f:
text = f.read(max_chars)
text = self.read_file_handle_unicode_errors(document_path)
img = Image.new("RGB", (500, 700), color="white")
draw = ImageDraw.Draw(img)
@@ -33,7 +25,7 @@ class TextDocumentParser(DocumentParser):
size=20,
layout_engine=ImageFont.Layout.BASIC,
)
draw.multiline_text((5, 5), text, font=font, fill="black", spacing=4)
draw.text((5, 5), text, font=font, fill="black")
out_path = self.tempdir / "thumb.webp"
img.save(out_path, format="WEBP")

View File

@@ -1,4 +1,3 @@
import tempfile
from pathlib import Path
from paperless_text.parsers import TextDocumentParser
@@ -36,26 +35,3 @@ class TestTextParser:
assert text_parser.get_text() == "Pantothens<EFBFBD>ure\n"
assert text_parser.get_archive_path() is None
def test_thumbnail_large_file(self, text_parser: TextDocumentParser):
"""
GIVEN:
- A very large text file (>50MB)
WHEN:
- A thumbnail is requested
THEN:
- A thumbnail is created without reading the entire file into memory
"""
with tempfile.NamedTemporaryFile(
delete=False,
mode="w",
encoding="utf-8",
suffix=".txt",
) as tmp:
tmp.write("A" * (51 * 1024 * 1024)) # 51 MB of 'A'
large_file = Path(tmp.name)
thumb = text_parser.get_thumbnail(large_file, "text/plain")
assert thumb.exists()
assert thumb.is_file()
large_file.unlink()

1291
uv.lock generated

File diff suppressed because it is too large Load Diff