feat(drive): remove all owner_id from {File,Folder}Dto

This commit is contained in:
Edouard Vanbelle
2026-07-03 00:48:14 +02:00
parent 29bcf48eb7
commit 37467ed9d3
33 changed files with 116 additions and 149 deletions
+9 -24
View File
@@ -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<String>` (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<String>` 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 {
@@ -28,7 +28,6 @@ function file(over: Record<string, unknown> = {}) {
mime_type: 'image/png',
category: 'Image',
folder_id: '',
owner_id: '',
created_by: null,
updated_by: null,
path: '',
@@ -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',
@@ -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: '',
-1
View File
@@ -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: '',
+4 -6
View File
@@ -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');
@@ -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',
@@ -1798,7 +1798,7 @@
<span class="grid-meta__date">{relativeTimeAgo(folder.modified_at)}</span>
</div>
<div class="owner-cell">
{ownerLabel(folder.created_by ?? folder.owner_id, session.user?.id ?? null)}
{ownerLabel(folder.created_by, session.user?.id ?? null)}
</div>
<div class="type-cell">{t('files.file_types.folder', 'Folder')}</div>
<div class="size-cell">—</div>
@@ -1930,7 +1930,7 @@
{#if file.size != null}<span class="grid-meta__size">{formatBytes(file.size)}</span>{/if}
</div>
<div class="owner-cell">
{ownerLabel(file.created_by ?? file.owner_id, session.user?.id ?? null)}
{ownerLabel(file.created_by, session.user?.id ?? null)}
</div>
<div class="type-cell">{typeLabel(file.category)}</div>
<div class="size-cell">{file.size != null ? formatBytes(file.size) : ''}</div>
-2
View File
@@ -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',
-1
View File
@@ -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',
+5 -6
View File
@@ -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');
-1
View File
@@ -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',
-1
View File
@@ -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,
@@ -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,
-9
View File
@@ -55,11 +55,6 @@ pub struct FavoriteItemDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub item_path: Option<String>,
/// 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<String>,
// ── 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<Utc>,
pub modified_at: DateTime<Utc>,
/// 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<Uuid>,
/// 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 +
+3 -10
View File
@@ -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<String>,
/// 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<File> 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<File> 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,
+2 -14
View File
@@ -48,10 +48,6 @@ pub struct FolderDto {
/// Parent folder ID
pub parent_id: Option<String>,
/// Owner user ID (scopes visibility per user)
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_id: Option<String>,
/// 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<Folder> 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<Utc>,
pub modified_at: DateTime<Utc>,
/// 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<Uuid>,
/// 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
-4
View File
@@ -104,10 +104,6 @@ pub struct RecentResourceRow {
pub size: i64,
pub resource_created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
/// 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<Uuid>,
/// 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
-4
View File
@@ -60,10 +60,6 @@ pub struct TrashResourceRow {
pub size: i64,
pub resource_created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
/// 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<Uuid>,
/// 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
+8 -10
View File
@@ -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<Vec<FileDto>, 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
}
}
@@ -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,
@@ -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),
@@ -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::Utc>,
chrono::DateTime<chrono::Utc>,
Option<Uuid>, // user_id (post-D7: nullable on new rows)
Uuid,
Uuid, // drive_id
Option<String>,
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())
}
@@ -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),
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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(),
+7 -1
View File
@@ -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(),
@@ -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,
@@ -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,