feat(drive): improve Drive model
now Drive is purely a metadata
each drive has always a root folder
this model minimize Oxicloud changes, and simplify
the Drive name is simply the folder's root's name
note: owner of Drive has more permission that an owner of the root folder
This commit is contained in:
@@ -399,10 +399,14 @@ pub async fn handle_search(
|
||||
let mut entries: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
// Map file results
|
||||
// TODO(D1): drop the hardcoded "Personal/" prefix and read the
|
||||
// caller's default-drive root folder name from `drives.root_folder_id`
|
||||
// instead. Correct for D0-provisioned default drives; secondary
|
||||
// drives keep their original root name.
|
||||
for file in &results.files {
|
||||
let display_path = file
|
||||
.path
|
||||
.strip_prefix(&format!("My Folder - {}/", user.username))
|
||||
.strip_prefix("Personal/")
|
||||
.unwrap_or(&file.path);
|
||||
let display_path = format!("/{}", display_path);
|
||||
|
||||
@@ -427,11 +431,11 @@ pub async fn handle_search(
|
||||
}));
|
||||
}
|
||||
|
||||
// Map folder results
|
||||
// Map folder results — same TODO(D1) as above.
|
||||
for folder in &results.folders {
|
||||
let display_path = folder
|
||||
.path
|
||||
.strip_prefix(&format!("My Folder - {}/", user.username))
|
||||
.strip_prefix("Personal/")
|
||||
.unwrap_or(&folder.path);
|
||||
let display_path = format!("/{}", display_path);
|
||||
|
||||
|
||||
@@ -83,7 +83,11 @@ async fn handle_filter_files(
|
||||
// All items in this response are favorites.
|
||||
let favorite_ids: HashSet<String> = favorites.iter().map(|f| f.item_id.clone()).collect();
|
||||
|
||||
let home_prefix = format!("My Folder - {}/", user.username);
|
||||
// TODO(D1): replace the hardcoded "Personal/" prefix with the
|
||||
// caller's default-drive root folder name read from
|
||||
// `drives.root_folder_id`. Correct for D0-provisioned default
|
||||
// drives; secondary drives keep their original root name.
|
||||
let home_prefix = "Personal/";
|
||||
|
||||
// Pass 1: resolve the favorited DTOs in two batch queries (was one
|
||||
// get_* per favorite — up to N serial round-trips on a sync client's
|
||||
@@ -146,7 +150,7 @@ async fn handle_filter_files(
|
||||
write_multistatus_start(&mut xml)?;
|
||||
|
||||
for file in &files {
|
||||
let subpath = strip_home_prefix(&file.path, &home_prefix);
|
||||
let subpath = strip_home_prefix(&file.path, home_prefix);
|
||||
let href = nc_href(&user.username, subpath);
|
||||
let fid = file_id_map.get(&file.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
@@ -163,7 +167,7 @@ async fn handle_filter_files(
|
||||
}
|
||||
|
||||
for folder in &folders {
|
||||
let subpath = strip_home_prefix(&folder.path, &home_prefix);
|
||||
let subpath = strip_home_prefix(&folder.path, home_prefix);
|
||||
let href = format!("{}/", nc_href(&user.username, subpath));
|
||||
let fid = folder_id_map.get(&folder.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
@@ -210,7 +214,7 @@ async fn handle_search(
|
||||
let nresults = parse_nresults(body).unwrap_or(100);
|
||||
|
||||
// Resolve folder scope from <d:href> inside <d:scope>.
|
||||
let folder_id = resolve_scope_folder(&state, body, &user.username).await;
|
||||
let folder_id = resolve_scope_folder(&state, body, &user.username, user.id).await;
|
||||
|
||||
let criteria = SearchCriteriaDto {
|
||||
name_contains: Some(term),
|
||||
@@ -227,7 +231,10 @@ async fn handle_search(
|
||||
|
||||
let nc = state.nextcloud.as_ref();
|
||||
let file_id_svc = nc.map(|n| &n.file_ids);
|
||||
let home_prefix = format!("My Folder - {}/", user.username);
|
||||
// TODO(D1): same as the favorites pass above — replace the
|
||||
// hardcoded "Personal/" with the caller's actual default-drive
|
||||
// root folder name from `drives.root_folder_id`.
|
||||
let home_prefix = "Personal/";
|
||||
|
||||
// No favorite checking for search results -- pass an empty set.
|
||||
let favorite_ids: HashSet<String> = HashSet::new();
|
||||
@@ -249,7 +256,7 @@ async fn handle_search(
|
||||
|
||||
// Files.
|
||||
for file in &files {
|
||||
let subpath = strip_home_prefix(&file.path, &home_prefix);
|
||||
let subpath = strip_home_prefix(&file.path, home_prefix);
|
||||
let href = nc_href(&user.username, subpath);
|
||||
let fid = file_id_map.get(&file.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
@@ -267,7 +274,7 @@ async fn handle_search(
|
||||
|
||||
// Folders.
|
||||
for folder in &folders {
|
||||
let subpath = strip_home_prefix(&folder.path, &home_prefix);
|
||||
let subpath = strip_home_prefix(&folder.path, home_prefix);
|
||||
let href = format!("{}/", nc_href(&user.username, subpath));
|
||||
let fid = folder_id_map.get(&folder.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
@@ -461,7 +468,12 @@ fn xml_extract_text(body: &str, local_name: &[u8]) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Resolve a scope href (e.g. `/files/username/Documents`) to a folder ID.
|
||||
async fn resolve_scope_folder(state: &AppState, body: &str, username: &str) -> Option<String> {
|
||||
async fn resolve_scope_folder(
|
||||
state: &AppState,
|
||||
body: &str,
|
||||
username: &str,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Option<String> {
|
||||
let href = parse_scope_href(body)?;
|
||||
|
||||
// The href is typically `/files/{user}/subpath` or `/remote.php/dav/files/{user}/subpath`.
|
||||
@@ -477,7 +489,7 @@ async fn resolve_scope_folder(state: &AppState, body: &str, username: &str) -> O
|
||||
|
||||
let folder_service = &state.applications.folder_service;
|
||||
folder_service
|
||||
.get_folder_by_path(&internal_path)
|
||||
.get_folder_by_path(&internal_path, user_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(|f| f.id)
|
||||
|
||||
@@ -133,7 +133,7 @@ async fn handle_restore(
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let dest_taken = file_service.get_file_by_path(&dest_internal).await.is_ok()
|
||||
|| folder_service
|
||||
.get_folder_by_path(&dest_internal)
|
||||
.get_folder_by_path(&dest_internal, user.id)
|
||||
.await
|
||||
.is_ok();
|
||||
if dest_taken {
|
||||
@@ -248,11 +248,18 @@ fn mime_from_name(name: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Strip the "My Folder - {username}/" prefix from an original path to produce
|
||||
/// the Nextcloud-relative original location.
|
||||
fn strip_home_prefix<'a>(original_path: &'a str, username: &str) -> &'a str {
|
||||
let prefix = format!("My Folder - {}/", username);
|
||||
original_path.strip_prefix(&prefix).unwrap_or(original_path)
|
||||
/// Strip the home-folder prefix from an original path to produce the
|
||||
/// Nextcloud-relative original location.
|
||||
///
|
||||
/// TODO(D1): replace the hardcoded "Personal/" with the caller's actual
|
||||
/// default-drive root folder name read from `drives.root_folder_id`.
|
||||
/// Correct for D0-provisioned default drives; secondary drives keep
|
||||
/// their original root name. The `_username` arg stays for now so the
|
||||
/// upcoming dynamic lookup has a way to identify the caller.
|
||||
fn strip_home_prefix<'a>(original_path: &'a str, _username: &str) -> &'a str {
|
||||
original_path
|
||||
.strip_prefix("Personal/")
|
||||
.unwrap_or(original_path)
|
||||
}
|
||||
|
||||
// ────────────── Trashbin PROPFIND XML Generation ──────────────
|
||||
|
||||
@@ -256,11 +256,12 @@ async fn handle_assemble(
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
let internal_path = format!(
|
||||
"My Folder - {}/{}",
|
||||
user.username,
|
||||
dest_subpath.trim_matches('/')
|
||||
);
|
||||
// TODO(D1): read the caller's default-drive root folder name from
|
||||
// `drives.root_folder_id` instead of hardcoding "Personal". The
|
||||
// constant is correct for every default personal drive provisioned
|
||||
// by the D0 lifecycle hook, but secondary drives (M2 backfill from
|
||||
// SQL-created sibling root folders) keep their original name.
|
||||
let internal_path = format!("Personal/{}", dest_subpath.trim_matches('/'));
|
||||
|
||||
let filename = filename_from_path(&dest_subpath).to_string();
|
||||
let ingested = ingest_stream_to_cas(
|
||||
@@ -291,15 +292,11 @@ async fn handle_assemble(
|
||||
Some((p, n)) => (p, n),
|
||||
None => ("", dest_subpath.as_str()),
|
||||
};
|
||||
let parent_internal = format!(
|
||||
"My Folder - {}/{}",
|
||||
user.username,
|
||||
parent_sub.trim_matches('/')
|
||||
);
|
||||
let parent_internal = format!("Personal/{}", parent_sub.trim_matches('/'));
|
||||
let parent_internal = parent_internal.trim_end_matches('/');
|
||||
|
||||
use crate::application::ports::folder_ports::FolderUseCase;
|
||||
let parent_folder = match folder_service.get_folder_by_path(parent_internal).await {
|
||||
let parent_folder = match folder_service.get_folder_by_path(parent_internal, user.id).await {
|
||||
Ok(folder) => folder,
|
||||
Err(e) => {
|
||||
discard_ingested(&state.core.dedup_service, &ingested).await;
|
||||
|
||||
@@ -53,8 +53,15 @@ const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||
/// Internal: My Folder - {username}/{subpath}
|
||||
///
|
||||
/// An empty subpath maps to the user's home folder root.
|
||||
pub fn nc_to_internal_path(username: &str, subpath: &str) -> Result<String, AppError> {
|
||||
let home = format!("My Folder - {}", username);
|
||||
pub fn nc_to_internal_path(_username: &str, subpath: &str) -> Result<String, AppError> {
|
||||
// D0: every default personal drive's root folder is named "Personal"
|
||||
// (docs/plan/drive.md §3 — the canonical post-D0 default). The NC
|
||||
// dispatcher chroots into the caller's default drive, so the leading
|
||||
// segment of the internal path is always the drive's root folder
|
||||
// name. Hardcoded for now; a follow-up will read it from
|
||||
// `drives.root_folder_id`'s name to support secondary drives with
|
||||
// custom root-folder names.
|
||||
let home = "Personal".to_string();
|
||||
let subpath = subpath.trim_matches('/');
|
||||
if subpath.is_empty() {
|
||||
return Ok(home);
|
||||
@@ -203,7 +210,7 @@ async fn handle_propfind(
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
|
||||
// Try to resolve as folder first.
|
||||
let folder_result = folder_service.get_folder_by_path(&internal_path).await;
|
||||
let folder_result = folder_service.get_folder_by_path(&internal_path, user.id).await;
|
||||
|
||||
if let Ok(folder) = folder_result {
|
||||
// It's a folder — stream the multistatus: children are fetched in
|
||||
@@ -281,7 +288,7 @@ async fn handle_get(
|
||||
|
||||
// Check if path is a folder first (NC clients use GET as existence check)
|
||||
if folder_service
|
||||
.get_folder_by_path(&internal_path)
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -358,7 +365,7 @@ async fn handle_head(
|
||||
|
||||
// Check if path is a folder (NC clients use HEAD as existence check)
|
||||
if folder_service
|
||||
.get_folder_by_path(&internal_path)
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -433,7 +440,7 @@ async fn handle_proppatch(
|
||||
let folder_service = &state.applications.folder_service;
|
||||
let resource = if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
|
||||
Some((file.id, "file"))
|
||||
} else if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
||||
} else if let Ok(folder) = folder_service.get_folder_by_path(&internal_path, user.id).await {
|
||||
Some((folder.id, "folder"))
|
||||
} else {
|
||||
None
|
||||
@@ -733,7 +740,7 @@ async fn handle_mkcol(
|
||||
// auto-create doesn't break real clients.
|
||||
|
||||
if folder_service
|
||||
.get_folder_by_path(&internal_path)
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -758,7 +765,7 @@ async fn handle_mkcol(
|
||||
format!("{}/{}", user_root, parent_segments.join("/"))
|
||||
};
|
||||
|
||||
let parent_folder = match folder_service.get_folder_by_path(&parent_path).await {
|
||||
let parent_folder = match folder_service.get_folder_by_path(&parent_path, user.id).await {
|
||||
Ok(folder) => folder,
|
||||
Err(_) => {
|
||||
return Ok(Response::builder()
|
||||
@@ -797,7 +804,7 @@ async fn handle_delete(
|
||||
// Prefer soft-delete (move to trash) when trash service is available.
|
||||
// This is what Nextcloud clients expect — items appear in the trashbin.
|
||||
if let Some(trash_svc) = state.trash_service.as_ref() {
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path, user.id).await {
|
||||
trash_svc
|
||||
.move_to_trash(&folder.id, "folder", user.id)
|
||||
.await
|
||||
@@ -823,7 +830,7 @@ async fn handle_delete(
|
||||
// Fallback: hard delete when trash service is not available.
|
||||
let file_mgmt = &state.applications.file_management_service;
|
||||
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path, user.id).await {
|
||||
folder_service
|
||||
.delete_folder_with_perms(&folder.id, user.id)
|
||||
.await
|
||||
@@ -897,7 +904,7 @@ async fn handle_move(
|
||||
.await
|
||||
.ok();
|
||||
let dest_existing_folder = folder_service
|
||||
.get_folder_by_path(&dest_internal_precheck)
|
||||
.get_folder_by_path(&dest_internal_precheck, user.id)
|
||||
.await
|
||||
.ok();
|
||||
let dest_existed_before = dest_existing_file.is_some() || dest_existing_folder.is_some();
|
||||
@@ -962,7 +969,7 @@ async fn handle_move(
|
||||
} else {
|
||||
// Different parent → move.
|
||||
let dest_parent = folder_service
|
||||
.get_folder_by_path(&dest_parent_internal)
|
||||
.get_folder_by_path(&dest_parent_internal, user.id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("Destination folder not found"))?;
|
||||
|
||||
@@ -997,7 +1004,7 @@ async fn handle_move(
|
||||
}
|
||||
|
||||
// Try as folder.
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&src_internal).await {
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&src_internal, user.id).await {
|
||||
let (dest_parent_sub, dest_name) = match dest_subpath.rsplit_once('/') {
|
||||
Some((parent, name)) => (parent, name),
|
||||
None => ("", dest_subpath.as_str()),
|
||||
@@ -1025,7 +1032,7 @@ async fn handle_move(
|
||||
} else {
|
||||
// Different parent → move.
|
||||
let dest_parent = folder_service
|
||||
.get_folder_by_path(&dest_parent_internal)
|
||||
.get_folder_by_path(&dest_parent_internal, user.id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("Destination parent not found"))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user