//! `GroupFilesService` — the §11.6 shared group-collection FILES contract. //! //! The cross-app-sharing counterpart to [`crate::FilesService`], analogous to //! [`crate::GroupKvService`] / [`crate::GroupDocsService`]. A script reaches it //! via the explicit `files::shared_collection("name")` handle. The owner of the //! data is not `cx.app_id`: the impl resolves the **owning group** by walking the //! calling app's ancestor chain for the nearest group that declares the //! collection shared with `kind='files'`. That ancestry walk is the isolation //! boundary — the trait surface takes **no** group id (owner derivation stays in //! the impl). //! //! Trust model (§11.6): **reads are open** to any subtree script (anonymous //! public HTTP included); **writes require an authenticated principal** with //! editor+ on the owning group. Same shape as group KV/docs. use async_trait::async_trait; use thiserror::Error; use uuid::Uuid; use crate::{FileMeta, FileUpdate, FilesListPage, NewFile, SdkCallCx}; #[async_trait] pub trait GroupFilesService: Send + Sync { async fn create( &self, cx: &SdkCallCx, collection: &str, new: NewFile, ) -> Result; async fn head( &self, cx: &SdkCallCx, collection: &str, id: &str, ) -> Result, GroupFilesError>; async fn get( &self, cx: &SdkCallCx, collection: &str, id: &str, ) -> Result>, GroupFilesError>; async fn update( &self, cx: &SdkCallCx, collection: &str, id: &str, upd: FileUpdate, ) -> Result<(), GroupFilesError>; async fn delete( &self, cx: &SdkCallCx, collection: &str, id: &str, ) -> Result; async fn list( &self, cx: &SdkCallCx, collection: &str, cursor: Option<&str>, limit: u32, ) -> Result; } /// Stub for test bundles that don't exercise shared files collections. #[derive(Debug, Default, Clone, Copy)] pub struct NoopGroupFilesService; #[async_trait] impl GroupFilesService for NoopGroupFilesService { async fn create( &self, _cx: &SdkCallCx, _collection: &str, _new: NewFile, ) -> Result { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } async fn head( &self, _cx: &SdkCallCx, _collection: &str, _id: &str, ) -> Result, GroupFilesError> { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } async fn get( &self, _cx: &SdkCallCx, _collection: &str, _id: &str, ) -> Result>, GroupFilesError> { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } async fn update( &self, _cx: &SdkCallCx, _collection: &str, _id: &str, _upd: FileUpdate, ) -> Result<(), GroupFilesError> { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } async fn delete( &self, _cx: &SdkCallCx, _collection: &str, _id: &str, ) -> Result { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } async fn list( &self, _cx: &SdkCallCx, _collection: &str, _cursor: Option<&str>, _limit: u32, ) -> Result { Err(GroupFilesError::Backend( "group files is not wired in".into(), )) } } /// Failure modes surfaced to the Rhai bridge. Mirrors [`crate::FilesError`] plus /// the §11.6 `CollectionNotShared` boundary. #[derive(Debug, Error)] pub enum GroupFilesError { #[error("invalid collection name: {0}")] InvalidCollection(String), /// No group on the calling app's ancestor chain declares this collection /// shared with `kind='files'` — the structural "not shared with you" /// boundary (also the outcome for a foreign app). #[error("collection {0:?} is not a shared group files collection for this app")] CollectionNotShared(String), #[error("missing required field: {0}")] MissingField(&'static str), #[error("file too large: {size} bytes exceeds limit of {limit} bytes")] TooLarge { size: usize, limit: usize }, /// The owning group's shared-files store is at its total-bytes quota /// (`PICLOUD_GROUP_FILES_MAX_TOTAL_BYTES`); the write is rejected. #[error("group files: quota exceeded ({actual} bytes; limit {limit})")] QuotaExceeded { limit: u64, actual: u64 }, #[error("file name too long: {0} bytes exceeds 255")] NameTooLong(usize), #[error("content_type too long: {0} bytes exceeds 127")] ContentTypeTooLong(usize), #[error("file not found")] NotFound, #[error("file content corrupted (checksum mismatch)")] Corrupted, /// For reads this is only raised when `cx.principal.is_some()`; for WRITES /// it is also raised when the principal is absent (writes fail closed). #[error("forbidden")] Forbidden, #[error("group files backend error: {0}")] Backend(String), } /// Map the shared [`crate::FilesError`] — returned by `validate_collection` /// (re-exported as `validate_files_collection`) and the `NewFile`/`FileUpdate` /// validators that the group-files service reuses — onto the group error. Lives /// here (not in manager-core) so the orphan rule is satisfied. impl From for GroupFilesError { fn from(e: crate::FilesError) -> Self { use crate::FilesError as F; match e { F::InvalidCollection(c) => Self::InvalidCollection(c), F::MissingField(f) => Self::MissingField(f), F::TooLarge { size, limit } => Self::TooLarge { size, limit }, F::NameTooLong(n) => Self::NameTooLong(n), F::ContentTypeTooLong(n) => Self::ContentTypeTooLong(n), F::NotFound => Self::NotFound, F::Corrupted => Self::Corrupted, F::Forbidden => Self::Forbidden, F::Backend(s) => Self::Backend(s), } } }