Files
Oxicloud/src/domain/entities/drive.rs
T

137 lines
5.9 KiB
Rust
Raw Normal View History

2026-06-18 13:29:41 +02:00
//! Drive — the top-level container that owns a tree of folders/files.
//!
//! Drives replaced the per-user `My Folder - <username>` wrapper at D0.
//! Every folder and file row carries a `drive_id` (added by D0's
//! migration); a drive is the natural unit of quota, sharing, and
//! lifecycle. Membership is expressed through `storage.role_grants` rows
//! with `resource_type='drive'` — there is no separate `drive_members`
//! table.
//!
//! ## Kinds
//!
//! Two kinds today; the discriminant is the `kind` column with a CHECK
//! constraint.
//!
//! - **`personal`** — single-user, single-owner. The owner is captured
//! by `default_for_user` (for the default Personal drive) or by an
//! Owner role_grant on a secondary personal drive. Personal drives
//! refuse `add_member`, `remove_member`, and `delete_drive` (when
//! it's the user's only or default drive). A user can have multiple
//! personal drives — one is marked default (`default_for_user =
//! <uid>`), the others are secondaries (`default_for_user = NULL`,
//! one Owner row in role_grants pinning them to the same user).
//!
//! - **`shared`** — multi-member, group-aware, full role roster
//! (viewer / commenter / contributor / editor / owner). Members
//! come from role_grants; group subjects expand transitively via
//! the existing `subject_groups` machinery. Last-owner protection
//! applies on member removal and drive deletion. Quota is set by
//! the drive owner (or admin); `used_bytes` tracks consumption.
//!
//! Future kinds (e.g. `system` for built-in scratch space) drop in by
//! extending the CHECK + the `DriveKind` enum.
//!
//! ## Policies
//!
//! `policies` is a JSONB bag carrying feature flags / capability toggles
//! that drive owners can flip without a schema change. Known keys live in
//! `docs/plan/drive.md` §8 and §15 (e.g. `forbid_public_links`,
//! `include_in_photo_index`, `forbid_music_index`). Unknown keys are
//! preserved by the application — the schema is intentionally permissive
//! so future capability flags can land without a migration.
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Drive kind discriminant. Mirrors the `storage.drives.kind` CHECK
/// constraint values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DriveKind {
/// Single-owner storage compartment. Cannot have members added or
/// removed via the membership API; the owner is fixed for the drive's
/// lifetime.
Personal,
/// Multi-member drive supporting the full role roster. Membership is
/// open to admin/owner-driven changes through the membership API.
Shared,
}
impl DriveKind {
pub fn as_str(self) -> &'static str {
match self {
DriveKind::Personal => "personal",
DriveKind::Shared => "shared",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"personal" => Some(DriveKind::Personal),
"shared" => Some(DriveKind::Shared),
_ => None,
}
}
}
/// Domain entity for a row in `storage.drives`.
///
2026-06-18 23:02:17 +02:00
/// Drives are pure metadata under the D0 design (docs/plan/drive.md §3):
/// no `name` column — the display name lives on the root folder pointed
/// at by `root_folder_id`. Code that needs the name pairs this struct
/// with a JOIN through `storage.folders`; see the repository's
/// `DriveWithRootName` view-model.
///
2026-06-18 13:29:41 +02:00
/// Field-level constraints are enforced at the SQL layer (CHECK on
/// `kind`, partial UNIQUE on `default_for_user`). The struct mirrors
/// the column set 1:1; behaviour beyond field access lives in
2026-06-18 23:02:17 +02:00
/// `DriveRepository` and `DriveService` (post-D0).
2026-06-18 13:29:41 +02:00
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Drive {
/// Stable identifier. Generated server-side at creation.
pub id: Uuid,
/// Discriminant — see [`DriveKind`].
pub kind: DriveKind,
/// Set iff this is the user's default personal drive (UNIQUE in SQL
/// via a partial index `WHERE default_for_user IS NOT NULL`). NULL
/// on shared drives and on secondary personal drives.
pub default_for_user: Option<Uuid>,
2026-06-18 23:02:17 +02:00
/// The drive's mount-point folder. The column is NULLable in SQL
/// only because the atomic creation CTE writes it mid-statement
/// (a column-level `NOT NULL` would refuse the initial drive INSERT
/// — see docs/plan/drive.md §3). After any successful creation path,
/// this is populated; code reading `Drive` may treat it as `Uuid`,
/// not `Option<Uuid>`. A NULL at read time is a data-invariant bug.
pub root_folder_id: Uuid,
2026-06-18 13:29:41 +02:00
/// Soft cap on this drive's storage usage, in bytes. `None` means
/// "no quota" (rare; reserved for admin overrides). The default
/// initial quota for a fresh personal drive is taken from the
2026-06-18 23:02:17 +02:00
/// owner's `auth.users.storage_quota_bytes` at creation time.
/// **Mutation is OxiCloud-admin only** (docs/plan/drive.md §7) —
/// not in the drive `owner` role bundle.
2026-06-18 13:29:41 +02:00
pub quota_bytes: Option<i64>,
/// Running total of bytes consumed. Maintained incrementally by
/// upload/delete paths in D4; on D0 still reflects the pre-Drive
/// per-user counters via the backfill.
pub used_bytes: i64,
/// Capability flags / feature toggles. Extensible JSONB — see
/// `docs/plan/drive.md` §8 and §15 for the known keys.
pub policies: serde_json::Value,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl Drive {
/// `true` for the user's default personal drive (the only drive for
/// which `default_for_user` is set to that user's id).
pub fn is_default_for(&self, user_id: Uuid) -> bool {
self.default_for_user == Some(user_id)
}
/// `true` if this drive is a personal drive of any kind (default or
/// secondary). Encapsulates the kind check at the call site.
pub fn is_personal(&self) -> bool {
matches!(self.kind, DriveKind::Personal)
}
}