diff --git a/README.md b/README.md index 6f71d52..f80bb57 100644 --- a/README.md +++ b/README.md @@ -54,34 +54,38 @@ Two images: the application, and a Redis sidecar. ## Volume and Data Layout -One volume, mounted four times at four different subpaths. +One volume, mounted four times at four different subpaths, plus an optional fifth mount from FileBrowser Quantum. -| Volume | Subpath | Mount Point | Purpose | -| ------ | --------- | ----------- | --------------------------------- | -| `main` | `data` | `…/data` | The database and the search index | -| `main` | `media` | `…/media` | The stored documents | -| `main` | `consume` | `…/consume` | The watched intake folder | -| `main` | `export` | `…/export` | Where exports are written | +| Volume | Subpath | Mount Point | Purpose | +| ---------------------- | --------- | ------------------ | --------------------------------------- | +| `main` | `data` | `…/data` | The database and the search index | +| `main` | `media` | `…/media` | The stored documents | +| `main` | `consume` | `…/consume` | The private intake folder | +| `main` | `export` | `…/export` | Where exports are written | +| `filebrowser` → `data` | — | `/mnt/filebrowser` | FileBrowser Quantum's files, read-write | -**All four mounts are required, not just the ones being used.** Django's startup checks verify every one of those paths exists and is writable, and refuse to run otherwise — which is why the same mount set is used by the password action's temporary container as by the daemon itself. +**All four `main` mounts are required, not just the ones being used.** Django's startup checks verify every one of those paths exists and is writable, and refuse to run otherwise — which is why the same mount set is used by the password action's temporary container as by the daemon itself. -| Path | Written by | Holds | -| ------------ | ----------- | ------------------------------------- | -| `db.sqlite3` | Paperless | Documents' metadata, tags, users | -| `store.json` | The package | The admin password and the secret key | +**The FileBrowser Quantum mount exists only while the consume folder points there** (see Set Consume Folder). `PAPERLESS_CONSUMPTION_DIR` is then the chosen subfolder under `/mnt/filebrowser`, and the private `consume` subpath stays mounted for Django's checks but is not watched. The mount is read-write because consumption **deletes** the source file after a successful import — inside the same database transaction, so a read-only mount would roll back every import. Paperless runs as uid 1000, the uid FileBrowser Quantum serves its volume as, so no id mapping is needed. + +| Path | Written by | Holds | +| ------------ | ----------- | ----------------------------------------------------------------- | +| `db.sqlite3` | Paperless | Documents' metadata, tags, users | +| `store.json` | The package | The admin password, the secret key, and the consume folder choice | **Documents are files, and their metadata is a database.** Both are on this volume, and neither is much use without the other. ## File Models -One model, holding two generated values. +One model, holding two generated values and one user choice. -| File | Format | Modelled | Written by | -| ------------ | ------ | ----------------------- | ------------------- | -| `store.json` | JSON | Yes — `FileHelper.json` | Init and the action | +| File | Format | Modelled | Written by | +| ------------ | ------ | ----------------------- | -------------------- | +| `store.json` | JSON | Yes — `FileHelper.json` | Init and the actions | - **The Django secret key**, generated once at install. It signs sessions and is not rotatable — changing it invalidates every session and anything else derived from it. - **The admin password**, recorded so the package knows whether one has been set. +- **The consume folder** — `consumeSource` (`local` or `filebrowser`) and `filebrowserSubfolder`. Read reactively by the daemon and by the dependency declaration, so changing them restarts the service with the right mounts and re-evaluates the dependency. Everything else Paperless needs is **passed as environment**, and two of those values are computed rather than fixed: the allowed CORS origins and the CSRF trusted origins are built from the interface's **current addresses**. StartOS terminates TLS in front of the application, so without those the browser's origin would not match what Django expects and **logins would be rejected as CSRF failures** — which presents as a wrong password rather than a proxy problem. @@ -89,7 +93,15 @@ Paperless's own settings — document types, tags, mail rules, workflows — liv ## Dependencies -None. Redis runs as a private sidecar of this service rather than as a StartOS dependency. +One, optional, and declared only while it is in use. + +| Dependency | Id | Required | Kind | Purpose | +| ------------------- | ------------- | -------- | -------- | ------------------------------------ | +| FileBrowser Quantum | `filebrowser` | No | `exists` | Hosts the consume folder, read-write | + +While the consume folder points at FileBrowser Quantum, `setupDependencies` declares it as `exists` — it only has to be installed, not running, for the volume to be there — and StartOS shows the usual dependency warning if it is missing. With the private folder selected, no dependency is declared at all. + +Redis runs as a private sidecar of this service rather than as a StartOS dependency. Nothing here needs internet: OCR runs locally, and the service only reaches out if you configure mail fetching yourself. @@ -119,7 +131,7 @@ After that, the `critical` task asks for the admin password. Once set, the servi ## Actions -One action. +Two actions. ### Set Admin Password @@ -132,6 +144,15 @@ Generates a password for the `admin` account and shows it once. - **Refuses clearly when the database is missing**, telling you to start the service and wait for it to become healthy, instead of failing with a Django traceback. - **Repeat safety:** each run generates a **new** password and invalidates the old one. It is never user-chosen. +### Set Consume Folder + +Chooses where Paperless watches for new documents: the private `consume` subpath (the default — reachable only by Paperless itself, so in practice "web upload only"), or a subfolder of FileBrowser Quantum's data volume (default `paperless`). + +- **What it changes:** `consumeSource` and `filebrowserSubfolder` in the store. +- **Cost:** a restart. The daemon reads both values reactively, mounts FileBrowser Quantum's volume when selected, and points `PAPERLESS_CONSUMPTION_DIR` at the subfolder; Paperless's own entrypoint creates the subfolder if it is missing. +- **Runnable at any status.** Selecting FileBrowser Quantum before it is installed still starts the service: StartOS mounts an empty placeholder where the volume would be, so Paperless watches a folder nothing can reach, and the dependency warning is the only sign. Install FileBrowser Quantum, then restart Paperless. +- **Repeat safety:** switching back to the private folder keeps the last subfolder, so it is pre-filled if FileBrowser Quantum is selected again. Files left in either folder are not moved. + ## Tasks One, and it is reactive. @@ -157,6 +178,8 @@ The application waits for the broker, so a failing broker shows as the applicati **Neither check says anything about document processing.** A stuck OCR job, an unreadable scan, or a consume folder nobody is writing to all show two green checks; those are visible in the interface's own task list. +**Nor do they cover the FileBrowser Quantum mount.** A file dropped there and never imported is a consumer problem — Paperless's log in the interface is where it surfaces — not a health-check failure. + ## Backups and Restore The `main` volume is copied wholesale — `sdk.Backups.ofVolumes('main')`. That is all four subpaths: the database, the stored documents, whatever is sitting in the intake folder, and the exports. @@ -167,6 +190,8 @@ The `main` volume is copied wholesale — `sdk.Backups.ofVolumes('main')`. That Note that the intake and export folders are backed up along with everything else, so a backup taken mid-import is larger than the library alone. +**A consume folder in FileBrowser Quantum is FileBrowser Quantum's data**, backed up by that package, not this one. The store records the choice, so a restore on a server without FileBrowser Quantum starts but imports nothing until it is installed and Paperless restarted. + ## Limitations and Differences 1. **SQLite only.** There is no option to point Paperless at PostgreSQL, and no migration path from one. @@ -176,6 +201,7 @@ Note that the intake and export folders are backed up along with everything else 5. **The task broker is private.** It cannot be shared, substituted, or reached from outside the service. 6. **The timezone is fixed to UTC** and OCR is configured for English; other languages are set in Paperless's own settings. 7. **Backups include the intake and export folders**, not just the library. +8. **The consume folder can be shared only through FileBrowser Quantum**, and only one folder is watched, non-recursively. Paperless's own document store (`media`) is not exposed to other services. --- @@ -194,10 +220,12 @@ volumes: main: # mounted four times by subpath data: /usr/src/paperless/data # db.sqlite3, search index, store.json at the volume root media: /usr/src/paperless/media - consume: /usr/src/paperless/consume + consume: /usr/src/paperless/consume # the private intake folder; idle while FileBrowser Quantum is the consume source export: /usr/src/paperless/export +dependency_mounts: + filebrowser/data: /mnt/filebrowser # read-write, only while consumeSource is filebrowser file_models: - - store.json # adminPassword and the generated Django secretKey + - store.json # adminPassword, the generated Django secretKey, consumeSource, filebrowserSubfolder startos_managed_env_vars: - PAPERLESS_REDIS - PAPERLESS_PORT @@ -207,13 +235,16 @@ startos_managed_env_vars: - PAPERLESS_CSRF_TRUSTED_ORIGINS # same — omit and logins 403 on CSRF - PAPERLESS_TIME_ZONE - PAPERLESS_OCR_LANGUAGE + - PAPERLESS_CONSUMPTION_DIR # /usr/src/paperless/consume, or /mnt/filebrowser/ - USERMAP_UID - USERMAP_GID -dependencies: [] +dependencies: + - { id: filebrowser, optional: true, kind: exists } # declared only while it is the consume source interfaces: ui: { type: ui, port: 8000 } # Paperless's own login; no gate added by StartOS actions: - set-admin-password # temp container, writes to the DB directly, no restart + - set-consume-folder # writes the store; the daemon re-mounts and restarts tasks: - { action: set-admin-password, severity: critical } # reactive health_checks: diff --git a/instructions.md b/instructions.md index 4865d38..788f9c7 100644 --- a/instructions.md +++ b/instructions.md @@ -13,10 +13,11 @@ Forgot your password, or want a new one? Run **Set Admin Password** again at any ## Adding documents - **Web upload**: use the drag-and-drop area in the Paperless-ngx UI. +- **Consume folder in FileBrowser Quantum**: run the **Set Consume Folder** action, choose **FileBrowser Quantum**, and pick a subfolder (the default is `paperless`). Paperless-ngx watches that folder, imports anything you drop into it, and then deletes the file. FileBrowser Quantum must be installed (if you install it afterwards, restart Paperless-ngx); the subfolder is created for you. If you also have Nextcloud with FileBrowser Quantum mounted as external storage, dropping a file into that folder from Nextcloud works the same way. - **Email**: configure a mail account under **Settings → Mail** in the Paperless-ngx UI and it will fetch and consume attachments automatically — handy for scanners that scan-to-email. - **Mobile apps and API**: any Paperless-ngx-compatible app can upload via the API using your Web UI address and an API token from your user profile. -> **Note**: The watched _consume folder_ lives on a volume that is not reachable from other StartOS services or your other devices today, so use one of the methods above instead. +Changing the consume folder restarts Paperless-ngx. ## Documentation diff --git a/package-lock.json b/package-lock.json index fc75402..ad2dc51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "name": "paperless-startos", "dependencies": { - "@start9labs/start-sdk": "2.0.9" + "@start9labs/start-sdk": "2.0.9", + "filebrowser-startos": "github:Start9Labs/filebrowser-startos#next" }, "devDependencies": { "@types/node": "^22.19.0", @@ -334,6 +335,163 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core": { + "inBundle": true, + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "deep-equality-data-structures": "^2.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.1.0", + "yaml": "^2.8.3", + "zod": "4.4.3", + "zod-deep-partial": "^1.4.4" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/@iarna/toml": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/@noble/curves": { + "version": "1.9.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/@noble/hashes": { + "version": "1.8.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/mime": { + "version": "4.1.0", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "inBundle": true, + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/node-fetch": { + "version": "2.7.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/object-hash": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/tr46": { + "version": "0.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/webidl-conversions": { + "version": "3.0.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/whatwg-fetch": { + "version": "3.6.20", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/whatwg-url": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/yaml": { + "version": "2.9.0", + "inBundle": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/zod": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@start9labs/start-core/node_modules/zod-deep-partial": { + "version": "1.4.4", + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + }, "node_modules/@start9labs/start-sdk/node_modules/@types/estree": { "version": "1.0.9", "inBundle": true, @@ -1461,6 +1619,12 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/filebrowser-startos": { + "resolved": "git+ssh://git@github.com/Start9Labs/filebrowser-startos.git#77539bc3940cc99ff9f5c1f52024f32474f357c4", + "dependencies": { + "@start9labs/start-sdk": "2.0.9" + } + }, "node_modules/ini": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", diff --git a/package.json b/package.json index 24640ce..bf6cd66 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "check": "tsc --noEmit" }, "dependencies": { - "@start9labs/start-sdk": "2.0.9" + "@start9labs/start-sdk": "2.0.9", + "filebrowser-startos": "github:Start9Labs/filebrowser-startos#next" }, "devDependencies": { "@types/node": "^22.19.0", diff --git a/startos/actions/index.ts b/startos/actions/index.ts index 9055a34..f7dc91c 100644 --- a/startos/actions/index.ts +++ b/startos/actions/index.ts @@ -1,4 +1,7 @@ import { sdk } from '../sdk' import { setAdminPassword } from './setAdminPassword' +import { setConsumeFolder } from './setConsumeFolder' -export const actions = sdk.Actions.of().addAction(setAdminPassword) +export const actions = sdk.Actions.of() + .addAction(setAdminPassword) + .addAction(setConsumeFolder) diff --git a/startos/actions/setConsumeFolder.ts b/startos/actions/setConsumeFolder.ts new file mode 100644 index 0000000..0530d4a --- /dev/null +++ b/startos/actions/setConsumeFolder.ts @@ -0,0 +1,81 @@ +import { storeJson } from '../fileModels/store.json' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { defaultConsumeSubfolder } from '../utils' + +const { InputSpec, Value, Variants } = sdk + +export const inputSpec = InputSpec.of({ + source: Value.union({ + name: i18n('Consume Folder'), + description: i18n( + 'Where Paperless-ngx watches for new documents. Anything placed there is imported and then deleted.', + ), + default: 'local', + variants: Variants.of({ + local: { + name: i18n('Private (web upload only)'), + spec: InputSpec.of({}), + }, + filebrowser: { + name: i18n('FileBrowser Quantum'), + spec: InputSpec.of({ + subfolder: Value.text({ + name: i18n('FileBrowser Quantum Subfolder'), + description: i18n( + 'Folder inside FileBrowser Quantum that Paperless-ngx watches. Created automatically; FileBrowser Quantum must be installed.', + ), + default: defaultConsumeSubfolder, + required: true, + placeholder: defaultConsumeSubfolder, + }), + }), + }, + }), + }), +}) + +export const setConsumeFolder = sdk.Action.withInput( + 'set-consume-folder', + + async () => ({ + name: i18n('Set Consume Folder'), + description: i18n( + 'Choose where Paperless-ngx watches for new documents: a private folder, or a folder in FileBrowser Quantum you can drop files into.', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + inputSpec, + + async ({ effects }) => { + const subfolder = + (await storeJson.read((s) => s.filebrowserSubfolder).const(effects)) ?? + defaultConsumeSubfolder + return { + source: + (await storeJson.read((s) => s.consumeSource).const(effects)) === + 'filebrowser' + ? { selection: 'filebrowser' as const, value: { subfolder } } + : { + selection: 'local' as const, + value: {}, + other: { filebrowser: { subfolder } }, + }, + } + }, + + async ({ effects, input }) => + storeJson.merge( + effects, + input.source.selection === 'filebrowser' + ? { + consumeSource: 'filebrowser', + filebrowserSubfolder: input.source.value.subfolder, + } + : { consumeSource: 'local' }, + ), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts index 7221c4b..8584b27 100644 --- a/startos/dependencies.ts +++ b/startos/dependencies.ts @@ -1,5 +1,9 @@ +import { storeJson } from './fileModels/store.json' import { sdk } from './sdk' -export const setDependencies = sdk.setupDependencies( - async ({ effects }) => ({}), +export const setDependencies = sdk.setupDependencies(async ({ effects }) => + (await storeJson.read((s) => s.consumeSource).const(effects)) === + 'filebrowser' + ? { filebrowser: { kind: 'exists', versionRange: '>=2.63.18:3' } } + : {}, ) diff --git a/startos/fileModels/store.json.ts b/startos/fileModels/store.json.ts index ded92c2..001f44f 100644 --- a/startos/fileModels/store.json.ts +++ b/startos/fileModels/store.json.ts @@ -1,11 +1,14 @@ import { FileHelper, z } from '@start9labs/start-sdk' import { sdk } from '../sdk' +import { defaultConsumeSubfolder } from '../utils' const shape = z.object({ adminPassword: z.string().catch(''), // Generated once at install and not rotatable: changing it invalidates every // session and anything else Django derived from it. secretKey: z.string().catch(''), + consumeSource: z.enum(['local', 'filebrowser']).catch('local'), + filebrowserSubfolder: z.string().catch(defaultConsumeSubfolder), }) export const storeJson = FileHelper.json( diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 8dea7b8..16da8b7 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -27,6 +27,16 @@ const dict = { // init/bootstrapDatabase.ts 'Initializing Paperless-ngx database': 16, + + // actions/setConsumeFolder.ts + 'Consume Folder': 17, + 'Where Paperless-ngx watches for new documents. Anything placed there is imported and then deleted.': 18, + 'Private (web upload only)': 19, + 'FileBrowser Quantum': 20, + 'FileBrowser Quantum Subfolder': 21, + 'Folder inside FileBrowser Quantum that Paperless-ngx watches. Created automatically; FileBrowser Quantum must be installed.': 22, + 'Set Consume Folder': 23, + 'Choose where Paperless-ngx watches for new documents: a private folder, or a folder in FileBrowser Quantum you can drop files into.': 24, } as const /** diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts index 405bef3..7ffc761 100644 --- a/startos/i18n/dictionaries/translations.ts +++ b/startos/i18n/dictionaries/translations.ts @@ -19,6 +19,14 @@ export default { 14: 'Contraseña', 15: 'Establece la contraseña de administrador antes de iniciar sesión en Paperless-ngx', 16: 'Inicializando la base de datos de Paperless-ngx', + 17: 'Carpeta de consumo', + 18: 'Dónde Paperless-ngx vigila la llegada de nuevos documentos. Todo lo que se coloque allí se importa y después se elimina.', + 19: 'Privada (solo subida web)', + 20: 'FileBrowser Quantum', + 21: 'Subcarpeta de FileBrowser Quantum', + 22: 'Carpeta dentro de FileBrowser Quantum que Paperless-ngx vigila. Se crea automáticamente; FileBrowser Quantum debe estar instalado.', + 23: 'Establecer carpeta de consumo', + 24: 'Elige dónde Paperless-ngx vigila la llegada de nuevos documentos: una carpeta privada o una carpeta de FileBrowser Quantum en la que puedes dejar archivos.', }, de_DE: { 0: 'Aufgaben-Broker', @@ -38,6 +46,14 @@ export default { 14: 'Passwort', 15: 'Lege das Admin-Passwort fest, bevor du dich bei Paperless-ngx anmeldest', 16: 'Paperless-ngx-Datenbank wird initialisiert', + 17: 'Konsum-Ordner', + 18: 'Wo Paperless-ngx auf neue Dokumente wartet. Alles, was dort abgelegt wird, wird importiert und anschließend gelöscht.', + 19: 'Privat (nur Web-Upload)', + 20: 'FileBrowser Quantum', + 21: 'FileBrowser-Quantum-Unterordner', + 22: 'Ordner in FileBrowser Quantum, den Paperless-ngx überwacht. Wird automatisch erstellt; FileBrowser Quantum muss installiert sein.', + 23: 'Konsum-Ordner festlegen', + 24: 'Wähle, wo Paperless-ngx auf neue Dokumente wartet: in einem privaten Ordner oder in einem Ordner in FileBrowser Quantum, in dem du Dateien ablegen kannst.', }, pl_PL: { 0: 'Broker zadań', @@ -57,6 +73,14 @@ export default { 14: 'Hasło', 15: 'Ustaw hasło administratora przed zalogowaniem się do Paperless-ngx', 16: 'Inicjowanie bazy danych Paperless-ngx', + 17: 'Folder konsumpcji', + 18: 'Gdzie Paperless-ngx wypatruje nowych dokumentów. Wszystko, co tam trafi, zostaje zaimportowane, a następnie usunięte.', + 19: 'Prywatny (tylko przesyłanie przez WWW)', + 20: 'FileBrowser Quantum', + 21: 'Podfolder FileBrowser Quantum', + 22: 'Folder w FileBrowser Quantum obserwowany przez Paperless-ngx. Tworzony automatycznie; FileBrowser Quantum musi być zainstalowany.', + 23: 'Ustaw folder konsumpcji', + 24: 'Wybierz, gdzie Paperless-ngx ma wypatrywać nowych dokumentów: w prywatnym folderze lub w folderze FileBrowser Quantum, do którego możesz wrzucać pliki.', }, fr_FR: { 0: 'File de tâches', @@ -76,5 +100,13 @@ export default { 14: 'Mot de passe', 15: 'Définissez le mot de passe admin avant de vous connecter à Paperless-ngx', 16: 'Initialisation de la base de données de Paperless-ngx', + 17: 'Dossier de consommation', + 18: "Où Paperless-ngx surveille l'arrivée de nouveaux documents. Tout ce qui y est déposé est importé puis supprimé.", + 19: 'Privé (téléversement web uniquement)', + 20: 'FileBrowser Quantum', + 21: 'Sous-dossier FileBrowser Quantum', + 22: 'Dossier dans FileBrowser Quantum surveillé par Paperless-ngx. Créé automatiquement ; FileBrowser Quantum doit être installé.', + 23: 'Définir le dossier de consommation', + 24: "Choisissez où Paperless-ngx surveille l'arrivée de nouveaux documents : un dossier privé, ou un dossier de FileBrowser Quantum dans lequel vous pouvez déposer des fichiers.", }, } satisfies Record diff --git a/startos/init/bootstrapDatabase.ts b/startos/init/bootstrapDatabase.ts index b8fc80f..6572422 100644 --- a/startos/init/bootstrapDatabase.ts +++ b/startos/init/bootstrapDatabase.ts @@ -1,5 +1,7 @@ +import { storeJson } from '../fileModels/store.json' import { i18n } from '../i18n' -import { paperlessDaemons } from '../main' +import { uiHostId } from '../interfaces' +import { paperlessDaemons, uiUrls } from '../main' import { sdk } from '../sdk' export const bootstrapDatabase = sdk.setupOnInit( @@ -15,8 +17,18 @@ export const bootstrapDatabase = sdk.setupOnInit( // on first boot can take a while on slower hardware. const phase = progress.addPhase(i18n('Initializing Paperless-ngx database')) phase.start() - const daemons = await paperlessDaemons(effects) - await daemons.runUntilSuccess(300_000) + // .once(): a .const() here would re-run the bootstrap against the live service. + const secretKey = await storeJson.read((s) => s.secretKey).once() + if (!secretKey) { + throw new Error('store.json is missing the generated secret key') + } + await paperlessDaemons(effects, { + secretKey, + trustedOrigins: ( + await sdk.host.getOwn(effects, uiHostId, uiUrls).once() + ).join(','), + filebrowserSubfolder: null, + }).runUntilSuccess(300_000) phase.complete() }, ) diff --git a/startos/main.ts b/startos/main.ts index 294e5c5..d138846 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -1,34 +1,43 @@ -import { T } from '@start9labs/start-sdk' +import { T, utils } from '@start9labs/start-sdk' +import { manifest as filebrowserManifest } from 'filebrowser-startos/startos/manifest' import { storeJson } from './fileModels/store.json' import { i18n } from './i18n' import { uiHostId, uiInterfaceId } from './interfaces' import { sdk } from './sdk' -import { paperlessMounts, redisPort, uiPort } from './utils' +import { + consumeMountpoint, + filebrowserMountpoint, + paperlessMounts, + redisPort, + uiPort, +} from './utils' + +export const uiUrls = (host: utils.FilledHost | null) => { + const iface = + host && + Object.values(host.bindings) + .flatMap((b) => Object.values(b.interfaces)) + .find((i) => i.id === uiInterfaceId) + return iface ? iface.addressInfo.format('urlstring') : [] +} // The redis + paperless daemon chain. setupMain returns it to run the service; // bootstrapDatabase (on install) calls .runUntilSuccess() on the same chain so // Paperless migrates and creates its database before the first real start — // which lets the critical Set Admin Password task succeed against an existing DB // instead of erroring on a never-started install. -export async function paperlessDaemons(effects: T.Effects) { - const secretKey = await storeJson.read((s) => s.secretKey).const(effects) - if (!secretKey) { - throw new Error('store.json is missing the generated secret key') - } - - const trustedOrigins = ( - await sdk.host - .getOwn(effects, uiHostId, (host) => { - const iface = - host && - Object.values(host.bindings) - .flatMap((b) => Object.values(b.interfaces)) - .find((i) => i.id === uiInterfaceId) - return iface ? iface.addressInfo.format('urlstring') : [] - }) - .const() - ).join(',') - +export function paperlessDaemons( + effects: T.Effects, + { + secretKey, + trustedOrigins, + filebrowserSubfolder, + }: { + secretKey: string + trustedOrigins: string + filebrowserSubfolder: string | null + }, +) { return sdk.Daemons.of(effects) .addDaemon('redis', { subcontainer: sdk.SubContainer.of( @@ -64,7 +73,15 @@ export async function paperlessDaemons(effects: T.Effects) { subcontainer: sdk.SubContainer.of( effects, { imageId: 'paperless' }, - paperlessMounts, + filebrowserSubfolder + ? paperlessMounts.mountDependency({ + dependencyId: 'filebrowser', + volumeId: 'data', + subpath: null, + mountpoint: filebrowserMountpoint, + readonly: false, + }) + : paperlessMounts, 'paperless-app', ), exec: { @@ -79,6 +96,9 @@ export async function paperlessDaemons(effects: T.Effects) { PAPERLESS_CSRF_TRUSTED_ORIGINS: trustedOrigins, PAPERLESS_TIME_ZONE: 'UTC', PAPERLESS_OCR_LANGUAGE: 'eng', + PAPERLESS_CONSUMPTION_DIR: filebrowserSubfolder + ? `${filebrowserMountpoint}/${filebrowserSubfolder}` + : consumeMountpoint, USERMAP_UID: '1000', USERMAP_GID: '1000', }, @@ -96,6 +116,20 @@ export async function paperlessDaemons(effects: T.Effects) { }) } -export const main = sdk.setupMain(async ({ effects }) => - paperlessDaemons(effects), -) +export const main = sdk.setupMain(async ({ effects }) => { + const secretKey = await storeJson.read((s) => s.secretKey).const(effects) + if (!secretKey) { + throw new Error('store.json is missing the generated secret key') + } + return paperlessDaemons(effects, { + secretKey, + trustedOrigins: ( + await sdk.host.getOwn(effects, uiHostId, uiUrls).const() + ).join(','), + filebrowserSubfolder: await storeJson + .read((s) => + s.consumeSource === 'filebrowser' ? s.filebrowserSubfolder : null, + ) + .const(effects), + }) +}) diff --git a/startos/manifest/i18n.ts b/startos/manifest/i18n.ts index f0c06bf..9cfb4e6 100644 --- a/startos/manifest/i18n.ts +++ b/startos/manifest/i18n.ts @@ -18,3 +18,16 @@ export const long = { fr_FR: "Paperless-ngx numérise, indexe et archive vos documents papier afin de retrouver n'importe quoi en quelques secondes. Il effectue l'OCR, organise par étiquettes et correspondants et propose une interface web rapide et recherchable.", } + +export const filebrowserDescription = { + en_US: + 'Optional. Lets Paperless-ngx watch a folder in FileBrowser Quantum, so documents you drop there are imported automatically.', + es_ES: + 'Opcional. Permite que Paperless-ngx vigile una carpeta de FileBrowser Quantum, de modo que los documentos que dejes allí se importen automáticamente.', + de_DE: + 'Optional. Lässt Paperless-ngx einen Ordner in FileBrowser Quantum überwachen, sodass dort abgelegte Dokumente automatisch importiert werden.', + pl_PL: + 'Opcjonalne. Pozwala Paperless-ngx obserwować folder w FileBrowser Quantum, dzięki czemu umieszczone tam dokumenty są importowane automatycznie.', + fr_FR: + 'Facultatif. Permet à Paperless-ngx de surveiller un dossier de FileBrowser Quantum, afin que les documents que vous y déposez soient importés automatiquement.', +} diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 8f419d4..03e2d99 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -1,5 +1,5 @@ import { setupManifest } from '@start9labs/start-sdk' -import { long, short } from './i18n' +import { filebrowserDescription, long, short } from './i18n' export const manifest = setupManifest({ id: 'paperless-ngx', @@ -21,5 +21,14 @@ export const manifest = setupManifest({ arch: ['x86_64', 'aarch64'], }, }, - dependencies: {}, + dependencies: { + filebrowser: { + description: filebrowserDescription, + optional: true, + metadata: { + title: 'FileBrowser Quantum', + icon: 'https://raw.githubusercontent.com/Start9Labs/filebrowser-quantum-startos/e936a6c85a97b930b43cad5e9c0dd4898a2df567/icon.svg', + }, + }, + }, }) diff --git a/startos/utils.ts b/startos/utils.ts index 1f61a38..c394ffc 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -7,6 +7,9 @@ export const uiPort = 8000 export const redisPort = 6379 export const dataMountpoint = '/usr/src/paperless/data' +export const consumeMountpoint = '/usr/src/paperless/consume' +export const filebrowserMountpoint = '/mnt/filebrowser' +export const defaultConsumeSubfolder = 'paperless' export const srcDir = '/usr/src/paperless/src' // Both the main daemon and the set-admin-password action mount the same set of @@ -28,7 +31,7 @@ export const paperlessMounts = sdk.Mounts.of() .mountVolume({ volumeId: 'main', subpath: 'consume', - mountpoint: '/usr/src/paperless/consume', + mountpoint: consumeMountpoint, readonly: false, }) .mountVolume({ diff --git a/startos/versions/current.ts b/startos/versions/current.ts index 8930803..7cbe0bc 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,13 +1,18 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '2.20.15:4', + version: '2.20.15:5', releaseNotes: { - en_US: 'Internal updates (start-sdk 2.0.x)', - es_ES: 'Actualizaciones internas (start-sdk 2.0.x)', - de_DE: 'Interne Aktualisierungen (start-sdk 2.0.x)', - pl_PL: 'Aktualizacje wewnętrzne (start-sdk 2.0.x)', - fr_FR: 'Mises à jour internes (start-sdk 2.0.x)', + en_US: + 'New **Set Consume Folder** action: Paperless-ngx can watch a folder in FileBrowser Quantum, so documents dropped there are imported automatically.', + es_ES: + 'Nueva acción **Establecer carpeta de consumo**: Paperless-ngx puede vigilar una carpeta de FileBrowser Quantum, de modo que los documentos que se dejen allí se importen automáticamente.', + de_DE: + 'Neue Aktion **Konsum-Ordner festlegen**: Paperless-ngx kann einen Ordner in FileBrowser Quantum überwachen, sodass dort abgelegte Dokumente automatisch importiert werden.', + pl_PL: + 'Nowa akcja **Ustaw folder konsumpcji**: Paperless-ngx może obserwować folder w FileBrowser Quantum, dzięki czemu umieszczone tam dokumenty są importowane automatycznie.', + fr_FR: + 'Nouvelle action **Définir le dossier de consommation** : Paperless-ngx peut surveiller un dossier de FileBrowser Quantum, afin que les documents qui y sont déposés soient importés automatiquement.', }, migrations: { up: async () => {},