feat(nextcloud): per-user Nextcloud library integration

Lets each mule-image user (matched via OIDC preferred_username,
overridable in Settings) browse their Nextcloud files/ tree from the
mule-image UI and register subfolders as per-user SourceRoots. Reads
stay direct on the bind-mounted /nextcloud-users path; mutations
(upload, delete, rename, move within NC) dispatch through Nextcloud
WebDAV so oc_filecache, trashbin, comments, and desktop-sync clients
stay coherent.

Backend:
- users.nextcloud_username + nextcloud_app_password_enc (Fernet at rest,
  key derived from SECRET_KEY) — alembic 0016
- services/nextcloud_dav.py: minimal WebDAV client (PUT, MKCOL, DELETE,
  MOVE) with HTTP Basic auth via the per-user app password
- routers/nextcloud.py: GET /browse, /whoami, GET/POST/DELETE
  /source-roots (path-scoped to current_user.nextcloud_username with
  realpath traversal guard)
- PATCH /api/v1/auth/me to update nextcloud_username and app password
- OIDC callback defaults nextcloud_username from preferred_username on
  first login; backfill on existing users; never overwrites a manual
  override
- routers/upload.py: stream upload to NamedTemporaryFile, then PUT to
  WebDAV (with MKCOL chain) when destination is NC-rooted; existing
  Photo row creation runs unchanged
- routers/discard.py empty-trash: WebDAV DELETE for NC files
- routers/photos.py rename + move: WebDAV MOVE for NC paths;
  cross-system move/copy returns a clean error
- routers/folders.py rename + create + permanent-delete: dispatch via
  WebDAV when targeting NC-rooted paths

Frontend:
- AuthUser carries nextcloud_username + has_nextcloud_app_password
- services/api.ts: nextcloud + account namespaces
- components/dialogs/NextcloudFolderPicker.tsx: lazy tree browser, name
  + submit -> POST /source-roots
- SettingsDialog: new "Nextcloud library" card with username override +
  validate, app-password input, list/remove of NC libraries, and the
  picker entry point

docker-compose.yml: NEXTCLOUD_USERS_HOST_PATH bind to /nextcloud-users
on backend + 3 workers; NEXTCLOUD_USERS_ROOT + NEXTCLOUD_BASE_URL env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-04-26 01:06:37 +02:00
parent 80dd9d0a8b
commit bc0bb44c05
16 changed files with 1635 additions and 48 deletions

View File

@@ -32,6 +32,14 @@ services:
# filesystem; flip to :ro for a strict read-only library and the
# write endpoints will return EROFS.
- ${PHOTO_DIRS:-./photos}:/photos:rw
# Optional Nextcloud integration: mount the homecloud data dir so
# users can register subfolders of their Nextcloud `files/` tree
# as per-user SourceRoots. Reads use this path directly; mutations
# (upload, delete, rename, move) dispatch via WebDAV against
# NEXTCLOUD_BASE_URL so Nextcloud's oc_filecache stays in sync.
# Leave NEXTCLOUD_USERS_HOST_PATH unset (or pointing at a no-op
# path) to disable.
- ${NEXTCLOUD_USERS_HOST_PATH:-./photos}:/nextcloud-users:rw
- thumbs_data:/data/thumbs
- proxies_data:/data/proxies
- db_data:/data/db # retained so the docker-compose.sqlite.yml override has somewhere to put mulita.db
@@ -71,6 +79,16 @@ services:
- OIDC_ADMIN_GROUPS=${OIDC_ADMIN_GROUPS:-}
- OIDC_LINK_BY_USERNAME=${OIDC_LINK_BY_USERNAME:-false}
- SESSION_SECRET=${SESSION_SECRET:-}
# Nextcloud integration. NEXTCLOUD_USERS_ROOT is the in-container
# path that NEXTCLOUD_USERS_HOST_PATH binds to. NEXTCLOUD_BASE_URL
# is the public-facing Nextcloud URL used for outgoing WebDAV
# calls (must be reachable from the backend container; e.g.
# https://cloud.example.com or http://nextcloud:80 if you put it
# on the same docker network). Leave NEXTCLOUD_BASE_URL unset to
# keep the integration off — the router endpoints stay registered
# but mutating endpoints fail with a clear error.
- NEXTCLOUD_USERS_ROOT=${NEXTCLOUD_USERS_ROOT:-/nextcloud-users}
- NEXTCLOUD_BASE_URL=${NEXTCLOUD_BASE_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
depends_on:
@@ -113,6 +131,7 @@ services:
volumes:
- ./mulita.yml:/app/config/mulita.yml:ro
- ${PHOTO_DIRS:-./photos}:/photos:rw
- ${NEXTCLOUD_USERS_HOST_PATH:-./photos}:/nextcloud-users:rw
- thumbs_data:/data/thumbs
- proxies_data:/data/proxies
- db_data:/data/db
@@ -123,6 +142,8 @@ services:
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
- PHOTO_DIRS=/photos
- NEXTCLOUD_USERS_ROOT=${NEXTCLOUD_USERS_ROOT:-/nextcloud-users}
- NEXTCLOUD_BASE_URL=${NEXTCLOUD_BASE_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
# NullPool — see app/database.py for rationale.
@@ -156,6 +177,7 @@ services:
volumes:
- ./mulita.yml:/app/config/mulita.yml:ro
- ${PHOTO_DIRS:-./photos}:/photos:rw
- ${NEXTCLOUD_USERS_HOST_PATH:-./photos}:/nextcloud-users:rw
- db_data:/data/db
environment:
- DATABASE_URL=postgresql+asyncpg://mulita:mulita@db:5432/mulita
@@ -163,6 +185,8 @@ services:
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
- PHOTO_DIRS=/photos
- NEXTCLOUD_USERS_ROOT=${NEXTCLOUD_USERS_ROOT:-/nextcloud-users}
- NEXTCLOUD_BASE_URL=${NEXTCLOUD_BASE_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
- MULITA_CELERY_WORKER=1
@@ -185,6 +209,7 @@ services:
volumes:
- ./mulita.yml:/app/config/mulita.yml:ro
- ${PHOTO_DIRS:-./photos}:/photos:rw
- ${NEXTCLOUD_USERS_HOST_PATH:-./photos}:/nextcloud-users:rw
- thumbs_data:/data/thumbs
- proxies_data:/data/proxies
- db_data:/data/db
@@ -195,6 +220,8 @@ services:
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
- PHOTO_DIRS=/photos
- NEXTCLOUD_USERS_ROOT=${NEXTCLOUD_USERS_ROOT:-/nextcloud-users}
- NEXTCLOUD_BASE_URL=${NEXTCLOUD_BASE_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
- MULITA_CELERY_WORKER=1