3c31695579
Adds the foundation for external file mounts: admin-configured backends (raw host filesystem in v1; sftp/webdav/… as future provider kinds) surfaced as a folder inside a user's drive. Mount contents are virtual/live-passthrough — read straight from the backend, never stored in storage.files — and are a deliberately separate, limited storage type (no dedup/sharing/trash/search). The feature is dark by default (OXICLOUD_ENABLE_EXTERNAL_MOUNTS=false). P1 scope (this PR): data model, the pluggable provider abstraction, and the read-only REST surface (mount listing + download). Read-write (P2), WebDAV/NextCloud path resolution (P3), and the admin UI (P4) follow. Core model - Mount root = a real storage.folders row; authorization for everything inside collapses onto that folder UUID (ltree-ancestry grant cascade). - Children are virtual, addressed by ext:<mount_id>:<base64url(node_id)> where node_id is provider-owned and opaque to the rest of the system. - A lock-free (arc-swap) MountRegistry maps mount-root UUID -> provider; a thin MountRouter::classify() is the single cheap hook handlers call before parsing an id as a UUID. With no mounts configured it always returns Regular, so existing code paths are unchanged. Added - migrations/20260805000000_external_mounts.sql (storage.external_mounts, kind + config JSONB) - domain/services/external_mount_id (id envelope + virtual etags) - application/ports/external_mount_ports (ExternalMountProvider, MountProviderFactory, repo port) - infrastructure local_fs_mount_provider (tokio::fs, symlink-escape-safe) + factory - application MountRegistry + MountRouter, pg ExternalMountRepository - DI wiring (AppState.mount_router), FeaturesConfig.enable_external_mounts - listing branch (FolderService::list_mount_dir_with_perms + folder_handler) and download branch (FileRetrievalService stat/open mount methods + file_handler) Authorization stays in the service layer (authz.require(Resource::Folder(mount_id))); handlers only classify. Cross-backend operations are out of scope for P1. Tests: 529 unit tests + 5 testcontainers integration tests (real Postgres 17), including end-to-end authorization (owner allowed, stranger denied). Line coverage of the new modules is 84–100% (cargo-llvm-cov). Known gap: file_handler::download_mount_file (HTTP glue) needs a full-app test (P4).
48 lines
3.2 KiB
SQL
48 lines
3.2 KiB
SQL
-- ════════════════════════════════════════════════════════════════════════════
|
|
-- External file mounts
|
|
-- ════════════════════════════════════════════════════════════════════════════
|
|
-- Admin-configured mounts that expose an external backend (raw host filesystem
|
|
-- in v1; SFTP/WebDAV/… as future provider `kind`s) as a folder inside a user's
|
|
-- drive. The mount ROOT is a normal `storage.folders` row (so it participates in
|
|
-- ltree, drive scoping, and ACL grants like any folder); everything BELOW it is
|
|
-- virtual — read live from the backend, never stored in `storage.files`.
|
|
--
|
|
-- This table maps a mount-root folder to its backend. `kind` selects the
|
|
-- provider implementation; `config` carries provider-specific connection data
|
|
-- (a stable JSONB bag so adding a new provider kind needs no schema change):
|
|
-- * local_fs → {"path": "/mnt/share"}
|
|
-- * sftp (future) → {"host": "...", "port": 22, "user": "...", "base_path": "..."}
|
|
--
|
|
-- Mount contents are deliberately a LIMITED, SEPARATE storage type: no blob
|
|
-- dedup, no per-file sharing/favorites/trash/search in v1 (deletes are real,
|
|
-- permanent backend deletes). See docs / plan for the forward path.
|
|
-- ════════════════════════════════════════════════════════════════════════════
|
|
|
|
CREATE TABLE IF NOT EXISTS storage.external_mounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- The mount-root folder. Deleting that folder row removes the mount mapping.
|
|
mount_folder_id UUID NOT NULL UNIQUE
|
|
REFERENCES storage.folders(id) ON DELETE CASCADE,
|
|
-- Provider discriminator (selects the ExternalMountProvider implementation).
|
|
kind TEXT NOT NULL DEFAULT 'local_fs',
|
|
-- Provider-specific connection config; shape depends on `kind`.
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
-- Display name (mirrors the folder name; kept for admin listings).
|
|
name TEXT NOT NULL,
|
|
-- Admin/user who owns the mount configuration.
|
|
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
-- When true, the provider refuses all mutations (browse/download only).
|
|
read_only BOOLEAN NOT NULL DEFAULT FALSE,
|
|
-- Visibility policy. 'owner' = only the owner's drive sees it (v1).
|
|
-- Reserved for future 'shared' semantics.
|
|
visibility TEXT NOT NULL DEFAULT 'owner',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_external_mounts_folder
|
|
ON storage.external_mounts(mount_folder_id);
|
|
|
|
COMMENT ON TABLE storage.external_mounts IS
|
|
'Admin-configured external backends (local_fs/sftp/…) surfaced as a mount-root folder; contents are virtual and read live from the provider.';
|