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;
|
is_root: boolean;
|
||||||
modified_at: number;
|
modified_at: number;
|
||||||
name: string;
|
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
|
// §14 provenance — who originally created the folder. `null` when
|
||||||
// the creating user has since been deleted (backend FK is
|
// the creating user has since been deleted (backend FK is
|
||||||
// `ON DELETE SET NULL`). Preferred over `owner_id` on the Files
|
// `ON DELETE SET NULL`), or when the folder is returned to a
|
||||||
// browser owner column and the Favorites / Shared surfaces.
|
// 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;
|
created_by: string | null;
|
||||||
// §14 provenance — who last touched the folder (rename / move /
|
// §14 provenance — who last touched the folder (rename / move /
|
||||||
// metadata change). Preferred over `owner_id` on the Recent
|
// metadata change). The canonical "who touched this recently"
|
||||||
// surface, where "who touched this recently" is the intent.
|
// signal on the Recent surface.
|
||||||
updated_by: string | null;
|
updated_by: string | null;
|
||||||
parent_id: string | null;
|
parent_id: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -59,13 +50,8 @@ export interface FileItem {
|
|||||||
mime_type: string;
|
mime_type: string;
|
||||||
modified_at: number;
|
modified_at: number;
|
||||||
name: string;
|
name: string;
|
||||||
// `null` on share-recipient responses (same as FolderItem above).
|
// §14 provenance — see FolderItem for semantics. Replaced the
|
||||||
// Backend serialises `Option<String>` at file_dto.rs:59.
|
// retired `owner_id` field in D7.
|
||||||
//
|
|
||||||
// Deprecated post-D7 (frontend cutover): prefer `created_by` /
|
|
||||||
// `updated_by` as documented on `FolderItem`.
|
|
||||||
owner_id: string | null;
|
|
||||||
// §14 provenance — see FolderItem for semantics.
|
|
||||||
created_by: string | null;
|
created_by: string | null;
|
||||||
updated_by: string | null;
|
updated_by: string | null;
|
||||||
folder_id: string;
|
folder_id: string;
|
||||||
@@ -124,7 +110,6 @@ export interface FavoriteItem {
|
|||||||
icon_special_class: string;
|
icon_special_class: string;
|
||||||
category: string;
|
category: string;
|
||||||
size_formatted: string;
|
size_formatted: string;
|
||||||
owner_id: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecentItem {
|
export interface RecentItem {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function file(over: Record<string, unknown> = {}) {
|
|||||||
mime_type: 'image/png',
|
mime_type: 'image/png',
|
||||||
category: 'Image',
|
category: 'Image',
|
||||||
folder_id: '',
|
folder_id: '',
|
||||||
owner_id: '',
|
|
||||||
created_by: null,
|
created_by: null,
|
||||||
updated_by: null,
|
updated_by: null,
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ function folder(id: string, name: string) {
|
|||||||
is_root: false,
|
is_root: false,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name,
|
name,
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
parent_id: 'home',
|
parent_id: 'home',
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ function item(id: string) {
|
|||||||
mime_type: 'image/jpeg',
|
mime_type: 'image/jpeg',
|
||||||
category: 'Image',
|
category: 'Image',
|
||||||
folder_id: '',
|
folder_id: '',
|
||||||
owner_id: '',
|
|
||||||
created_by: null,
|
created_by: null,
|
||||||
updated_by: null,
|
updated_by: null,
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export function minimalPhotoItem(id: string): FileItem {
|
|||||||
mime_type: 'image/jpeg',
|
mime_type: 'image/jpeg',
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name: '',
|
name: '',
|
||||||
owner_id: '',
|
|
||||||
created_by: null,
|
created_by: null,
|
||||||
updated_by: null,
|
updated_by: null,
|
||||||
folder_id: '',
|
folder_id: '',
|
||||||
|
|||||||
@@ -40,11 +40,9 @@
|
|||||||
const entries = $derived(
|
const entries = $derived(
|
||||||
raw.map((it): ResourceEntry => {
|
raw.map((it): ResourceEntry => {
|
||||||
const isFile = it.resource_type === 'file';
|
const isFile = it.resource_type === 'file';
|
||||||
// §14 provenance — prefer `created_by` (who put the item
|
// §14 provenance: `created_by` names who put the item into
|
||||||
// into the system) over the deprecated `owner_id`. Fall
|
// the system (Files browser / Favorites / Shared semantic).
|
||||||
// back to `owner_id` for pre-D7 rows whose `created_by`
|
const ownerId = it.resource.created_by ?? null;
|
||||||
// column wasn't backfilled.
|
|
||||||
const ownerId = it.resource.created_by ?? it.resource.owner_id ?? null;
|
|
||||||
return {
|
return {
|
||||||
id: it.resource.id,
|
id: it.resource.id,
|
||||||
name: it.resource.name,
|
name: it.resource.name,
|
||||||
@@ -110,7 +108,7 @@
|
|||||||
});
|
});
|
||||||
raw = reset ? page.items : [...raw, ...page.items];
|
raw = reset ? page.items : [...raw, ...page.items];
|
||||||
cursor = page.next_cursor;
|
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) {
|
} catch (e) {
|
||||||
console.error('favorites: load error', e);
|
console.error('favorites: load error', e);
|
||||||
error = t('errors_loadFailed', 'Failed to load items');
|
error = t('errors_loadFailed', 'Failed to load items');
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ function withOneFile() {
|
|||||||
mime_type: 'image/png',
|
mime_type: 'image/png',
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name: 'photo.png',
|
name: 'photo.png',
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
folder_id: 'root',
|
folder_id: 'root',
|
||||||
|
|||||||
@@ -1798,7 +1798,7 @@
|
|||||||
<span class="grid-meta__date">{relativeTimeAgo(folder.modified_at)}</span>
|
<span class="grid-meta__date">{relativeTimeAgo(folder.modified_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="owner-cell">
|
<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>
|
||||||
<div class="type-cell">{t('files.file_types.folder', 'Folder')}</div>
|
<div class="type-cell">{t('files.file_types.folder', 'Folder')}</div>
|
||||||
<div class="size-cell">—</div>
|
<div class="size-cell">—</div>
|
||||||
@@ -1930,7 +1930,7 @@
|
|||||||
{#if file.size != null}<span class="grid-meta__size">{formatBytes(file.size)}</span>{/if}
|
{#if file.size != null}<span class="grid-meta__size">{formatBytes(file.size)}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="owner-cell">
|
<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>
|
||||||
<div class="type-cell">{typeLabel(file.category)}</div>
|
<div class="type-cell">{typeLabel(file.category)}</div>
|
||||||
<div class="size-cell">{file.size != null ? formatBytes(file.size) : ''}</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',
|
mime_type: 'text/plain',
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name,
|
name,
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
folder_id: 'home',
|
folder_id: 'home',
|
||||||
@@ -112,7 +111,6 @@ function folderItem(id: string, name: string) {
|
|||||||
is_root: false,
|
is_root: false,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name,
|
name,
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
parent_id: 'home',
|
parent_id: 'home',
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ function photo(id: string) {
|
|||||||
mime_type: 'image/jpeg',
|
mime_type: 'image/jpeg',
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name: id + '.jpg',
|
name: id + '.jpg',
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
folder_id: 'home',
|
folder_id: 'home',
|
||||||
|
|||||||
@@ -42,11 +42,10 @@
|
|||||||
const entries = $derived(
|
const entries = $derived(
|
||||||
raw.map((it): ResourceEntry => {
|
raw.map((it): ResourceEntry => {
|
||||||
const isFile = it.resource_type === 'file';
|
const isFile = it.resource_type === 'file';
|
||||||
// §14 provenance — Recent's mental model is "who touched
|
// §14 provenance: Recent's mental model is "who touched this
|
||||||
// this recently", so `updated_by` (the last mutator) is
|
// recently", so `updated_by` (the last mutator) is the right
|
||||||
// preferred over `created_by` (who put it in). Fall back
|
// signal — distinct from Favorites/Files which use `created_by`.
|
||||||
// to `owner_id` for pre-D7 rows with no provenance.
|
const ownerId = it.resource.updated_by ?? null;
|
||||||
const ownerId = it.resource.updated_by ?? it.resource.owner_id ?? null;
|
|
||||||
return {
|
return {
|
||||||
id: it.resource.id,
|
id: it.resource.id,
|
||||||
name: it.resource.name,
|
name: it.resource.name,
|
||||||
@@ -122,7 +121,7 @@
|
|||||||
});
|
});
|
||||||
raw = reset ? page.items : [...raw, ...page.items];
|
raw = reset ? page.items : [...raw, ...page.items];
|
||||||
cursor = page.next_cursor;
|
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) {
|
} catch (e) {
|
||||||
console.error('recent: load error', e);
|
console.error('recent: load error', e);
|
||||||
error = t('errors_loadFailed', 'Failed to load items');
|
error = t('errors_loadFailed', 'Failed to load items');
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ function withOneFile() {
|
|||||||
mime_type: 'text/plain',
|
mime_type: 'text/plain',
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name: 'notes.txt',
|
name: 'notes.txt',
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
folder_id: 'root',
|
folder_id: 'root',
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ function grantItem() {
|
|||||||
is_root: false,
|
is_root: false,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
name: 'Docs',
|
name: 'Docs',
|
||||||
owner_id: 'me',
|
|
||||||
created_by: 'me',
|
created_by: 'me',
|
||||||
updated_by: 'me',
|
updated_by: 'me',
|
||||||
parent_id: null,
|
parent_id: null,
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ impl PluginLifecycleHook {
|
|||||||
|
|
||||||
dispatch.dispatch(PluginEvent {
|
dispatch.dispatch(PluginEvent {
|
||||||
name: EVENT_FILE_UPLOADED,
|
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(),
|
invocation_id: Uuid::new_v4().to_string(),
|
||||||
payload: serde_json::json!({
|
payload: serde_json::json!({
|
||||||
"path": dto.path,
|
"path": dto.path,
|
||||||
|
|||||||
@@ -55,11 +55,6 @@ pub struct FavoriteItemDto {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub item_path: Option<String>,
|
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 ──
|
// ── Pre-computed display fields ──
|
||||||
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
||||||
pub icon_class: String,
|
pub icon_class: String,
|
||||||
@@ -124,10 +119,6 @@ pub struct FavoriteResourceRow {
|
|||||||
pub size: i64,
|
pub size: i64,
|
||||||
pub resource_created_at: DateTime<Utc>,
|
pub resource_created_at: DateTime<Utc>,
|
||||||
pub modified_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
|
/// Drive that owns this row. Surfaced on the favorites listing
|
||||||
/// so a UI can tell when a favorited item lives in a different
|
/// so a UI can tell when a favorited item lives in a different
|
||||||
/// drive than the user's home (post-D6 cross-drive moves +
|
/// 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")
|
/// Human-readable formatted size (e.g. "3.27 MB")
|
||||||
pub size_formatted: String,
|
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).
|
/// Sort date for Photos timeline — COALESCE(EXIF captured_at, created_at).
|
||||||
/// Only populated by the /api/photos endpoint.
|
/// Only populated by the /api/photos endpoint.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -102,7 +98,7 @@ impl From<File> for FileDto {
|
|||||||
let content_hash = file.content_hash().to_string();
|
let content_hash = file.content_hash().to_string();
|
||||||
|
|
||||||
// Consume the entity by moving all fields — zero heap allocations
|
// 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 parts = file.into_parts();
|
||||||
|
|
||||||
let icon_class = Arc::from(icon_class_for(&parts.name, &parts.mime_type));
|
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,
|
icon_special_class,
|
||||||
category,
|
category,
|
||||||
size_formatted,
|
size_formatted,
|
||||||
owner_id: parts.owner_id.map(|u| u.to_string()),
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash,
|
content_hash,
|
||||||
etag,
|
etag,
|
||||||
@@ -157,9 +152,8 @@ impl FileDto {
|
|||||||
///
|
///
|
||||||
/// Used when a file is returned to a share recipient: `path` reveals the
|
/// 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
|
/// full folder hierarchy above the file which the recipient may not have
|
||||||
/// access to. `folder_id` and `owner_id` are intentionally kept — the
|
/// access to. `folder_id` is intentionally kept — it's needed for
|
||||||
/// former is needed for sub-folder navigation (covered by the cascade
|
/// sub-folder navigation (covered by the cascade grant).
|
||||||
/// grant), and the latter is harmless metadata.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn without_hierarchy_info(self) -> Self {
|
pub fn without_hierarchy_info(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -183,7 +177,6 @@ impl FileDto {
|
|||||||
icon_special_class: Arc::from(""),
|
icon_special_class: Arc::from(""),
|
||||||
category: Arc::from("Document"),
|
category: Arc::from("Document"),
|
||||||
size_formatted: "0 Bytes".to_string(),
|
size_formatted: "0 Bytes".to_string(),
|
||||||
owner_id: None,
|
|
||||||
content_hash: String::new(),
|
content_hash: String::new(),
|
||||||
etag: String::new(),
|
etag: String::new(),
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ pub struct FolderDto {
|
|||||||
/// Parent folder ID
|
/// Parent folder ID
|
||||||
pub parent_id: Option<String>,
|
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
|
/// Drive that owns this folder. The scope axis for path-based
|
||||||
/// lookups across REST / WebDAV / NextCloud / CalDAV / CardDAV.
|
/// lookups across REST / WebDAV / NextCloud / CalDAV / CardDAV.
|
||||||
/// Post-D0 `storage.folders.drive_id` is `NOT NULL`; stub /
|
/// Post-D0 `storage.folders.drive_id` is `NOT NULL`; stub /
|
||||||
@@ -111,7 +107,6 @@ impl From<Folder> for FolderDto {
|
|||||||
name: folder.name().to_string(),
|
name: folder.name().to_string(),
|
||||||
path: folder.path_string().to_string(),
|
path: folder.path_string().to_string(),
|
||||||
parent_id: folder.parent_id().map(String::from),
|
parent_id: folder.parent_id().map(String::from),
|
||||||
owner_id: folder.owner_id().map(|u| u.to_string()),
|
|
||||||
drive_id: folder.drive_id(),
|
drive_id: folder.drive_id(),
|
||||||
created_at: folder.created_at(),
|
created_at: folder.created_at(),
|
||||||
modified_at: folder.modified_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
|
/// Used when a folder is returned to a share recipient: `path` reveals the
|
||||||
/// full folder hierarchy above the shared folder which the recipient may
|
/// full folder hierarchy above the shared folder which the recipient may
|
||||||
/// not have access to. `parent_id` and `owner_id` are intentionally kept
|
/// not have access to. `parent_id` is intentionally kept — it's needed
|
||||||
/// — the former is needed for sub-folder navigation (covered by the
|
/// for sub-folder navigation (covered by the cascade grant).
|
||||||
/// cascade grant), and the latter is harmless metadata.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn without_hierarchy_info(self) -> Self {
|
pub fn without_hierarchy_info(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -165,7 +159,6 @@ impl FolderDto {
|
|||||||
name: "stub-folder".to_string(),
|
name: "stub-folder".to_string(),
|
||||||
path: "/stub/path".to_string(),
|
path: "/stub/path".to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
owner_id: None,
|
|
||||||
drive_id: Uuid::nil(),
|
drive_id: Uuid::nil(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
@@ -205,11 +198,6 @@ pub struct FolderResourceRow {
|
|||||||
pub size: i64,
|
pub size: i64,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub modified_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
|
/// Drive that owns this row. Same column as
|
||||||
/// `storage.folders.drive_id` / `storage.files.drive_id`. Surfaced
|
/// `storage.folders.drive_id` / `storage.files.drive_id`. Surfaced
|
||||||
/// on the listing so a UI can tell when a child lives in a
|
/// 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 size: i64,
|
||||||
pub resource_created_at: DateTime<Utc>,
|
pub resource_created_at: DateTime<Utc>,
|
||||||
pub modified_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
|
/// Drive that owns this row. Surfaced on the recent listing
|
||||||
/// so a UI can tell when a recently-accessed item lives in a
|
/// so a UI can tell when a recently-accessed item lives in a
|
||||||
/// different drive than the user's home (post-D6 cross-drive
|
/// different drive than the user's home (post-D6 cross-drive
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ pub struct TrashResourceRow {
|
|||||||
pub size: i64,
|
pub size: i64,
|
||||||
pub resource_created_at: DateTime<Utc>,
|
pub resource_created_at: DateTime<Utc>,
|
||||||
pub modified_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
|
/// Drive the trashed item belongs to. Surfaced verbatim on the wire
|
||||||
/// (`TrashResourceItemDto.drive_id`) so the `/trash` UI can group by
|
/// (`TrashResourceItemDto.drive_id`) so the `/trash` UI can group by
|
||||||
/// drive without an extra lookup per row. D2b: filtering by drive is
|
/// drive without an extra lookup per row. D2b: filtering by drive is
|
||||||
|
|||||||
@@ -241,23 +241,21 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
|||||||
.collect())
|
.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
|
/// Used by streaming WebDAV PROPFIND. Post-D7 the concrete
|
||||||
/// own files, even in shared folder_id namespaces.
|
/// 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(
|
async fn list_files_batch_with_perms(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: Uuid,
|
_owner_id: Uuid,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -962,7 +962,6 @@ fn row_to_item_dto(row: TrashResourceRow) -> TrashResourceItemDto {
|
|||||||
name: row.name.clone(),
|
name: row.name.clone(),
|
||||||
path,
|
path,
|
||||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
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
|
// D2b: the trash listing query now SELECTs `drive_id` (the
|
||||||
// unified view exposes it). Surfaced so per-drive grouping
|
// unified view exposes it). Surfaced so per-drive grouping
|
||||||
// in the `/trash` UI doesn't need an extra lookup per row.
|
// 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)),
|
icon_special_class: std::sync::Arc::from(icon_special_class_for(&row.name, mime)),
|
||||||
category: std::sync::Arc::from(category_for(&row.name, mime)),
|
category: std::sync::Arc::from(category_for(&row.name, mime)),
|
||||||
size_formatted: format_file_size(size_bytes),
|
size_formatted: format_file_size(size_bytes),
|
||||||
owner_id: row.owner_id.map(|u| u.to_string()),
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash,
|
content_hash,
|
||||||
etag,
|
etag,
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
WHEN uf.item_type = 'folder' THEN fld.path
|
WHEN uf.item_type = 'folder' THEN fld.path
|
||||||
WHEN uf.item_type = 'file' THEN COALESCE(pfld.path || '/' || f.name, f.name)
|
WHEN uf.item_type = 'file' THEN COALESCE(pfld.path || '/' || f.name, f.name)
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END AS "item_path",
|
END AS "item_path"
|
||||||
COALESCE(f.user_id, fld.user_id)::TEXT AS "owner_id"
|
|
||||||
FROM auth.user_favorites uf
|
FROM auth.user_favorites uf
|
||||||
LEFT JOIN storage.files f ON uf.item_type = 'file'
|
LEFT JOIN storage.files f ON uf.item_type = 'file'
|
||||||
AND f.id = uf.item_id::UUID
|
AND f.id = uf.item_id::UUID
|
||||||
@@ -82,7 +81,6 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
parent_id: row.try_get("parent_id").ok(),
|
parent_id: row.try_get("parent_id").ok(),
|
||||||
modified_at: row.try_get("modified_at").ok(),
|
modified_at: row.try_get("modified_at").ok(),
|
||||||
item_path: row.try_get("item_path").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
|
// Temporary defaults; with_display_fields() computes the real values
|
||||||
icon_class: String::new(),
|
icon_class: String::new(),
|
||||||
icon_special_class: String::new(),
|
icon_special_class: String::new(),
|
||||||
@@ -309,10 +307,18 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
-1::bigint AS size,
|
-1::bigint AS size,
|
||||||
fld.created_at AS resource_created_at,
|
fld.created_at AS resource_created_at,
|
||||||
fld.updated_at AS modified_at,
|
fld.updated_at AS modified_at,
|
||||||
fld.user_id AS owner_id,
|
|
||||||
fld.drive_id AS drive_id,
|
fld.drive_id AS drive_id,
|
||||||
NULL::text AS blob_hash,
|
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,
|
uf.created_at AS favorited_at,
|
||||||
fld.path::text AS resource_path,
|
fld.path::text AS resource_path,
|
||||||
LOWER(fld.name) AS sort_str,
|
LOWER(fld.name) AS sort_str,
|
||||||
@@ -333,10 +339,18 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
f.size::bigint,
|
f.size::bigint,
|
||||||
f.created_at AS resource_created_at,
|
f.created_at AS resource_created_at,
|
||||||
f.updated_at AS modified_at,
|
f.updated_at AS modified_at,
|
||||||
f.user_id AS owner_id,
|
|
||||||
f.drive_id AS drive_id,
|
f.drive_id AS drive_id,
|
||||||
f.blob_hash,
|
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,
|
uf.created_at AS favorited_at,
|
||||||
COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path,
|
COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path,
|
||||||
LOWER(f.name) AS sort_str,
|
LOWER(f.name) AS sort_str,
|
||||||
@@ -489,7 +503,8 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let user_join = if need_user_join {
|
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 {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
@@ -506,7 +521,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
SELECT
|
SELECT
|
||||||
r.resource_type, r.resource_id, r.name, r.parent_id,
|
r.resource_type, r.resource_id, r.name, r.parent_id,
|
||||||
r.mime_type, r.size, r.resource_created_at, r.modified_at,
|
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}
|
r.sort_str, r.type_order, r.folder_first{username_col}
|
||||||
FROM resources r
|
FROM resources r
|
||||||
{user_join}
|
{user_join}
|
||||||
@@ -577,7 +592,6 @@ LIMIT $6"
|
|||||||
size,
|
size,
|
||||||
resource_created_at: row.get("resource_created_at"),
|
resource_created_at: row.get("resource_created_at"),
|
||||||
modified_at: row.get("modified_at"),
|
modified_at: row.get("modified_at"),
|
||||||
owner_id: row.try_get("owner_id").ok(),
|
|
||||||
drive_id: row.get("drive_id"),
|
drive_id: row.get("drive_id"),
|
||||||
blob_hash: row.try_get("blob_hash").ok(),
|
blob_hash: row.try_get("blob_hash").ok(),
|
||||||
is_owner: row.try_get("is_owner").unwrap_or(false),
|
is_owner: row.try_get("is_owner").unwrap_or(false),
|
||||||
|
|||||||
@@ -1390,7 +1390,6 @@ impl FolderDbRepository {
|
|||||||
-1::bigint AS size,
|
-1::bigint AS size,
|
||||||
f.created_at,
|
f.created_at,
|
||||||
f.updated_at AS modified_at,
|
f.updated_at AS modified_at,
|
||||||
f.user_id,
|
|
||||||
f.drive_id,
|
f.drive_id,
|
||||||
NULL::text AS blob_hash,
|
NULL::text AS blob_hash,
|
||||||
LOWER(f.name) AS sort_str,
|
LOWER(f.name) AS sort_str,
|
||||||
@@ -1410,7 +1409,6 @@ impl FolderDbRepository {
|
|||||||
fm.size::bigint,
|
fm.size::bigint,
|
||||||
fm.created_at,
|
fm.created_at,
|
||||||
fm.updated_at AS modified_at,
|
fm.updated_at AS modified_at,
|
||||||
fm.user_id,
|
|
||||||
fm.drive_id,
|
fm.drive_id,
|
||||||
fm.blob_hash,
|
fm.blob_hash,
|
||||||
LOWER(fm.name) AS sort_str,
|
LOWER(fm.name) AS sort_str,
|
||||||
@@ -1536,7 +1534,7 @@ impl FolderDbRepository {
|
|||||||
let sql = format!(
|
let sql = format!(
|
||||||
"WITH resources AS ({cte_inner}) \
|
"WITH resources AS ({cte_inner}) \
|
||||||
SELECT resource_type, id, name, folder_id, mime_type, size, \
|
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 \
|
sort_str, type_order, folder_first \
|
||||||
FROM resources \
|
FROM resources \
|
||||||
{where_clause} \
|
{where_clause} \
|
||||||
@@ -1545,7 +1543,7 @@ impl FolderDbRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Row: (resource_type, id, name, folder_id, mime_type, size,
|
// 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)
|
// sort_str, type_order, folder_first)
|
||||||
type Row = (
|
type Row = (
|
||||||
String,
|
String,
|
||||||
@@ -1556,8 +1554,7 @@ impl FolderDbRepository {
|
|||||||
i64,
|
i64,
|
||||||
chrono::DateTime<chrono::Utc>,
|
chrono::DateTime<chrono::Utc>,
|
||||||
chrono::DateTime<chrono::Utc>,
|
chrono::DateTime<chrono::Utc>,
|
||||||
Option<Uuid>, // user_id (post-D7: nullable on new rows)
|
Uuid, // drive_id
|
||||||
Uuid,
|
|
||||||
Option<String>,
|
Option<String>,
|
||||||
String,
|
String,
|
||||||
i64,
|
i64,
|
||||||
@@ -1588,12 +1585,11 @@ impl FolderDbRepository {
|
|||||||
size: r.5,
|
size: r.5,
|
||||||
created_at: r.6,
|
created_at: r.6,
|
||||||
modified_at: r.7,
|
modified_at: r.7,
|
||||||
owner_id: r.8,
|
drive_id: r.8,
|
||||||
drive_id: r.9,
|
blob_hash: r.9,
|
||||||
blob_hash: r.10,
|
sort_str: r.10,
|
||||||
sort_str: r.11,
|
type_order: r.11,
|
||||||
type_order: r.12,
|
folder_first: r.12,
|
||||||
folder_first: r.13,
|
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,20 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
// ── Build the UNION ALL CTE ─────────────────────────────────────────
|
// ── Build the UNION ALL CTE ─────────────────────────────────────────
|
||||||
let mut cte_branches: Vec<&str> = Vec::new();
|
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#"
|
let folder_branch = r#"
|
||||||
SELECT
|
SELECT
|
||||||
'folder'::text AS resource_type,
|
'folder'::text AS resource_type,
|
||||||
@@ -216,10 +230,18 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
-1::bigint AS size,
|
-1::bigint AS size,
|
||||||
fld.created_at AS resource_created_at,
|
fld.created_at AS resource_created_at,
|
||||||
fld.updated_at AS modified_at,
|
fld.updated_at AS modified_at,
|
||||||
fld.user_id AS owner_id,
|
|
||||||
fld.drive_id AS drive_id,
|
fld.drive_id AS drive_id,
|
||||||
NULL::text AS blob_hash,
|
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,
|
ur.accessed_at AS accessed_at,
|
||||||
fld.path::text AS resource_path,
|
fld.path::text AS resource_path,
|
||||||
LOWER(fld.name) AS sort_str,
|
LOWER(fld.name) AS sort_str,
|
||||||
@@ -240,10 +262,18 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
f.size::bigint,
|
f.size::bigint,
|
||||||
f.created_at AS resource_created_at,
|
f.created_at AS resource_created_at,
|
||||||
f.updated_at AS modified_at,
|
f.updated_at AS modified_at,
|
||||||
f.user_id AS owner_id,
|
|
||||||
f.drive_id AS drive_id,
|
f.drive_id AS drive_id,
|
||||||
f.blob_hash,
|
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,
|
ur.accessed_at AS accessed_at,
|
||||||
COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path,
|
COALESCE(pfld.path::text || '/' || f.name, f.name) AS resource_path,
|
||||||
LOWER(f.name) AS sort_str,
|
LOWER(f.name) AS sort_str,
|
||||||
@@ -394,7 +424,9 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let user_join = if need_user_join {
|
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 {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
@@ -411,7 +443,7 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
SELECT
|
SELECT
|
||||||
r.resource_type, r.resource_id, r.name, r.parent_id,
|
r.resource_type, r.resource_id, r.name, r.parent_id,
|
||||||
r.mime_type, r.size, r.resource_created_at, r.modified_at,
|
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}
|
r.sort_str, r.type_order, r.folder_first{username_col}
|
||||||
FROM resources r
|
FROM resources r
|
||||||
{user_join}
|
{user_join}
|
||||||
@@ -486,7 +518,6 @@ LIMIT $6"
|
|||||||
size,
|
size,
|
||||||
resource_created_at: row.get("resource_created_at"),
|
resource_created_at: row.get("resource_created_at"),
|
||||||
modified_at: row.get("modified_at"),
|
modified_at: row.get("modified_at"),
|
||||||
owner_id: row.try_get("owner_id").ok(),
|
|
||||||
drive_id: row.get("drive_id"),
|
drive_id: row.get("drive_id"),
|
||||||
blob_hash: row.try_get("blob_hash").ok(),
|
blob_hash: row.try_get("blob_hash").ok(),
|
||||||
is_owner: row.try_get("is_owner").unwrap_or(false),
|
is_owner: row.try_get("is_owner").unwrap_or(false),
|
||||||
|
|||||||
@@ -374,7 +374,6 @@ impl TrashDbRepository {
|
|||||||
-1::bigint AS size,
|
-1::bigint AS size,
|
||||||
fld.created_at AS resource_created_at,
|
fld.created_at AS resource_created_at,
|
||||||
fld.updated_at AS modified_at,
|
fld.updated_at AS modified_at,
|
||||||
fld.user_id AS owner_id,
|
|
||||||
fld.drive_id AS drive_id,
|
fld.drive_id AS drive_id,
|
||||||
NULL::text AS blob_hash,
|
NULL::text AS blob_hash,
|
||||||
fld.trashed_at AS trashed_at,
|
fld.trashed_at AS trashed_at,
|
||||||
@@ -401,7 +400,6 @@ impl TrashDbRepository {
|
|||||||
f.size::bigint AS size,
|
f.size::bigint AS size,
|
||||||
f.created_at AS resource_created_at,
|
f.created_at AS resource_created_at,
|
||||||
f.updated_at AS modified_at,
|
f.updated_at AS modified_at,
|
||||||
f.user_id AS owner_id,
|
|
||||||
f.drive_id AS drive_id,
|
f.drive_id AS drive_id,
|
||||||
f.blob_hash,
|
f.blob_hash,
|
||||||
f.trashed_at AS trashed_at,
|
f.trashed_at AS trashed_at,
|
||||||
@@ -533,7 +531,7 @@ impl TrashDbRepository {
|
|||||||
SELECT
|
SELECT
|
||||||
r.resource_type, r.resource_id, r.name, r.parent_id,
|
r.resource_type, r.resource_id, r.name, r.parent_id,
|
||||||
r.mime_type, r.size, r.resource_created_at, r.modified_at,
|
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
|
r.sort_str, r.type_order, r.folder_first
|
||||||
FROM resources r
|
FROM resources r
|
||||||
{keyset}
|
{keyset}
|
||||||
@@ -590,7 +588,6 @@ LIMIT $6"
|
|||||||
size,
|
size,
|
||||||
resource_created_at: row.get("resource_created_at"),
|
resource_created_at: row.get("resource_created_at"),
|
||||||
modified_at: row.get("modified_at"),
|
modified_at: row.get("modified_at"),
|
||||||
owner_id: row.try_get("owner_id").ok(),
|
|
||||||
drive_id: row.get("drive_id"),
|
drive_id: row.get("drive_id"),
|
||||||
blob_hash: row.try_get("blob_hash").ok(),
|
blob_hash: row.try_get("blob_hash").ok(),
|
||||||
trashed_at,
|
trashed_at,
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ impl PathResolverService {
|
|||||||
name,
|
name,
|
||||||
res_path,
|
res_path,
|
||||||
parent_id,
|
parent_id,
|
||||||
uid,
|
_uid, // Post-D7: `user_id` column no longer flowed into the DTO.
|
||||||
drive_id,
|
drive_id,
|
||||||
created_at,
|
created_at,
|
||||||
modified_at,
|
modified_at,
|
||||||
@@ -180,7 +180,6 @@ impl PathResolverService {
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
path: res_path,
|
path: res_path,
|
||||||
parent_id,
|
parent_id,
|
||||||
owner_id: uid,
|
|
||||||
drive_id,
|
drive_id,
|
||||||
created_at: created_at as u64,
|
created_at: created_at as u64,
|
||||||
modified_at: modified_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)),
|
icon_special_class: Arc::from(icon_special_class_for(&name, &mime)),
|
||||||
category: Arc::from(category_for(&name, &mime)),
|
category: Arc::from(category_for(&name, &mime)),
|
||||||
size_formatted: format_file_size(sz),
|
size_formatted: format_file_size(sz),
|
||||||
owner_id: uid,
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash: hash,
|
content_hash: hash,
|
||||||
etag,
|
etag,
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ pub async fn list_favorites_resources(
|
|||||||
name: row.name.clone(),
|
name: row.name.clone(),
|
||||||
path,
|
path,
|
||||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
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,
|
drive_id: row.drive_id,
|
||||||
created_at: row.resource_created_at.timestamp() as u64,
|
created_at: row.resource_created_at.timestamp() as u64,
|
||||||
modified_at: row.modified_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)),
|
category: std::sync::Arc::from(category_for(&row.name, mime)),
|
||||||
size_formatted: format_file_size(size_bytes),
|
size_formatted: format_file_size(size_bytes),
|
||||||
owner_id: row.owner_id.map(|u| u.to_string()),
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash,
|
content_hash,
|
||||||
etag,
|
etag,
|
||||||
|
|||||||
@@ -504,7 +504,6 @@ pub async fn list_folder_resources(
|
|||||||
name: row.name.clone(),
|
name: row.name.clone(),
|
||||||
path: String::new(), // cleared — share recipients must not see hierarchy
|
path: String::new(), // cleared — share recipients must not see hierarchy
|
||||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
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,
|
drive_id: row.drive_id,
|
||||||
created_at: row.created_at.timestamp() as u64,
|
created_at: row.created_at.timestamp() as u64,
|
||||||
modified_at: row.modified_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)),
|
icon_special_class: Arc::from(icon_special_class_for(&row.name, mime)),
|
||||||
category: Arc::from(category_for(&row.name, mime)),
|
category: Arc::from(category_for(&row.name, mime)),
|
||||||
size_formatted: format_file_size(size_bytes),
|
size_formatted: format_file_size(size_bytes),
|
||||||
owner_id: row.owner_id.map(|u| u.to_string()),
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash,
|
content_hash,
|
||||||
etag,
|
etag,
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ pub async fn list_recent_resources(
|
|||||||
name: row.name.clone(),
|
name: row.name.clone(),
|
||||||
path,
|
path,
|
||||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
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,
|
drive_id: row.drive_id,
|
||||||
created_at: row.resource_created_at.timestamp() as u64,
|
created_at: row.resource_created_at.timestamp() as u64,
|
||||||
modified_at: row.modified_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)),
|
category: std::sync::Arc::from(category_for(&row.name, mime)),
|
||||||
size_formatted: format_file_size(size_bytes),
|
size_formatted: format_file_size(size_bytes),
|
||||||
owner_id: row.owner_id.map(|u| u.to_string()),
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash,
|
content_hash,
|
||||||
etag,
|
etag,
|
||||||
|
|||||||
@@ -427,7 +427,6 @@ async fn handle_propfind(
|
|||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
path: "".to_string(),
|
path: "".to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
owner_id: None,
|
|
||||||
// Synthetic root folder for PROPFIND on `/`; not an
|
// Synthetic root folder for PROPFIND on `/`; not an
|
||||||
// actual DB row, so drive_id has no meaningful value.
|
// actual DB row, so drive_id has no meaningful value.
|
||||||
drive_id: Uuid::nil(),
|
drive_id: Uuid::nil(),
|
||||||
|
|||||||
@@ -101,7 +101,13 @@ async fn check_file_info(
|
|||||||
|
|
||||||
let response = CheckFileInfoResponse {
|
let response = CheckFileInfoResponse {
|
||||||
base_file_name: file.name.clone(),
|
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,
|
size: file.size,
|
||||||
user_id: claims.sub.clone(),
|
user_id: claims.sub.clone(),
|
||||||
version: file.modified_at.to_string(),
|
version: file.modified_at.to_string(),
|
||||||
|
|||||||
@@ -340,7 +340,6 @@ fn file_dto_from_search(fr: &crate::application::dtos::search_dto::SearchFileRes
|
|||||||
.into(),
|
.into(),
|
||||||
category: category_for(&fr.name, &fr.mime_type).to_string().into(),
|
category: category_for(&fr.name, &fr.mime_type).to_string().into(),
|
||||||
size_formatted: format_file_size(fr.size),
|
size_formatted: format_file_size(fr.size),
|
||||||
owner_id: None,
|
|
||||||
sort_date: None,
|
sort_date: None,
|
||||||
content_hash: fr.blob_hash.clone(),
|
content_hash: fr.blob_hash.clone(),
|
||||||
etag,
|
etag,
|
||||||
@@ -360,7 +359,6 @@ fn folder_dto_from_search(
|
|||||||
name: sr.name.clone(),
|
name: sr.name.clone(),
|
||||||
path: sr.path.clone(),
|
path: sr.path.clone(),
|
||||||
parent_id: sr.parent_id.clone(),
|
parent_id: sr.parent_id.clone(),
|
||||||
owner_id: None,
|
|
||||||
drive_id: sr.drive_id,
|
drive_id: sr.drive_id,
|
||||||
created_at: sr.created_at,
|
created_at: sr.created_at,
|
||||||
modified_at: sr.modified_at,
|
modified_at: sr.modified_at,
|
||||||
|
|||||||
@@ -1811,7 +1811,6 @@ mod tests {
|
|||||||
name: path.rsplit('/').next().unwrap_or("").to_string(),
|
name: path.rsplit('/').next().unwrap_or("").to_string(),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
owner_id: None,
|
|
||||||
// Test stub — path mapper doesn't read drive_id.
|
// Test stub — path mapper doesn't read drive_id.
|
||||||
drive_id: uuid::Uuid::nil(),
|
drive_id: uuid::Uuid::nil(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user