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).
75 lines
3.0 KiB
Rust
75 lines
3.0 KiB
Rust
mod address_book_pg_repository;
|
|
mod app_password_pg_repository;
|
|
mod calendar_event_pg_repository;
|
|
mod calendar_pg_repository;
|
|
mod contact_group_pg_repository;
|
|
mod contact_persistence_dto;
|
|
mod contact_pg_repository;
|
|
mod device_code_pg_repository;
|
|
mod drive_pg_repository;
|
|
mod external_mount_repository;
|
|
mod face_pg_repository;
|
|
mod favorites_pg_repository;
|
|
pub mod file_metadata_repository;
|
|
mod magic_link_token_pg_repository;
|
|
mod nextcloud_object_id_repository;
|
|
pub mod playlist_pg_repository;
|
|
mod recent_items_pg_repository;
|
|
mod session_pg_repository;
|
|
mod settings_pg_repository;
|
|
mod share_pg_repository;
|
|
mod subject_group_pg_repository;
|
|
pub(crate) mod transaction_utils;
|
|
mod user_pg_repository;
|
|
|
|
// ── Blob-storage repositories ──
|
|
pub mod file_blob_read_repository;
|
|
pub mod file_blob_write_repository;
|
|
pub mod folder_db_repository;
|
|
pub mod trash_db_repository;
|
|
|
|
pub use address_book_pg_repository::AddressBookPgRepository;
|
|
pub use app_password_pg_repository::AppPasswordPgRepository;
|
|
pub use calendar_event_pg_repository::CalendarEventPgRepository;
|
|
pub use calendar_pg_repository::CalendarPgRepository;
|
|
pub use contact_group_pg_repository::ContactGroupPgRepository;
|
|
pub use contact_persistence_dto::*;
|
|
pub use contact_pg_repository::ContactPgRepository;
|
|
pub use device_code_pg_repository::DeviceCodePgRepository;
|
|
pub use drive_pg_repository::DrivePgRepository;
|
|
pub use external_mount_repository::ExternalMountPgRepository;
|
|
pub use face_pg_repository::FacePgRepository;
|
|
pub use favorites_pg_repository::FavoritesPgRepository;
|
|
pub use file_blob_read_repository::FileBlobReadRepository;
|
|
pub use file_blob_write_repository::FileBlobWriteRepository;
|
|
pub use file_metadata_repository::FileMetadataRepository;
|
|
pub use folder_db_repository::FolderDbRepository;
|
|
pub use magic_link_token_pg_repository::MagicLinkTokenPgRepository;
|
|
pub use nextcloud_object_id_repository::NextcloudObjectIdRepository;
|
|
pub use playlist_pg_repository::{
|
|
AudioMetadataPgRepository, PlaylistItemPgRepository, PlaylistPgRepository,
|
|
};
|
|
pub use recent_items_pg_repository::RecentItemsPgRepository;
|
|
pub use session_pg_repository::SessionPgRepository;
|
|
pub use settings_pg_repository::SettingsPgRepository;
|
|
pub use share_pg_repository::SharePgRepository;
|
|
pub use subject_group_pg_repository::SubjectGroupPgRepository;
|
|
pub use trash_db_repository::TrashDbRepository;
|
|
pub use user_pg_repository::UserPgRepository;
|
|
|
|
// ── SQL helpers ─────────────────────────────────────────────────────────────
|
|
|
|
/// Escape SQL `LIKE` / `ILIKE` wildcard characters (`%` and `_`) in user
|
|
/// input and wrap the result in `%…%` for a contains-match.
|
|
///
|
|
/// Without this, a user searching for `100%` would match *every* row because
|
|
/// `%` is a wildcard in LIKE patterns.
|
|
#[inline]
|
|
pub fn like_escape(raw: &str) -> String {
|
|
let escaped = raw
|
|
.replace('\\', "\\\\")
|
|
.replace('%', "\\%")
|
|
.replace('_', "\\_");
|
|
format!("%{escaped}%")
|
|
}
|