feat(drive): remove all owner_id from {File,Folder}Dto
This commit is contained in:
@@ -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: '',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user