feat(drive): limit NC request by disk
This commit is contained in:
@@ -262,6 +262,11 @@ pub async fn list_favorites_resources(
|
||||
path,
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
// Listing handler — drive_id is informational
|
||||
// and the favorites row doesn't currently
|
||||
// SELECT it. Path-based lookups never enter
|
||||
// this code path.
|
||||
drive_id: uuid::Uuid::nil(),
|
||||
created_at: row.resource_created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
is_root: false,
|
||||
|
||||
@@ -701,6 +701,10 @@ pub async fn list_folder_resources(
|
||||
path: String::new(), // cleared — share recipients must not see hierarchy
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
// Resources listing — drive_id is informational
|
||||
// here; not selected by the underlying query.
|
||||
// Path-based lookups never enter this code path.
|
||||
drive_id: uuid::Uuid::nil(),
|
||||
created_at: row.created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
is_root: false,
|
||||
|
||||
@@ -292,6 +292,11 @@ pub async fn list_recent_resources(
|
||||
path,
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
// Listing handler — drive_id is informational
|
||||
// and the recents row doesn't currently SELECT
|
||||
// it. Path-based lookups never enter this code
|
||||
// path.
|
||||
drive_id: uuid::Uuid::nil(),
|
||||
created_at: row.resource_created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
is_root: false,
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::application::ports::storage_ports::StorageUsagePort;
|
||||
use crate::application::services::file_retrieval_service::FileRetrievalService;
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::common::di::AppState;
|
||||
use crate::domain::repositories::drive_repository::DriveRepository;
|
||||
use crate::infrastructure::services::path_resolver_service::ResolvedResource;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
@@ -247,6 +248,29 @@ async fn resolve_webdav_path(state: &Arc<AppState>, user_id: Uuid, path: &str) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Native WebDAV protocol entry: resolve the caller's default drive
|
||||
/// once per handler so every downstream path-based lookup
|
||||
/// (`get_folder_by_path`, `get_file_by_path`, `update_file_streaming`)
|
||||
/// can pass the same `drive_id` scope.
|
||||
///
|
||||
/// Post-D0 `storage.{folders,files}.path` repeats across drives — the
|
||||
/// scope is mandatory. Native WebDAV today lives in a single-drive
|
||||
/// surface (one default drive per user), so the lookup is unambiguous.
|
||||
/// Multi-drive support via path segments (`/webdav/drives/<uuid>/…`)
|
||||
/// is tracked separately and will derive `drive_id` directly from the
|
||||
/// URL instead of going through `find_default_for_user`.
|
||||
async fn resolve_drive_id_for_native_webdav(
|
||||
state: &Arc<AppState>,
|
||||
user_id: Uuid,
|
||||
) -> Result<Uuid, AppError> {
|
||||
state
|
||||
.drive_repo
|
||||
.find_default_for_user(user_id)
|
||||
.await
|
||||
.map(|d| d.drive.id)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to resolve default drive: {:?}", e)))
|
||||
}
|
||||
|
||||
async fn handle_webdav_dispatch(
|
||||
state: Arc<AppState>,
|
||||
req: Request<Body>,
|
||||
@@ -405,6 +429,9 @@ async fn handle_propfind(
|
||||
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(),
|
||||
created_at: Utc::now().timestamp() as u64,
|
||||
modified_at: Utc::now().timestamp() as u64,
|
||||
is_root: true,
|
||||
@@ -468,8 +495,11 @@ async fn handle_propfind(
|
||||
Err(_) => {}
|
||||
}
|
||||
} else {
|
||||
// Fallback: legacy double-query path when PathResolver is unavailable
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&path, user.id).await {
|
||||
// Fallback: legacy double-query path when PathResolver is unavailable.
|
||||
// `drive_id` is mandatory post-D0 for path-based lookups — derive
|
||||
// the caller's default drive once and reuse it for both probes.
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&path, drive_id).await {
|
||||
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||
let folder_id = folder.id.clone();
|
||||
return build_streaming_propfind_response(
|
||||
@@ -484,7 +514,10 @@ async fn handle_propfind(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if let Ok(file) = file_retrieval_service.get_file_by_path(&path).await {
|
||||
if let Ok(file) = file_retrieval_service
|
||||
.get_file_by_path(&path, drive_id)
|
||||
.await
|
||||
{
|
||||
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
{
|
||||
@@ -688,10 +721,11 @@ async fn handle_proppatch(
|
||||
let is_collection = if path.is_empty() || path == "/" {
|
||||
true
|
||||
} else {
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
state
|
||||
.applications
|
||||
.folder_service
|
||||
.get_folder_by_path(&path, user.id)
|
||||
.get_folder_by_path(&path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
@@ -776,9 +810,12 @@ async fn handle_get(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy fallback — fetch + ownership check
|
||||
// Legacy fallback — fetch + ownership check. `drive_id` is the
|
||||
// path-lookup scope post-D0 (`storage.files.path` repeats across
|
||||
// drives), derived once from the caller's default drive.
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
let f = file_retrieval_service
|
||||
.get_file_by_path(&path)
|
||||
.get_file_by_path(&path, drive_id)
|
||||
.await
|
||||
.map_err(|_e| AppError::not_found(format!("File not found: {}", path)))?;
|
||||
assert_owner(f.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||
@@ -876,8 +913,11 @@ async fn handle_head(
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: legacy double-query path (with ownership check)
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&path, user.id).await {
|
||||
// Fallback: legacy double-query path (with ownership check).
|
||||
// `drive_id` is the path-lookup scope post-D0 — derive once and
|
||||
// reuse for both the folder and file probes.
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(&path, drive_id).await {
|
||||
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
@@ -890,7 +930,7 @@ async fn handle_head(
|
||||
|
||||
// Try as file — use metadata only, never load content for HEAD
|
||||
let file = file_retrieval_service
|
||||
.get_file_by_path(&path)
|
||||
.get_file_by_path(&path, drive_id)
|
||||
.await
|
||||
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
|
||||
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||
@@ -939,15 +979,27 @@ async fn resolve_or_legacy(
|
||||
return Some(r);
|
||||
}
|
||||
|
||||
// Path-lookup scope post-D0 — derive the caller's default drive
|
||||
// for both legacy probes. `find_default_for_user` returning Err
|
||||
// (e.g. external user, or boot before the lifecycle hook fired)
|
||||
// means no fallback resolution is possible: return None.
|
||||
let drive_id = state
|
||||
.drive_repo
|
||||
.find_default_for_user(user_id)
|
||||
.await
|
||||
.ok()?
|
||||
.drive
|
||||
.id;
|
||||
|
||||
let user_id_str = user_id.to_string();
|
||||
let folder_service = &state.applications.folder_service;
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(path, user_id).await
|
||||
if let Ok(folder) = folder_service.get_folder_by_path(path, drive_id).await
|
||||
&& folder.owner_id.as_deref() == Some(&user_id_str)
|
||||
{
|
||||
return Some(ResolvedResource::Folder(folder));
|
||||
}
|
||||
let file_retrieval = &state.applications.file_retrieval_service;
|
||||
if let Ok(file) = file_retrieval.get_file_by_path(path).await
|
||||
if let Ok(file) = file_retrieval.get_file_by_path(path, drive_id).await
|
||||
&& file.owner_id.as_deref() == Some(&user_id_str)
|
||||
{
|
||||
return Some(ResolvedResource::File(file));
|
||||
@@ -1143,8 +1195,9 @@ async fn handle_put(
|
||||
|
||||
// ── Atomic store: swap the file row onto the ingested blob ──
|
||||
let content_type = ingested.content_type.clone();
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
let result = file_upload_service
|
||||
.update_file_streaming(&path, ingested.stored(), &content_type, None)
|
||||
.update_file_streaming(&path, drive_id, ingested.stored(), &content_type, None)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
@@ -1198,6 +1251,9 @@ async fn handle_mkcol(
|
||||
// Path is already translated by dispatch (e.g. "My Folder - jared/03/01").
|
||||
// Walk each segment: the first is the home folder (already exists),
|
||||
// subsequent segments are created as needed with proper parent_id.
|
||||
// `drive_id` scopes each per-segment path probe to the caller's default
|
||||
// drive (post-D0 invariant: `storage.folders.path` repeats across drives).
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||
let mut parent_id: Option<String> = None;
|
||||
let mut accumulated_path = String::new();
|
||||
@@ -1209,7 +1265,7 @@ async fn handle_mkcol(
|
||||
accumulated_path.push_str(segment);
|
||||
|
||||
match folder_service
|
||||
.get_folder_by_path(&accumulated_path, user.id)
|
||||
.get_folder_by_path(&accumulated_path, drive_id)
|
||||
.await
|
||||
{
|
||||
Ok(existing) => {
|
||||
@@ -1395,6 +1451,11 @@ async fn handle_move(
|
||||
let file_management_service = &state.applications.file_management_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
// `drive_id` scopes every path-based lookup below to the caller's
|
||||
// default drive (post-D0 invariant: `storage.{files,folders}.path`
|
||||
// repeats across drives).
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
|
||||
// Check if destination already exists (for Overwrite header compliance)
|
||||
if !overwrite {
|
||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||
@@ -1404,11 +1465,11 @@ async fn handle_move(
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
folder_service
|
||||
.get_folder_by_path(&destination_path, user.id)
|
||||
.get_folder_by_path(&destination_path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
|| file_retrieval_service
|
||||
.get_file_by_path(&destination_path)
|
||||
.get_file_by_path(&destination_path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
@@ -1447,7 +1508,7 @@ async fn handle_move(
|
||||
parent_id: if dest_parent_path.is_empty() {
|
||||
None
|
||||
} else if let Ok(parent) = folder_service
|
||||
.get_folder_by_path(dest_parent_path, user.id)
|
||||
.get_folder_by_path(dest_parent_path, drive_id)
|
||||
.await
|
||||
{
|
||||
assert_owner(
|
||||
@@ -1488,7 +1549,7 @@ async fn handle_move(
|
||||
None
|
||||
} else {
|
||||
let parent = folder_service
|
||||
.get_folder_by_path(dest_parent_path, user.id)
|
||||
.get_folder_by_path(dest_parent_path, drive_id)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::not_found(format!(
|
||||
@@ -1606,6 +1667,11 @@ async fn handle_copy(
|
||||
let file_retrieval_service = &state.applications.file_retrieval_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
// `drive_id` scopes every path-based lookup below to the caller's
|
||||
// default drive (post-D0 invariant: `storage.{files,folders}.path`
|
||||
// repeats across drives).
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
|
||||
// Check if destination already exists (for Overwrite header compliance)
|
||||
if !overwrite {
|
||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||
@@ -1615,11 +1681,11 @@ async fn handle_copy(
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
folder_service
|
||||
.get_folder_by_path(&destination_path, user.id)
|
||||
.get_folder_by_path(&destination_path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
|| file_retrieval_service
|
||||
.get_file_by_path(&destination_path)
|
||||
.get_file_by_path(&destination_path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
@@ -1650,7 +1716,7 @@ async fn handle_copy(
|
||||
let target_parent_id = if dest_parent_path.is_empty() {
|
||||
None
|
||||
} else if let Ok(parent) = folder_service
|
||||
.get_folder_by_path(dest_parent_path, user.id)
|
||||
.get_folder_by_path(dest_parent_path, drive_id)
|
||||
.await
|
||||
{
|
||||
assert_owner(
|
||||
@@ -1748,10 +1814,11 @@ async fn handle_lock(
|
||||
let is_collection = if path.is_empty() || path == "/" {
|
||||
true
|
||||
} else {
|
||||
let drive_id = resolve_drive_id_for_native_webdav(&state, user.id).await?;
|
||||
state
|
||||
.applications
|
||||
.folder_service
|
||||
.get_folder_by_path(&path, user.id)
|
||||
.get_folder_by_path(&path, drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::sync::Arc;
|
||||
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
|
||||
use crate::application::services::wopi_lock_service::WopiLockService;
|
||||
use crate::application::services::wopi_token_service::WopiTokenService;
|
||||
use crate::domain::repositories::drive_repository::DriveRepository;
|
||||
use crate::infrastructure::services::wopi_discovery_service::WopiDiscoveryService;
|
||||
|
||||
/// Shared state for WOPI handlers.
|
||||
@@ -233,11 +234,31 @@ async fn put_file(
|
||||
};
|
||||
|
||||
// ── Atomic store: swap the file row onto the ingested blob ──
|
||||
// `drive_id` scopes the path-based lookups in `update_file_streaming`
|
||||
// post-D0. WOPI tokens carry the user UUID in `claims.sub`; we resolve
|
||||
// that to the caller's default drive (WOPI today is a single-drive
|
||||
// editing surface — no drive marker travels in the token).
|
||||
let claims_sub_uuid = match uuid::Uuid::parse_str(&claims.sub) {
|
||||
Ok(u) => u,
|
||||
Err(_) => return StatusCode::UNAUTHORIZED.into_response(),
|
||||
};
|
||||
let drive_id = match state
|
||||
.app_state
|
||||
.drive_repo
|
||||
.find_default_for_user(claims_sub_uuid)
|
||||
.await
|
||||
{
|
||||
Ok(d) => d.drive.id,
|
||||
Err(e) => {
|
||||
tracing::error!("WOPI PutFile: default-drive lookup failed: {:?}", e);
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
};
|
||||
let result = state
|
||||
.app_state
|
||||
.applications
|
||||
.file_upload_service
|
||||
.update_file_streaming(&file.path, ingested.stored(), &content_type, None)
|
||||
.update_file_streaming(&file.path, drive_id, ingested.stored(), &content_type, None)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
|
||||
@@ -186,7 +186,11 @@ pub async fn basic_auth_middleware(
|
||||
use crate::domain::repositories::drive_repository::DriveRepository;
|
||||
let chroot = match drive_marker.as_deref() {
|
||||
None => {
|
||||
match state.drive_repo.find_default_for_user(current_user.id).await {
|
||||
match state
|
||||
.drive_repo
|
||||
.find_default_for_user(current_user.id)
|
||||
.await
|
||||
{
|
||||
Ok(drive_with_name) => state
|
||||
.applications
|
||||
.folder_service
|
||||
|
||||
@@ -358,6 +358,10 @@ fn folder_dto_from_search(
|
||||
path: sr.path.clone(),
|
||||
parent_id: sr.parent_id.clone(),
|
||||
owner_id: None,
|
||||
// Search result — drive_id is informational. The search row
|
||||
// doesn't currently SELECT it, and path-based lookups never
|
||||
// enter this code path.
|
||||
drive_id: uuid::Uuid::nil(),
|
||||
created_at: sr.created_at,
|
||||
modified_at: sr.modified_at,
|
||||
is_root: sr.is_root,
|
||||
@@ -490,7 +494,6 @@ async fn resolve_scope_folder(
|
||||
body: &str,
|
||||
session: &crate::interfaces::nextcloud::session::NcSession,
|
||||
) -> Option<String> {
|
||||
let user = &session.user;
|
||||
let chroot = session.require_chroot().ok()?;
|
||||
let url_user = &session.raw_username;
|
||||
let href = parse_scope_href(body)?;
|
||||
@@ -511,7 +514,7 @@ async fn resolve_scope_folder(
|
||||
|
||||
let folder_service = &state.applications.folder_service;
|
||||
folder_service
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(|f| f.id)
|
||||
|
||||
@@ -137,9 +137,12 @@ async fn handle_restore(
|
||||
let dest_internal = nc_to_internal_path(chroot, &dest_subpath)?;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let dest_taken = file_service.get_file_by_path(&dest_internal).await.is_ok()
|
||||
let dest_taken = file_service
|
||||
.get_file_by_path(&dest_internal, chroot.drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
|| folder_service
|
||||
.get_folder_by_path(&dest_internal, user.id)
|
||||
.get_folder_by_path(&dest_internal, chroot.drive_id)
|
||||
.await
|
||||
.is_ok();
|
||||
if dest_taken {
|
||||
|
||||
@@ -259,6 +259,13 @@ async fn handle_assemble(
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
// Path-based lookups below scope by `drive_id`. The NC session's
|
||||
// chroot is always populated for path-scoped handlers (see
|
||||
// `NcSession::require_chroot`); the FolderDto carries `drive_id`
|
||||
// post-D0.
|
||||
let chroot = session.require_chroot()?;
|
||||
let drive_id = chroot.drive_id;
|
||||
|
||||
// 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
|
||||
@@ -279,11 +286,19 @@ async fn handle_assemble(
|
||||
let content_type = ingested.content_type.clone();
|
||||
|
||||
// Check if file exists (update vs create).
|
||||
let existing = file_service.get_file_by_path(&internal_path).await;
|
||||
let existing = file_service
|
||||
.get_file_by_path(&internal_path, drive_id)
|
||||
.await;
|
||||
|
||||
let etag: Option<String> = if existing.is_ok() {
|
||||
let dto = upload_service
|
||||
.update_file_streaming(&internal_path, ingested.stored(), &content_type, oc_mtime)
|
||||
.update_file_streaming(
|
||||
&internal_path,
|
||||
drive_id,
|
||||
ingested.stored(),
|
||||
&content_type,
|
||||
oc_mtime,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to update file: {}", e)))?;
|
||||
|
||||
@@ -300,7 +315,7 @@ async fn handle_assemble(
|
||||
|
||||
use crate::application::ports::folder_ports::FolderUseCase;
|
||||
let parent_folder = match folder_service
|
||||
.get_folder_by_path(parent_internal, user.id)
|
||||
.get_folder_by_path(parent_internal, drive_id)
|
||||
.await
|
||||
{
|
||||
Ok(folder) => folder,
|
||||
|
||||
@@ -235,7 +235,7 @@ async fn handle_propfind(
|
||||
|
||||
// Try to resolve as folder first.
|
||||
let folder_result = folder_service
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await;
|
||||
|
||||
if let Ok(folder) = folder_result {
|
||||
@@ -260,7 +260,9 @@ async fn handle_propfind(
|
||||
}
|
||||
|
||||
// Not a folder — try as a file.
|
||||
let file_result = file_service.get_file_by_path(&internal_path).await;
|
||||
let file_result = file_service
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await;
|
||||
if let Ok(file) = file_result {
|
||||
// Batch-check favorites for this single file.
|
||||
let favorite_ids = if let Some(fav_svc) = state.favorites_service.as_ref() {
|
||||
@@ -307,7 +309,6 @@ async fn handle_get(
|
||||
subpath: &str,
|
||||
headers: &axum::http::HeaderMap,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = &session.user;
|
||||
let chroot = session.require_chroot()?;
|
||||
// GET on root folder — NC clients use this as an existence check
|
||||
if subpath.is_empty() || subpath == "/" {
|
||||
@@ -324,7 +325,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, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -336,7 +337,7 @@ async fn handle_get(
|
||||
}
|
||||
|
||||
let file = file_service
|
||||
.get_file_by_path(&internal_path)
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("File not found"))?;
|
||||
|
||||
@@ -386,7 +387,6 @@ async fn handle_head(
|
||||
session: &crate::interfaces::nextcloud::session::NcSession,
|
||||
subpath: &str,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = &session.user;
|
||||
let chroot = session.require_chroot()?;
|
||||
// HEAD on root folder — NC clients use this as an existence check
|
||||
if subpath.is_empty() || subpath == "/" {
|
||||
@@ -403,7 +403,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, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -415,7 +415,7 @@ async fn handle_head(
|
||||
}
|
||||
|
||||
let file = file_service
|
||||
.get_file_by_path(&internal_path)
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("File not found"))?;
|
||||
|
||||
@@ -479,10 +479,13 @@ async fn handle_proppatch(
|
||||
let internal_path = nc_to_internal_path(chroot, subpath)?;
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
let resource = if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
|
||||
let resource = if let Ok(file) = file_service
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
Some((file.id, "file"))
|
||||
} else if let Ok(folder) = folder_service
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
Some((folder.id, "folder"))
|
||||
@@ -687,7 +690,10 @@ async fn handle_put(
|
||||
// bandwidth or disk I/O on a body the server is going to throw away.
|
||||
// The lookup is reused for the create-vs-update distinction below,
|
||||
// so this is also free of an extra DB hit.
|
||||
let existing = file_service.get_file_by_path(&internal_path).await.ok();
|
||||
let existing = file_service
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.ok();
|
||||
let current_etag = existing.as_ref().map(|f| f.etag.as_str());
|
||||
|
||||
if let Some(value) = req
|
||||
@@ -742,7 +748,13 @@ async fn handle_put(
|
||||
// Single streaming path — handles both update and create internally,
|
||||
// swapping the file row onto the already-ingested blob.
|
||||
let stored = upload_service
|
||||
.update_file_streaming(&internal_path, ingested.stored(), &content_type, oc_mtime)
|
||||
.update_file_streaming(
|
||||
&internal_path,
|
||||
chroot.drive_id,
|
||||
ingested.stored(),
|
||||
&content_type,
|
||||
oc_mtime,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to store file: {}", e)))?;
|
||||
|
||||
@@ -787,7 +799,7 @@ async fn handle_mkcol(
|
||||
// auto-create doesn't break real clients.
|
||||
|
||||
if folder_service
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -817,7 +829,7 @@ async fn handle_mkcol(
|
||||
};
|
||||
|
||||
let parent_folder = match folder_service
|
||||
.get_folder_by_path(&parent_path, user.id)
|
||||
.get_folder_by_path(&parent_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
Ok(folder) => folder,
|
||||
@@ -861,7 +873,7 @@ async fn handle_delete(
|
||||
// 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, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
trash_svc
|
||||
@@ -873,7 +885,10 @@ async fn handle_delete(
|
||||
.body(Body::empty())
|
||||
.unwrap());
|
||||
}
|
||||
if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
|
||||
if let Ok(file) = file_service
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
trash_svc
|
||||
.move_to_trash(&file.id, "file", user.id)
|
||||
.await
|
||||
@@ -890,7 +905,7 @@ async fn handle_delete(
|
||||
let file_mgmt = &state.applications.file_management_service;
|
||||
|
||||
if let Ok(folder) = folder_service
|
||||
.get_folder_by_path(&internal_path, user.id)
|
||||
.get_folder_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
folder_service
|
||||
@@ -904,7 +919,10 @@ async fn handle_delete(
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
|
||||
if let Ok(file) = file_service
|
||||
.get_file_by_path(&internal_path, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
file_mgmt
|
||||
.delete_file_with_perms(&file.id, user.id)
|
||||
.await
|
||||
@@ -969,11 +987,11 @@ async fn handle_move(
|
||||
// 204-vs-201 selector at response time.
|
||||
let dest_internal_precheck = nc_to_internal_path(chroot, &dest_subpath)?;
|
||||
let dest_existing_file = file_service
|
||||
.get_file_by_path(&dest_internal_precheck)
|
||||
.get_file_by_path(&dest_internal_precheck, chroot.drive_id)
|
||||
.await
|
||||
.ok();
|
||||
let dest_existing_folder = folder_service
|
||||
.get_folder_by_path(&dest_internal_precheck, user.id)
|
||||
.get_folder_by_path(&dest_internal_precheck, chroot.drive_id)
|
||||
.await
|
||||
.ok();
|
||||
let dest_existed_before = dest_existing_file.is_some() || dest_existing_folder.is_some();
|
||||
@@ -1016,7 +1034,10 @@ async fn handle_move(
|
||||
};
|
||||
|
||||
// Try as file first.
|
||||
if let Ok(file) = file_service.get_file_by_path(&src_internal).await {
|
||||
if let Ok(file) = file_service
|
||||
.get_file_by_path(&src_internal, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
let (dest_parent_sub, dest_name) = match dest_subpath.rsplit_once('/') {
|
||||
Some((parent, name)) => (parent, name),
|
||||
None => ("", dest_subpath.as_str()),
|
||||
@@ -1038,7 +1059,7 @@ async fn handle_move(
|
||||
} else {
|
||||
// Different parent → move.
|
||||
let dest_parent = folder_service
|
||||
.get_folder_by_path(&dest_parent_internal, user.id)
|
||||
.get_folder_by_path(&dest_parent_internal, chroot.drive_id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("Destination folder not found"))?;
|
||||
|
||||
@@ -1062,7 +1083,10 @@ async fn handle_move(
|
||||
// existed — RFC 4918 §9.9.4 distinguishes create vs overwrite).
|
||||
let dest_internal = nc_to_internal_path(chroot, &dest_subpath)?;
|
||||
let mut builder = Response::builder().status(final_status);
|
||||
if let Ok(moved) = file_service.get_file_by_path(&dest_internal).await {
|
||||
if let Ok(moved) = file_service
|
||||
.get_file_by_path(&dest_internal, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
// Route through `FileDto::etag` so the MOVE response
|
||||
// matches what a subsequent PROPFIND on the destination
|
||||
// will return — `moved.id` (UUID) would differ from the
|
||||
@@ -1077,7 +1101,7 @@ async fn handle_move(
|
||||
|
||||
// Try as folder.
|
||||
if let Ok(folder) = folder_service
|
||||
.get_folder_by_path(&src_internal, user.id)
|
||||
.get_folder_by_path(&src_internal, chroot.drive_id)
|
||||
.await
|
||||
{
|
||||
let (dest_parent_sub, dest_name) = match dest_subpath.rsplit_once('/') {
|
||||
@@ -1107,7 +1131,7 @@ async fn handle_move(
|
||||
} else {
|
||||
// Different parent → move.
|
||||
let dest_parent = folder_service
|
||||
.get_folder_by_path(&dest_parent_internal, user.id)
|
||||
.get_folder_by_path(&dest_parent_internal, chroot.drive_id)
|
||||
.await
|
||||
.map_err(|_| AppError::not_found("Destination parent not found"))?;
|
||||
|
||||
@@ -1634,6 +1658,8 @@ mod tests {
|
||||
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,
|
||||
modified_at: 0,
|
||||
is_root: false,
|
||||
|
||||
Reference in New Issue
Block a user