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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ verified, tried, and decided belongs in the commit message and the PR body.
- **A `config.php` number the double cannot print back exactly is carried through as source text too**, so `9223372036854775807`, `1.0` and `1.0E+30` survive a write. Ordinary values stay real numbers, which is what keeps `maintenance_window_start` and `redis.port` reaching their validators — do not widen this into writing every integer as a float.
- **The File Browser mount's `idmap` (uid 1000 → `www-data` 33) is what makes the integration work at all**, and it needs StartOS 0.4.0-beta.10+. Files other services drop into File Browser's volume under a different uid surface as `nobody` until those services idmap their own mount to 1000 too.
- **Adding an external-storage source is a registry edit in `startos/externalStorage.ts` plus a typed mount.** File Browser is the shared hub most services route through, so a direct source is worth adding only for a service whose files live browsably on its own volume.
- **`richdocuments` drops the Microsoft formats the moment a second office connector is enabled** (`CapabilitiesService::hasOtherOOXMLApps` checks `onlyoffice` and `officeonline`), and the second app does not claim them unless it is configured — so Word, Excel and PowerPoint open in neither and silently download. That is what the `office-connectors` health check exists to catch; don't simplify it away because "only one backend is ever configured", which is true and is not the condition that breaks this.
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Six oneshots run alongside them, in order: `chown` hands the data directory to `

**`finish-upgrade` runs after the web daemon is ready, not before it**, which is what makes it safe. In the normal case the upgrade has already happened during init and this is a no-op; when it does have work to do, Apache is up serving the maintenance page while `occ upgrade` runs, exactly as a manual recovery would. It fails open — nothing it does can prevent the service from serving.

Apache also carries a generated `startos-office.conf` and the four proxy modules it needs, written into the container's filesystem on every start. It puts the chosen document server on Nextcloud's own origin — `/browser`, `/cool` and `/hosting` for Collabora, `/ds-vpath` for ONLYOFFICE — which is what lets the editor work on every address Nextcloud is reachable at rather than one. The file is empty when no office suite is selected.

For Collabora it also rewrites the WOPI discovery response, stripping the absolute origin out of every `urlsrc` so the editor loads same-origin — Nextcloud otherwise copies Collabora's own absolute address into the editor frame verbatim, pinning it to one address. Tracked upstream as nextcloud/richdocuments#6019; if that lands, the rewrite can go.

## Volume and Data Layout

Three volumes.
Expand Down Expand Up @@ -93,22 +97,26 @@ Three settings depart from what upstream would do:
| `updater.server.url` | a reserved never-resolving address | `occ update:check` is the one path `updatechecker` does not gate, and it would otherwise reach Nextcloud's real update server |
| `integrity.check.disabled` | `true` | The image adds `ffmpeg` and the package rewrites `config.php`, so the signature check would fail on a correct install |

`store.json` is StartOS state that has no place in `config.php`: the install-time admin password (held only until it is shown once), the queue of long-running `occ` tasks, the external-storage selection, and the Talk STUN/TURN relay toggle alongside the entries it last applied.
`store.json` is StartOS state that has no place in `config.php`: the install-time admin password (held only until it is shown once), the queue of long-running `occ` tasks, the external-storage selection, the Talk STUN/TURN relay toggle, and the office-suite choice — each of the last two alongside a signature of what was last applied for it, which is how a reconcile knows what to clear before it writes.

## Dependencies

None are required. Both are optional and exist only while they are selected.

| Dependency | Kind | Health checks | Required |
| ------------- | --------- | ------------- | ----------------------------------------------------------- |
| `filebrowser` | `exists` | — | Only while chosen in the External Storage action |
| `coturn` | `running` | **none** | Only while Talk call relaying is on in the Configure action |
| `filebrowser` | `exists` | — | Only while chosen in the External Storage action |
| `coturn` | `running` | **none** | Only while Talk call relaying is on in the Configure action |
| `collabora-online` | `running` | `cool` | Only while chosen in the Office Suite action |
| `onlyoffice-docs` | `running` | `documentserver` | Only while chosen in the Office Suite action; published to the Community Registry, not the Start9 one |

The External Storage action offers only the sources whose backing service is actually installed, so an uninstalled one never appears in the form.

**Coturn declares no health check, deliberately.** Coturn's own `TURN Server` check fails until you attach a public domain to it, and naming it here would leave Nextcloud showing a permanently unmet dependency even though Talk works fine without a relay. Coturn's own check already says what is missing.

The shared secret is read through a throwaway container that mounts only Coturn's `shared` subpath read-only — so a missing or broken Coturn can never take Nextcloud's own daemons down, and the rest of Coturn's volume stays out of view.
The shared secret is read through a throwaway container that mounts only Coturn's `shared` subpath read-only — so a missing or broken Coturn can never take Nextcloud's own daemons down, and the rest of Coturn's volume stays out of view. ONLYOFFICE's JWT secret is read the same way, from its own `shared` subpath.

**The office backends are mutually exclusive**, and only the chosen one is declared. Unlike Coturn they do name a health check: Nextcloud can hand a document to a document server that is up, and cannot to one that is starting.

## Network Access and Interfaces

Expand Down Expand Up @@ -161,6 +169,23 @@ Surfaces another StartOS service's files as a folder in Nextcloud Files, using N
- **Availability: only while the service is running**, since the per-source user picker reads the live Nextcloud user list.
- **Per-source scoping.** Each source is off, available to all users, or restricted to a chosen set. Clearing a source deletes its `files_external` entry; it does not delete any files.

### Office Suite

Selects the document server that opens office files — Collabora Online, ONLYOFFICE Docs, or none. Collabora is labelled recommended in the form and is the right answer for most installs; the trade-off is set out in `instructions.md` under **Which one to choose**, and rests on a measured round-trip rather than a marketing claim: both engines preserve text, tables, images, links, footnotes and fields exactly, but LibreOffice rewrites style-inherited formatting as direct formatting on each run, where ONLYOFFICE returns the file byte-identical in structure.

- **When to run it:** after installing one of the two services and its Nextcloud app, and again to switch or to turn editing off.
- **What it changes:** `officeSuite` in `store.json`. Through it: the package's dependency on that service, the host bridge's IP in `trusted_domains`, the generated Apache proxy that serves the editor from Nextcloud's own origin, and — on the next start, via the `office-suite` oneshot — the connector app's own settings.
- **Cost:** seconds, then a restart.
- **Repeat safety:** idempotent; the form is pre-filled with the current choice.

**Only one connector app may be enabled.** `richdocuments` drops the Microsoft formats out of its default-open capability whenever it finds `onlyoffice` or `officeonline` enabled, and the other app does not pick them up unless it is configured too — so Word, Excel and PowerPoint files open in neither and download instead, with nothing in Nextcloud saying why. The `office-connectors` health check fails while that is the case and names the app to disable.

**Switching first deletes the settings written for the previous backend**, so a connector is never left pointed at a service that has since been uninstalled. That teardown is also what keeps the choice unambiguous: an unconfigured connector registers no file actions of its own, so exactly one handler is live in the Files UI.

**It waits on the connector app.** The setting does nothing until **Nextcloud Office** (for Collabora) or **ONLYOFFICE** (for ONLYOFFICE Docs) is installed from the Nextcloud app store and enabled. Until then the oneshot logs that it is waiting and applies the settings on a later start.

**The `trusted_domains` entry is load-bearing.** A document server fetches and saves files over the host bridge, and without that entry Nextcloud answers every one of those requests with `Trusted domain error` — the editor opens and then fails to load the document. Nextcloud matches on the host alone, so the bare IP covers whatever port the binding was assigned.

### Maintenance — Reset Admin Password, Disable Maintenance Mode, Disable Non-default Apps, Scan Files, Repair

- **Reset Admin Password** generates a new password for a chosen admin account and shows it once. Only while running; the account list is read live.
Expand Down Expand Up @@ -218,6 +243,10 @@ A web-interface failure after the grace period is Nextcloud itself: an app that

The transient checks — Recognize Model Download, Memories Indexing, Memories Map Setup, File Scan, Repair — exist only while their task is pending, and report `loading` with a progress message throughout.

**Office Connector** (`office-connectors`) — present only while an office suite is selected. It reads Nextcloud's enabled-app list and fails while more than one office connector app is enabled.

A failure here is not a fault in any service: everything is running, and OpenDocument files still open. What breaks is Word, Excel and PowerPoint, silently — `richdocuments` demotes those formats the moment it sees a rival connector enabled, and the rival does not claim them unless it is configured. The message names the app to disable; disabling it in Nextcloud's Apps page clears the check on the next poll. It is a check rather than a task because a task can be dismissed while the breakage remains.

## Backups and Restore

Mixed, and each half is scoped deliberately.
Expand Down Expand Up @@ -280,12 +309,15 @@ startos_managed_env_vars:
dependencies:
- filebrowser # optional, exists; only while selected as an external-storage source
- coturn # optional, running, no health checks; only while Talk call relaying is on
- collabora-online # optional, running, health check `cool`; only while selected as the office suite
- onlyoffice-docs # optional, running, health check `documentserver`; only while selected as the office suite
interfaces:
ui: { type: ui, port: 80 }
webdav: { type: api, port: 80 } # same binding, path /remote.php/dav/
actions:
- set-config
- external-storage # only-running
- set-office-suite
- reset-admin # only-running
- disable-maintenance # only-running
- disable-unstable-apps # only-running
Expand All @@ -302,6 +334,7 @@ health_checks:
- valkey # hidden
- nextcloud # displayed "Web Interface"
- cron # hidden
- office-connectors # only while an office suite is selected
- recognize-models # only while that task is pending
- memories-indexing # only while that task is pending
- memories-map-setup # only while that task is pending
Expand Down
18 changes: 18 additions & 0 deletions instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ Then turn the toggle on. Until all three are done, calls still work wherever a d

Nextcloud is what tells Talk where the relay is, so there is nothing to enter in Talk's own admin settings — and nothing you have already entered there gets overwritten. Talk's default STUN server, `stun.nextcloud.com:443`, is also left in place; remove it under **Administration settings → Talk** if you would rather every part of call setup stayed on your own server.

## Editing documents

Nextcloud on its own shows your documents but cannot edit them. To edit in the browser, install one of the two office services from the Marketplace and connect it:

1. Install **Collabora Online**, unless you have a specific reason for the other one — see below. ONLYOFFICE Docs is in the Community Registry, so you would need that registry added to your server to find it.
2. In Nextcloud's web interface, open **Apps** and install the matching app — **Nextcloud Office** for Collabora, **ONLYOFFICE** for ONLYOFFICE Docs. Enable only that one: with both enabled, Nextcloud stops opening Word, Excel and PowerPoint files in either, and this service's page will name the one to disable.
3. Run the **Office Suite** action here and pick the one you installed.

Documents, spreadsheets and presentations then open in the browser from Files, and several people can edit the same file at once. The editor is served from your Nextcloud address, so it works the same on your local network, a public domain, or Tor.

### Which one to choose

**Collabora Online for almost everyone.** It uses roughly a quarter of the memory — a gigabyte or so against the four ONLYOFFICE asks for — and opens more kinds of file, including Visio, WordPerfect and Apple iWork documents that ONLYOFFICE cannot. It edits Word, Excel and PowerPoint files perfectly well.

**ONLYOFFICE Docs only for a large body of style-heavy documents that keeps going back to Microsoft Office.** Its native format is the same one Word and Excel use, so it saves a file back exactly as it found it. Collabora is built on LibreOffice, which reads a Word file into its own model and writes it out again; nothing is lost — the text, tables, images, links, footnotes and page layout all survive — but it writes the formatting out in its own way, spelling out on each paragraph what the document had left to its styles. You would not see a difference on screen. You would see one if a colleague later opened that file in Word and changed a style, because the parts Collabora spelled out no longer follow it.

For one document that costs you nothing — you would never notice, and you could fix it by hand. It adds up when there are hundreds of them, when the styles are doing real work (a house template, a legal or academic format), and when the files keep cycling back to Word. Short of that, the memory is better spent elsewhere.

## Limitations

- **No arbitrary host directory mounts.** You can surface another StartOS service's files with the **External Storage** action (currently File Browser), and you can attach remote storage (S3, WebDAV, SMB, etc.) through Nextcloud's built-in External Storage app. StartOS does not expose arbitrary host directories to the container.
Expand Down
2 changes: 2 additions & 0 deletions startos/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { repair } from './maintenance/repair'
import { resetAdmin } from './maintenance/resetAdmin'
import { scanFiles } from './maintenance/scanFiles'
import { setConfig } from './setConfig'
import { setOfficeSuite } from './setOfficeSuite'

export const actions = sdk.Actions.of()
.addAction(setConfig)
.addAction(externalStorage)
.addAction(setOfficeSuite)
.addAction(resetAdmin)
.addAction(disableMaintenanceMode)
.addAction(disableUnstableApps)
Expand Down
60 changes: 60 additions & 0 deletions startos/actions/setOfficeSuite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { storeJson } from '../fileModels/store.json'
import { i18n } from '../i18n'
import { isOfficeSuite } from '../officeSuite'
import { sdk } from '../sdk'

const { InputSpec, Value } = sdk

export const inputSpec = InputSpec.of({
office_suite: Value.select({
name: i18n('Office Suite'),
description: i18n(
'Which document server opens documents, spreadsheets and presentations in your browser. Collabora Online is recommended: it needs about a quarter of the memory and opens more formats. ONLYOFFICE Docs, from the Community Registry, is worth its size only if you shuttle a large body of style-heavy documents back and forth with Microsoft Office. Install the service first, then its Nextcloud app — Nextcloud Office for Collabora, ONLYOFFICE for ONLYOFFICE Docs — and enable only that one.',
),
default: 'none',
values: {
none: i18n('None'),
collabora: 'Collabora Online (recommended)',
onlyoffice: 'ONLYOFFICE Docs',
},
}),
})

export const setOfficeSuite = sdk.Action.withInput(
// id
'set-office-suite',

// metadata
async ({ effects }) => ({
name: i18n('Office Suite'),
description: i18n(
'Choose the document server that opens office files in your browser.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),

// form input specification
inputSpec,

// optionally pre-fill the input form
async ({ effects }) => {
const officeSuite = await storeJson.read((s) => s.officeSuite).once()
return {
office_suite: isOfficeSuite(officeSuite) ? officeSuite : ('none' as const),
}
},

// the execution function
async ({ effects, input }) => {
// `main` reads this, resolves the document server's bridge address and
// reconciles the connector app's settings; setDependencies reads it to add
// or drop the dependency.
await storeJson.merge(effects, {
officeSuite:
input.office_suite === 'none' ? undefined : input.office_suite,
})
},
)
12 changes: 12 additions & 0 deletions startos/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { T } from '@start9labs/start-sdk'
import { externalStorageMeta } from './externalStorage'
import { storeJson } from './fileModels/store.json'
import { sdk } from './sdk'
import { officeSuiteMeta } from './officeSuite'
import { coturnId, coturnVersionRange } from './utils'

export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
const sources =
(await storeJson.read((s) => s.externalStorages).const(effects)) ?? []
const talkTurn = await storeJson.read((s) => s.talkTurn).const(effects)
const officeSuite = await storeJson.read((s) => s.officeSuite).const(effects)

const deps: T.CurrentDependenciesResult<any> = {}

Expand All @@ -30,5 +32,15 @@ export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
healthChecks: [],
}
}
// The document server has to be up before Nextcloud can hand it a document,
// and its own health check is the readiness signal.
if (officeSuite) {
const meta = officeSuiteMeta[officeSuite]
deps[meta.packageId] = {
kind: 'running',
versionRange: meta.versionRange,
healthChecks: [meta.healthCheckId],
}
}
return deps
})
7 changes: 7 additions & 0 deletions startos/fileModels/store.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ const shape = z.object({
// desired-vs-actual split (and the same non-reactive read) as
// `externalStoragesConfigured` above.
talkTurnConfigured: z.string().catch('').default(''),
// Which document server serves the office editors, if any. DESIRED state:
// written by the Configure action, read reactively in setupMain/setDependencies.
officeSuite: z.enum(['collabora', 'onlyoffice']).optional().catch(undefined),
// ACTUAL state: an opaque signature of the office settings this package last
// applied, and the record of what to clear on the next change. Same
// desired-vs-actual split, and the same non-reactive read, as the two above.
officeConfigured: z.string().catch('').default(''),
})

export type Store = z.infer<typeof shape>
Expand Down
Loading