Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 52 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,42 +54,54 @@ 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.

Paperless's own settings — document types, tags, mail rules, workflows — live in its database and are edited in the interface.

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

Expand Down Expand Up @@ -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

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

---

Expand All @@ -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
Expand All @@ -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/<subfolder>
- 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:
Expand Down
3 changes: 2 additions & 1 deletion instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
166 changes: 165 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion startos/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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)
Loading