Skip to content
Draft
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
53 changes: 30 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,23 @@ Two images: the application, and a Redis sidecar.

## Volume and Data Layout

One volume, mounted four times at four different subpaths, plus an optional fifth mount from FileBrowser Quantum.
One volume, mounted four times at four different subpaths, plus an optional fifth mount from FileBrowser Quantum or NextExplorer.

| 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 |
| 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 |
| `nextexplorer` → `data` | — | `/mnt/nextexplorer` | NextExplorer's locations, read-write |

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

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

**The NextExplorer mount works the same way while the consume folder points there**: `PAPERLESS_CONSUMPTION_DIR` is the chosen location, `/mnt/nextexplorer/<location>`. NextExplorer also serves its volume as uid 1000.

| Path | Written by | Holds |
| ------------ | ----------- | ----------------------------------------------------------------- |
| `db.sqlite3` | Paperless | Documents' metadata, tags, users |
Expand All @@ -85,21 +88,22 @@ One model, holding two generated values and one user choice.

- **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.
- **The consume folder** — `consumeSource` (`local`, `filebrowser` or `nextexplorer`), `filebrowserSubfolder` and `nextexplorerLocation`. 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

One, optional, and declared only while it is in use.
Two, both optional, and each declared only while the consume folder points at it.

| Dependency | Id | Required | Kind | Purpose |
| ------------------- | ------------- | -------- | -------- | ------------------------------------ |
| FileBrowser Quantum | `filebrowser` | No | `exists` | Hosts the consume folder, read-write |
| Dependency | Id | Required | Kind | Purpose |
| ------------------- | -------------- | -------- | -------- | ------------------------------------ |
| FileBrowser Quantum | `filebrowser` | No | `exists` | Hosts the consume folder, read-write |
| NextExplorer | `nextexplorer` | 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.
While the consume folder points at one of them, `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.

Expand Down Expand Up @@ -146,12 +150,13 @@ Generates a password for the `admin` account and shows it once.

### 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`).
Chooses where Paperless watches for new documents: the private `consume` subpath (the default — reachable only by Paperless itself, so in practice "web upload only"), a subfolder of FileBrowser Quantum's data volume (default `paperless`), or a NextExplorer location (default `Paperless`).

- **What it changes:** `consumeSource` and `filebrowserSubfolder` in the store.
- **What it changes:** `consumeSource`, and `filebrowserSubfolder` or `nextexplorerLocation`, 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.
- **NextExplorer:** refused unless NextExplorer is installed. The action declares the dependency, then runs NextExplorer's `add-location` itself (`access: 'dependent'`), which creates the location or accepts one that already exists, and writes the store only once that succeeds. A name NextExplorer rejects leaves the previous choice in place.
- **Repeat safety:** switching away keeps the last subfolder and location, so each is pre-filled if selected again. Files left in any folder are not moved.

## Tasks

Expand All @@ -178,7 +183,7 @@ 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.
**Nor do they cover the FileBrowser Quantum or NextExplorer 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

Expand All @@ -190,7 +195,7 @@ 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.
**A consume folder in FileBrowser Quantum or NextExplorer is that package's data**, backed up by it, not this one. The store records the choice, so a restore on a server without that package starts but imports nothing until it is installed and Paperless restarted.

## Limitations and Differences

Expand All @@ -201,7 +206,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.
8. **The consume folder can be shared only through FileBrowser Quantum or NextExplorer**, and only one folder is watched, non-recursively. Paperless's own document store (`media`) is not exposed to other services.

---

Expand All @@ -220,12 +225,13 @@ 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 # the private intake folder; idle while FileBrowser Quantum is the consume source
consume: /usr/src/paperless/consume # the private intake folder; idle while another consume source is selected
export: /usr/src/paperless/export
dependency_mounts:
filebrowser/data: /mnt/filebrowser # read-write, only while consumeSource is filebrowser
nextexplorer/data: /mnt/nextexplorer # read-write, only while consumeSource is nextexplorer
file_models:
- store.json # adminPassword, the generated Django secretKey, consumeSource, filebrowserSubfolder
- store.json # adminPassword, the generated Django secretKey, consumeSource, filebrowserSubfolder, nextexplorerLocation
startos_managed_env_vars:
- PAPERLESS_REDIS
- PAPERLESS_PORT
Expand All @@ -235,11 +241,12 @@ 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>
- PAPERLESS_CONSUMPTION_DIR # /usr/src/paperless/consume, /mnt/filebrowser/<subfolder> or /mnt/nextexplorer/<location>
- USERMAP_UID
- USERMAP_GID
dependencies:
- { id: filebrowser, optional: true, kind: exists } # declared only while it is the consume source
- { id: nextexplorer, optional: true, kind: exists } # same; set-consume-folder runs its add-location
interfaces:
ui: { type: ui, port: 8000 } # Paperless's own login; no gate added by StartOS
actions:
Expand Down
1 change: 1 addition & 0 deletions instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Forgot your password, or want a new one? Run **Set Admin Password** again at any

