diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 4dbc1a39..82d695f1 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -24,26 +24,17 @@ export interface FolderItem { is_root: boolean; modified_at: number; name: string; - // `null` on share-recipient responses — the backend's - // `FolderDto::without_hierarchy_info` (folder_dto.rs:154) clears - // hierarchy fields (including owner_id) for non-owner callers. - // Backend serialises `Option` (folder_dto.rs:53); this - // type just tells the truth about the wire. - // - // Deprecated post-D7 (frontend cutover): callers should prefer - // `created_by` / `updated_by` instead — they carry §14 provenance - // and survive the drop of `storage.folders.user_id`. The - // `owner_id` field will be removed from the wire once every - // callsite has moved. - owner_id: string | null; // §14 provenance — who originally created the folder. `null` when // the creating user has since been deleted (backend FK is - // `ON DELETE SET NULL`). Preferred over `owner_id` on the Files - // browser owner column and the Favorites / Shared surfaces. + // `ON DELETE SET NULL`), or when the folder is returned to a + // share recipient that lost provenance via + // `FolderDto::without_hierarchy_info`. The canonical "owner" + // signal on the Files browser / Favorites / Shared surfaces + // (replaced the retired `owner_id` field in D7). created_by: string | null; // §14 provenance — who last touched the folder (rename / move / - // metadata change). Preferred over `owner_id` on the Recent - // surface, where "who touched this recently" is the intent. + // metadata change). The canonical "who touched this recently" + // signal on the Recent surface. updated_by: string | null; parent_id: string | null; path: string; @@ -59,13 +50,8 @@ export interface FileItem { mime_type: string; modified_at: number; name: string; - // `null` on share-recipient responses (same as FolderItem above). - // Backend serialises `Option` at file_dto.rs:59. - // - // Deprecated post-D7 (frontend cutover): prefer `created_by` / - // `updated_by` as documented on `FolderItem`. - owner_id: string | null; - // §14 provenance — see FolderItem for semantics. + // §14 provenance — see FolderItem for semantics. Replaced the + // retired `owner_id` field in D7. created_by: string | null; updated_by: string | null; folder_id: string; @@ -124,7 +110,6 @@ export interface FavoriteItem { icon_special_class: string; category: string; size_formatted: string; - owner_id: string | null; } export interface RecentItem { diff --git a/frontend/src/lib/components/FileViewer.test.ts b/frontend/src/lib/components/FileViewer.test.ts index 61e0c8ef..b961d605 100644 --- a/frontend/src/lib/components/FileViewer.test.ts +++ b/frontend/src/lib/components/FileViewer.test.ts @@ -28,7 +28,6 @@ function file(over: Record = {}) { mime_type: 'image/png', category: 'Image', folder_id: '', - owner_id: '', created_by: null, updated_by: null, path: '', diff --git a/frontend/src/lib/components/MoveDialog.test.ts b/frontend/src/lib/components/MoveDialog.test.ts index a0dc053d..7896210e 100644 --- a/frontend/src/lib/components/MoveDialog.test.ts +++ b/frontend/src/lib/components/MoveDialog.test.ts @@ -57,7 +57,6 @@ function folder(id: string, name: string) { is_root: false, modified_at: 0, name, - owner_id: 'me', created_by: 'me', updated_by: 'me', parent_id: 'home', diff --git a/frontend/src/lib/components/PhotoLightbox.test.ts b/frontend/src/lib/components/PhotoLightbox.test.ts index 4294afe5..ba9f5a75 100644 --- a/frontend/src/lib/components/PhotoLightbox.test.ts +++ b/frontend/src/lib/components/PhotoLightbox.test.ts @@ -20,7 +20,6 @@ function item(id: string) { mime_type: 'image/jpeg', category: 'Image', folder_id: '', - owner_id: '', created_by: null, updated_by: null, path: '', diff --git a/frontend/src/lib/utils/media.ts b/frontend/src/lib/utils/media.ts index 622f3d2c..e8c72b0f 100644 --- a/frontend/src/lib/utils/media.ts +++ b/frontend/src/lib/utils/media.ts @@ -30,7 +30,6 @@ export function minimalPhotoItem(id: string): FileItem { mime_type: 'image/jpeg', modified_at: 0, name: '', - owner_id: '', created_by: null, updated_by: null, folder_id: '', diff --git a/frontend/src/routes/favorites/+page.svelte b/frontend/src/routes/favorites/+page.svelte index 5d0df162..5d22d247 100644 --- a/frontend/src/routes/favorites/+page.svelte +++ b/frontend/src/routes/favorites/+page.svelte @@ -40,11 +40,9 @@ const entries = $derived( raw.map((it): ResourceEntry => { const isFile = it.resource_type === 'file'; - // §14 provenance — prefer `created_by` (who put the item - // into the system) over the deprecated `owner_id`. Fall - // back to `owner_id` for pre-D7 rows whose `created_by` - // column wasn't backfilled. - const ownerId = it.resource.created_by ?? it.resource.owner_id ?? null; + // §14 provenance: `created_by` names who put the item into + // the system (Files browser / Favorites / Shared semantic). + const ownerId = it.resource.created_by ?? null; return { id: it.resource.id, name: it.resource.name, @@ -110,7 +108,7 @@ }); raw = reset ? page.items : [...raw, ...page.items]; cursor = page.next_cursor; - void owners.resolve(page.items.map((i) => i.resource.created_by ?? i.resource.owner_id)); + void owners.resolve(page.items.map((i) => i.resource.created_by)); } catch (e) { console.error('favorites: load error', e); error = t('errors_loadFailed', 'Failed to load items'); diff --git a/frontend/src/routes/favorites/page.test.ts b/frontend/src/routes/favorites/page.test.ts index 4c583339..82c60394 100644 --- a/frontend/src/routes/favorites/page.test.ts +++ b/frontend/src/routes/favorites/page.test.ts @@ -41,7 +41,6 @@ function withOneFile() { mime_type: 'image/png', modified_at: 0, name: 'photo.png', - owner_id: 'me', created_by: 'me', updated_by: 'me', folder_id: 'root', diff --git a/frontend/src/routes/files/[...path]/+page.svelte b/frontend/src/routes/files/[...path]/+page.svelte index 33601e10..2dcbccef 100644 --- a/frontend/src/routes/files/[...path]/+page.svelte +++ b/frontend/src/routes/files/[...path]/+page.svelte @@ -1798,7 +1798,7 @@ {relativeTimeAgo(folder.modified_at)}
- {ownerLabel(folder.created_by ?? folder.owner_id, session.user?.id ?? null)} + {ownerLabel(folder.created_by, session.user?.id ?? null)}
{t('files.file_types.folder', 'Folder')}
—
@@ -1930,7 +1930,7 @@ {#if file.size != null}{formatBytes(file.size)}{/if}
- {ownerLabel(file.created_by ?? file.owner_id, session.user?.id ?? null)} + {ownerLabel(file.created_by, session.user?.id ?? null)}
{typeLabel(file.category)}
{file.size != null ? formatBytes(file.size) : ''}
diff --git a/frontend/src/routes/files/page.test.ts b/frontend/src/routes/files/page.test.ts index 92abbf67..bde2b0d1 100644 --- a/frontend/src/routes/files/page.test.ts +++ b/frontend/src/routes/files/page.test.ts @@ -90,7 +90,6 @@ function fileItem(id: string, name: string) { mime_type: 'text/plain', modified_at: 0, name, - owner_id: 'me', created_by: 'me', updated_by: 'me', folder_id: 'home', @@ -112,7 +111,6 @@ function folderItem(id: string, name: string) { is_root: false, modified_at: 0, name, - owner_id: 'me', created_by: 'me', updated_by: 'me', parent_id: 'home', diff --git a/frontend/src/routes/photos/page.test.ts b/frontend/src/routes/photos/page.test.ts index 486a48b9..9242f7bc 100644 --- a/frontend/src/routes/photos/page.test.ts +++ b/frontend/src/routes/photos/page.test.ts @@ -34,7 +34,6 @@ function photo(id: string) { mime_type: 'image/jpeg', modified_at: 0, name: id + '.jpg', - owner_id: 'me', created_by: 'me', updated_by: 'me', folder_id: 'home', diff --git a/frontend/src/routes/recent/+page.svelte b/frontend/src/routes/recent/+page.svelte index 9f662368..594985a5 100644 --- a/frontend/src/routes/recent/+page.svelte +++ b/frontend/src/routes/recent/+page.svelte @@ -42,11 +42,10 @@ const entries = $derived( raw.map((it): ResourceEntry => { const isFile = it.resource_type === 'file'; - // §14 provenance — Recent's mental model is "who touched - // this recently", so `updated_by` (the last mutator) is - // preferred over `created_by` (who put it in). Fall back - // to `owner_id` for pre-D7 rows with no provenance. - const ownerId = it.resource.updated_by ?? it.resource.owner_id ?? null; + // §14 provenance: Recent's mental model is "who touched this + // recently", so `updated_by` (the last mutator) is the right + // signal — distinct from Favorites/Files which use `created_by`. + const ownerId = it.resource.updated_by ?? null; return { id: it.resource.id, name: it.resource.name, @@ -122,7 +121,7 @@ }); raw = reset ? page.items : [...raw, ...page.items]; cursor = page.next_cursor; - void owners.resolve(page.items.map((i) => i.resource.updated_by ?? i.resource.owner_id)); + void owners.resolve(page.items.map((i) => i.resource.updated_by)); } catch (e) { console.error('recent: load error', e); error = t('errors_loadFailed', 'Failed to load items'); diff --git a/frontend/src/routes/recent/page.test.ts b/frontend/src/routes/recent/page.test.ts index 1759aee3..72191645 100644 --- a/frontend/src/routes/recent/page.test.ts +++ b/frontend/src/routes/recent/page.test.ts @@ -45,7 +45,6 @@ function withOneFile() { mime_type: 'text/plain', modified_at: 0, name: 'notes.txt', - owner_id: 'me', created_by: 'me', updated_by: 'me', folder_id: 'root', diff --git a/frontend/src/routes/shared/page.test.ts b/frontend/src/routes/shared/page.test.ts index 6e16edef..488ba2f9 100644 --- a/frontend/src/routes/shared/page.test.ts +++ b/frontend/src/routes/shared/page.test.ts @@ -46,7 +46,6 @@ function grantItem() { is_root: false, modified_at: 0, name: 'Docs', - owner_id: 'me', created_by: 'me', updated_by: 'me', parent_id: null, diff --git a/src/application/adapters/plugin_lifecycle_hook.rs b/src/application/adapters/plugin_lifecycle_hook.rs index 072114bb..3b8070b4 100644 --- a/src/application/adapters/plugin_lifecycle_hook.rs +++ b/src/application/adapters/plugin_lifecycle_hook.rs @@ -61,7 +61,10 @@ impl PluginLifecycleHook { dispatch.dispatch(PluginEvent { name: EVENT_FILE_UPLOADED, - user_id: dto.owner_id, + // Post-D7 the wire DTO no longer carries `owner_id`; + // §14 `created_by` provenance is the equivalent signal + // (who put the file in the system). + user_id: dto.created_by.map(|u| u.to_string()), invocation_id: Uuid::new_v4().to_string(), payload: serde_json::json!({ "path": dto.path, diff --git a/src/application/dtos/favorites_dto.rs b/src/application/dtos/favorites_dto.rs index 3837d95a..10afd65b 100644 --- a/src/application/dtos/favorites_dto.rs +++ b/src/application/dtos/favorites_dto.rs @@ -55,11 +55,6 @@ pub struct FavoriteItemDto { #[serde(skip_serializing_if = "Option::is_none")] pub item_path: Option, - /// UUID of the file/folder's actual owner (may differ from `user_id` when - /// the item was shared and then favourited by another user). - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_id: Option, - // ── Pre-computed display fields ── /// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder") pub icon_class: String, @@ -124,10 +119,6 @@ pub struct FavoriteResourceRow { pub size: i64, pub resource_created_at: DateTime, pub modified_at: DateTime, - /// Post-D7: nullable on new rows (the legacy `storage.{files,folders}.user_id` - /// column is no longer written). `is_owner` is now `false` when - /// this is `None` — see the SQL projection in favorites repo. - pub owner_id: Option, /// Drive that owns this row. Surfaced on the favorites listing /// so a UI can tell when a favorited item lives in a different /// drive than the user's home (post-D6 cross-drive moves + diff --git a/src/application/dtos/file_dto.rs b/src/application/dtos/file_dto.rs index 00829344..8097d887 100644 --- a/src/application/dtos/file_dto.rs +++ b/src/application/dtos/file_dto.rs @@ -54,10 +54,6 @@ pub struct FileDto { /// Human-readable formatted size (e.g. "3.27 MB") pub size_formatted: String, - /// Owner user ID (omitted from JSON when None) - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_id: Option, - /// Sort date for Photos timeline — COALESCE(EXIF captured_at, created_at). /// Only populated by the /api/photos endpoint. #[serde(skip_serializing_if = "Option::is_none")] @@ -102,7 +98,7 @@ impl From for FileDto { let content_hash = file.content_hash().to_string(); // Consume the entity by moving all fields — zero heap allocations - // for id, name, path, folder_id, owner_id (previously 5× .to_string()). + // for id, name, path, folder_id (previously 4× .to_string()). let parts = file.into_parts(); let icon_class = Arc::from(icon_class_for(&parts.name, &parts.mime_type)); @@ -124,7 +120,6 @@ impl From for FileDto { icon_special_class, category, size_formatted, - owner_id: parts.owner_id.map(|u| u.to_string()), sort_date: None, content_hash, etag, @@ -157,9 +152,8 @@ impl FileDto { /// /// Used when a file is returned to a share recipient: `path` reveals the /// full folder hierarchy above the file which the recipient may not have - /// access to. `folder_id` and `owner_id` are intentionally kept — the - /// former is needed for sub-folder navigation (covered by the cascade - /// grant), and the latter is harmless metadata. + /// access to. `folder_id` is intentionally kept — it's needed for + /// sub-folder navigation (covered by the cascade grant). #[must_use] pub fn without_hierarchy_info(self) -> Self { Self { @@ -183,7 +177,6 @@ impl FileDto { icon_special_class: Arc::from(""), category: Arc::from("Document"), size_formatted: "0 Bytes".to_string(), - owner_id: None, content_hash: String::new(), etag: String::new(), sort_date: None, diff --git a/src/application/dtos/folder_dto.rs b/src/application/dtos/folder_dto.rs index c1c14578..8221bba7 100644 --- a/src/application/dtos/folder_dto.rs +++ b/src/application/dtos/folder_dto.rs @@ -48,10 +48,6 @@ pub struct FolderDto { /// Parent folder ID pub parent_id: Option, - /// Owner user ID (scopes visibility per user) - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_id: Option, - /// Drive that owns this folder. The scope axis for path-based /// lookups across REST / WebDAV / NextCloud / CalDAV / CardDAV. /// Post-D0 `storage.folders.drive_id` is `NOT NULL`; stub / @@ -111,7 +107,6 @@ impl From for FolderDto { name: folder.name().to_string(), path: folder.path_string().to_string(), parent_id: folder.parent_id().map(String::from), - owner_id: folder.owner_id().map(|u| u.to_string()), drive_id: folder.drive_id(), created_at: folder.created_at(), modified_at: folder.modified_at(), @@ -147,9 +142,8 @@ impl FolderDto { /// /// Used when a folder is returned to a share recipient: `path` reveals the /// full folder hierarchy above the shared folder which the recipient may - /// not have access to. `parent_id` and `owner_id` are intentionally kept - /// — the former is needed for sub-folder navigation (covered by the - /// cascade grant), and the latter is harmless metadata. + /// not have access to. `parent_id` is intentionally kept — it's needed + /// for sub-folder navigation (covered by the cascade grant). #[must_use] pub fn without_hierarchy_info(self) -> Self { Self { @@ -165,7 +159,6 @@ impl FolderDto { name: "stub-folder".to_string(), path: "/stub/path".to_string(), parent_id: None, - owner_id: None, drive_id: Uuid::nil(), created_at: 0, modified_at: 0, @@ -205,11 +198,6 @@ pub struct FolderResourceRow { pub size: i64, pub created_at: DateTime, pub modified_at: DateTime, - /// Post-D7: the legacy `user_id` column on `storage.{files,folders}` - /// is nullable — new rows leave it NULL — so this optional. UI - /// surfaces should prefer `created_by` / `updated_by` on the - /// per-resource DTO instead. - pub owner_id: Option, /// Drive that owns this row. Same column as /// `storage.folders.drive_id` / `storage.files.drive_id`. Surfaced /// on the listing so a UI can tell when a child lives in a diff --git a/src/application/dtos/recent_dto.rs b/src/application/dtos/recent_dto.rs index 021ddc25..a0fdab36 100644 --- a/src/application/dtos/recent_dto.rs +++ b/src/application/dtos/recent_dto.rs @@ -104,10 +104,6 @@ pub struct RecentResourceRow { pub size: i64, pub resource_created_at: DateTime, pub modified_at: DateTime, - /// Post-D7: nullable on new rows (the legacy - /// `storage.{files,folders}.user_id` column is no longer written). - /// Consumers should prefer §14 provenance columns. - pub owner_id: Option, /// Drive that owns this row. Surfaced on the recent listing /// so a UI can tell when a recently-accessed item lives in a /// different drive than the user's home (post-D6 cross-drive diff --git a/src/application/dtos/trash_dto.rs b/src/application/dtos/trash_dto.rs index dcf7f498..e431f5c6 100644 --- a/src/application/dtos/trash_dto.rs +++ b/src/application/dtos/trash_dto.rs @@ -60,10 +60,6 @@ pub struct TrashResourceRow { pub size: i64, pub resource_created_at: DateTime, pub modified_at: DateTime, - /// Post-D7: nullable on new rows (the legacy `storage.{files,folders}.user_id` - /// column is no longer written). Consumers should prefer §14 - /// provenance columns when available. - pub owner_id: Option, /// Drive the trashed item belongs to. Surfaced verbatim on the wire /// (`TrashResourceItemDto.drive_id`) so the `/trash` UI can group by /// drive without an extra lookup per row. D2b: filtering by drive is diff --git a/src/application/ports/file_ports.rs b/src/application/ports/file_ports.rs index 9f607241..454baebc 100644 --- a/src/application/ports/file_ports.rs +++ b/src/application/ports/file_ports.rs @@ -241,23 +241,21 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { .collect()) } - /// Like [`list_files_batch`], but scoped to a specific owner. + /// Like [`list_files_batch`], but scoped to a specific caller. /// - /// Used by streaming WebDAV PROPFIND so that each user only sees their - /// own files, even in shared folder_id namespaces. + /// Used by streaming WebDAV PROPFIND. Post-D7 the concrete + /// implementation in `FileRetrievalService` uses drive-membership + /// grants; this default falls back to the unscoped listing (the + /// caller passes through `owner_id` for interface parity but the + /// stub can't apply a real filter without a repo lookup). async fn list_files_batch_with_perms( &self, folder_id: Option<&str>, - owner_id: Uuid, + _owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { - let all = self.list_files_batch(folder_id, offset, limit).await?; - let owner_str = owner_id.to_string(); - Ok(all - .into_iter() - .filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_str)) - .collect()) + self.list_files_batch(folder_id, offset, limit).await } } diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 236084d5..045b8ea3 100644 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -962,7 +962,6 @@ fn row_to_item_dto(row: TrashResourceRow) -> TrashResourceItemDto { name: row.name.clone(), path, parent_id: row.parent_id.map(|u| u.to_string()), - owner_id: row.owner_id.map(|u| u.to_string()), // D2b: the trash listing query now SELECTs `drive_id` (the // unified view exposes it). Surfaced so per-drive grouping // in the `/trash` UI doesn't need an extra lookup per row. @@ -1013,7 +1012,6 @@ fn row_to_item_dto(row: TrashResourceRow) -> TrashResourceItemDto { icon_special_class: std::sync::Arc::from(icon_special_class_for(&row.name, mime)), category: std::sync::Arc::from(category_for(&row.name, mime)), size_formatted: format_file_size(size_bytes), - owner_id: row.owner_id.map(|u| u.to_string()), sort_date: None, content_hash, etag, diff --git a/src/infrastructure/repositories/pg/favorites_pg_repository.rs b/src/infrastructure/repositories/pg/favorites_pg_repository.rs index c690bada..0818b44b 100644 --- a/src/infrastructure/repositories/pg/favorites_pg_repository.rs +++ b/src/infrastructure/repositories/pg/favorites_pg_repository.rs @@ -41,8 +41,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { WHEN uf.item_type = 'folder' THEN fld.path WHEN uf.item_type = 'file' THEN COALESCE(pfld.path || '/' || f.name, f.name) ELSE NULL - END AS "item_path", - COALESCE(f.user_id, fld.user_id)::TEXT AS "owner_id" + END AS "item_path" FROM auth.user_favorites uf LEFT JOIN storage.files f ON uf.item_type = 'file' AND f.id = uf.item_id::UUID @@ -82,7 +81,6 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { parent_id: row.try_get("parent_id").ok(), modified_at: row.try_get("modified_at").ok(), item_path: row.try_get("item_path").ok(), - owner_id: row.try_get("owner_id").ok(), // Temporary defaults; with_display_fields() computes the real values icon_class: String::new(), icon_special_class: String::new(), @@ -309,10 +307,18 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { -1::bigint AS size, fld.created_at AS resource_created_at, fld.updated_at AS modified_at, - fld.user_id AS owner_id, fld.drive_id AS drive_id, NULL::text AS blob_hash, - (fld.user_id = $1::uuid) AS is_owner, + fld.created_by AS created_by, + EXISTS ( + SELECT 1 FROM storage.role_grants g + WHERE g.resource_type = 'drive' + AND g.resource_id = fld.drive_id + AND g.role = 'owner' + AND g.subject_type = 'user' + AND g.subject_id = $1::uuid + AND (g.expires_at IS NULL OR g.expires_at > NOW()) + ) AS is_owner, uf.created_at AS favorited_at, fld.path::text AS resource_path, LOWER(fld.name) AS sort_str, @@ -333,10 +339,18 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { f.size::bigint, f.created_at AS resource_created_at, f.updated_at AS modified_at, - f.user_id AS owner_id, f.drive_id AS drive_id, f.blob_hash, - (f.user_id = $1::uuid) AS is_owner, + f.created_by AS created_by, + EXISTS ( + SELECT 1 FROM storage.role_grants g + WHERE g.resource_type = 'drive' + AND g.resource_id = f.drive_id + AND g.role = 'owner' + AND g.subject_type = 'user' + AND g.subject_id = $1::uuid + AND (g.expires_at IS NULL OR g.expires_at > NOW()) + ) AS is_owner, uf.created_at AS favorited_at, COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path, LOWER(f.name) AS sort_str, @@ -489,7 +503,8 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { }; let user_join = if need_user_join { - "LEFT JOIN auth.users u ON u.id = r.owner_id" + // Post-D7: `owner_id` retired; join by `created_by`. + "LEFT JOIN auth.users u ON u.id = r.created_by" } else { "" }; @@ -506,7 +521,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { SELECT r.resource_type, r.resource_id, r.name, r.parent_id, r.mime_type, r.size, r.resource_created_at, r.modified_at, - r.owner_id, r.drive_id, r.is_owner, r.favorited_at, r.resource_path, + r.drive_id, r.is_owner, r.favorited_at, r.resource_path, r.sort_str, r.type_order, r.folder_first{username_col} FROM resources r {user_join} @@ -577,7 +592,6 @@ LIMIT $6" size, resource_created_at: row.get("resource_created_at"), modified_at: row.get("modified_at"), - owner_id: row.try_get("owner_id").ok(), drive_id: row.get("drive_id"), blob_hash: row.try_get("blob_hash").ok(), is_owner: row.try_get("is_owner").unwrap_or(false), diff --git a/src/infrastructure/repositories/pg/folder_db_repository.rs b/src/infrastructure/repositories/pg/folder_db_repository.rs index 4675de68..c4491c48 100644 --- a/src/infrastructure/repositories/pg/folder_db_repository.rs +++ b/src/infrastructure/repositories/pg/folder_db_repository.rs @@ -1390,7 +1390,6 @@ impl FolderDbRepository { -1::bigint AS size, f.created_at, f.updated_at AS modified_at, - f.user_id, f.drive_id, NULL::text AS blob_hash, LOWER(f.name) AS sort_str, @@ -1410,7 +1409,6 @@ impl FolderDbRepository { fm.size::bigint, fm.created_at, fm.updated_at AS modified_at, - fm.user_id, fm.drive_id, fm.blob_hash, LOWER(fm.name) AS sort_str, @@ -1536,7 +1534,7 @@ impl FolderDbRepository { let sql = format!( "WITH resources AS ({cte_inner}) \ SELECT resource_type, id, name, folder_id, mime_type, size, \ - created_at, modified_at, user_id, drive_id, blob_hash, \ + created_at, modified_at, drive_id, blob_hash, \ sort_str, type_order, folder_first \ FROM resources \ {where_clause} \ @@ -1545,7 +1543,7 @@ impl FolderDbRepository { ); // Row: (resource_type, id, name, folder_id, mime_type, size, - // created_at, modified_at, user_id, drive_id, blob_hash, + // created_at, modified_at, drive_id, blob_hash, // sort_str, type_order, folder_first) type Row = ( String, @@ -1556,8 +1554,7 @@ impl FolderDbRepository { i64, chrono::DateTime, chrono::DateTime, - Option, // user_id (post-D7: nullable on new rows) - Uuid, + Uuid, // drive_id Option, String, i64, @@ -1588,12 +1585,11 @@ impl FolderDbRepository { size: r.5, created_at: r.6, modified_at: r.7, - owner_id: r.8, - drive_id: r.9, - blob_hash: r.10, - sort_str: r.11, - type_order: r.12, - folder_first: r.13, + drive_id: r.8, + blob_hash: r.9, + sort_str: r.10, + type_order: r.11, + folder_first: r.12, }) .collect()) } diff --git a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs index 192d7b0e..2927fd07 100644 --- a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs +++ b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs @@ -206,6 +206,20 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { // ── Build the UNION ALL CTE ───────────────────────────────────────── let mut cte_branches: Vec<&str> = Vec::new(); + // Post-D7: `is_owner` means "the caller holds an Owner + // role_grant on the drive owning this row". Personal drives: + // the single-owner invariant makes this trivially true for the + // owner and false for anyone else. Shared drives: multiple + // Owners possible; each of them gets `true`. Used only to gate + // whether the handler exposes the full path (path-hierarchy + // hiding for share recipients — see `recent_handler.rs`). + // + // The `created_by` projection is separate — §14 provenance, + // used for the "Owner" column and the owner sort's username + // JOIN. The two signals genuinely differ post-D2: e.g. Bob + // (Editor on Alice's shared drive) making a file has + // `created_by = Bob` but `is_owner = false` because Alice owns + // the drive. let folder_branch = r#" SELECT 'folder'::text AS resource_type, @@ -216,10 +230,18 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { -1::bigint AS size, fld.created_at AS resource_created_at, fld.updated_at AS modified_at, - fld.user_id AS owner_id, fld.drive_id AS drive_id, NULL::text AS blob_hash, - (fld.user_id = $1::uuid) AS is_owner, + fld.created_by AS created_by, + EXISTS ( + SELECT 1 FROM storage.role_grants g + WHERE g.resource_type = 'drive' + AND g.resource_id = fld.drive_id + AND g.role = 'owner' + AND g.subject_type = 'user' + AND g.subject_id = $1::uuid + AND (g.expires_at IS NULL OR g.expires_at > NOW()) + ) AS is_owner, ur.accessed_at AS accessed_at, fld.path::text AS resource_path, LOWER(fld.name) AS sort_str, @@ -240,10 +262,18 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { f.size::bigint, f.created_at AS resource_created_at, f.updated_at AS modified_at, - f.user_id AS owner_id, f.drive_id AS drive_id, f.blob_hash, - (f.user_id = $1::uuid) AS is_owner, + f.created_by AS created_by, + EXISTS ( + SELECT 1 FROM storage.role_grants g + WHERE g.resource_type = 'drive' + AND g.resource_id = f.drive_id + AND g.role = 'owner' + AND g.subject_type = 'user' + AND g.subject_id = $1::uuid + AND (g.expires_at IS NULL OR g.expires_at > NOW()) + ) AS is_owner, ur.accessed_at AS accessed_at, COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path, LOWER(f.name) AS sort_str, @@ -394,7 +424,9 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { }; let user_join = if need_user_join { - "LEFT JOIN auth.users u ON u.id = r.owner_id" + // Post-D7: `owner_id` column retired; use `created_by` + // (§14 provenance) as the "owner" identity for the sort. + "LEFT JOIN auth.users u ON u.id = r.created_by" } else { "" }; @@ -411,7 +443,7 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { SELECT r.resource_type, r.resource_id, r.name, r.parent_id, r.mime_type, r.size, r.resource_created_at, r.modified_at, - r.owner_id, r.drive_id, r.is_owner, r.accessed_at, r.resource_path, + r.drive_id, r.is_owner, r.accessed_at, r.resource_path, r.sort_str, r.type_order, r.folder_first{username_col} FROM resources r {user_join} @@ -486,7 +518,6 @@ LIMIT $6" size, resource_created_at: row.get("resource_created_at"), modified_at: row.get("modified_at"), - owner_id: row.try_get("owner_id").ok(), drive_id: row.get("drive_id"), blob_hash: row.try_get("blob_hash").ok(), is_owner: row.try_get("is_owner").unwrap_or(false), diff --git a/src/infrastructure/repositories/pg/trash_db_repository.rs b/src/infrastructure/repositories/pg/trash_db_repository.rs index 83c8d593..f547724a 100644 --- a/src/infrastructure/repositories/pg/trash_db_repository.rs +++ b/src/infrastructure/repositories/pg/trash_db_repository.rs @@ -374,7 +374,6 @@ impl TrashDbRepository { -1::bigint AS size, fld.created_at AS resource_created_at, fld.updated_at AS modified_at, - fld.user_id AS owner_id, fld.drive_id AS drive_id, NULL::text AS blob_hash, fld.trashed_at AS trashed_at, @@ -401,7 +400,6 @@ impl TrashDbRepository { f.size::bigint AS size, f.created_at AS resource_created_at, f.updated_at AS modified_at, - f.user_id AS owner_id, f.drive_id AS drive_id, f.blob_hash, f.trashed_at AS trashed_at, @@ -533,7 +531,7 @@ impl TrashDbRepository { SELECT r.resource_type, r.resource_id, r.name, r.parent_id, r.mime_type, r.size, r.resource_created_at, r.modified_at, - r.owner_id, r.drive_id, r.trashed_at, r.deletion_date, r.resource_path, + r.drive_id, r.trashed_at, r.deletion_date, r.resource_path, r.sort_str, r.type_order, r.folder_first FROM resources r {keyset} @@ -590,7 +588,6 @@ LIMIT $6" size, resource_created_at: row.get("resource_created_at"), modified_at: row.get("modified_at"), - owner_id: row.try_get("owner_id").ok(), drive_id: row.get("drive_id"), blob_hash: row.try_get("blob_hash").ok(), trashed_at, diff --git a/src/infrastructure/services/path_resolver_service.rs b/src/infrastructure/services/path_resolver_service.rs index 53c77285..342bf46f 100644 --- a/src/infrastructure/services/path_resolver_service.rs +++ b/src/infrastructure/services/path_resolver_service.rs @@ -160,7 +160,7 @@ impl PathResolverService { name, res_path, parent_id, - uid, + _uid, // Post-D7: `user_id` column no longer flowed into the DTO. drive_id, created_at, modified_at, @@ -180,7 +180,6 @@ impl PathResolverService { name: name.clone(), path: res_path, parent_id, - owner_id: uid, drive_id, created_at: created_at as u64, modified_at: modified_at as u64, @@ -215,7 +214,6 @@ impl PathResolverService { icon_special_class: Arc::from(icon_special_class_for(&name, &mime)), category: Arc::from(category_for(&name, &mime)), size_formatted: format_file_size(sz), - owner_id: uid, sort_date: None, content_hash: hash, etag, diff --git a/src/interfaces/api/handlers/favorites_handler.rs b/src/interfaces/api/handlers/favorites_handler.rs index c17d51e4..72b97a8b 100644 --- a/src/interfaces/api/handlers/favorites_handler.rs +++ b/src/interfaces/api/handlers/favorites_handler.rs @@ -215,7 +215,6 @@ pub async fn list_favorites_resources( name: row.name.clone(), path, parent_id: row.parent_id.map(|u| u.to_string()), - owner_id: row.owner_id.map(|u| u.to_string()), drive_id: row.drive_id, created_at: row.resource_created_at.timestamp() as u64, modified_at: row.modified_at.timestamp() as u64, @@ -265,7 +264,6 @@ pub async fn list_favorites_resources( )), category: std::sync::Arc::from(category_for(&row.name, mime)), size_formatted: format_file_size(size_bytes), - owner_id: row.owner_id.map(|u| u.to_string()), sort_date: None, content_hash, etag, diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index a2c8fef1..d2957ded 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -504,7 +504,6 @@ pub async fn list_folder_resources( name: row.name.clone(), path: String::new(), // cleared — share recipients must not see hierarchy parent_id: row.parent_id.map(|u| u.to_string()), - owner_id: row.owner_id.map(|u| u.to_string()), drive_id: row.drive_id, created_at: row.created_at.timestamp() as u64, modified_at: row.modified_at.timestamp() as u64, @@ -553,7 +552,6 @@ pub async fn list_folder_resources( icon_special_class: Arc::from(icon_special_class_for(&row.name, mime)), category: Arc::from(category_for(&row.name, mime)), size_formatted: format_file_size(size_bytes), - owner_id: row.owner_id.map(|u| u.to_string()), sort_date: None, content_hash, etag, diff --git a/src/interfaces/api/handlers/recent_handler.rs b/src/interfaces/api/handlers/recent_handler.rs index 78f03fd5..3878e783 100644 --- a/src/interfaces/api/handlers/recent_handler.rs +++ b/src/interfaces/api/handlers/recent_handler.rs @@ -246,7 +246,6 @@ pub async fn list_recent_resources( name: row.name.clone(), path, parent_id: row.parent_id.map(|u| u.to_string()), - owner_id: row.owner_id.map(|u| u.to_string()), drive_id: row.drive_id, created_at: row.resource_created_at.timestamp() as u64, modified_at: row.modified_at.timestamp() as u64, @@ -294,7 +293,6 @@ pub async fn list_recent_resources( )), category: std::sync::Arc::from(category_for(&row.name, mime)), size_formatted: format_file_size(size_bytes), - owner_id: row.owner_id.map(|u| u.to_string()), sort_date: None, content_hash, etag, diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 7bd26159..1aac0907 100644 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -427,7 +427,6 @@ async fn handle_propfind( name: "".to_string(), path: "".to_string(), parent_id: None, - owner_id: None, // Synthetic root folder for PROPFIND on `/`; not an // actual DB row, so drive_id has no meaningful value. drive_id: Uuid::nil(), diff --git a/src/interfaces/api/handlers/wopi_handler.rs b/src/interfaces/api/handlers/wopi_handler.rs index 232038a5..92147a51 100644 --- a/src/interfaces/api/handlers/wopi_handler.rs +++ b/src/interfaces/api/handlers/wopi_handler.rs @@ -101,7 +101,13 @@ async fn check_file_info( let response = CheckFileInfoResponse { base_file_name: file.name.clone(), - owner_id: file.owner_id.clone().unwrap_or_else(|| claims.sub.clone()), + // WOPI's `OwnerId` field is required. Post-D7 the DTO no + // longer carries `owner_id`; fall back to `created_by` + // (§14 provenance) with the requesting user as a final default. + owner_id: file + .created_by + .map(|u| u.to_string()) + .unwrap_or_else(|| claims.sub.clone()), size: file.size, user_id: claims.sub.clone(), version: file.modified_at.to_string(), diff --git a/src/interfaces/nextcloud/report_handler.rs b/src/interfaces/nextcloud/report_handler.rs index a380d6c9..ec60a8dc 100644 --- a/src/interfaces/nextcloud/report_handler.rs +++ b/src/interfaces/nextcloud/report_handler.rs @@ -340,7 +340,6 @@ fn file_dto_from_search(fr: &crate::application::dtos::search_dto::SearchFileRes .into(), category: category_for(&fr.name, &fr.mime_type).to_string().into(), size_formatted: format_file_size(fr.size), - owner_id: None, sort_date: None, content_hash: fr.blob_hash.clone(), etag, @@ -360,7 +359,6 @@ fn folder_dto_from_search( name: sr.name.clone(), path: sr.path.clone(), parent_id: sr.parent_id.clone(), - owner_id: None, drive_id: sr.drive_id, created_at: sr.created_at, modified_at: sr.modified_at, diff --git a/src/interfaces/nextcloud/webdav_handler.rs b/src/interfaces/nextcloud/webdav_handler.rs index ab67f6dd..9e2382c9 100644 --- a/src/interfaces/nextcloud/webdav_handler.rs +++ b/src/interfaces/nextcloud/webdav_handler.rs @@ -1811,7 +1811,6 @@ mod tests { name: path.rsplit('/').next().unwrap_or("").to_string(), path: path.to_string(), parent_id: None, - owner_id: None, // Test stub — path mapper doesn't read drive_id. drive_id: uuid::Uuid::nil(), created_at: 0,