perf: Arc<AppState>, streaming PROPFIND, spawn_blocking SHA-256
- Issue #4: Wrap AppState in Arc — eliminates 42 Arc::clone + 16 String::clone per request - Issue #2: Reject Depth:infinity with 403 + streaming XML with paginated DB queries - Issue #5: Move chunked upload assembly (SHA-256 hash-on-write) to spawn_blocking - Remove ~270 lines dead code from di.rs (unused builders, Default impl, stubs) - Clean up unused tokio imports in chunked_upload_service.rs
This commit is contained in:
@@ -213,81 +213,6 @@ impl WebDavAdapter {
|
|||||||
Ok(PropFindRequest { prop_find_type })
|
Ok(PropFindRequest { prop_find_type })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a PROPFIND response for files and folders
|
|
||||||
pub fn generate_propfind_response<W: Write>(
|
|
||||||
writer: W,
|
|
||||||
folder: Option<&FolderDto>,
|
|
||||||
files: &[FileDto],
|
|
||||||
subfolders: &[FolderDto],
|
|
||||||
request: &PropFindRequest,
|
|
||||||
_depth: &str,
|
|
||||||
base_href: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut xml_writer = Writer::new(writer);
|
|
||||||
|
|
||||||
// Start multistatus response
|
|
||||||
xml_writer.write_event(Event::Start(
|
|
||||||
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// Add response for current folder if provided
|
|
||||||
if let Some(folder) = folder {
|
|
||||||
Self::write_folder_response(&mut xml_writer, folder, request, base_href)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If depth allows, add responses for files and subfolders
|
|
||||||
if _depth != "0" {
|
|
||||||
// Add responses for files
|
|
||||||
for file in files {
|
|
||||||
Self::write_file_response(
|
|
||||||
&mut xml_writer,
|
|
||||||
file,
|
|
||||||
request,
|
|
||||||
&format!("{}{}", base_href, file.name),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add responses for subfolders
|
|
||||||
for subfolder in subfolders {
|
|
||||||
Self::write_folder_response(
|
|
||||||
&mut xml_writer,
|
|
||||||
subfolder,
|
|
||||||
request,
|
|
||||||
&format!("{}{}/", base_href, subfolder.name),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End multistatus
|
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a PROPFIND response for a single file
|
|
||||||
pub fn generate_propfind_response_for_file<W: Write>(
|
|
||||||
writer: W,
|
|
||||||
file: &FileDto,
|
|
||||||
request: &PropFindRequest,
|
|
||||||
_depth: &str,
|
|
||||||
href: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut xml_writer = Writer::new(writer);
|
|
||||||
|
|
||||||
// Start multistatus response
|
|
||||||
xml_writer.write_event(Event::Start(
|
|
||||||
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// Add response for file
|
|
||||||
Self::write_file_response(&mut xml_writer, file, request, href)?;
|
|
||||||
|
|
||||||
// End multistatus
|
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write folder properties as a response
|
/// Write folder properties as a response
|
||||||
fn write_folder_response<W: Write>(
|
fn write_folder_response<W: Write>(
|
||||||
xml_writer: &mut Writer<W>,
|
xml_writer: &mut Writer<W>,
|
||||||
@@ -1102,4 +1027,46 @@ impl WebDavAdapter {
|
|||||||
}
|
}
|
||||||
name.to_string()
|
name.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Streaming PROPFIND helpers
|
||||||
|
//
|
||||||
|
// These methods write incremental XML fragments so the caller
|
||||||
|
// can flush chunks to the HTTP body without buffering the whole
|
||||||
|
// response in memory.
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Writes the opening `<D:multistatus>` tag.
|
||||||
|
pub fn write_multistatus_start<W: Write>(writer: &mut Writer<W>) -> Result<()> {
|
||||||
|
writer.write_event(Event::Start(
|
||||||
|
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the closing `</D:multistatus>` tag.
|
||||||
|
pub fn write_multistatus_end<W: Write>(writer: &mut Writer<W>) -> Result<()> {
|
||||||
|
writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a single `<D:response>` element for a folder.
|
||||||
|
pub fn write_folder_entry<W: Write>(
|
||||||
|
writer: &mut Writer<W>,
|
||||||
|
folder: &FolderDto,
|
||||||
|
request: &PropFindRequest,
|
||||||
|
href: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
Self::write_folder_response(writer, folder, request, href)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a single `<D:response>` element for a file.
|
||||||
|
pub fn write_file_entry<W: Write>(
|
||||||
|
writer: &mut Writer<W>,
|
||||||
|
file: &FileDto,
|
||||||
|
request: &PropFindRequest,
|
||||||
|
href: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
Self::write_file_response(writer, file, request, href)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,20 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
|||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
self.list_files(Some(folder_id)).await
|
self.list_files(Some(folder_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists files in a folder with LIMIT/OFFSET pagination.
|
||||||
|
///
|
||||||
|
/// Used by streaming WebDAV PROPFIND to avoid loading all files at once.
|
||||||
|
/// Default: falls back to `list_files` (loads all, then slices in memory).
|
||||||
|
async fn list_files_batch(
|
||||||
|
&self,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
offset: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
|
let all = self.list_files(folder_id).await?;
|
||||||
|
Ok(all.into_iter().skip(offset as usize).take(limit as usize).collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -78,6 +78,22 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists files in a folder with LIMIT/OFFSET pagination.
|
||||||
|
///
|
||||||
|
/// Used by streaming WebDAV PROPFIND to avoid loading all files at once.
|
||||||
|
/// Default: falls back to `list_files` (loads all, then slices in memory).
|
||||||
|
async fn list_files_batch(
|
||||||
|
&self,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
offset: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<File>, DomainError> {
|
||||||
|
let all = self.list_files(folder_id).await?;
|
||||||
|
let start = (offset as usize).min(all.len());
|
||||||
|
let end = (start + limit as usize).min(all.len());
|
||||||
|
Ok(all.into_iter().skip(start).take(end - start).collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Lists every file in the subtree rooted at `folder_id`.
|
/// Lists every file in the subtree rooted at `folder_id`.
|
||||||
///
|
///
|
||||||
/// Uses an ltree `<@` join against `storage.folders` so the entire
|
/// Uses an ltree `<@` join against `storage.folders` so the entire
|
||||||
|
|||||||
@@ -317,4 +317,14 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
let files = self.file_read.list_files_in_subtree(folder_id).await?;
|
let files = self.file_read.list_files_in_subtree(folder_id).await?;
|
||||||
Ok(files.into_iter().map(FileDto::from).collect())
|
Ok(files.into_iter().map(FileDto::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_files_batch(
|
||||||
|
&self,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
offset: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
|
let files = self.file_read.list_files_batch(folder_id, offset, limit).await?;
|
||||||
|
Ok(files.into_iter().map(FileDto::from).collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-319
@@ -45,11 +45,7 @@ use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nSer
|
|||||||
use crate::infrastructure::services::path_service::PathService;
|
use crate::infrastructure::services::path_service::PathService;
|
||||||
use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService;
|
use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService;
|
||||||
|
|
||||||
use crate::common::stubs::{
|
use crate::common::stubs::StubZipPort;
|
||||||
StubDedupPort, StubFileManagementUseCase, StubFileReadPort,
|
|
||||||
StubFileRetrievalUseCase, StubFileUploadUseCase, StubFileUseCaseFactory, StubFileWritePort,
|
|
||||||
StubFolderStoragePort, StubFolderUseCase, StubI18nService, StubSearchUseCase, StubZipPort,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Factory for the different application components
|
/// Factory for the different application components
|
||||||
///
|
///
|
||||||
@@ -765,317 +761,4 @@ pub struct AppState {
|
|||||||
Option<Arc<crate::infrastructure::services::wopi_discovery_service::WopiDiscoveryService>>,
|
Option<Arc<crate::infrastructure::services::wopi_discovery_service::WopiDiscoveryService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
// All AppState construction is done via struct literal in build_app_state().
|
||||||
fn default() -> Self {
|
|
||||||
// Minimal stub version for auth middleware and route construction.
|
|
||||||
// Real services are wired in main.rs via AppServiceFactory.
|
|
||||||
|
|
||||||
let config = crate::common::config::AppConfig::default();
|
|
||||||
let path_service = Arc::new(
|
|
||||||
crate::infrastructure::services::path_service::PathService::new(
|
|
||||||
std::path::PathBuf::from("./storage"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let i18n_repository = Arc::new(StubI18nService)
|
|
||||||
as Arc<dyn crate::domain::services::i18n_service::I18nService>;
|
|
||||||
let folder_service = Arc::new(StubFolderUseCase)
|
|
||||||
as Arc<dyn crate::application::ports::inbound::FolderUseCase>;
|
|
||||||
let file_upload_service = Arc::new(StubFileUploadUseCase)
|
|
||||||
as Arc<dyn crate::application::ports::file_ports::FileUploadUseCase>;
|
|
||||||
let file_retrieval_service = Arc::new(StubFileRetrievalUseCase)
|
|
||||||
as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>;
|
|
||||||
let file_management_service = Arc::new(StubFileManagementUseCase)
|
|
||||||
as Arc<dyn crate::application::ports::file_ports::FileManagementUseCase>;
|
|
||||||
let file_use_case_factory = Arc::new(StubFileUseCaseFactory)
|
|
||||||
as Arc<dyn crate::application::ports::file_ports::FileUseCaseFactory>;
|
|
||||||
|
|
||||||
// Create file content cache for stub
|
|
||||||
let file_content_cache = Arc::new(FileContentCache::new(FileContentCacheConfig::default()));
|
|
||||||
|
|
||||||
// Create dummy thumbnail service
|
|
||||||
let dummy_thumbnail_service: Arc<dyn ThumbnailPort> = Arc::new(
|
|
||||||
crate::infrastructure::services::thumbnail_service::ThumbnailService::new(
|
|
||||||
&std::path::PathBuf::from("./storage"),
|
|
||||||
100,
|
|
||||||
10 * 1024 * 1024,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create dummy chunked upload service
|
|
||||||
let dummy_chunked_upload_service: Arc<dyn ChunkedUploadPort> = Arc::new(
|
|
||||||
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new_stub(
|
|
||||||
std::path::PathBuf::from("./storage/.uploads"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create dummy image transcode service
|
|
||||||
let dummy_image_transcode_service: Arc<dyn ImageTranscodePort> = Arc::new(
|
|
||||||
crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new(
|
|
||||||
&std::path::PathBuf::from("./storage"),
|
|
||||||
100,
|
|
||||||
10 * 1024 * 1024,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stub dedup service (Default is only used for routing stubs, never for real I/O)
|
|
||||||
let dummy_dedup_service: Arc<dyn DedupPort> = Arc::new(StubDedupPort);
|
|
||||||
|
|
||||||
// Core services using stubs
|
|
||||||
let core_services = CoreServices {
|
|
||||||
path_service: path_service.clone(),
|
|
||||||
file_content_cache,
|
|
||||||
thumbnail_service: dummy_thumbnail_service,
|
|
||||||
chunked_upload_service: dummy_chunked_upload_service,
|
|
||||||
image_transcode_service: dummy_image_transcode_service,
|
|
||||||
dedup_service: dummy_dedup_service,
|
|
||||||
zip_service: Arc::new(StubZipPort) as Arc<dyn ZipPort>,
|
|
||||||
config: config.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stub folder repo concrete — uses a dummy PgPool-less constructor
|
|
||||||
// In practice, Default is only used for routing stubs; real app always goes through build_app_state
|
|
||||||
let dummy_folder_repo_concrete = Arc::new(FolderDbRepository::new_stub());
|
|
||||||
|
|
||||||
// Repository services using stubs
|
|
||||||
let repository_services = RepositoryServices {
|
|
||||||
folder_repository: Arc::new(StubFolderStoragePort)
|
|
||||||
as Arc<dyn crate::application::ports::outbound::FolderStoragePort>,
|
|
||||||
folder_repo_concrete: dummy_folder_repo_concrete,
|
|
||||||
file_read_repository: Arc::new(StubFileReadPort)
|
|
||||||
as Arc<dyn crate::application::ports::storage_ports::FileReadPort>,
|
|
||||||
file_write_repository: Arc::new(StubFileWritePort)
|
|
||||||
as Arc<dyn crate::application::ports::storage_ports::FileWritePort>,
|
|
||||||
i18n_repository,
|
|
||||||
trash_repository: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dummy concrete services for compatibility
|
|
||||||
let dummy_folder_storage = Arc::new(StubFolderStoragePort)
|
|
||||||
as Arc<dyn crate::application::ports::outbound::FolderStoragePort>;
|
|
||||||
let folder_service_concrete = Arc::new(FolderService::new(dummy_folder_storage));
|
|
||||||
|
|
||||||
// Dummy I18nApplicationService
|
|
||||||
let dummy_i18n_app_service =
|
|
||||||
crate::application::services::i18n_application_service::I18nApplicationService::new(
|
|
||||||
Arc::new(StubI18nService)
|
|
||||||
as Arc<dyn crate::domain::services::i18n_service::I18nService>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Application services using stubs
|
|
||||||
let application_services = ApplicationServices {
|
|
||||||
folder_service_concrete: folder_service_concrete.clone(),
|
|
||||||
folder_service,
|
|
||||||
file_upload_service,
|
|
||||||
file_retrieval_service,
|
|
||||||
file_management_service,
|
|
||||||
file_use_case_factory,
|
|
||||||
i18n_service: Arc::new(dummy_i18n_app_service),
|
|
||||||
trash_service: None,
|
|
||||||
search_service: Some(Arc::new(StubSearchUseCase)
|
|
||||||
as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
|
|
||||||
share_service: None,
|
|
||||||
favorites_service: None,
|
|
||||||
recent_service: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
core: core_services,
|
|
||||||
repositories: repository_services,
|
|
||||||
applications: application_services,
|
|
||||||
db_pool: None,
|
|
||||||
auth_service: None,
|
|
||||||
admin_settings_service: None,
|
|
||||||
trash_service: None,
|
|
||||||
share_service: None,
|
|
||||||
favorites_service: None,
|
|
||||||
recent_service: None,
|
|
||||||
storage_usage_service: None,
|
|
||||||
calendar_service: None,
|
|
||||||
contact_service: None,
|
|
||||||
calendar_use_case: None,
|
|
||||||
addressbook_use_case: None,
|
|
||||||
contact_use_case: None,
|
|
||||||
wopi_token_service: None,
|
|
||||||
wopi_lock_service: None,
|
|
||||||
wopi_discovery_service: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub fn new(
|
|
||||||
core: CoreServices,
|
|
||||||
repositories: RepositoryServices,
|
|
||||||
applications: ApplicationServices,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
core,
|
|
||||||
repositories,
|
|
||||||
applications,
|
|
||||||
db_pool: None,
|
|
||||||
auth_service: None,
|
|
||||||
admin_settings_service: None,
|
|
||||||
trash_service: None,
|
|
||||||
share_service: None,
|
|
||||||
favorites_service: None,
|
|
||||||
recent_service: None,
|
|
||||||
storage_usage_service: None,
|
|
||||||
calendar_service: None,
|
|
||||||
contact_service: None,
|
|
||||||
calendar_use_case: None,
|
|
||||||
addressbook_use_case: None,
|
|
||||||
contact_use_case: None,
|
|
||||||
wopi_token_service: None,
|
|
||||||
wopi_lock_service: None,
|
|
||||||
wopi_discovery_service: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_database(mut self, db_pool: Arc<PgPool>) -> Self {
|
|
||||||
self.db_pool = Some(db_pool);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a minimal AppState for route construction.
|
|
||||||
///
|
|
||||||
/// Uses `Default` stubs for infrastructure services, then overlays the real
|
|
||||||
/// application-level services that arrive as parameters from `main.rs`.
|
|
||||||
/// This keeps `routes.rs` free of any `crate::infrastructure` references.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn for_routing(
|
|
||||||
folder_service: Arc<FolderService>,
|
|
||||||
file_retrieval_service: Arc<
|
|
||||||
dyn crate::application::ports::file_ports::FileRetrievalUseCase,
|
|
||||||
>,
|
|
||||||
file_upload_service: Arc<dyn FileUploadUseCase>,
|
|
||||||
file_management_service: Arc<dyn FileManagementUseCase>,
|
|
||||||
folder_use_case: Arc<dyn crate::application::ports::inbound::FolderUseCase>,
|
|
||||||
i18n_service: Option<
|
|
||||||
Arc<crate::application::services::i18n_application_service::I18nApplicationService>,
|
|
||||||
>,
|
|
||||||
trash_service: Option<Arc<dyn TrashUseCase>>,
|
|
||||||
search_service: Option<Arc<dyn crate::application::ports::inbound::SearchUseCase>>,
|
|
||||||
share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
|
||||||
favorites_service: Option<Arc<dyn FavoritesUseCase>>,
|
|
||||||
recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
|
||||||
) -> Self {
|
|
||||||
let mut state = Self::default();
|
|
||||||
|
|
||||||
// Override application services with real ones
|
|
||||||
state.applications.folder_service_concrete = folder_service.clone();
|
|
||||||
state.applications.folder_service = folder_use_case;
|
|
||||||
state.applications.file_upload_service = file_upload_service;
|
|
||||||
state.applications.file_retrieval_service = file_retrieval_service.clone();
|
|
||||||
state.applications.file_management_service = file_management_service;
|
|
||||||
|
|
||||||
if let Some(i18n) = i18n_service {
|
|
||||||
state.applications.i18n_service = i18n;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.applications.trash_service = trash_service.clone();
|
|
||||||
state.applications.search_service = search_service.clone();
|
|
||||||
state.applications.share_service = share_service.clone();
|
|
||||||
state.applications.favorites_service = favorites_service.clone();
|
|
||||||
state.applications.recent_service = recent_service.clone();
|
|
||||||
|
|
||||||
// Also set top-level optional services
|
|
||||||
state.trash_service = trash_service;
|
|
||||||
state.share_service = share_service;
|
|
||||||
state.favorites_service = favorites_service;
|
|
||||||
state.recent_service = recent_service;
|
|
||||||
|
|
||||||
// Create real ZipService with the actual file/folder services
|
|
||||||
state.core.zip_service = Arc::new(
|
|
||||||
crate::infrastructure::services::zip_service::ZipService::new(
|
|
||||||
file_retrieval_service
|
|
||||||
as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>,
|
|
||||||
folder_service.clone()
|
|
||||||
as Arc<dyn crate::application::ports::inbound::FolderUseCase>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_auth_services(mut self, auth_services: AuthServices) -> Self {
|
|
||||||
self.auth_service = Some(auth_services);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_trash_service(mut self, trash_service: Arc<dyn TrashUseCase>) -> Self {
|
|
||||||
self.trash_service = Some(trash_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_share_service(
|
|
||||||
mut self,
|
|
||||||
share_service: Arc<dyn crate::application::ports::share_ports::ShareUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.share_service = Some(share_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_favorites_service(mut self, favorites_service: Arc<dyn FavoritesUseCase>) -> Self {
|
|
||||||
self.favorites_service = Some(favorites_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_recent_service(mut self, recent_service: Arc<dyn RecentItemsUseCase>) -> Self {
|
|
||||||
self.recent_service = Some(recent_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_storage_usage_service(
|
|
||||||
mut self,
|
|
||||||
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
|
|
||||||
) -> Self {
|
|
||||||
self.storage_usage_service = Some(storage_usage_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_calendar_service(
|
|
||||||
mut self,
|
|
||||||
calendar_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.calendar_service = Some(calendar_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_contact_service(
|
|
||||||
mut self,
|
|
||||||
contact_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.contact_service = Some(contact_service);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_calendar_use_case(
|
|
||||||
mut self,
|
|
||||||
calendar_use_case: Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.calendar_use_case = Some(calendar_use_case);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_addressbook_use_case(
|
|
||||||
mut self,
|
|
||||||
addressbook_use_case: Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.addressbook_use_case = Some(addressbook_use_case);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_contact_use_case(
|
|
||||||
mut self,
|
|
||||||
contact_use_case: Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>,
|
|
||||||
) -> Self {
|
|
||||||
self.contact_use_case = Some(contact_use_case);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_zip_service(mut self, zip_service: Arc<dyn ZipPort>) -> Self {
|
|
||||||
self.core.zip_service = zip_service;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -207,6 +207,75 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
self.resolve_blob_hash(file_id).await
|
self.resolve_blob_hash(file_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paginated file listing — fetches only `limit` rows starting at `offset`.
|
||||||
|
///
|
||||||
|
/// Uses a single SQL query with `LIMIT/OFFSET` to avoid loading the full
|
||||||
|
/// folder contents into memory. Ideal for streaming WebDAV PROPFIND.
|
||||||
|
async fn list_files_batch(
|
||||||
|
&self,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
offset: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<File>, DomainError> {
|
||||||
|
let rows: Vec<(
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
Option<String>,
|
||||||
|
)> = if let Some(fid) = folder_id {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
|
fi.size, fi.mime_type,
|
||||||
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
|
fi.user_id::text
|
||||||
|
FROM storage.files fi
|
||||||
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
|
WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed
|
||||||
|
ORDER BY fi.name
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(fid)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
|
fi.size, fi.mime_type,
|
||||||
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
|
fi.user_id::text
|
||||||
|
FROM storage.files fi
|
||||||
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
|
WHERE fi.folder_id IS NULL AND NOT fi.is_trashed
|
||||||
|
ORDER BY fi.name
|
||||||
|
LIMIT $1 OFFSET $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list_batch: {e}")))?;
|
||||||
|
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||||
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_file_stream(
|
async fn get_file_stream(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ use std::collections::HashMap;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::fs::{self, File, OpenOptions};
|
use tokio::fs::{self, File};
|
||||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -633,59 +633,73 @@ impl ChunkedUploadService {
|
|||||||
session.clone()
|
session.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assemble file with hash-on-write
|
// Assemble file with hash-on-write.
|
||||||
|
//
|
||||||
|
// The entire loop is offloaded to spawn_blocking because SHA-256
|
||||||
|
// hashing is CPU-bound (~130 ms for 500 MB) and would otherwise
|
||||||
|
// block a Tokio worker, starving all other connections.
|
||||||
|
// Synchronous I/O is used inside the blocking thread — it avoids
|
||||||
|
// the async reactor overhead and is actually faster for this
|
||||||
|
// sequential workload.
|
||||||
let assembled_path = session.temp_dir.join("assembled");
|
let assembled_path = session.temp_dir.join("assembled");
|
||||||
let raw_output = OpenOptions::new()
|
let chunks_meta: Vec<(usize, PathBuf)> = session
|
||||||
.create(true)
|
.chunks
|
||||||
.write(true)
|
.iter()
|
||||||
.truncate(true)
|
.map(|c| (c.index, session.temp_dir.join(format!("chunk_{:06}", c.index))))
|
||||||
.open(&assembled_path)
|
.collect();
|
||||||
.await
|
let total_size = session.total_size;
|
||||||
.map_err(|e| format!("Failed to create assembled file: {e}"))?;
|
|
||||||
|
|
||||||
// Pre-allocate assembled file to reduce fragmentation
|
let hash = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||||
let _ = raw_output.set_len(session.total_size).await;
|
use std::io::{Read, Write, BufWriter as StdBufWriter};
|
||||||
|
|
||||||
// 512 KB I/O buffers — 8× fewer syscalls than 64 KB
|
let raw_output = std::fs::OpenOptions::new()
|
||||||
let mut output = BufWriter::with_capacity(524_288, raw_output);
|
.create(true)
|
||||||
let mut hasher = Sha256::new();
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&assembled_path)
|
||||||
|
.map_err(|e| format!("Failed to create assembled file: {e}"))?;
|
||||||
|
|
||||||
// Stream each chunk into the assembled file + hash
|
// Pre-allocate assembled file to reduce fragmentation
|
||||||
// Single 512 KB read buffer reused across all chunks (avoids N allocations)
|
let _ = raw_output.set_len(total_size);
|
||||||
let mut buf = vec![0u8; 524_288];
|
|
||||||
for chunk in &session.chunks {
|
// 512 KB I/O buffers — 8× fewer syscalls than 64 KB
|
||||||
let chunk_path = session.temp_dir.join(format!("chunk_{:06}", chunk.index));
|
let mut output = StdBufWriter::with_capacity(524_288, raw_output);
|
||||||
let mut chunk_file = File::open(&chunk_path)
|
let mut hasher = Sha256::new();
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to open chunk {}: {}", chunk.index, e))?;
|
// Single 512 KB read buffer reused across all chunks (avoids N allocations)
|
||||||
loop {
|
let mut buf = vec![0u8; 524_288];
|
||||||
let n = tokio::io::AsyncReadExt::read(&mut chunk_file, &mut buf)
|
for (index, chunk_path) in &chunks_meta {
|
||||||
.await
|
let mut chunk_file = std::fs::File::open(chunk_path)
|
||||||
.map_err(|e| format!("Failed to read chunk {}: {}", chunk.index, e))?;
|
.map_err(|e| format!("Failed to open chunk {index}: {e}"))?;
|
||||||
if n == 0 {
|
loop {
|
||||||
break;
|
let n = chunk_file
|
||||||
|
.read(&mut buf)
|
||||||
|
.map_err(|e| format!("Failed to read chunk {index}: {e}"))?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.update(&buf[..n]);
|
||||||
|
output.write_all(&buf[..n]).map_err(|e| {
|
||||||
|
format!("Failed to write chunk {index} to assembled file: {e}")
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
hasher.update(&buf[..n]);
|
|
||||||
output.write_all(&buf[..n]).await.map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"Failed to write chunk {} to assembled file: {}",
|
|
||||||
chunk.index, e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tokio::io::AsyncWriteExt::flush(&mut output)
|
output
|
||||||
.await
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush assembled file: {e}"))?;
|
.map_err(|e| format!("Failed to flush assembled file: {e}"))?;
|
||||||
|
|
||||||
let hash = hex::encode(hasher.finalize());
|
// Clean up chunk files (keep assembled) — already on a blocking thread
|
||||||
|
for (_index, chunk_path) in &chunks_meta {
|
||||||
|
let _ = std::fs::remove_file(chunk_path);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up chunk files (keep assembled)
|
Ok(hex::encode(hasher.finalize()))
|
||||||
for chunk in &session.chunks {
|
})
|
||||||
let chunk_path = session.temp_dir.join(format!("chunk_{:06}", chunk.index));
|
.await
|
||||||
let _ = fs::remove_file(&chunk_path).await;
|
.map_err(|e| format!("Assembly task panicked: {e}"))??;
|
||||||
}
|
|
||||||
|
let assembled_path = session.temp_dir.join("assembled");
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"✅ Assembled chunked upload: {} ({} bytes from {} chunks)",
|
"✅ Assembled chunked upload: {} ({} bytes from {} chunks)",
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ use crate::application::dtos::settings_dto::{
|
|||||||
};
|
};
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::errors::AppError;
|
use crate::interfaces::errors::AppError;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Admin API routes — all require admin role.
|
/// Admin API routes — all require admin role.
|
||||||
pub fn admin_routes() -> Router<AppState> {
|
pub fn admin_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
// OIDC settings
|
// OIDC settings
|
||||||
.route("/settings/oidc", get(get_oidc_settings))
|
.route("/settings/oidc", get(get_oidc_settings))
|
||||||
@@ -69,7 +70,7 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, S
|
|||||||
|
|
||||||
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
||||||
async fn get_oidc_settings(
|
async fn get_oidc_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
@@ -89,7 +90,7 @@ async fn get_oidc_settings(
|
|||||||
|
|
||||||
/// PUT /api/admin/settings/oidc — save OIDC settings + hot-reload
|
/// PUT /api/admin/settings/oidc — save OIDC settings + hot-reload
|
||||||
async fn save_oidc_settings(
|
async fn save_oidc_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(dto): Json<SaveOidcSettingsDto>,
|
Json(dto): Json<SaveOidcSettingsDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -114,7 +115,7 @@ async fn save_oidc_settings(
|
|||||||
|
|
||||||
/// POST /api/admin/settings/oidc/test — test OIDC discovery
|
/// POST /api/admin/settings/oidc/test — test OIDC discovery
|
||||||
async fn test_oidc_connection(
|
async fn test_oidc_connection(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(dto): Json<TestOidcConnectionDto>,
|
Json(dto): Json<TestOidcConnectionDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -135,7 +136,7 @@ async fn test_oidc_connection(
|
|||||||
|
|
||||||
/// GET /api/admin/settings/general — system overview (backward compat)
|
/// GET /api/admin/settings/general — system overview (backward compat)
|
||||||
async fn get_general_settings(
|
async fn get_general_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
@@ -166,7 +167,7 @@ async fn get_general_settings(
|
|||||||
|
|
||||||
/// GET /api/admin/dashboard — full dashboard statistics
|
/// GET /api/admin/dashboard — full dashboard statistics
|
||||||
async fn get_dashboard_stats(
|
async fn get_dashboard_stats(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
@@ -242,7 +243,7 @@ async fn get_dashboard_stats(
|
|||||||
|
|
||||||
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
||||||
async fn list_users(
|
async fn list_users(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListUsersQueryDto>,
|
Query(query): Query<ListUsersQueryDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -278,7 +279,7 @@ async fn list_users(
|
|||||||
|
|
||||||
/// GET /api/admin/users/:id — get single user
|
/// GET /api/admin/users/:id — get single user
|
||||||
async fn get_user(
|
async fn get_user(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -300,7 +301,7 @@ async fn get_user(
|
|||||||
|
|
||||||
/// DELETE /api/admin/users/:id — delete a user
|
/// DELETE /api/admin/users/:id — delete a user
|
||||||
async fn delete_user(
|
async fn delete_user(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -335,7 +336,7 @@ async fn delete_user(
|
|||||||
|
|
||||||
/// PUT /api/admin/users/:id/role — change user role
|
/// PUT /api/admin/users/:id/role — change user role
|
||||||
async fn update_user_role(
|
async fn update_user_role(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<UpdateUserRoleDto>,
|
Json(dto): Json<UpdateUserRoleDto>,
|
||||||
@@ -371,7 +372,7 @@ async fn update_user_role(
|
|||||||
|
|
||||||
/// PUT /api/admin/users/:id/active — activate/deactivate user
|
/// PUT /api/admin/users/:id/active — activate/deactivate user
|
||||||
async fn update_user_active(
|
async fn update_user_active(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<UpdateUserActiveDto>,
|
Json(dto): Json<UpdateUserActiveDto>,
|
||||||
@@ -412,7 +413,7 @@ async fn update_user_active(
|
|||||||
|
|
||||||
/// PUT /api/admin/users/:id/quota — update user storage quota
|
/// PUT /api/admin/users/:id/quota — update user storage quota
|
||||||
async fn update_user_quota(
|
async fn update_user_quota(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<UpdateUserQuotaDto>,
|
Json(dto): Json<UpdateUserQuotaDto>,
|
||||||
@@ -444,7 +445,7 @@ async fn update_user_quota(
|
|||||||
|
|
||||||
/// POST /api/admin/users — create a new user (admin only)
|
/// POST /api/admin/users — create a new user (admin only)
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(dto): Json<AdminCreateUserDto>,
|
Json(dto): Json<AdminCreateUserDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -472,7 +473,7 @@ async fn create_user(
|
|||||||
|
|
||||||
/// PUT /api/admin/users/:id/password — reset a user's password (admin only)
|
/// PUT /api/admin/users/:id/password — reset a user's password (admin only)
|
||||||
async fn reset_user_password(
|
async fn reset_user_password(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<AdminResetPasswordDto>,
|
Json(dto): Json<AdminResetPasswordDto>,
|
||||||
@@ -509,7 +510,7 @@ async fn reset_user_password(
|
|||||||
|
|
||||||
/// GET /api/admin/settings/registration — check if public registration is enabled
|
/// GET /api/admin/settings/registration — check if public registration is enabled
|
||||||
async fn get_registration_setting(
|
async fn get_registration_setting(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
@@ -528,7 +529,7 @@ async fn get_registration_setting(
|
|||||||
|
|
||||||
/// PUT /api/admin/settings/registration — enable/disable public registration
|
/// PUT /api/admin/settings/registration — enable/disable public registration
|
||||||
async fn set_registration_setting(
|
async fn set_registration_setting(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(body): Json<serde_json::Value>,
|
Json(body): Json<serde_json::Value>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
|||||||
///
|
///
|
||||||
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
||||||
/// Registers `/caldav`, `/caldav/`, and `/caldav/{*path}` explicitly.
|
/// Registers `/caldav`, `/caldav/`, and `/caldav/{*path}` explicitly.
|
||||||
pub fn caldav_routes() -> Router<AppState> {
|
pub fn caldav_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/caldav/{*path}", axum::routing::any(handle_caldav_methods))
|
.route("/caldav/{*path}", axum::routing::any(handle_caldav_methods))
|
||||||
.route("/caldav/", axum::routing::any(handle_caldav_methods_root))
|
.route("/caldav/", axum::routing::any(handle_caldav_methods_root))
|
||||||
@@ -48,14 +48,14 @@ pub fn caldav_routes() -> Router<AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_caldav_methods_root(
|
async fn handle_caldav_methods_root(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
handle_caldav_methods_inner(state, req, String::new()).await
|
handle_caldav_methods_inner(state, req, String::new()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_caldav_methods(
|
async fn handle_caldav_methods(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let uri = req.uri().clone();
|
let uri = req.uri().clone();
|
||||||
@@ -64,12 +64,11 @@ async fn handle_caldav_methods(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_caldav_methods_inner(
|
async fn handle_caldav_methods_inner(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
let state = Arc::new(state);
|
|
||||||
|
|
||||||
match method.as_str() {
|
match method.as_str() {
|
||||||
"OPTIONS" => handle_options().await,
|
"OPTIONS" => handle_options().await,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
|||||||
///
|
///
|
||||||
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
||||||
/// Registers `/carddav`, `/carddav/`, and `/carddav/{*path}` explicitly.
|
/// Registers `/carddav`, `/carddav/`, and `/carddav/{*path}` explicitly.
|
||||||
pub fn carddav_routes() -> Router<AppState> {
|
pub fn carddav_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/carddav/{*path}",
|
"/carddav/{*path}",
|
||||||
@@ -53,14 +53,14 @@ pub fn carddav_routes() -> Router<AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_carddav_methods_root(
|
async fn handle_carddav_methods_root(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
handle_carddav_methods_inner(state, req, String::new()).await
|
handle_carddav_methods_inner(state, req, String::new()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_carddav_methods(
|
async fn handle_carddav_methods(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let uri = req.uri().clone();
|
let uri = req.uri().clone();
|
||||||
@@ -69,11 +69,10 @@ async fn handle_carddav_methods(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_carddav_methods_inner(
|
async fn handle_carddav_methods_inner(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let state = Arc::new(state);
|
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
match method.as_str() {
|
match method.as_str() {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::application::ports::dedup_ports::DedupResultDto;
|
use crate::application::ports::dedup_ports::DedupResultDto;
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Global application state for dependency injection
|
/// Global application state for dependency injection
|
||||||
type GlobalState = AppState;
|
type GlobalState = Arc<AppState>;
|
||||||
|
|
||||||
/// Response for hash check endpoint
|
/// Response for hash check endpoint
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ use std::collections::HashMap;
|
|||||||
use crate::application::ports::file_ports::OptimizedFileContent;
|
use crate::application::ports::file_ports::OptimizedFileContent;
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::middleware::auth::{AuthUser, OptionalUserId};
|
use crate::interfaces::middleware::auth::{AuthUser, OptionalUserId};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type aliases for dependency injection state.
|
* Type aliases for dependency injection state.
|
||||||
*/
|
*/
|
||||||
/// Global application state for dependency injection
|
/// Global application state for dependency injection
|
||||||
type GlobalState = AppState;
|
type GlobalState = Arc<AppState>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API handler for file-related operations.
|
* API handler for file-related operations.
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ impl FolderHandler {
|
|||||||
/// Both queries run concurrently via `tokio::join!`.
|
/// Both queries run concurrently via `tokio::join!`.
|
||||||
/// Supports `If-None-Match` / ETag for conditional responses (304).
|
/// Supports `If-None-Match` / ETag for conditional responses (304).
|
||||||
pub async fn list_folder_listing(
|
pub async fn list_folder_listing(
|
||||||
State(state): State<GlobalAppState>,
|
State(state): State<Arc<GlobalAppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -339,7 +339,7 @@ impl FolderHandler {
|
|||||||
|
|
||||||
/// Deletes a folder with trash functionality (ownership enforced by service layer)
|
/// Deletes a folder with trash functionality (ownership enforced by service layer)
|
||||||
pub async fn delete_folder_with_trash(
|
pub async fn delete_folder_with_trash(
|
||||||
State(state): State<GlobalAppState>,
|
State(state): State<Arc<GlobalAppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -392,7 +392,7 @@ impl FolderHandler {
|
|||||||
|
|
||||||
/// Downloads a folder as a ZIP file (ownership enforced)
|
/// Downloads a folder as a ZIP file (ownership enforced)
|
||||||
pub async fn download_folder_zip(
|
pub async fn download_folder_zip(
|
||||||
State(state): State<GlobalAppState>,
|
State(state): State<Arc<GlobalAppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Query(_params): Query<HashMap<String, String>>,
|
Query(_params): Query<HashMap<String, String>>,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use crate::application::dtos::search_dto::SearchCriteriaDto;
|
use crate::application::dtos::search_dto::SearchCriteriaDto;
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for search operations through the API.
|
* Handler for search operations through the API.
|
||||||
@@ -21,7 +22,7 @@ pub struct SearchHandler;
|
|||||||
impl SearchHandler {
|
impl SearchHandler {
|
||||||
/// GET /search — simple query-parameter-based search.
|
/// GET /search — simple query-parameter-based search.
|
||||||
pub async fn search_files_get(
|
pub async fn search_files_get(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(params): Query<SearchParams>,
|
Query(params): Query<SearchParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: File search with parameters: {:?}", params);
|
info!("API: File search with parameters: {:?}", params);
|
||||||
@@ -79,7 +80,7 @@ impl SearchHandler {
|
|||||||
|
|
||||||
/// POST /search/advanced — full criteria in the request body.
|
/// POST /search/advanced — full criteria in the request body.
|
||||||
pub async fn search_files_post(
|
pub async fn search_files_post(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(criteria): Json<SearchCriteriaDto>,
|
Json(criteria): Json<SearchCriteriaDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: Advanced file search");
|
info!("API: Advanced file search");
|
||||||
@@ -119,7 +120,7 @@ impl SearchHandler {
|
|||||||
|
|
||||||
/// GET /search/suggest — lightweight autocomplete suggestions.
|
/// GET /search/suggest — lightweight autocomplete suggestions.
|
||||||
pub async fn suggest_files(
|
pub async fn suggest_files(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(params): Query<SuggestParams>,
|
Query(params): Query<SuggestParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: Search suggestions for {:?}", params.query);
|
info!("API: Search suggestions for {:?}", params.query);
|
||||||
@@ -162,7 +163,7 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// DELETE /search/cache — clears the search results cache.
|
/// DELETE /search/cache — clears the search results cache.
|
||||||
pub async fn clear_search_cache(State(state): State<AppState>) -> impl IntoResponse {
|
pub async fn clear_search_cache(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
info!("API: Clearing search cache");
|
info!("API: Clearing search cache");
|
||||||
|
|
||||||
let search_service = match &state.applications.search_service {
|
let search_service = match &state.applications.search_service {
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ use tracing::{debug, error, instrument, warn};
|
|||||||
// use crate::application::ports::trash_ports::TrashUseCase;
|
// use crate::application::ports::trash_ports::TrashUseCase;
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
|
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Gets all items in the trash for the current user
|
/// Gets all items in the trash for the current user
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn get_trash_items(
|
pub async fn get_trash_items(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
// SECURITY: Always use the authenticated user's ID from the JWT token.
|
// SECURITY: Always use the authenticated user's ID from the JWT token.
|
||||||
@@ -55,7 +56,7 @@ pub async fn get_trash_items(
|
|||||||
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
|
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_to_trash(
|
pub async fn move_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
@@ -109,7 +110,7 @@ pub async fn move_to_trash(
|
|||||||
/// Moves a file to the trash
|
/// Moves a file to the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_file_to_trash(
|
pub async fn move_file_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
@@ -163,7 +164,7 @@ pub async fn move_file_to_trash(
|
|||||||
/// Moves a folder to the trash
|
/// Moves a folder to the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_folder_to_trash(
|
pub async fn move_folder_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
@@ -219,7 +220,7 @@ pub async fn move_folder_to_trash(
|
|||||||
/// Restores an item from the trash to its original location
|
/// Restores an item from the trash to its original location
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn restore_from_trash(
|
pub async fn restore_from_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(trash_id): Path<String>,
|
Path(trash_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
@@ -280,7 +281,7 @@ pub async fn restore_from_trash(
|
|||||||
/// Permanently deletes an item from the trash
|
/// Permanently deletes an item from the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn delete_permanently(
|
pub async fn delete_permanently(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(trash_id): Path<String>,
|
Path(trash_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
@@ -343,7 +344,7 @@ pub async fn delete_permanently(
|
|||||||
/// Empties the trash completely for the current user
|
/// Empties the trash completely for the current user
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn empty_trash(
|
pub async fn empty_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<Arc<AppState>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Request to empty trash for user {}", auth_user.id);
|
debug!("Request to empty trash for user {}", auth_user.id);
|
||||||
|
|||||||
@@ -12,17 +12,22 @@ use axum::{
|
|||||||
http::{HeaderName, Request, StatusCode, header},
|
http::{HeaderName, Request, StatusCode, header},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use bytes::Buf;
|
use bytes::{Buf, Bytes};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use quick_xml::Writer;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::adapters::webdav_adapter::{
|
use crate::application::adapters::webdav_adapter::{
|
||||||
LockInfo, LockScope, LockType, PropFindRequest, WebDavAdapter,
|
LockInfo, LockScope, LockType, PropFindRequest, WebDavAdapter,
|
||||||
};
|
};
|
||||||
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
|
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
||||||
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::errors::AppError;
|
use crate::interfaces::errors::AppError;
|
||||||
use crate::interfaces::middleware::auth::CurrentUser;
|
use crate::interfaces::middleware::auth::CurrentUser;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Create a custom DAV header since it's not in the standard headers
|
// Create a custom DAV header since it's not in the standard headers
|
||||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||||
@@ -36,6 +41,10 @@ const MAX_XML_BODY: usize = 1_048_576;
|
|||||||
/// Maximum body size for MKCOL requests (RFC 4918: body must be empty).
|
/// Maximum body size for MKCOL requests (RFC 4918: body must be empty).
|
||||||
const MAX_MKCOL_BODY: usize = 4096;
|
const MAX_MKCOL_BODY: usize = 4096;
|
||||||
|
|
||||||
|
/// Batch size for streaming PROPFIND — files and folders are fetched in pages
|
||||||
|
/// of this size to keep memory constant regardless of folder contents.
|
||||||
|
const PROPFIND_BATCH_SIZE: i64 = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns the WebDAV router with all required endpoints.
|
* Creates and returns the WebDAV router with all required endpoints.
|
||||||
*
|
*
|
||||||
@@ -44,7 +53,7 @@ const MAX_MKCOL_BODY: usize = 4096;
|
|||||||
*
|
*
|
||||||
* @return Router configured with WebDAV endpoints
|
* @return Router configured with WebDAV endpoints
|
||||||
*/
|
*/
|
||||||
pub fn webdav_routes() -> Router<AppState> {
|
pub fn webdav_routes() -> Router<Arc<AppState>> {
|
||||||
// Three explicit routes to avoid Axum trailing-slash gaps
|
// Three explicit routes to avoid Axum trailing-slash gaps
|
||||||
// (same pattern used for CalDAV/CardDAV)
|
// (same pattern used for CalDAV/CardDAV)
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -72,14 +81,14 @@ fn extract_webdav_path(uri: &axum::http::Uri) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_webdav_methods_root(
|
async fn handle_webdav_methods_root(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
handle_webdav_dispatch(state, req, String::new()).await
|
handle_webdav_dispatch(state, req, String::new()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_webdav_methods(
|
async fn handle_webdav_methods(
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let path = extract_webdav_path(req.uri());
|
let path = extract_webdav_path(req.uri());
|
||||||
@@ -87,7 +96,7 @@ async fn handle_webdav_methods(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_webdav_dispatch(
|
async fn handle_webdav_dispatch(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -140,27 +149,54 @@ async fn handle_options(_path: String) -> Result<Response<Body>, AppError> {
|
|||||||
*
|
*
|
||||||
* This handler processes WebDAV PROPFIND requests according to RFC 4918,
|
* This handler processes WebDAV PROPFIND requests according to RFC 4918,
|
||||||
* retrieving properties of files and folders in the specified path.
|
* retrieving properties of files and folders in the specified path.
|
||||||
* It supports the Depth header to control recursion depth.
|
*
|
||||||
|
* **Security hardening (Sol.2):** `Depth: infinity` is rejected with
|
||||||
|
* `403 Forbidden` and the RFC 4918 `propfind-finite-depth` precondition
|
||||||
|
* error body. The default depth when the header is absent is `1`.
|
||||||
|
*
|
||||||
|
* **Streaming response (Sol.3):** For `Depth: 1`, files and sub-folders
|
||||||
|
* are fetched in batches of `PROPFIND_BATCH_SIZE` and the XML response
|
||||||
|
* is written incrementally to a streaming body. Memory usage is O(batch)
|
||||||
|
* regardless of how many children the folder contains.
|
||||||
*
|
*
|
||||||
* @param state The application state containing service dependencies
|
* @param state The application state containing service dependencies
|
||||||
* @param user The authenticated user information
|
* @param req The HTTP request containing the PROPFIND XML body
|
||||||
* @param path The requested resource path
|
* @param path The requested resource path
|
||||||
* @param req The HTTP request containing the PROPFIND XML body
|
* @return 207 Multi-Status XML response with resource properties
|
||||||
* @return XML response with resource properties
|
|
||||||
*/
|
*/
|
||||||
async fn handle_propfind(
|
async fn handle_propfind(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract depth header (cloning to avoid borrowing issues)
|
// ── 1. Extract and validate Depth header ─────────────────────
|
||||||
let depth = req
|
let depth = req
|
||||||
.headers()
|
.headers()
|
||||||
.get("Depth")
|
.get("Depth")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("infinity")
|
.unwrap_or("1");
|
||||||
.to_string();
|
|
||||||
|
|
||||||
|
// RFC 4918 §9.1: servers MAY reject Depth:infinity with 403
|
||||||
|
if depth == "infinity" {
|
||||||
|
let body = r#"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<D:error xmlns:D="DAV:">
|
||||||
|
<D:propfind-finite-depth/>
|
||||||
|
</D:error>"#;
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(StatusCode::FORBIDDEN)
|
||||||
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||||
|
.body(Body::from(body))
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize: anything other than "0" or "1" is treated as "0"
|
||||||
|
let depth = match depth {
|
||||||
|
"0" | "1" => depth,
|
||||||
|
_ => "0",
|
||||||
|
};
|
||||||
|
let depth_owned = depth.to_string();
|
||||||
|
|
||||||
|
// ── 2. Authenticate ──────────────────────────────────────────
|
||||||
let _user = {
|
let _user = {
|
||||||
let user_ref = req
|
let user_ref = req
|
||||||
.extensions()
|
.extensions()
|
||||||
@@ -169,50 +205,37 @@ async fn handle_propfind(
|
|||||||
user_ref.clone()
|
user_ref.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract the body separately to avoid borrow issues
|
// ── 3. Parse PROPFIND XML body ───────────────────────────────
|
||||||
let body_bytes = {
|
let body_bytes = {
|
||||||
// Convert the request into a body
|
|
||||||
let body = req.into_body();
|
let body = req.into_body();
|
||||||
|
|
||||||
// Read request body (PROPFIND is XML, 1 MB is more than enough)
|
|
||||||
body::to_bytes(body, MAX_XML_BODY)
|
body::to_bytes(body, MAX_XML_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse PROPFIND request
|
|
||||||
let propfind_request = if body_bytes.is_empty() {
|
let propfind_request = if body_bytes.is_empty() {
|
||||||
// Empty body means get all properties
|
|
||||||
PropFindRequest {
|
PropFindRequest {
|
||||||
prop_find_type: crate::application::adapters::webdav_adapter::PropFindType::AllProp,
|
prop_find_type: crate::application::adapters::webdav_adapter::PropFindType::AllProp,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Parse XML body
|
|
||||||
WebDavAdapter::parse_propfind(body_bytes.reader()).map_err(|e| {
|
WebDavAdapter::parse_propfind(body_bytes.reader()).map_err(|e| {
|
||||||
AppError::bad_request(format!("Failed to parse PROPFIND request: {}", e))
|
AppError::bad_request(format!("Failed to parse PROPFIND request: {}", e))
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get folder service from state
|
// ── 4. Services ──────────────────────────────────────────────
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = state.applications.folder_service.clone();
|
||||||
let file_retrieval_service = &state.applications.file_retrieval_service;
|
let file_retrieval_service = state.applications.file_retrieval_service.clone();
|
||||||
|
|
||||||
// Determine base HREF
|
let base_href = if path.is_empty() || path == "/" {
|
||||||
let base_href = format!("/webdav/{}/", path);
|
"/webdav/".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/webdav/{}/", path)
|
||||||
|
};
|
||||||
|
|
||||||
// Check if path exists as a file or folder
|
// ── 5. Determine target resource ─────────────────────────────
|
||||||
if path.is_empty() || path == "/" {
|
if path.is_empty() || path == "/" {
|
||||||
// Root folder — run both queries concurrently
|
// Root folder
|
||||||
let (subfolders_result, files_result) = tokio::join!(
|
|
||||||
folder_service.list_folders(None),
|
|
||||||
file_retrieval_service.list_files(None)
|
|
||||||
);
|
|
||||||
let subfolders = subfolders_result
|
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to get subfolders: {}", e)))?;
|
|
||||||
let files = files_result
|
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to get files: {}", e)))?;
|
|
||||||
|
|
||||||
// Create root folder DTO for response
|
|
||||||
let root_folder = FolderDto {
|
let root_folder = FolderDto {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
@@ -227,94 +250,182 @@ async fn handle_propfind(
|
|||||||
category: "Folder".to_string(),
|
category: "Folder".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate response
|
return build_streaming_propfind_response(
|
||||||
let mut response_body = Vec::new();
|
root_folder,
|
||||||
WebDavAdapter::generate_propfind_response(
|
None, // folder_id = None → root children
|
||||||
&mut response_body,
|
&depth_owned,
|
||||||
Some(&root_folder),
|
|
||||||
&files,
|
|
||||||
&subfolders,
|
|
||||||
&propfind_request,
|
|
||||||
&depth,
|
|
||||||
&base_href,
|
&base_href,
|
||||||
|
propfind_request,
|
||||||
|
folder_service,
|
||||||
|
file_retrieval_service,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.await;
|
||||||
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
}
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Response::builder()
|
// Try folder first
|
||||||
.status(StatusCode::MULTI_STATUS)
|
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
|
||||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
let folder_id = folder.id.clone();
|
||||||
.body(Body::from(response_body))
|
return build_streaming_propfind_response(
|
||||||
.unwrap())
|
folder,
|
||||||
} else {
|
Some(folder_id),
|
||||||
// Check if path is a folder
|
&depth_owned,
|
||||||
let folder_result = folder_service.get_folder_by_path(&path).await;
|
&base_href,
|
||||||
|
propfind_request,
|
||||||
|
folder_service,
|
||||||
|
file_retrieval_service,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
// Try file
|
||||||
// Path is a folder — run both queries concurrently
|
if let Ok(file) = file_retrieval_service.get_file_by_path(&path).await {
|
||||||
let (files, subfolders) = if depth != "0" {
|
let mut buf = Vec::with_capacity(1024);
|
||||||
let (files_r, folders_r) = tokio::join!(
|
{
|
||||||
file_retrieval_service.list_files(Some(&folder.id)),
|
let mut xml_writer = Writer::new(&mut buf);
|
||||||
folder_service.list_folders(Some(&folder.id))
|
WebDavAdapter::write_multistatus_start(&mut xml_writer)
|
||||||
);
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||||
(
|
WebDavAdapter::write_file_entry(
|
||||||
files_r.map_err(|e| AppError::internal_error(format!("Failed to get files: {}", e)))?,
|
&mut xml_writer,
|
||||||
folders_r.map_err(|e| AppError::internal_error(format!("Failed to get subfolders: {}", e)))?,
|
&file,
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(vec![], vec![])
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate response
|
|
||||||
let mut response_body = Vec::new();
|
|
||||||
WebDavAdapter::generate_propfind_response(
|
|
||||||
&mut response_body,
|
|
||||||
Some(&folder),
|
|
||||||
&files,
|
|
||||||
&subfolders,
|
|
||||||
&propfind_request,
|
&propfind_request,
|
||||||
&depth,
|
|
||||||
&base_href,
|
&base_href,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||||
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
WebDavAdapter::write_multistatus_end(&mut xml_writer)
|
||||||
})?;
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||||
|
}
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(StatusCode::MULTI_STATUS)
|
||||||
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||||
|
.body(Body::from(buf))
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Response::builder()
|
Err(AppError::not_found(format!("Resource not found: {}", path)))
|
||||||
.status(StatusCode::MULTI_STATUS)
|
}
|
||||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
||||||
.body(Body::from(response_body))
|
|
||||||
.unwrap())
|
|
||||||
} else {
|
|
||||||
// Check if path is a file
|
|
||||||
let file_result = file_retrieval_service.get_file_by_path(&path).await;
|
|
||||||
|
|
||||||
if let Ok(file) = file_result {
|
/// Builds a streaming 207 Multi-Status PROPFIND response.
|
||||||
// Path is a file
|
///
|
||||||
let mut response_body = Vec::new();
|
/// The XML is written incrementally: first the folder itself, then children
|
||||||
WebDavAdapter::generate_propfind_response_for_file(
|
/// (sub-folders and files) are fetched in batches of `PROPFIND_BATCH_SIZE`.
|
||||||
&mut response_body,
|
/// Each batch is serialised to XML and sent as a chunk, so memory stays
|
||||||
&file,
|
/// constant at O(batch_size) regardless of the total number of children.
|
||||||
&propfind_request,
|
async fn build_streaming_propfind_response(
|
||||||
&depth,
|
folder: FolderDto,
|
||||||
&base_href,
|
folder_id: Option<String>,
|
||||||
)
|
depth: &str,
|
||||||
.map_err(|e| {
|
base_href: &str,
|
||||||
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
propfind_request: PropFindRequest,
|
||||||
})?;
|
folder_service: std::sync::Arc<dyn FolderUseCase>,
|
||||||
|
file_retrieval_service: std::sync::Arc<dyn FileRetrievalUseCase>,
|
||||||
|
) -> Result<Response<Body>, AppError> {
|
||||||
|
let depth = depth.to_string();
|
||||||
|
let base_href = base_href.to_string();
|
||||||
|
let propfind_request = Arc::new(propfind_request);
|
||||||
|
|
||||||
Ok(Response::builder()
|
let stream = async_stream::try_stream! {
|
||||||
.status(StatusCode::MULTI_STATUS)
|
// ── XML header + <D:multistatus> + folder entry ──────────
|
||||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
let mut buf = Vec::with_capacity(4096);
|
||||||
.body(Body::from(response_body))
|
{
|
||||||
.unwrap())
|
let mut w = Writer::new(&mut buf);
|
||||||
} else {
|
WebDavAdapter::write_multistatus_start(&mut w)
|
||||||
// Path does not exist
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
Err(AppError::not_found(format!("Resource not found: {}", path)))
|
WebDavAdapter::write_folder_entry(&mut w, &folder, &propfind_request, &base_href)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
}
|
||||||
|
yield Bytes::from(buf);
|
||||||
|
|
||||||
|
// ── Children (only if Depth == 1) ────────────────────────
|
||||||
|
if depth == "1" {
|
||||||
|
let pagination = crate::application::dtos::pagination::PaginationRequestDto {
|
||||||
|
page: 0,
|
||||||
|
page_size: PROPFIND_BATCH_SIZE as usize,
|
||||||
|
};
|
||||||
|
let fid_ref = folder_id.as_deref();
|
||||||
|
|
||||||
|
// Stream sub-folders in pages
|
||||||
|
let mut page = 0usize;
|
||||||
|
loop {
|
||||||
|
let pag = crate::application::dtos::pagination::PaginationRequestDto {
|
||||||
|
page,
|
||||||
|
page_size: pagination.page_size,
|
||||||
|
};
|
||||||
|
let result = folder_service
|
||||||
|
.list_folders_paginated(fid_ref, &pag)
|
||||||
|
.await
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
|
||||||
|
if result.items.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chunk = Vec::with_capacity(result.items.len() * 800);
|
||||||
|
{
|
||||||
|
let mut w = Writer::new(&mut chunk);
|
||||||
|
for subfolder in &result.items {
|
||||||
|
let href = format!("{}{}/", base_href, subfolder.name);
|
||||||
|
WebDavAdapter::write_folder_entry(&mut w, subfolder, &propfind_request, &href)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let has_more = result.pagination.has_next;
|
||||||
|
yield Bytes::from(chunk);
|
||||||
|
|
||||||
|
if !has_more {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream files in pages
|
||||||
|
let mut offset: i64 = 0;
|
||||||
|
loop {
|
||||||
|
let batch: Vec<FileDto> = file_retrieval_service
|
||||||
|
.list_files_batch(fid_ref, offset, PROPFIND_BATCH_SIZE)
|
||||||
|
.await
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
|
||||||
|
if batch.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch_len = batch.len();
|
||||||
|
let mut chunk = Vec::with_capacity(batch_len * 800);
|
||||||
|
{
|
||||||
|
let mut w = Writer::new(&mut chunk);
|
||||||
|
for file in &batch {
|
||||||
|
let href = format!("{}{}", base_href, file.name);
|
||||||
|
WebDavAdapter::write_file_entry(&mut w, file, &propfind_request, &href)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield Bytes::from(chunk);
|
||||||
|
|
||||||
|
if (batch_len as i64) < PROPFIND_BATCH_SIZE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += batch_len as i64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// ── Close </D:multistatus> ───────────────────────────────
|
||||||
|
let mut buf = Vec::with_capacity(32);
|
||||||
|
{
|
||||||
|
let mut w = Writer::new(&mut buf);
|
||||||
|
WebDavAdapter::write_multistatus_end(&mut w)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
}
|
||||||
|
yield Bytes::from(buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
let stream = stream.map_err(|e: std::io::Error| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) });
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::MULTI_STATUS)
|
||||||
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||||
|
.body(Body::from_stream(stream))
|
||||||
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,7 +441,7 @@ async fn handle_propfind(
|
|||||||
* @return XML response with property modification results
|
* @return XML response with property modification results
|
||||||
*/
|
*/
|
||||||
async fn handle_proppatch(
|
async fn handle_proppatch(
|
||||||
_state: AppState,
|
_state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -387,7 +498,7 @@ async fn handle_proppatch(
|
|||||||
* @return HTTP response with file contents
|
* @return HTTP response with file contents
|
||||||
*/
|
*/
|
||||||
async fn handle_get(
|
async fn handle_get(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
_req: Request<Body>,
|
_req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -431,7 +542,7 @@ async fn handle_get(
|
|||||||
* Handles HEAD requests — same as GET but returns only headers, no body.
|
* Handles HEAD requests — same as GET but returns only headers, no body.
|
||||||
*/
|
*/
|
||||||
async fn handle_head(
|
async fn handle_head(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
_req: Request<Body>,
|
_req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -494,7 +605,7 @@ async fn handle_head(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_put(
|
async fn handle_put(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -598,7 +709,7 @@ async fn handle_put(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_mkcol(
|
async fn handle_mkcol(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -673,7 +784,7 @@ async fn handle_mkcol(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_delete(
|
async fn handle_delete(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
_req: Request<Body>,
|
_req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -728,7 +839,7 @@ async fn handle_delete(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_move(
|
async fn handle_move(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -889,7 +1000,7 @@ async fn handle_move(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_copy(
|
async fn handle_copy(
|
||||||
state: AppState,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -1059,7 +1170,7 @@ async fn handle_copy(
|
|||||||
* @return XML response with lock information
|
* @return XML response with lock information
|
||||||
*/
|
*/
|
||||||
async fn handle_lock(
|
async fn handle_lock(
|
||||||
_state: AppState,
|
_state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
@@ -1185,7 +1296,7 @@ async fn handle_lock(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_unlock(
|
async fn handle_unlock(
|
||||||
_state: AppState,
|
_state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
_path: String,
|
_path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub struct WopiState {
|
|||||||
pub token_service: Arc<WopiTokenService>,
|
pub token_service: Arc<WopiTokenService>,
|
||||||
pub lock_service: Arc<WopiLockService>,
|
pub lock_service: Arc<WopiLockService>,
|
||||||
pub discovery_service: Arc<WopiDiscoveryService>,
|
pub discovery_service: Arc<WopiDiscoveryService>,
|
||||||
pub app_state: crate::common::di::AppState,
|
pub app_state: Arc<crate::common::di::AppState>,
|
||||||
/// Public base URL for host page origin and postMessage origin
|
/// Public base URL for host page origin and postMessage origin
|
||||||
pub public_base_url: String,
|
pub public_base_url: String,
|
||||||
/// Base URL used for WOPISrc callbacks from Collabora to OxiCloud
|
/// Base URL used for WOPISrc callbacks from Collabora to OxiCloud
|
||||||
@@ -506,8 +506,8 @@ async fn get_supported_extensions(State(state): State<WopiState>) -> Response {
|
|||||||
pub fn wopi_routes(
|
pub fn wopi_routes(
|
||||||
wopi_state: WopiState,
|
wopi_state: WopiState,
|
||||||
) -> (
|
) -> (
|
||||||
Router<crate::common::di::AppState>,
|
Router<Arc<crate::common::di::AppState>>,
|
||||||
Router<crate::common::di::AppState>,
|
Router<Arc<crate::common::di::AppState>>,
|
||||||
) {
|
) {
|
||||||
let protocol_router = Router::new()
|
let protocol_router = Router::new()
|
||||||
// CheckFileInfo
|
// CheckFileInfo
|
||||||
|
|||||||
@@ -28,12 +28,7 @@ use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
|
|||||||
use crate::interfaces::api::handlers::trash_handler;
|
use crate::interfaces::api::handlers::trash_handler;
|
||||||
|
|
||||||
/// Creates public API routes that should NOT require authentication.
|
/// Creates public API routes that should NOT require authentication.
|
||||||
///
|
pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
/// Currently this includes:
|
|
||||||
/// - `/s/{token}` — public access to shared items via share link
|
|
||||||
/// - `/s/{token}/verify` — password verification for protected share links
|
|
||||||
/// - `/i18n/*` — internationalization/translation endpoints
|
|
||||||
pub fn create_public_api_routes(app_state: &AppState) -> Router<AppState> {
|
|
||||||
let share_service = app_state.share_service.clone();
|
let share_service = app_state.share_service.clone();
|
||||||
let i18n_service = Some(app_state.applications.i18n_service.clone());
|
let i18n_service = Some(app_state.applications.i18n_service.clone());
|
||||||
|
|
||||||
@@ -79,7 +74,7 @@ pub fn create_public_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
/// These routes require authentication when auth is enabled.
|
/// These routes require authentication when auth is enabled.
|
||||||
/// Receives the fully-assembled `AppState` and extracts all needed services
|
/// Receives the fully-assembled `AppState` and extracts all needed services
|
||||||
/// from it, avoiding a long parameter list.
|
/// from it, avoiding a long parameter list.
|
||||||
pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
// Extract services from the pre-built AppState
|
// Extract services from the pre-built AppState
|
||||||
let folder_service = app_state.applications.folder_service_concrete.clone();
|
let folder_service = app_state.applications.folder_service_concrete.clone();
|
||||||
let file_retrieval_service = app_state.applications.file_retrieval_service.clone();
|
let file_retrieval_service = app_state.applications.file_retrieval_service.clone();
|
||||||
@@ -276,7 +271,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
post(ChunkedUploadHandler::complete_upload),
|
post(ChunkedUploadHandler::complete_upload),
|
||||||
)
|
)
|
||||||
.route("/{upload_id}", delete(ChunkedUploadHandler::cancel_upload))
|
.route("/{upload_id}", delete(ChunkedUploadHandler::cancel_upload))
|
||||||
.with_state(Arc::new(app_state.clone()));
|
.with_state(app_state.clone());
|
||||||
|
|
||||||
// Create routes for deduplication endpoints
|
// Create routes for deduplication endpoints
|
||||||
let dedup_router = Router::new()
|
let dedup_router = Router::new()
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ use crate::common::config::AppConfig;
|
|||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use axum::http::header::{CACHE_CONTROL, HeaderValue};
|
use axum::http::header::{CACHE_CONTROL, HeaderValue};
|
||||||
use axum::{Router, response::Html, routing::get};
|
use axum::{Router, response::Html, routing::get};
|
||||||
|
use std::sync::Arc;
|
||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_http::set_header::SetResponseHeaderLayer;
|
use tower_http::set_header::SetResponseHeaderLayer;
|
||||||
|
|
||||||
/// Creates web routes for serving static files
|
/// Creates web routes for serving static files
|
||||||
pub fn create_web_routes() -> Router<AppState> {
|
pub fn create_web_routes() -> Router<Arc<AppState>> {
|
||||||
// Get config to access static path
|
// Get config to access static path
|
||||||
let config = AppConfig::from_env();
|
let config = AppConfig::from_env();
|
||||||
let static_path = config.static_path.clone();
|
let static_path = config.static_path.clone();
|
||||||
|
|||||||
+10
-7
@@ -92,6 +92,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let app_state = factory.build_app_state(db_pool).await
|
let app_state = factory.build_app_state(db_pool).await
|
||||||
.expect("Failed to build application state. If running in Docker, ensure the storage volume is writable by the oxicloud user (UID 1001)");
|
.expect("Failed to build application state. If running in Docker, ensure the storage volume is writable by the oxicloud user (UID 1001)");
|
||||||
|
|
||||||
|
// Wrap in Arc so that Axum clones a single refcount per request
|
||||||
|
// instead of deep-copying ~42 Arc fields + 16 String/PathBuf allocations.
|
||||||
|
let app_state = Arc::new(app_state);
|
||||||
|
|
||||||
// Build application router
|
// Build application router
|
||||||
let api_routes = create_api_routes(&app_state);
|
let api_routes = create_api_routes(&app_state);
|
||||||
let public_api_routes = create_public_api_routes(&app_state);
|
let public_api_routes = create_public_api_routes(&app_state);
|
||||||
@@ -161,26 +165,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
use interfaces::api::handlers::auth_handler::auth_routes;
|
use interfaces::api::handlers::auth_handler::auth_routes;
|
||||||
use oxicloud::interfaces::middleware::auth::auth_middleware;
|
use oxicloud::interfaces::middleware::auth::auth_middleware;
|
||||||
|
|
||||||
let app_state_arc = Arc::new(app_state.clone());
|
let auth_router = auth_routes().with_state(app_state.clone());
|
||||||
let auth_router = auth_routes().with_state(app_state_arc.clone());
|
|
||||||
|
|
||||||
// Protected API routes — require valid JWT token
|
// Protected API routes — require valid JWT token
|
||||||
let protected_api = api_routes.layer(axum::middleware::from_fn_with_state(
|
let protected_api = api_routes.layer(axum::middleware::from_fn_with_state(
|
||||||
app_state_arc.clone(),
|
app_state.clone(),
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
));
|
));
|
||||||
|
|
||||||
// CalDAV/CardDAV/WebDAV with auth middleware (merged, not nested)
|
// CalDAV/CardDAV/WebDAV with auth middleware (merged, not nested)
|
||||||
let caldav_protected = caldav_router.layer(axum::middleware::from_fn_with_state(
|
let caldav_protected = caldav_router.layer(axum::middleware::from_fn_with_state(
|
||||||
app_state_arc.clone(),
|
app_state.clone(),
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
));
|
));
|
||||||
let carddav_protected = carddav_router.layer(axum::middleware::from_fn_with_state(
|
let carddav_protected = carddav_router.layer(axum::middleware::from_fn_with_state(
|
||||||
app_state_arc.clone(),
|
app_state.clone(),
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
));
|
));
|
||||||
let webdav_protected = webdav_router.layer(axum::middleware::from_fn_with_state(
|
let webdav_protected = webdav_router.layer(axum::middleware::from_fn_with_state(
|
||||||
app_state_arc,
|
app_state.clone(),
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -201,7 +204,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Mount WOPI routes (protocol routes use own token auth, API routes behind auth middleware)
|
// Mount WOPI routes (protocol routes use own token auth, API routes behind auth middleware)
|
||||||
if let Some((wopi_protocol, wopi_api)) = wopi_routes {
|
if let Some((wopi_protocol, wopi_api)) = wopi_routes {
|
||||||
let wopi_api_protected = wopi_api.layer(axum::middleware::from_fn_with_state(
|
let wopi_api_protected = wopi_api.layer(axum::middleware::from_fn_with_state(
|
||||||
Arc::new(app_state.clone()),
|
app_state.clone(),
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
));
|
));
|
||||||
app = app
|
app = app
|
||||||
|
|||||||
Reference in New Issue
Block a user