- **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.
- **Consume folder in NextExplorer**: run the **Set Consume Folder** action, choose **NextExplorer**, and pick a location (the default is `Paperless`). NextExplorer must be installed first; the location is added to NextExplorer for you. NextExplorer's admin sees it straight away; to let another NextExplorer account drop files into it, add the location in that account's **Volumes** tab. Nextcloud shows NextExplorer's locations as external storage, so dropping a file into it from Nextcloud works too.
- **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.

Expand Down
10 changes: 9 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 @@ -7,7 +7,8 @@
},
"dependencies": {
"@start9labs/start-sdk": "2.0.9",
"filebrowser-startos": "github:Start9Labs/filebrowser-startos#next"
"filebrowser-startos": "github:Start9Labs/filebrowser-startos#next",
"nextexplorer-startos": "github:Start9Labs/nextexplorer-startos#next"
},
"devDependencies": {
"@types/node": "^22.19.0",
Expand Down
108 changes: 84 additions & 24 deletions startos/actions/setConsumeFolder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { setDependencies, nextexplorerVersionRange } from '../dependencies'
import { storeJson } from '../fileModels/store.json'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { defaultConsumeSubfolder } from '../utils'
import { defaultConsumeLocation, defaultConsumeSubfolder } from '../utils'

const { InputSpec, Value, Variants } = sdk

Expand Down Expand Up @@ -31,6 +32,20 @@ export const inputSpec = InputSpec.of({
}),
}),
},
nextexplorer: {
name: i18n('NextExplorer'),
spec: InputSpec.of({
location: Value.text({
name: i18n('NextExplorer Location'),
description: i18n(
'Location in NextExplorer that Paperless-ngx watches. Added to NextExplorer if it does not exist; NextExplorer must be installed.',
),
default: defaultConsumeLocation,
required: true,
placeholder: defaultConsumeLocation,
}),
}),
},
}),
}),
})
Expand All @@ -41,7 +56,7 @@ export const setConsumeFolder = sdk.Action.withInput(
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.',
'Choose where Paperless-ngx watches for new documents: a private folder, or a folder in FileBrowser Quantum or NextExplorer you can drop files into.',
),
warning: null,
allowedStatuses: 'any',
Expand All @@ -51,31 +66,76 @@ export const setConsumeFolder = sdk.Action.withInput(

inputSpec,

async ({ effects }) => {
const subfolder =
(await storeJson.read((s) => s.filebrowserSubfolder).const(effects)) ??
defaultConsumeSubfolder
async () => {
const store = await storeJson.read().once()
const subfolder = store?.filebrowserSubfolder ?? defaultConsumeSubfolder
const location = store?.nextexplorerLocation ?? defaultConsumeLocation
const other = {
filebrowser: { subfolder },
nextexplorer: { location },
}
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 } },
},
store?.consumeSource === 'filebrowser'
? { selection: 'filebrowser' as const, value: { subfolder }, other }
: store?.consumeSource === 'nextexplorer'
? { selection: 'nextexplorer' as const, value: { location }, other }
: { selection: 'local' as const, value: {}, other },
}
},

async ({ effects, input }) =>
storeJson.merge(
effects,
input.source.selection === 'filebrowser'
? {
consumeSource: 'filebrowser',
filebrowserSubfolder: input.source.value.subfolder,
}
: { consumeSource: 'local' },
),
async ({ effects, input }) => {
if (input.source.selection !== 'nextexplorer') {
await storeJson.merge(
effects,
input.source.selection === 'filebrowser'
? {
consumeSource: 'filebrowser',
filebrowserSubfolder: input.source.value.subfolder,
}
: { consumeSource: 'local' },
)
return null
}

if (!(await sdk.getInstalledPackages(effects)).includes('nextexplorer')) {
throw new Error(i18n('Install NextExplorer first'))
}
const location = input.source.value.location.trim()
// add-location admits only a declared dependent, and the store must not name the location until it exists.
await effects.setDependencies({
dependencies: [
{
id: 'nextexplorer',
kind: 'exists',
versionRange: nextexplorerVersionRange,
},
],
})
try {
await sdk.action.run({
effects,
packageId: 'nextexplorer',
actionId: 'add-location',
input: () => ({ name: location }),
})
} catch (e) {
await setDependencies(effects)
throw e
}
await storeJson.merge(effects, {
consumeSource: 'nextexplorer',
nextexplorerLocation: location,
})

return {
version: '1',
title: i18n('Consume Folder Set'),
message: i18n(
'Paperless-ngx now watches the ${location} location in NextExplorer. NextExplorer accounts other than the admin see it only once you add it in their Volumes tab.',
{ location },
),
result: null,
}
},
)
Loading