From 20d9ca87223263bcba003f1eb94f00f07d4059cd Mon Sep 17 00:00:00 2001 From: Matt Hill <9935159+MattDHill@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:21:45 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20surface=20NextExplorer=20as=20external?= =?UTF-8?q?=20storage;=2034.0.4:1=20=E2=86=92=2034.0.4:3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NextExplorer replaces FileBrowser Quantum as the recommended file server and the hub other services route through, so it joins the external storage registry. The mount is the same shape as FileBrowser Quantum's, uid 1000 idmapped to www-data, but the files_external entry exposes only the Files drive: NextExplorer's volume root also holds `_users`, one private directory per account, which an all-users mount would hand to every Nextcloud user. That is what the new `dataDir` field carries. The dependency floor is the community release, 2.2.7:0, so a box that installed it from there before the move still satisfies it. The revision skips :2, which is already on alpha. Verified on the dev box: selecting NextExplorer declares the dependency, Nextcloud restarts with the volume mounted as www-data, files_external lists /NextExplorer with datadir /mnt/nextexplorer/Files, a file written as www-data lands on disk as uid 1000, and NextExplorer reads and deletes it. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 4 ++-- README.md | 24 ++++++++++++----------- instructions.md | 4 ++-- package-lock.json | 10 +++++++++- package.json | 3 ++- startos/dependencies.ts | 6 ++++++ startos/externalStorage.ts | 17 ++++++++++++++-- startos/fileModels/store.json.ts | 2 +- startos/i18n/dictionaries/default.ts | 1 + startos/i18n/dictionaries/translations.ts | 4 ++++ startos/main.ts | 15 ++++++++++++-- startos/manifest/i18n.ts | 13 ++++++++++++ startos/manifest/index.ts | 9 +++++++++ startos/versions/current.ts | 22 +++++++++++++++------ 14 files changed, 106 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4409fed..7b33a38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,8 @@ verified, tried, and decided belongs in the commit message and the PR body. - **A value the `config.php` grammar cannot model is carried through as source text — never narrow `Raw` into a hard failure.** `occ` runs inside the service container, so a value that stops the read is a brick with no way to reach the command that would remove it. Malformed input must still fail loudly, which is what bounds `Raw` to a single line: `var_export` never spreads a scalar expression over two, so a run that reaches a newline is a broken file rather than an unmodelled value. A carried-through value is `{ __raw: }`, so a PHP array whose only key is `__raw` and whose value is a string is indistinguishable from one. - **Comments have to parse.** Nextcloud 34 prepends a banner comment between `=16" } }, + "node_modules/nextexplorer-startos": { + "resolved": "git+ssh://git@github.com/Start9Labs/nextexplorer-startos.git#d8588c6874e0f7e5ca1e160dc58e1fcf06c0ef59", + "dependencies": { + "@start9labs/start-sdk": "2.0.9", + "filebrowser-startos": "github:Start9Labs/filebrowser-startos#next" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/package.json b/package.json index d038842..37ace15 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,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", diff --git a/startos/dependencies.ts b/startos/dependencies.ts index c1925d4..6de7890 100644 --- a/startos/dependencies.ts +++ b/startos/dependencies.ts @@ -16,6 +16,12 @@ export const setDependencies = sdk.setupDependencies(async ({ effects }) => { // Only require a source while it's selected. `exists` (not `running`) — we // only need the source's volume present on disk to mount and read/write it. + if (sources.includes('nextexplorer')) { + deps['nextexplorer'] = { + kind: 'exists', + versionRange: externalStorageMeta.nextexplorer.versionRange, + } + } if (sources.includes('filebrowser')) { deps['filebrowser'] = { kind: 'exists', diff --git a/startos/externalStorage.ts b/startos/externalStorage.ts index 991f982..ad85af9 100644 --- a/startos/externalStorage.ts +++ b/startos/externalStorage.ts @@ -23,12 +23,12 @@ import { I18nKey } from './i18n/dictionaries/default' * downloads/media/documents directory) — e.g. qBittorrent (`main`/`downloads`). * AVOID app-managed content-addressed stores that intermingle a database with * hash-named blobs (e.g. Docuseal): surfacing those exposes the DB and shows - * unbrowsable files. FileBrowser Quantum is the intended shared hub other services + * unbrowsable files. NextExplorer is the intended shared hub other services * route through, so a direct source is only worth adding for a service whose * files live in its own volume. */ -export const EXTERNAL_STORAGE_SOURCES = ['filebrowser'] as const +export const EXTERNAL_STORAGE_SOURCES = ['nextexplorer', 'filebrowser'] as const export type ExternalStorageSource = (typeof EXTERNAL_STORAGE_SOURCES)[number] export type ExternalStorageMeta = { @@ -38,6 +38,8 @@ export type ExternalStorageMeta = { label: I18nKey /** Where the source's volume is mounted inside Nextcloud's container. */ mountpoint: string + /** The directory under `mountpoint` the `files_external` entry exposes. */ + dataDir: string /** * The mount point (folder name) of the `files_external` entry as shown in * the Nextcloud Files UI. Also used to find the entry again for deletion. @@ -56,10 +58,21 @@ export const externalStorageMeta: Record< ExternalStorageSource, ExternalStorageMeta > = { + nextexplorer: { + packageId: 'nextexplorer', + label: 'NextExplorer', + mountpoint: '/mnt/nextexplorer', + // The default drive only. The volume root also holds `_users`, one private + // directory per NextExplorer account, which an all-users mount would expose. + dataDir: '/mnt/nextexplorer/Files', + ncMountPoint: '/NextExplorer', + versionRange: '>=2.2.7:0', + }, filebrowser: { packageId: 'filebrowser', label: 'FileBrowser Quantum', mountpoint: '/mnt/filebrowser', + dataDir: '/mnt/filebrowser', ncMountPoint: '/FileBrowser', // 2.62.2:1 restructured the volumes (`data` volume, files owned by uid // 1000) — everything the idmap mount in main.ts relies on. diff --git a/startos/fileModels/store.json.ts b/startos/fileModels/store.json.ts index c09e52d..90df39e 100644 --- a/startos/fileModels/store.json.ts +++ b/startos/fileModels/store.json.ts @@ -32,7 +32,7 @@ const shape = z.object({ // `external-storage` action, read reactively in setupMain/setDependencies. // Keep the enum in sync with EXTERNAL_STORAGE_SOURCES. externalStorages: z - .array(z.enum(['filebrowser'])) + .array(z.enum(['nextexplorer', 'filebrowser'])) .catch([]) .default([]), // Per-source applicable users: source id -> Nextcloud user list. An empty or diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 9fcee6c..07ddbfb 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -182,6 +182,7 @@ const dict = { 'External Storage': 121, 'Each selected service is mounted into Nextcloud and added to Files. FileBrowser Quantum is mounted read-write, so you can move its files into Nextcloud. Clear a selection to remove it.': 122, 'FileBrowser Quantum': 123, + NextExplorer: 163, "Show other StartOS services' files as folders in Nextcloud Files, via the built-in External Storage app.": 124, 'Available to Users': 125, 'Which Nextcloud users see the selected storage in their Files. Leave empty to make it available to all users.': 126, diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts index a7a5922..9aa3d30 100644 --- a/startos/i18n/dictionaries/translations.ts +++ b/startos/i18n/dictionaries/translations.ts @@ -126,6 +126,7 @@ export default { 121: 'Almacenamiento externo', 122: 'Cada servicio seleccionado se monta en Nextcloud y se añade a Archivos. FileBrowser Quantum se monta con permisos de lectura y escritura, para que pueda mover sus archivos a Nextcloud. Anule la selección para eliminarlo.', 123: 'FileBrowser Quantum', + 163: 'NextExplorer', 124: 'Muestra los archivos de otros servicios de StartOS como carpetas en Archivos de Nextcloud, mediante la aplicación integrada de Almacenamiento externo.', 125: 'Disponible para los usuarios', 126: 'Qué usuarios de Nextcloud ven el almacenamiento seleccionado en sus Archivos. Déjelo vacío para que esté disponible para todos los usuarios.', @@ -285,6 +286,7 @@ export default { 121: 'Externer Speicher', 122: 'Jeder ausgewählte Dienst wird in Nextcloud eingebunden und zu Dateien hinzugefügt. FileBrowser Quantum wird mit Lese- und Schreibzugriff eingebunden, sodass Sie dessen Dateien nach Nextcloud verschieben können. Heben Sie die Auswahl auf, um ihn zu entfernen.', 123: 'FileBrowser Quantum', + 163: 'NextExplorer', 124: 'Zeigt Dateien anderer StartOS-Dienste als Ordner in Nextcloud Dateien an, über die integrierte App für externen Speicher.', 125: 'Verfügbar für Benutzer', 126: 'Welche Nextcloud-Benutzer den ausgewählten Speicher in ihren Dateien sehen. Leer lassen, um ihn für alle Benutzer verfügbar zu machen.', @@ -444,6 +446,7 @@ export default { 121: 'Magazyn zewnętrzny', 122: 'Każda wybrana usługa jest montowana w Nextcloud i dodawana do aplikacji Pliki. FileBrowser Quantum jest montowany w trybie do odczytu i zapisu, dzięki czemu możesz przenosić jego pliki do Nextcloud. Usuń zaznaczenie, aby ją odłączyć.', 123: 'FileBrowser Quantum', + 163: 'NextExplorer', 124: 'Pokazuje pliki innych usług StartOS jako foldery w aplikacji Pliki w Nextcloud, korzystając z wbudowanej aplikacji Magazyn zewnętrzny.', 125: 'Dostępne dla użytkowników', 126: 'Którzy użytkownicy Nextcloud widzą wybrany magazyn w swoich Plikach. Pozostaw puste, aby był dostępny dla wszystkich użytkowników.', @@ -603,6 +606,7 @@ export default { 121: 'Stockage externe', 122: 'Chaque service sélectionné est monté dans Nextcloud et ajouté à Fichiers. FileBrowser Quantum est monté en lecture-écriture, ce qui vous permet de déplacer ses fichiers vers Nextcloud. Désélectionnez-le pour le retirer.', 123: 'FileBrowser Quantum', + 163: 'NextExplorer', 124: "Affiche les fichiers d'autres services StartOS sous forme de dossiers dans Fichiers de Nextcloud, via l'application intégrée Stockage externe.", 125: 'Disponible pour les utilisateurs', 126: 'Quels utilisateurs Nextcloud voient le stockage sélectionné dans leurs Fichiers. Laissez vide pour le rendre disponible à tous les utilisateurs.', diff --git a/startos/main.ts b/startos/main.ts index 6580834..06f0115 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -1,4 +1,5 @@ import { manifest as filebrowserManifest } from 'filebrowser-startos/startos/manifest' +import { manifest as nextexplorerManifest } from 'nextexplorer-startos/startos/manifest' import { T } from '@start9labs/start-sdk' import { EXTERNAL_STORAGE_SOURCES, @@ -310,6 +311,16 @@ export const main = sdk.setupMain(async ({ effects }) => { // writes and MOVES files with no permission machinery, and files it creates // land back on disk as the source's uid so the source can manage them too. let mounts = nextcloudMount + if (sources.includes('nextexplorer')) { + mounts = mounts.mountDependency({ + dependencyId: 'nextexplorer', + volumeId: 'data', + subpath: null, + mountpoint: externalStorageMeta.nextexplorer.mountpoint, + readonly: false, + idmap: [{ fromId: 1000, toId: 33 }], + }) + } if (sources.includes('filebrowser')) { mounts = mounts.mountDependency({ dependencyId: 'filebrowser', @@ -1209,7 +1220,7 @@ async function reconcileExternalStorage( // previous selection: selected → ensure + set applicable; unselected → delete. for (const id of EXTERNAL_STORAGE_SOURCES) { if (abort.aborted) return - const { ncMountPoint, mountpoint } = externalStorageMeta[id] + const { ncMountPoint, dataDir } = externalStorageMeta[id] try { if (desired.includes(id)) { let mount = matchingMounts(await listMounts(), ncMountPoint)[0] @@ -1220,7 +1231,7 @@ async function reconcileExternalStorage( 'local', 'null::null', '-c', - `datadir=${mountpoint}`, + `datadir=${dataDir}`, ]) if (create.exitCode !== 0) { allOk = false diff --git a/startos/manifest/i18n.ts b/startos/manifest/i18n.ts index 88aef7a..cf940af 100644 --- a/startos/manifest/i18n.ts +++ b/startos/manifest/i18n.ts @@ -6,6 +6,19 @@ export const short = { fr_FR: 'Un foyer sûr pour toutes vos données', } +export const nextexplorerDescription = { + en_US: + 'Provides shared file storage that Nextcloud can surface as a folder in Files.', + es_ES: + 'Proporciona almacenamiento de archivos compartido que Nextcloud puede mostrar como una carpeta en Archivos.', + de_DE: + 'Stellt gemeinsam genutzten Dateispeicher bereit, den Nextcloud als Ordner in Dateien anzeigen kann.', + pl_PL: + 'Zapewnia współdzielony magazyn plików, który Nextcloud może pokazać jako folder w Plikach.', + fr_FR: + 'Fournit un stockage de fichiers partagé que Nextcloud peut présenter comme un dossier dans Fichiers.', +} + export const filebrowserDescription = { en_US: 'Provides shared file storage that Nextcloud can surface as a folder in Files.', diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index e26f17e..03bfbb7 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -4,6 +4,7 @@ import { coturnDescription, filebrowserDescription, long, + nextexplorerDescription, onlyofficeDescription, short, } from './i18n' @@ -41,6 +42,14 @@ export const manifest = setupManifest({ }, }, dependencies: { + nextexplorer: { + description: nextexplorerDescription, + optional: true, + metadata: { + title: 'NextExplorer', + icon: 'https://raw.githubusercontent.com/Start9Labs/nextexplorer-startos/04f7ecbfc31ad2205e0222dd7568fb881aa06c79/icon.svg', + }, + }, filebrowser: { description: filebrowserDescription, optional: true, diff --git a/startos/versions/current.ts b/startos/versions/current.ts index b079c9f..bda02e6 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,13 +1,23 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '34.0.4:1', + version: '34.0.4:3', releaseNotes: { - en_US: `An update that would skip a Nextcloud major version is refused before anything on disk changes. Install the Nextcloud 33 release from the version list first, then update again — this also works on a server where an earlier attempt at this update failed.`, - es_ES: `Una actualización que saltaría una versión mayor de Nextcloud se rechaza antes de que cambie nada en el disco. Instale primero la versión con Nextcloud 33 desde la lista de versiones y vuelva a actualizar; esto también funciona en un servidor donde un intento anterior de esta actualización falló.`, - de_DE: `Ein Update, das eine Nextcloud-Hauptversion überspringen würde, wird abgelehnt, bevor sich etwas auf der Festplatte ändert. Installieren Sie zuerst die Version mit Nextcloud 33 aus der Versionsliste und aktualisieren Sie dann erneut — das funktioniert auch auf einem Server, auf dem ein früherer Versuch dieses Updates fehlgeschlagen ist.`, - pl_PL: `Aktualizacja, która pominęłaby główną wersję Nextcloud, jest odrzucana, zanim cokolwiek zmieni się na dysku. Najpierw zainstaluj wydanie z Nextcloud 33 z listy wersji, a następnie zaktualizuj ponownie — działa to również na serwerze, na którym wcześniejsza próba tej aktualizacji się nie powiodła.`, - fr_FR: `Une mise à jour qui sauterait une version majeure de Nextcloud est refusée avant que quoi que ce soit ne change sur le disque. Installez d'abord la version avec Nextcloud 33 depuis la liste des versions, puis mettez à jour à nouveau — cela fonctionne aussi sur un serveur où une tentative précédente de cette mise à jour a échoué.`, + en_US: `**External Storage** can now surface NextExplorer, the recommended file server: its Files drive appears as a NextExplorer folder in Nextcloud Files. + +An update that would skip a Nextcloud major version is refused before anything on disk changes. Install the Nextcloud 33 release from the version list first, then update again — this also works on a server where an earlier attempt at this update failed.`, + es_ES: `**Almacenamiento externo** ahora puede mostrar NextExplorer, el servidor de archivos recomendado: su unidad Files aparece como una carpeta NextExplorer en Archivos de Nextcloud. + +Una actualización que saltaría una versión mayor de Nextcloud se rechaza antes de que cambie nada en el disco. Instale primero la versión con Nextcloud 33 desde la lista de versiones y vuelva a actualizar; esto también funciona en un servidor donde un intento anterior de esta actualización falló.`, + de_DE: `**Externer Speicher** kann jetzt NextExplorer einbinden, den empfohlenen Dateiserver: sein Laufwerk Files erscheint als Ordner NextExplorer in Nextcloud Dateien. + +Ein Update, das eine Nextcloud-Hauptversion überspringen würde, wird abgelehnt, bevor sich etwas auf der Festplatte ändert. Installieren Sie zuerst die Version mit Nextcloud 33 aus der Versionsliste und aktualisieren Sie dann erneut — das funktioniert auch auf einem Server, auf dem ein früherer Versuch dieses Updates fehlgeschlagen ist.`, + pl_PL: `**Magazyn zewnętrzny** może teraz pokazać NextExplorer, zalecany serwer plików: jego dysk Files pojawia się jako folder NextExplorer w Plikach Nextcloud. + +Aktualizacja, która pominęłaby główną wersję Nextcloud, jest odrzucana, zanim cokolwiek zmieni się na dysku. Najpierw zainstaluj wydanie z Nextcloud 33 z listy wersji, a następnie zaktualizuj ponownie — działa to również na serwerze, na którym wcześniejsza próba tej aktualizacji się nie powiodła.`, + fr_FR: `**Stockage externe** peut désormais présenter NextExplorer, le serveur de fichiers recommandé : son lecteur Files apparaît comme un dossier NextExplorer dans Fichiers de Nextcloud. + +Une mise à jour qui sauterait une version majeure de Nextcloud est refusée avant que quoi que ce soit ne change sur le disque. Installez d'abord la version avec Nextcloud 33 depuis la liste des versions, puis mettez à jour à nouveau — cela fonctionne aussi sur un serveur où une tentative précédente de cette mise à jour a échoué.`, }, migrations: { up: async () => {},