feat(drive): complete updated_by created_by
This commit is contained in:
@@ -25,6 +25,10 @@ pub struct FileParts {
|
||||
pub owner_id: Option<Uuid>,
|
||||
/// BLAKE3 content hash. See [`File::content_hash`] for semantics.
|
||||
pub blob_hash: String,
|
||||
/// §14 provenance: original creator. See [`File::created_by`].
|
||||
pub created_by: Option<Uuid>,
|
||||
/// §14 provenance: most recent mutator. See [`File::updated_by`].
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +81,18 @@ pub struct File {
|
||||
/// ETag (the ETag formula may grow to include `modified_at` etc.,
|
||||
/// but `content_hash` remains the raw hash).
|
||||
blob_hash: String,
|
||||
|
||||
/// User that originally created this file (§14 provenance).
|
||||
/// Stamped at INSERT and never updated thereafter. `None` when
|
||||
/// the referenced user has been deleted (FK is `ON DELETE SET
|
||||
/// NULL`) or for stub/DTO-reconstructed files.
|
||||
created_by: Option<Uuid>,
|
||||
|
||||
/// User that performed the most recent mutation that bumped
|
||||
/// `updated_at` (rename, move, content overwrite, trash, restore).
|
||||
/// Authorship signal — distinct from ownership. `None` when the
|
||||
/// referenced user is deleted or for stub/DTO-reconstructed files.
|
||||
updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
// We no longer need this module, now we use a String directly
|
||||
@@ -95,6 +111,8 @@ impl Default for File {
|
||||
modified_at: 0,
|
||||
owner_id: None,
|
||||
blob_hash: String::new(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +152,8 @@ impl File {
|
||||
modified_at: now,
|
||||
owner_id: None,
|
||||
blob_hash: String::new(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +186,8 @@ impl File {
|
||||
modified_at,
|
||||
owner_id: None,
|
||||
blob_hash: String::new(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,6 +229,40 @@ impl File {
|
||||
modified_at: u64,
|
||||
owner_id: Option<Uuid>,
|
||||
blob_hash: String,
|
||||
) -> FileResult<Self> {
|
||||
Self::with_timestamps_blob_hash_and_provenance(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
owner_id,
|
||||
blob_hash,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Full constructor including the §14 provenance columns
|
||||
/// (`created_by` / `updated_by`). PG-row callers use this to
|
||||
/// preserve authorship across reconstruction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_timestamps_blob_hash_and_provenance(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
owner_id: Option<Uuid>,
|
||||
blob_hash: String,
|
||||
created_by: Option<Uuid>,
|
||||
updated_by: Option<Uuid>,
|
||||
) -> FileResult<Self> {
|
||||
let name = normalize_storage_name(&name);
|
||||
if let Err(reason) = validate_storage_name(&name) {
|
||||
@@ -228,6 +284,8 @@ impl File {
|
||||
modified_at,
|
||||
owner_id,
|
||||
blob_hash,
|
||||
created_by,
|
||||
updated_by,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,6 +306,8 @@ impl File {
|
||||
modified_at: self.modified_at,
|
||||
owner_id: self.owner_id,
|
||||
blob_hash: self.blob_hash,
|
||||
created_by: self.created_by,
|
||||
updated_by: self.updated_by,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +411,21 @@ impl File {
|
||||
self.owner_id
|
||||
}
|
||||
|
||||
/// User that originally created this file (§14 provenance).
|
||||
/// `None` when the referenced user has been deleted
|
||||
/// (FK is `ON DELETE SET NULL`) or for stub/DTO entities.
|
||||
pub fn created_by(&self) -> Option<Uuid> {
|
||||
self.created_by
|
||||
}
|
||||
|
||||
/// User that performed the most recent mutation that bumped
|
||||
/// `updated_at`. Authorship signal — distinct from ownership.
|
||||
/// `None` when the referenced user is deleted or for
|
||||
/// stub/DTO entities.
|
||||
pub fn updated_by(&self) -> Option<Uuid> {
|
||||
self.updated_by
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_dto(
|
||||
id: String,
|
||||
@@ -382,6 +457,10 @@ impl File {
|
||||
modified_at,
|
||||
owner_id: None,
|
||||
blob_hash: String::new(),
|
||||
// DTO round-trips don't carry provenance; callers needing
|
||||
// it must reload from the repository.
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,20 @@ pub struct Folder {
|
||||
/// HTTP ETag emitted in PROPFIND/GET/HEAD responses — see
|
||||
/// [`Folder::etag`] for the formula and rationale.
|
||||
tree_modified_at: u64,
|
||||
|
||||
/// User that originally created this folder. Stamped at INSERT
|
||||
/// from the caller's id and never updated afterwards (provenance,
|
||||
/// not ownership — see §14 of the Drive plan). `None` when the
|
||||
/// referenced user is later deleted (FK is `ON DELETE SET NULL`)
|
||||
/// or for stub/DTO-reconstructed folders that never touched the DB.
|
||||
created_by: Option<Uuid>,
|
||||
|
||||
/// User that performed the most recent mutation that touched
|
||||
/// `updated_at` (rename, move, trash, restore, content overwrite).
|
||||
/// Authorship signal — does NOT propagate via the tree-ETag flush
|
||||
/// trigger. `None` when the referenced user is deleted or for
|
||||
/// stub/DTO-reconstructed folders.
|
||||
updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
// We no longer need this module, now we use a String directly
|
||||
@@ -67,6 +81,8 @@ impl Default for Folder {
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
tree_modified_at: 0,
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +135,10 @@ impl Folder {
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
tree_modified_at: now,
|
||||
// Provenance is unknown for in-memory construction; the DB
|
||||
// reconstruction path supplies real values.
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,7 +199,10 @@ impl Folder {
|
||||
/// `tree_modified_at` comes from the trigger-maintained column on
|
||||
/// `storage.folders` and feeds [`Folder::etag`]. `drive_id` is the
|
||||
/// post-D0 `storage.folders.drive_id NOT NULL` column — every
|
||||
/// path-based lookup scopes by this axis.
|
||||
/// path-based lookup scopes by this axis. `created_by` /
|
||||
/// `updated_by` are the §14 provenance columns; both are nullable
|
||||
/// because the M1 FK is `ON DELETE SET NULL` (a deleted user
|
||||
/// leaves authored rows in place).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_timestamps_and_tree(
|
||||
id: String,
|
||||
@@ -191,6 +214,38 @@ impl Folder {
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
tree_modified_at: u64,
|
||||
) -> FolderResult<Self> {
|
||||
Self::with_timestamps_tree_and_provenance(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
parent_id,
|
||||
owner_id,
|
||||
drive_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
tree_modified_at,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Full constructor including the §14 provenance columns
|
||||
/// (`created_by` / `updated_by`). Direct PG-row callers use this
|
||||
/// to preserve authorship through the entity layer.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_timestamps_tree_and_provenance(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
parent_id: Option<String>,
|
||||
owner_id: Option<Uuid>,
|
||||
drive_id: Uuid,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
tree_modified_at: u64,
|
||||
created_by: Option<Uuid>,
|
||||
updated_by: Option<Uuid>,
|
||||
) -> FolderResult<Self> {
|
||||
let name = normalize_storage_name(&name);
|
||||
if let Err(reason) = validate_storage_name(&name) {
|
||||
@@ -210,6 +265,8 @@ impl Folder {
|
||||
created_at,
|
||||
modified_at,
|
||||
tree_modified_at,
|
||||
created_by,
|
||||
updated_by,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +310,22 @@ impl Folder {
|
||||
self.drive_id
|
||||
}
|
||||
|
||||
/// User that originally created this folder (§14 provenance).
|
||||
/// `None` when the referenced user has been deleted
|
||||
/// (FK is `ON DELETE SET NULL`) or for in-memory/DTO-reconstructed
|
||||
/// entities.
|
||||
pub fn created_by(&self) -> Option<Uuid> {
|
||||
self.created_by
|
||||
}
|
||||
|
||||
/// User that performed the most recent mutation that bumped
|
||||
/// `updated_at`. Authorship signal — distinct from ownership.
|
||||
/// `None` when the referenced user has been deleted or for
|
||||
/// in-memory/DTO-reconstructed entities.
|
||||
pub fn updated_by(&self) -> Option<Uuid> {
|
||||
self.updated_by
|
||||
}
|
||||
|
||||
/// Latest descendant-write timestamp. Statement-level Postgres
|
||||
/// triggers enqueue every file/folder write into
|
||||
/// `storage.tree_etag_dirty`; the background `TreeEtagFlushService`
|
||||
@@ -348,6 +421,10 @@ impl Folder {
|
||||
created_at,
|
||||
modified_at,
|
||||
tree_modified_at: modified_at,
|
||||
// DTO round-trips through this constructor lose
|
||||
// provenance; callers that need it reload through the repo.
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +468,10 @@ impl Folder {
|
||||
// ancestors' listings now show a new name, so the
|
||||
// collection has materially changed.
|
||||
tree_modified_at: now,
|
||||
// Provenance is preserved across the in-memory rebuild;
|
||||
// real persisted updates re-read from the DB.
|
||||
created_by: self.created_by,
|
||||
updated_by: self.updated_by,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -425,6 +506,8 @@ impl Folder {
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
tree_modified_at: now,
|
||||
created_by: self.created_by,
|
||||
updated_by: self.updated_by,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
|
||||
/// Registers a file row pointing at a blob already stored in the
|
||||
/// content-addressable chunk store (one blob reference is consumed).
|
||||
///
|
||||
/// `caller_id` stamps both `created_by` and `updated_by`
|
||||
/// (§14 provenance).
|
||||
async fn save_file_with_blob(
|
||||
&self,
|
||||
name: String,
|
||||
@@ -85,17 +88,26 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
content_type: String,
|
||||
blob_hash: &str,
|
||||
size: u64,
|
||||
caller_id: Uuid,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Moves a file to another folder.
|
||||
/// Moves a file to another folder. `caller_id` stamps `updated_by`
|
||||
/// in the same UPDATE that bumps `updated_at` (§14 provenance).
|
||||
async fn move_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
caller_id: Uuid,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Renames a file (same folder, different name).
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
|
||||
/// Renames a file (same folder, different name). `caller_id`
|
||||
/// stamps `updated_by` in the same UPDATE (§14 provenance).
|
||||
async fn rename_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_name: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Deletes a file.
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
@@ -108,24 +120,31 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
///
|
||||
/// Returns `(File, PathBuf)` where `PathBuf` is the destination path for
|
||||
/// the deferred write that the `WriteBehindCache` will perform.
|
||||
///
|
||||
/// `caller_id` stamps both `created_by` and `updated_by`
|
||||
/// (§14 provenance).
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
size: u64,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(File, PathBuf), DomainError>;
|
||||
|
||||
// ── Trash operations ──
|
||||
|
||||
/// Moves a file to the trash
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
/// Moves a file to the trash. `caller_id` stamps `updated_by`
|
||||
/// (§14 provenance).
|
||||
async fn move_to_trash(&self, file_id: &str, caller_id: Uuid) -> Result<(), DomainError>;
|
||||
|
||||
/// Restores a file from the trash to its original location
|
||||
/// Restores a file from the trash to its original location.
|
||||
/// `caller_id` stamps `updated_by` (§14 provenance).
|
||||
async fn restore_from_trash(
|
||||
&self,
|
||||
file_id: &str,
|
||||
original_path: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Permanently deletes a file (used by the trash)
|
||||
|
||||
@@ -18,11 +18,18 @@ use uuid::Uuid;
|
||||
/// Defines the CRUD and management operations required for
|
||||
/// the Folder entity in the storage system.
|
||||
pub trait FolderRepository: Send + Sync + 'static {
|
||||
/// Creates a new folder
|
||||
/// Creates a new folder.
|
||||
///
|
||||
/// `caller_id` is stamped into `created_by` and `updated_by`
|
||||
/// (D0 §14 provenance — authorship belongs to whoever issued the
|
||||
/// create, not to the parent folder's owner). Pre-D2 they're
|
||||
/// silently equivalent (only the owner can write); D2 ships
|
||||
/// shared drives where this distinction matters.
|
||||
async fn create_folder(
|
||||
&self,
|
||||
name: String,
|
||||
parent_id: Option<String>,
|
||||
caller_id: Uuid,
|
||||
) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Gets a folder by its ID
|
||||
@@ -74,14 +81,23 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
/// Renames a folder. `caller_id` is stamped into `updated_by`
|
||||
/// alongside the `updated_at = NOW()` bump (§14 provenance).
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
id: &str,
|
||||
new_name: String,
|
||||
caller_id: Uuid,
|
||||
) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Moves a folder to another parent
|
||||
/// Moves a folder to another parent. `caller_id` is stamped into
|
||||
/// `updated_by` alongside the `updated_at = NOW()` bump
|
||||
/// (§14 provenance).
|
||||
async fn move_folder(
|
||||
&self,
|
||||
id: &str,
|
||||
new_parent_id: Option<&str>,
|
||||
caller_id: Uuid,
|
||||
) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Deletes a folder
|
||||
@@ -102,14 +118,19 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
|
||||
// ── Trash operations ──
|
||||
|
||||
/// Moves a folder to the trash
|
||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
/// Moves a folder to the trash. `caller_id` is stamped into
|
||||
/// `updated_by` for the root row and every cascade-trashed
|
||||
/// descendant (§14 provenance).
|
||||
async fn move_to_trash(&self, folder_id: &str, caller_id: Uuid) -> Result<(), DomainError>;
|
||||
|
||||
/// Restores a folder from the trash to its original location
|
||||
/// Restores a folder from the trash to its original location.
|
||||
/// `caller_id` is stamped into `updated_by` for the root row and
|
||||
/// every cascade-restored descendant (§14 provenance).
|
||||
async fn restore_from_trash(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
original_path: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Permanently deletes a folder (used by the trash)
|
||||
|
||||
Reference in New Issue
Block a user