fix: resolve clippy warnings and rustfmt issues for CI compliance
Fix all clippy lints (collapsible if, clone on Copy, needless borrow, redundant bindings, unused params) and apply rustfmt across the codebase. Update test mocks to match Uuid-based trait signatures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -2527,7 +2527,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxicloud"
|
name = "oxicloud"
|
||||||
version = "0.5.0"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) {
|
|||||||
fs::write(dist_dir.join("css").join(&css_name), &css_bundle).expect("write css bundle");
|
fs::write(dist_dir.join("css").join(&css_name), &css_bundle).expect("write css bundle");
|
||||||
|
|
||||||
// ── 4. Minify ALL individual CSS in static-dist/ ─────────────────────────
|
// ── 4. Minify ALL individual CSS in static-dist/ ─────────────────────────
|
||||||
minify_tree_css(&dist_dir.join("css"), &css_name);
|
minify_tree_css(&dist_dir.join("css"));
|
||||||
|
|
||||||
// ── 5. Build JS bundle for index.html ────────────────────────────────────
|
// ── 5. Build JS bundle for index.html ────────────────────────────────────
|
||||||
let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html");
|
let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html");
|
||||||
@@ -120,11 +120,11 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) {
|
|||||||
fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle");
|
fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle");
|
||||||
|
|
||||||
// ── 6. Minify ALL individual JS in static-dist/ ──────────────────────────
|
// ── 6. Minify ALL individual JS in static-dist/ ──────────────────────────
|
||||||
minify_tree_js(&dist_dir.join("js"), &js_name);
|
minify_tree_js(&dist_dir.join("js"));
|
||||||
|
|
||||||
// ── 7. Inline theme-init.js & rewrite index.html ──────────────────────
|
// ── 7. Inline theme-init.js & rewrite index.html ──────────────────────
|
||||||
let theme_init = fs::read_to_string(static_dir.join("js/core/theme-init.js"))
|
let theme_init =
|
||||||
.unwrap_or_default();
|
fs::read_to_string(static_dir.join("js/core/theme-init.js")).unwrap_or_default();
|
||||||
let theme_init_min = js_minify_safe(&theme_init);
|
let theme_init_min = js_minify_safe(&theme_init);
|
||||||
let rewritten_index = rewrite_index_html(
|
let rewritten_index = rewrite_index_html(
|
||||||
&index_html,
|
&index_html,
|
||||||
@@ -153,9 +153,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) {
|
|||||||
// index.html too (future use / embedded route)
|
// index.html too (future use / embedded route)
|
||||||
fs::write(out_dir.join("index.html"), &rewritten_index).expect("write out index.html");
|
fs::write(out_dir.join("index.html"), &rewritten_index).expect("write out index.html");
|
||||||
|
|
||||||
eprintln!(
|
eprintln!("cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}");
|
||||||
"cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -206,8 +204,8 @@ fn css_minify_safe(source: &str) -> String {
|
|||||||
fn css_minify(source: &str) -> Result<String, String> {
|
fn css_minify(source: &str) -> Result<String, String> {
|
||||||
use lightningcss::stylesheet::{ParserOptions, PrinterOptions, StyleSheet};
|
use lightningcss::stylesheet::{ParserOptions, PrinterOptions, StyleSheet};
|
||||||
|
|
||||||
let mut sheet = StyleSheet::parse(source, ParserOptions::default())
|
let mut sheet =
|
||||||
.map_err(|e| format!("{e}"))?;
|
StyleSheet::parse(source, ParserOptions::default()).map_err(|e| format!("{e}"))?;
|
||||||
|
|
||||||
sheet
|
sheet
|
||||||
.minify(Default::default())
|
.minify(Default::default())
|
||||||
@@ -224,12 +222,14 @@ fn css_minify(source: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Walk a directory and minify every `.css` in-place (skips generated bundles).
|
/// Walk a directory and minify every `.css` in-place (skips generated bundles).
|
||||||
fn minify_tree_css(dir: &Path, skip_prefix: &str) {
|
fn minify_tree_css(dir: &Path) {
|
||||||
let Ok(entries) = fs::read_dir(dir) else { return };
|
let Ok(entries) = fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let p = entry.path();
|
let p = entry.path();
|
||||||
if p.is_dir() {
|
if p.is_dir() {
|
||||||
minify_tree_css(&p, skip_prefix);
|
minify_tree_css(&p);
|
||||||
} else if p.extension().is_some_and(|e| e == "css") {
|
} else if p.extension().is_some_and(|e| e == "css") {
|
||||||
let fname = p.file_name().unwrap().to_string_lossy();
|
let fname = p.file_name().unwrap().to_string_lossy();
|
||||||
// Skip the generated bundle and already-processed main.css
|
// Skip the generated bundle and already-processed main.css
|
||||||
@@ -336,12 +336,14 @@ fn js_minify(source: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Walk a directory and minify every `.js` in-place (skips generated bundles).
|
/// Walk a directory and minify every `.js` in-place (skips generated bundles).
|
||||||
fn minify_tree_js(dir: &Path, skip_prefix: &str) {
|
fn minify_tree_js(dir: &Path) {
|
||||||
let Ok(entries) = fs::read_dir(dir) else { return };
|
let Ok(entries) = fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let p = entry.path();
|
let p = entry.path();
|
||||||
if p.is_dir() {
|
if p.is_dir() {
|
||||||
minify_tree_js(&p, skip_prefix);
|
minify_tree_js(&p);
|
||||||
} else if p.extension().is_some_and(|e| e == "js") {
|
} else if p.extension().is_some_and(|e| e == "js") {
|
||||||
let fname = p.file_name().unwrap().to_string_lossy();
|
let fname = p.file_name().unwrap().to_string_lossy();
|
||||||
if fname.starts_with("app.") {
|
if fname.starts_with("app.") {
|
||||||
@@ -359,13 +361,15 @@ fn minify_tree_js(dir: &Path, skip_prefix: &str) {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
fn minify_tree_json(dir: &Path) {
|
fn minify_tree_json(dir: &Path) {
|
||||||
let Ok(entries) = fs::read_dir(dir) else { return };
|
let Ok(entries) = fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let p = entry.path();
|
let p = entry.path();
|
||||||
if p.extension().is_some_and(|e| e == "json") {
|
if p.extension().is_some_and(|e| e == "json")
|
||||||
if let Ok(src) = fs::read_to_string(&p) {
|
&& let Ok(src) = fs::read_to_string(&p)
|
||||||
let _ = fs::write(&p, json_minify(&src));
|
{
|
||||||
}
|
let _ = fs::write(&p, json_minify(&src));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,12 +411,7 @@ fn json_minify(source: &str) -> String {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/// Rewrite index.html: single CSS bundle, inline theme-init, single JS bundle.
|
/// Rewrite index.html: single CSS bundle, inline theme-init, single JS bundle.
|
||||||
fn rewrite_index_html(
|
fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js: &str) -> String {
|
||||||
html: &str,
|
|
||||||
css_path: &str,
|
|
||||||
js_path: &str,
|
|
||||||
inline_theme_js: &str,
|
|
||||||
) -> String {
|
|
||||||
let mut out: Vec<String> = Vec::with_capacity(html.lines().count());
|
let mut out: Vec<String> = Vec::with_capacity(html.lines().count());
|
||||||
let mut css_done = false;
|
let mut css_done = false;
|
||||||
let mut defer_done = false;
|
let mut defer_done = false;
|
||||||
@@ -423,19 +422,14 @@ fn rewrite_index_html(
|
|||||||
// ── Replace all stylesheet <link>s with single bundle ────────────────
|
// ── Replace all stylesheet <link>s with single bundle ────────────────
|
||||||
if t.starts_with("<link") && t.contains("stylesheet") && t.contains("href=\"/css/") {
|
if t.starts_with("<link") && t.contains("stylesheet") && t.contains("href=\"/css/") {
|
||||||
if !css_done {
|
if !css_done {
|
||||||
out.push(format!(
|
out.push(format!(" <link rel=\"stylesheet\" href=\"{css_path}\">"));
|
||||||
" <link rel=\"stylesheet\" href=\"{css_path}\">"
|
|
||||||
));
|
|
||||||
css_done = true;
|
css_done = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Replace sync theme-init.js with inline <script> ─────────────────
|
// ── Replace sync theme-init.js with inline <script> ─────────────────
|
||||||
if t.starts_with("<script")
|
if t.starts_with("<script") && !t.contains("defer") && t.contains("theme-init") {
|
||||||
&& !t.contains("defer")
|
|
||||||
&& t.contains("theme-init")
|
|
||||||
{
|
|
||||||
out.push(format!(" <script>{inline_theme_js}</script>"));
|
out.push(format!(" <script>{inline_theme_js}</script>"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -443,9 +437,7 @@ fn rewrite_index_html(
|
|||||||
// ── Replace all defer <script>s with single bundle ──────────────────
|
// ── Replace all defer <script>s with single bundle ──────────────────
|
||||||
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
|
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
|
||||||
if !defer_done {
|
if !defer_done {
|
||||||
out.push(format!(
|
out.push(format!(" <script defer src=\"{js_path}\"></script>"));
|
||||||
" <script defer src=\"{js_path}\"></script>"
|
|
||||||
));
|
|
||||||
defer_done = true;
|
defer_done = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -89,11 +89,7 @@ pub trait ThumbnailPort: Send + Sync + 'static {
|
|||||||
/// Returns `None` if no cached thumbnail exists on disk or in memory.
|
/// Returns `None` if no cached thumbnail exists on disk or in memory.
|
||||||
/// Used for non-image file types (videos) where thumbnails are
|
/// Used for non-image file types (videos) where thumbnails are
|
||||||
/// generated client-side and uploaded.
|
/// generated client-side and uploaded.
|
||||||
async fn get_cached_thumbnail(
|
async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes>;
|
||||||
&self,
|
|
||||||
file_id: &str,
|
|
||||||
size: ThumbnailSize,
|
|
||||||
) -> Option<Bytes>;
|
|
||||||
|
|
||||||
/// Store an externally-generated thumbnail (e.g. client-side video frame).
|
/// Store an externally-generated thumbnail (e.g. client-side video frame).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ use crate::infrastructure::services::jwt_service::JwtTokenService;
|
|||||||
use crate::infrastructure::services::oidc_service::OidcService;
|
use crate::infrastructure::services::oidc_service::OidcService;
|
||||||
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
||||||
use moka::sync::Cache;
|
use moka::sync::Cache;
|
||||||
use uuid::Uuid;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Result of a successful OIDC callback. The handler layer inspects this to
|
/// Result of a successful OIDC callback. The handler layer inspects this to
|
||||||
/// decide whether to redirect to the regular frontend or complete a Nextcloud
|
/// decide whether to redirect to the regular frontend or complete a Nextcloud
|
||||||
|
|||||||
@@ -448,11 +448,9 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let uid = user_id;
|
|
||||||
|
|
||||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let trash = trash_service.clone();
|
let trash = trash_service.clone();
|
||||||
let uid = uid;
|
let uid = user_id;
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let trash_result = trash.move_to_trash(&file_id, "file", uid).await;
|
let trash_result = trash.move_to_trash(&file_id, "file", uid).await;
|
||||||
@@ -513,11 +511,9 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let uid = user_id;
|
|
||||||
|
|
||||||
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||||
let trash = trash_service.clone();
|
let trash = trash_service.clone();
|
||||||
let uid = uid;
|
let uid = user_id;
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await;
|
let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use uuid::Uuid;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::address_book_dto::{
|
use crate::application::dtos::address_book_dto::{
|
||||||
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
||||||
@@ -296,8 +296,11 @@ impl AddressBookUseCase for ContactService {
|
|||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
let address_book = self
|
let address_book = self
|
||||||
.check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id)
|
.check_address_book_write_access(
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
&id,
|
||||||
|
&Uuid::parse_str(&update.user_id)
|
||||||
|
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Apply updates
|
// Apply updates
|
||||||
@@ -526,9 +529,12 @@ impl ContactUseCase for ContactService {
|
|||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
self.check_address_book_write_access(
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
&address_book_id,
|
||||||
.await?;
|
&Uuid::parse_str(&dto.user_id)
|
||||||
|
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Convert DTOs to domain entities
|
// Convert DTOs to domain entities
|
||||||
let email: Vec<Email> = dto
|
let email: Vec<Email> = dto
|
||||||
@@ -604,9 +610,12 @@ impl ContactUseCase for ContactService {
|
|||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
self.check_address_book_write_access(
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
&address_book_id,
|
||||||
.await?;
|
&Uuid::parse_str(&dto.user_id)
|
||||||
|
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Parse vCard data
|
// Parse vCard data
|
||||||
let mut contact = self.parse_vcard(&dto.vcard)?;
|
let mut contact = self.parse_vcard(&dto.vcard)?;
|
||||||
@@ -819,9 +828,12 @@ impl ContactUseCase for ContactService {
|
|||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
self.check_address_book_write_access(
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
&address_book_id,
|
||||||
.await?;
|
&Uuid::parse_str(&dto.user_id)
|
||||||
|
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let group = ContactGroup::new(address_book_id, dto.name);
|
let group = ContactGroup::new(address_book_id, dto.name);
|
||||||
|
|
||||||
@@ -845,9 +857,12 @@ impl ContactUseCase for ContactService {
|
|||||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
self.check_address_book_write_access(group.address_book_id(), &Uuid::parse_str(&update.user_id)
|
self.check_address_book_write_access(
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
group.address_book_id(),
|
||||||
.await?;
|
&Uuid::parse_str(&update.user_id)
|
||||||
|
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Update the group
|
// Update the group
|
||||||
let updated_group = ContactGroup::from_raw(
|
let updated_group = ContactGroup::from_raw(
|
||||||
|
|||||||
@@ -178,10 +178,10 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||||
self.file_repository.delete_file(id).await?;
|
self.file_repository.delete_file(id).await?;
|
||||||
// Best-effort thumbnail cleanup
|
// Best-effort thumbnail cleanup
|
||||||
if let Some(thumb) = &self.thumbnail_service {
|
if let Some(thumb) = &self.thumbnail_service
|
||||||
if let Err(e) = thumb.delete_thumbnails(id).await {
|
&& let Err(e) = thumb.delete_thumbnails(id).await
|
||||||
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
{
|
||||||
}
|
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -223,10 +223,10 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
warn!("Permanently deleting file: {}", id);
|
warn!("Permanently deleting file: {}", id);
|
||||||
self.file_repository.delete_file(id).await?;
|
self.file_repository.delete_file(id).await?;
|
||||||
// Best-effort thumbnail cleanup
|
// Best-effort thumbnail cleanup
|
||||||
if let Some(thumb) = &self.thumbnail_service {
|
if let Some(thumb) = &self.thumbnail_service
|
||||||
if let Err(e) = thumb.delete_thumbnails(id).await {
|
&& let Err(e) = thumb.delete_thumbnails(id).await
|
||||||
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
{
|
||||||
}
|
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
||||||
}
|
}
|
||||||
info!("File permanently deleted: {}", id);
|
info!("File permanently deleted: {}", id);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::collections::HashMap;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
@@ -22,7 +23,7 @@ use crate::domain::services::path_service::StoragePath;
|
|||||||
/// A simple in-memory mock that maps (file_id → (File, owner_id)).
|
/// A simple in-memory mock that maps (file_id → (File, owner_id)).
|
||||||
struct MockFileReadPort {
|
struct MockFileReadPort {
|
||||||
/// file_id → (File, owner_id)
|
/// file_id → (File, owner_id)
|
||||||
files: Mutex<HashMap<String, (File, String)>>,
|
files: Mutex<HashMap<String, (File, Uuid)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockFileReadPort {
|
impl MockFileReadPort {
|
||||||
@@ -33,7 +34,7 @@ impl MockFileReadPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a test file owned by `owner_id`.
|
/// Insert a test file owned by `owner_id`.
|
||||||
fn insert(&self, id: &str, name: &str, owner_id: &str) {
|
fn insert(&self, id: &str, name: &str, owner_id: Uuid) {
|
||||||
let file = File::new(
|
let file = File::new(
|
||||||
id.to_string(),
|
id.to_string(),
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
@@ -46,7 +47,7 @@ impl MockFileReadPort {
|
|||||||
self.files
|
self.files
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(id.to_string(), (file, owner_id.to_string()));
|
.insert(id.to_string(), (file, owner_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +60,10 @@ impl FileReadPort for MockFileReadPort {
|
|||||||
.ok_or_else(|| DomainError::not_found("File", id.to_string()))
|
.ok_or_else(|| DomainError::not_found("File", id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result<File, DomainError> {
|
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError> {
|
||||||
let files = self.files.lock().unwrap();
|
let files = self.files.lock().unwrap();
|
||||||
match files.get(id) {
|
match files.get(id) {
|
||||||
Some((file, actual_owner)) if actual_owner == owner_id => Ok(file.clone()),
|
Some((file, actual_owner)) if *actual_owner == owner_id => Ok(file.clone()),
|
||||||
// Return NotFound regardless — do not leak existence
|
// Return NotFound regardless — do not leak existence
|
||||||
_ => Err(DomainError::not_found("File", id.to_string())),
|
_ => Err(DomainError::not_found("File", id.to_string())),
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ impl FileReadPort for MockFileReadPort {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError> {
|
) -> Result<(Vec<File>, usize), DomainError> {
|
||||||
Ok((Vec::new(), 0))
|
Ok((Vec::new(), 0))
|
||||||
}
|
}
|
||||||
@@ -113,7 +114,7 @@ impl FileReadPort for MockFileReadPort {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<usize, DomainError> {
|
) -> Result<usize, DomainError> {
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
@@ -248,20 +249,23 @@ impl FileWritePort for MockFileWritePort {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_file_for_owner_returns_file_for_correct_owner() {
|
async fn get_file_for_owner_returns_file_for_correct_owner() {
|
||||||
|
let alice_id = Uuid::new_v4();
|
||||||
let repo = MockFileReadPort::new();
|
let repo = MockFileReadPort::new();
|
||||||
repo.insert("file-1", "secret.txt", "alice");
|
repo.insert("file-1", "secret.txt", alice_id);
|
||||||
|
|
||||||
let result = repo.get_file_for_owner("file-1", "alice").await;
|
let result = repo.get_file_for_owner("file-1", alice_id).await;
|
||||||
assert!(result.is_ok(), "owner should be able to read own file");
|
assert!(result.is_ok(), "owner should be able to read own file");
|
||||||
assert_eq!(result.unwrap().id(), "file-1");
|
assert_eq!(result.unwrap().id(), "file-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_file_for_owner_rejects_wrong_owner() {
|
async fn get_file_for_owner_rejects_wrong_owner() {
|
||||||
|
let alice_id = Uuid::new_v4();
|
||||||
|
let bob_id = Uuid::new_v4();
|
||||||
let repo = MockFileReadPort::new();
|
let repo = MockFileReadPort::new();
|
||||||
repo.insert("file-1", "secret.txt", "alice");
|
repo.insert("file-1", "secret.txt", alice_id);
|
||||||
|
|
||||||
let result = repo.get_file_for_owner("file-1", "bob").await;
|
let result = repo.get_file_for_owner("file-1", bob_id).await;
|
||||||
assert!(result.is_err(), "non-owner should be rejected");
|
assert!(result.is_err(), "non-owner should be rejected");
|
||||||
|
|
||||||
// Must be NotFound, NOT Forbidden — avoids leaking existence
|
// Must be NotFound, NOT Forbidden — avoids leaking existence
|
||||||
@@ -276,20 +280,23 @@ async fn get_file_for_owner_rejects_wrong_owner() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_file_for_owner_returns_not_found_for_missing_file() {
|
async fn get_file_for_owner_returns_not_found_for_missing_file() {
|
||||||
|
let alice_id = Uuid::new_v4();
|
||||||
let repo = MockFileReadPort::new();
|
let repo = MockFileReadPort::new();
|
||||||
|
|
||||||
let result = repo.get_file_for_owner("nonexistent", "alice").await;
|
let result = repo.get_file_for_owner("nonexistent", alice_id).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn verify_file_owner_uses_default_impl() {
|
async fn verify_file_owner_uses_default_impl() {
|
||||||
|
let alice_id = Uuid::new_v4();
|
||||||
|
let bob_id = Uuid::new_v4();
|
||||||
let repo = MockFileReadPort::new();
|
let repo = MockFileReadPort::new();
|
||||||
repo.insert("file-1", "secret.txt", "alice");
|
repo.insert("file-1", "secret.txt", alice_id);
|
||||||
|
|
||||||
// Default impl delegates to get_file_for_owner and maps to ()
|
// Default impl delegates to get_file_for_owner and maps to ()
|
||||||
assert!(repo.verify_file_owner("file-1", "alice").await.is_ok());
|
assert!(repo.verify_file_owner("file-1", alice_id).await.is_ok());
|
||||||
assert!(repo.verify_file_owner("file-1", "bob").await.is_err());
|
assert!(repo.verify_file_owner("file-1", bob_id).await.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -310,15 +317,17 @@ async fn verify_file_owner_uses_default_impl() {
|
|||||||
async fn verify_file_owner_delegates_to_read_port() {
|
async fn verify_file_owner_delegates_to_read_port() {
|
||||||
// This test verifies the FileReadPort contract that verify_file_owner
|
// This test verifies the FileReadPort contract that verify_file_owner
|
||||||
// returns Ok for the correct owner and Err for others.
|
// returns Ok for the correct owner and Err for others.
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
|
let attacker_id = Uuid::new_v4();
|
||||||
let read = MockFileReadPort::new();
|
let read = MockFileReadPort::new();
|
||||||
read.insert("abc-123", "report.pdf", "user-42");
|
read.insert("abc-123", "report.pdf", user_id);
|
||||||
|
|
||||||
// Same user → Ok
|
// Same user → Ok
|
||||||
let ok = read.verify_file_owner("abc-123", "user-42").await;
|
let ok = read.verify_file_owner("abc-123", user_id).await;
|
||||||
assert!(ok.is_ok(), "correct owner should pass verify_file_owner");
|
assert!(ok.is_ok(), "correct owner should pass verify_file_owner");
|
||||||
|
|
||||||
// Different user → Err
|
// Different user → Err
|
||||||
let err = read.verify_file_owner("abc-123", "attacker-99").await;
|
let err = read.verify_file_owner("abc-123", attacker_id).await;
|
||||||
assert!(err.is_err(), "wrong owner should fail verify_file_owner");
|
assert!(err.is_err(), "wrong owner should fail verify_file_owner");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,15 +335,17 @@ async fn verify_file_owner_delegates_to_read_port() {
|
|||||||
async fn owned_methods_require_ownership_check_first() {
|
async fn owned_methods_require_ownership_check_first() {
|
||||||
// Simulate what the _owned methods do: verify_owner then delegate.
|
// Simulate what the _owned methods do: verify_owner then delegate.
|
||||||
// We test with the mock read port to prove the sequence.
|
// We test with the mock read port to prove the sequence.
|
||||||
|
let owner_id = Uuid::new_v4();
|
||||||
|
let attacker_id = Uuid::new_v4();
|
||||||
let read = MockFileReadPort::new();
|
let read = MockFileReadPort::new();
|
||||||
read.insert("file-1", "data.csv", "owner-a");
|
read.insert("file-1", "data.csv", owner_id);
|
||||||
|
|
||||||
// Step 1: verify_owner for correct owner → Ok
|
// Step 1: verify_owner for correct owner → Ok
|
||||||
let step1 = read.verify_file_owner("file-1", "owner-a").await;
|
let step1 = read.verify_file_owner("file-1", owner_id).await;
|
||||||
assert!(step1.is_ok());
|
assert!(step1.is_ok());
|
||||||
|
|
||||||
// Step 2: verify_owner for attacker → Err, so the move/rename never executes
|
// Step 2: verify_owner for attacker → Err, so the move/rename never executes
|
||||||
let step2 = read.verify_file_owner("file-1", "attacker").await;
|
let step2 = read.verify_file_owner("file-1", attacker_id).await;
|
||||||
assert!(step2.is_err());
|
assert!(step2.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,18 +358,20 @@ use crate::common::stubs::StubFileManagementUseCase;
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stub_move_file_owned_returns_ok() {
|
async fn stub_move_file_owned_returns_ok() {
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
let stub = StubFileManagementUseCase;
|
let stub = StubFileManagementUseCase;
|
||||||
let result = stub
|
let result = stub
|
||||||
.move_file_owned("file-1", "user-1", Some("folder-2".to_string()))
|
.move_file_owned("file-1", user_id, Some("folder-2".to_string()))
|
||||||
.await;
|
.await;
|
||||||
assert!(result.is_ok(), "stub should return Ok for move_file_owned");
|
assert!(result.is_ok(), "stub should return Ok for move_file_owned");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stub_rename_file_owned_returns_ok() {
|
async fn stub_rename_file_owned_returns_ok() {
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
let stub = StubFileManagementUseCase;
|
let stub = StubFileManagementUseCase;
|
||||||
let result = stub
|
let result = stub
|
||||||
.rename_file_owned("file-1", "user-1", "new-name.txt")
|
.rename_file_owned("file-1", user_id, "new-name.txt")
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
@@ -371,16 +384,18 @@ use crate::common::stubs::StubFileRetrievalUseCase;
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stub_get_file_owned_returns_ok() {
|
async fn stub_get_file_owned_returns_ok() {
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
let stub = StubFileRetrievalUseCase;
|
let stub = StubFileRetrievalUseCase;
|
||||||
let result = stub.get_file_owned("file-1", "user-1").await;
|
let result = stub.get_file_owned("file-1", user_id).await;
|
||||||
assert!(result.is_ok(), "stub should return Ok for get_file_owned");
|
assert!(result.is_ok(), "stub should return Ok for get_file_owned");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stub_get_file_optimized_owned_returns_ok() {
|
async fn stub_get_file_optimized_owned_returns_ok() {
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
let stub = StubFileRetrievalUseCase;
|
let stub = StubFileRetrievalUseCase;
|
||||||
let result = stub
|
let result = stub
|
||||||
.get_file_optimized_owned("file-1", "user-1", true, false)
|
.get_file_optimized_owned("file-1", user_id, true, false)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
|
|||||||
@@ -806,7 +806,7 @@ mod tests {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<(Vec<crate::domain::entities::file::File>, usize), DomainError> {
|
) -> Result<(Vec<crate::domain::entities::file::File>, usize), DomainError> {
|
||||||
Ok((Vec::new(), 0))
|
Ok((Vec::new(), 0))
|
||||||
}
|
}
|
||||||
@@ -815,7 +815,7 @@ mod tests {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<usize, DomainError> {
|
) -> Result<usize, DomainError> {
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
@@ -839,7 +839,7 @@ mod tests {
|
|||||||
async fn get_file_for_owner(
|
async fn get_file_for_owner(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> Result<crate::domain::entities::file::File, DomainError> {
|
) -> Result<crate::domain::entities::file::File, DomainError> {
|
||||||
self.get_file(id).await
|
self.get_file(id).await
|
||||||
}
|
}
|
||||||
@@ -891,7 +891,7 @@ mod tests {
|
|||||||
async fn list_folders_by_owner(
|
async fn list_folders_by_owner(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
@@ -910,7 +910,7 @@ mod tests {
|
|||||||
async fn list_folders_by_owner_paginated(
|
async fn list_folders_by_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
_offset: usize,
|
_offset: usize,
|
||||||
_limit: usize,
|
_limit: usize,
|
||||||
_include_total: bool,
|
_include_total: bool,
|
||||||
@@ -971,7 +971,7 @@ mod tests {
|
|||||||
|
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
_name: String,
|
_name: String,
|
||||||
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
|
|||||||
@@ -566,10 +566,10 @@ impl TrashUseCase for TrashService {
|
|||||||
|
|
||||||
// Best-effort thumbnail cleanup — thumbnails are cache
|
// Best-effort thumbnail cleanup — thumbnails are cache
|
||||||
// artifacts, so failure must not block file deletion.
|
// artifacts, so failure must not block file deletion.
|
||||||
if let Some(thumb) = &self.thumbnail_service {
|
if let Some(thumb) = &self.thumbnail_service
|
||||||
if let Err(e) = thumb.delete_thumbnails(&file_id).await {
|
&& let Err(e) = thumb.delete_thumbnails(&file_id).await
|
||||||
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
|
{
|
||||||
}
|
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrashedItemType::Folder => {
|
TrashedItemType::Folder => {
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ where
|
|||||||
FW: FileWritePort,
|
FW: FileWritePort,
|
||||||
FoR: FolderRepository,
|
FoR: FolderRepository,
|
||||||
{
|
{
|
||||||
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
|
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
let items = self.trash_repository.get_trash_items(&user_id).await?;
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
|
||||||
Ok(items
|
Ok(items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
@@ -85,11 +83,9 @@ where
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
|
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> {
|
||||||
let item_uuid = Uuid::parse_str(item_id)
|
let item_uuid = Uuid::parse_str(item_id)
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?;
|
.map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?;
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
|
|
||||||
match item_type {
|
match item_type {
|
||||||
"file" => {
|
"file" => {
|
||||||
@@ -103,7 +99,7 @@ where
|
|||||||
let original_path = file.storage_path().to_string();
|
let original_path = file.storage_path().to_string();
|
||||||
let trashed_item = TrashedItem::new(
|
let trashed_item = TrashedItem::new(
|
||||||
item_uuid,
|
item_uuid,
|
||||||
user_uuid,
|
user_id,
|
||||||
TrashedItemType::File,
|
TrashedItemType::File,
|
||||||
file.name().to_string(),
|
file.name().to_string(),
|
||||||
original_path,
|
original_path,
|
||||||
@@ -145,7 +141,7 @@ where
|
|||||||
let original_path = folder.storage_path().to_string();
|
let original_path = folder.storage_path().to_string();
|
||||||
let trashed_item = TrashedItem::new(
|
let trashed_item = TrashedItem::new(
|
||||||
item_uuid,
|
item_uuid,
|
||||||
user_uuid,
|
user_id,
|
||||||
TrashedItemType::Folder,
|
TrashedItemType::Folder,
|
||||||
folder.name().to_string(),
|
folder.name().to_string(),
|
||||||
original_path,
|
original_path,
|
||||||
@@ -179,15 +175,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
|
||||||
let trash_uuid = Uuid::parse_str(trash_id)
|
let trash_uuid = Uuid::parse_str(trash_id)
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?;
|
.map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?;
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
|
|
||||||
let item = self
|
let item = self
|
||||||
.trash_repository
|
.trash_repository
|
||||||
.get_trash_item(&trash_uuid, &user_uuid)
|
.get_trash_item(&trash_uuid, &user_id)
|
||||||
.await?;
|
.await?;
|
||||||
match item {
|
match item {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
@@ -228,7 +222,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.trash_repository
|
self.trash_repository
|
||||||
.restore_from_trash(&trash_uuid, &user_uuid)
|
.restore_from_trash(&trash_uuid, &user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::new(
|
DomainError::new(
|
||||||
@@ -243,15 +237,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
|
||||||
let trash_uuid = Uuid::parse_str(trash_id)
|
let trash_uuid = Uuid::parse_str(trash_id)
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?;
|
.map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?;
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
|
|
||||||
let item = self
|
let item = self
|
||||||
.trash_repository
|
.trash_repository
|
||||||
.get_trash_item(&trash_uuid, &user_uuid)
|
.get_trash_item(&trash_uuid, &user_id)
|
||||||
.await?;
|
.await?;
|
||||||
match item {
|
match item {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
@@ -287,7 +279,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.trash_repository
|
self.trash_repository
|
||||||
.delete_permanently(&trash_uuid, &user_uuid)
|
.delete_permanently(&trash_uuid, &user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::new(
|
DomainError::new(
|
||||||
@@ -302,10 +294,8 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn empty_trash(&self, user_id: &str) -> Result<()> {
|
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
self.trash_repository.clear_trash(&user_id).await
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
self.trash_repository.clear_trash(&user_uuid).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +482,7 @@ impl FileReadPort for MockFileRepository {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> std::result::Result<(Vec<File>, usize), DomainError> {
|
) -> std::result::Result<(Vec<File>, usize), DomainError> {
|
||||||
Ok((Vec::new(), 0))
|
Ok((Vec::new(), 0))
|
||||||
}
|
}
|
||||||
@@ -501,7 +491,7 @@ impl FileReadPort for MockFileRepository {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
_criteria: &crate::application::dtos::search_dto::SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> std::result::Result<usize, DomainError> {
|
) -> std::result::Result<usize, DomainError> {
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
@@ -519,7 +509,7 @@ impl FileReadPort for MockFileRepository {
|
|||||||
async fn get_file_for_owner(
|
async fn get_file_for_owner(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> std::result::Result<File, DomainError> {
|
) -> std::result::Result<File, DomainError> {
|
||||||
// In this mock, ignore ownership — trash tests don't focus on ownership
|
// In this mock, ignore ownership — trash tests don't focus on ownership
|
||||||
self.get_file(id).await
|
self.get_file(id).await
|
||||||
@@ -697,7 +687,7 @@ impl FolderRepository for MockFolderRepository {
|
|||||||
async fn list_folders_by_owner(
|
async fn list_folders_by_owner(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> std::result::Result<Vec<Folder>, DomainError> {
|
) -> std::result::Result<Vec<Folder>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
@@ -715,7 +705,7 @@ impl FolderRepository for MockFolderRepository {
|
|||||||
async fn list_folders_by_owner_paginated(
|
async fn list_folders_by_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
_offset: usize,
|
_offset: usize,
|
||||||
_limit: usize,
|
_limit: usize,
|
||||||
_include_total: bool,
|
_include_total: bool,
|
||||||
@@ -799,7 +789,7 @@ impl FolderRepository for MockFolderRepository {
|
|||||||
|
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
_name: String,
|
_name: String,
|
||||||
) -> std::result::Result<Folder, DomainError> {
|
) -> std::result::Result<Folder, DomainError> {
|
||||||
Ok(Folder::default())
|
Ok(Folder::default())
|
||||||
@@ -839,18 +829,18 @@ mod tests {
|
|||||||
|
|
||||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||||
|
|
||||||
// Add a test file to the repository
|
// Add a test file to the repository
|
||||||
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let result = service.move_to_trash(file_id, "file", user_id).await;
|
let result = service.move_to_trash(file_id, "file", user_uuid).await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(result.is_ok(), "Moving file to trash failed: {:?}", result);
|
assert!(result.is_ok(), "Moving file to trash failed: {:?}", result);
|
||||||
|
|
||||||
// Verify the file is in trash
|
// Verify the file is in trash
|
||||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
|
||||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -913,12 +903,13 @@ mod tests {
|
|||||||
|
|
||||||
let folder_id = "550e8400-e29b-41d4-a716-446655440002";
|
let folder_id = "550e8400-e29b-41d4-a716-446655440002";
|
||||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||||
|
|
||||||
// Add a test folder to the repository
|
// Add a test folder to the repository
|
||||||
folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder");
|
folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let result = service.move_to_trash(folder_id, "folder", user_id).await;
|
let result = service.move_to_trash(folder_id, "folder", user_uuid).await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(
|
assert!(
|
||||||
@@ -928,7 +919,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Verify the folder is in trash
|
// Verify the folder is in trash
|
||||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
|
||||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -978,22 +968,22 @@ mod tests {
|
|||||||
|
|
||||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||||
let file_path = "/test/path/test.txt";
|
let file_path = "/test/path/test.txt";
|
||||||
|
|
||||||
// Add a test file and move it to trash
|
// Add a test file and move it to trash
|
||||||
file_repo.add_test_file(file_id, "test.txt", file_path);
|
file_repo.add_test_file(file_id, "test.txt", file_path);
|
||||||
service
|
service
|
||||||
.move_to_trash(file_id, "file", user_id)
|
.move_to_trash(file_id, "file", user_uuid)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Get the trash item ID
|
// Get the trash item ID
|
||||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
|
||||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||||
let trash_id = trash_items[0].id().to_string();
|
let trash_id = trash_items[0].id().to_string();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let result = service.restore_item(&trash_id, user_id).await;
|
let result = service.restore_item(&trash_id, user_uuid).await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1048,21 +1038,21 @@ mod tests {
|
|||||||
|
|
||||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||||
|
|
||||||
// Add a test file and move it to trash
|
// Add a test file and move it to trash
|
||||||
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
||||||
service
|
service
|
||||||
.move_to_trash(file_id, "file", user_id)
|
.move_to_trash(file_id, "file", user_uuid)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Get the trash item ID
|
// Get the trash item ID
|
||||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
|
||||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||||
let trash_id = trash_items[0].id().to_string();
|
let trash_id = trash_items[0].id().to_string();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let result = service.delete_permanently(&trash_id, user_id).await;
|
let result = service.delete_permanently(&trash_id, user_uuid).await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1116,6 +1106,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||||
|
|
||||||
// Add multiple files and folders to trash
|
// Add multiple files and folders to trash
|
||||||
let file_ids = [
|
let file_ids = [
|
||||||
@@ -1136,7 +1127,7 @@ mod tests {
|
|||||||
&format!("/test/path/test{}.txt", i),
|
&format!("/test/path/test{}.txt", i),
|
||||||
);
|
);
|
||||||
service
|
service
|
||||||
.move_to_trash(file_id, "file", user_id)
|
.move_to_trash(file_id, "file", user_uuid)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -1148,18 +1139,17 @@ mod tests {
|
|||||||
&format!("/test/path/folder{}", i),
|
&format!("/test/path/folder{}", i),
|
||||||
);
|
);
|
||||||
service
|
service
|
||||||
.move_to_trash(folder_id, "folder", user_id)
|
.move_to_trash(folder_id, "folder", user_uuid)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify items are in trash
|
// Verify items are in trash
|
||||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
|
||||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||||
assert_eq!(trash_items.len(), 4, "Should have 4 items in trash");
|
assert_eq!(trash_items.len(), 4, "Should have 4 items in trash");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let result = service.empty_trash(user_id).await;
|
let result = service.empty_trash(user_uuid).await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(result.is_ok(), "Emptying trash failed: {:?}", result);
|
assert!(result.is_ok(), "Emptying trash failed: {:?}", result);
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ impl File {
|
|||||||
folder_id: self.folder_id.clone(),
|
folder_id: self.folder_id.clone(),
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
owner_id: self.owner_id.clone(),
|
owner_id: self.owner_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ impl File {
|
|||||||
folder_id,
|
folder_id,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
owner_id: self.owner_id.clone(),
|
owner_id: self.owner_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ impl File {
|
|||||||
folder_id: self.folder_id.clone(),
|
folder_id: self.folder_id.clone(),
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
owner_id: self.owner_id.clone(),
|
owner_id: self.owner_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ impl Folder {
|
|||||||
storage_path: new_storage_path,
|
storage_path: new_storage_path,
|
||||||
path_string: new_path_string,
|
path_string: new_path_string,
|
||||||
parent_id: self.parent_id.clone(),
|
parent_id: self.parent_id.clone(),
|
||||||
owner_id: self.owner_id.clone(),
|
owner_id: self.owner_id,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
})
|
})
|
||||||
@@ -266,7 +266,7 @@ impl Folder {
|
|||||||
storage_path: new_storage_path,
|
storage_path: new_storage_path,
|
||||||
path_string: new_path_string,
|
path_string: new_path_string,
|
||||||
parent_id,
|
parent_id,
|
||||||
owner_id: self.owner_id.clone(),
|
owner_id: self.owner_id,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ impl ContactStorageAdapter {
|
|||||||
.address_book_repository
|
.address_book_repository
|
||||||
.get_address_book_shares(address_book_id)
|
.get_address_book_shares(address_book_id)
|
||||||
.await?;
|
.await?;
|
||||||
if shares.iter().any(|(shared_user, _)| shared_user == &user_id.to_string()) {
|
if shares
|
||||||
|
.iter()
|
||||||
|
.any(|(shared_user, _)| shared_user == &user_id.to_string())
|
||||||
|
{
|
||||||
return Ok(address_book);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +251,11 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
|
|
||||||
// Check write access
|
// Check write access
|
||||||
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
|
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid user ID format")
|
DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"AddressBook",
|
||||||
|
"Invalid user ID format",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let mut address_book = self.check_write_access(&uuid, user_id).await?;
|
let mut address_book = self.check_write_access(&uuid, user_id).await?;
|
||||||
|
|
||||||
@@ -364,7 +371,11 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format")
|
DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"AddressBook",
|
||||||
|
"Invalid target user ID format",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.address_book_repository
|
self.address_book_repository
|
||||||
@@ -397,7 +408,11 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format")
|
DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"AddressBook",
|
||||||
|
"Invalid target user ID format",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.address_book_repository
|
self.address_book_repository
|
||||||
@@ -443,8 +458,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
|
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
|
||||||
})?;
|
})?;
|
||||||
self.check_write_access(&address_book_id, user_id)
|
self.check_write_access(&address_book_id, user_id).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut contact = Contact::from_raw(
|
let mut contact = Contact::from_raw(
|
||||||
@@ -488,8 +502,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
|
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
|
||||||
})?;
|
})?;
|
||||||
self.check_write_access(&address_book_id, user_id)
|
self.check_write_access(&address_book_id, user_id).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Parse vCard fields
|
// Parse vCard fields
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
@@ -742,10 +755,13 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
|
|
||||||
// Check write access
|
// Check write access
|
||||||
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format")
|
DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"ContactGroup",
|
||||||
|
"Invalid user ID format",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
self.check_write_access(&address_book_id, user_id)
|
self.check_write_access(&address_book_id, user_id).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let group = ContactGroup::new(address_book_id, dto.name);
|
let group = ContactGroup::new(address_book_id, dto.name);
|
||||||
|
|
||||||
@@ -770,7 +786,11 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
|
|
||||||
// Check write access
|
// Check write access
|
||||||
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
|
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
|
||||||
DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format")
|
DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"ContactGroup",
|
||||||
|
"Invalid user ID format",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
self.check_write_access(group.address_book_id(), user_id)
|
self.check_write_access(group.address_book_id(), user_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -283,7 +283,8 @@ fn split_sql_statements(sql: &str) -> Vec<String> {
|
|||||||
if i >= len {
|
if i >= len {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if bytes[i] == b'$' && i + tag_bytes.len() <= len
|
if bytes[i] == b'$'
|
||||||
|
&& i + tag_bytes.len() <= len
|
||||||
&& &bytes[i..i + tag_bytes.len()] == tag_bytes
|
&& &bytes[i..i + tag_bytes.len()] == tag_bytes
|
||||||
{
|
{
|
||||||
current.push_str(&tag);
|
current.push_str(&tag);
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&ap.id)
|
.bind(ap.id)
|
||||||
.bind(&ap.user_id)
|
.bind(ap.user_id)
|
||||||
.bind(&ap.label)
|
.bind(&ap.label)
|
||||||
.bind(&ap.password_hash)
|
.bind(&ap.password_hash)
|
||||||
.bind(&ap.prefix)
|
.bind(&ap.prefix)
|
||||||
|
|||||||
@@ -21,16 +21,7 @@ use crate::domain::services::path_service::StoragePath;
|
|||||||
type FolderRow = (String, String, String, Option<String>, Uuid, i64, i64);
|
type FolderRow = (String, String, String, Option<String>, Uuid, i64, i64);
|
||||||
|
|
||||||
/// Type alias for paginated folder rows (includes total_count).
|
/// Type alias for paginated folder rows (includes total_count).
|
||||||
type FolderRowPaginated = (
|
type FolderRowPaginated = (String, String, String, Option<String>, Uuid, i64, i64, i64);
|
||||||
String,
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<String>,
|
|
||||||
Uuid,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Type alias for folder rows with optional user_id.
|
/// Type alias for folder rows with optional user_id.
|
||||||
type FolderRowOptUser = (
|
type FolderRowOptUser = (
|
||||||
@@ -108,14 +99,14 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
// caller to have set up the home folder beforehand (done during user
|
// caller to have set up the home folder beforehand (done during user
|
||||||
// registration).
|
// registration).
|
||||||
let user_id: Uuid = if let Some(ref pid) = parent_id {
|
let user_id: Uuid = if let Some(ref pid) = parent_id {
|
||||||
sqlx::query_scalar::<_, Uuid>(
|
sqlx::query_scalar::<_, Uuid>("SELECT user_id FROM storage.folders WHERE id = $1::uuid")
|
||||||
"SELECT user_id FROM storage.folders WHERE id = $1::uuid",
|
.bind(pid)
|
||||||
)
|
.fetch_optional(self.pool())
|
||||||
.bind(pid)
|
.await
|
||||||
.fetch_optional(self.pool())
|
.map_err(|e| {
|
||||||
.await
|
DomainError::internal_error("FolderDb", format!("parent lookup: {e}"))
|
||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("parent lookup: {e}")))?
|
})?
|
||||||
.ok_or_else(|| DomainError::not_found("Folder", pid))?
|
.ok_or_else(|| DomainError::not_found("Folder", pid))?
|
||||||
} else {
|
} else {
|
||||||
return Err(DomainError::internal_error(
|
return Err(DomainError::internal_error(
|
||||||
"FolderDb",
|
"FolderDb",
|
||||||
@@ -647,15 +638,9 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some((id, path, ca, ma)) => Self::row_to_folder(
|
Some((id, path, ca, ma)) => {
|
||||||
id,
|
Self::row_to_folder(id, name.clone(), path, None, Some(user_id), ca, ma)
|
||||||
name.clone(),
|
}
|
||||||
path,
|
|
||||||
None,
|
|
||||||
Some(user_id),
|
|
||||||
ca,
|
|
||||||
ma,
|
|
||||||
),
|
|
||||||
None => {
|
None => {
|
||||||
// Already exists — fetch it
|
// Already exists — fetch it
|
||||||
let existing = sqlx::query_as::<_, (String, String, i64, i64)>(
|
let existing = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||||
|
|||||||
@@ -195,16 +195,15 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
||||||
let result =
|
let result = sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2")
|
||||||
sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2")
|
.bind(id)
|
||||||
.bind(id)
|
.bind(user_id)
|
||||||
.bind(user_id)
|
.execute(&*self.db_pool)
|
||||||
.execute(&*self.db_pool)
|
.await
|
||||||
.await
|
.map_err(|e| {
|
||||||
.map_err(|e| {
|
tracing::error!("Database error deleting share for user: {}", e);
|
||||||
tracing::error!("Database error deleting share for user: {}", e);
|
DomainError::internal_error("Share", format!("Failed to delete share: {e}"))
|
||||||
DomainError::internal_error("Share", format!("Failed to delete share: {e}"))
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
// SECURITY: could be non-existent or owned by another user — same 404
|
// SECURITY: could be non-existent or owned by another user — same 404
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ impl TrashDbRepository {
|
|||||||
// In the soft-delete model, the trash entry ID is the same as the
|
// In the soft-delete model, the trash entry ID is the same as the
|
||||||
// original item ID since there is no separate trash table.
|
// original item ID since there is no separate trash table.
|
||||||
TrashedItem::from_raw(
|
TrashedItem::from_raw(
|
||||||
id, // trash entry id (same as original)
|
id, // trash entry id (same as original)
|
||||||
id, // original item id
|
id, // original item id
|
||||||
user_id, // owner
|
user_id, // owner
|
||||||
item_type_enum,
|
item_type_enum,
|
||||||
name.clone(),
|
name.clone(),
|
||||||
String::new(), // original_path — not stored separately in soft-delete model
|
String::new(), // original_path — not stored separately in soft-delete model
|
||||||
|
|||||||
@@ -262,10 +262,11 @@ impl TokenServicePort for JwtTokenService {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::domain::entities::user::{User, UserRole};
|
use crate::domain::entities::user::{User, UserRole};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
fn create_test_user() -> User {
|
fn create_test_user() -> User {
|
||||||
User::from_data(
|
User::from_data(
|
||||||
"test-user-id".to_string(),
|
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||||
"testuser".to_string(),
|
"testuser".to_string(),
|
||||||
"test@example.com".to_string(),
|
"test@example.com".to_string(),
|
||||||
"hashed_password".to_string(),
|
"hashed_password".to_string(),
|
||||||
@@ -295,7 +296,7 @@ mod tests {
|
|||||||
let claims = service
|
let claims = service
|
||||||
.validate_token(&token)
|
.validate_token(&token)
|
||||||
.expect("Should validate token");
|
.expect("Should validate token");
|
||||||
assert_eq!(claims.sub, user.id());
|
assert_eq!(claims.sub, user.id().to_string());
|
||||||
assert_eq!(claims.username, user.username());
|
assert_eq!(claims.username, user.username());
|
||||||
assert_eq!(claims.email, user.email());
|
assert_eq!(claims.email, user.email());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use image::imageops::FilterType;
|
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
use image::codecs::jpeg::JpegEncoder;
|
||||||
|
use image::imageops::FilterType;
|
||||||
/**
|
/**
|
||||||
* Thumbnail Generation Service
|
* Thumbnail Generation Service
|
||||||
*
|
*
|
||||||
@@ -236,20 +236,16 @@ impl ThumbnailService {
|
|||||||
/// Unlike `get_thumbnail`, this does **not** generate a new thumbnail.
|
/// Unlike `get_thumbnail`, this does **not** generate a new thumbnail.
|
||||||
/// Useful for non-image file types (videos) where a client-generated
|
/// Useful for non-image file types (videos) where a client-generated
|
||||||
/// thumbnail may have been uploaded previously.
|
/// thumbnail may have been uploaded previously.
|
||||||
pub async fn get_cached_thumbnail(
|
pub async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes> {
|
||||||
&self,
|
|
||||||
file_id: &str,
|
|
||||||
size: ThumbnailSize,
|
|
||||||
) -> Option<Bytes> {
|
|
||||||
// 1. Check in-memory cache
|
// 1. Check in-memory cache
|
||||||
let cache_key = ThumbnailCacheKey {
|
let cache_key = ThumbnailCacheKey {
|
||||||
file_id: file_id.to_string(),
|
file_id: file_id.to_string(),
|
||||||
size,
|
size,
|
||||||
};
|
};
|
||||||
if let Some(bytes) = self.cache.get(&cache_key).await {
|
if let Some(bytes) = self.cache.get(&cache_key).await
|
||||||
if !bytes.is_empty() {
|
&& !bytes.is_empty()
|
||||||
return Some(bytes);
|
{
|
||||||
}
|
return Some(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check disk
|
// 2. Check disk
|
||||||
@@ -285,17 +281,17 @@ impl ThumbnailService {
|
|||||||
let jpeg_bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, ThumbnailError> {
|
let jpeg_bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, ThumbnailError> {
|
||||||
// ── Fast path: already a correctly-sized JPEG ─────────────
|
// ── Fast path: already a correctly-sized JPEG ─────────────
|
||||||
// JPEG files start with SOI marker 0xFF 0xD8.
|
// JPEG files start with SOI marker 0xFF 0xD8.
|
||||||
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
|
if data.len() >= 2
|
||||||
if let Ok(reader) = image::ImageReader::new(std::io::Cursor::new(&data))
|
&& data[0] == 0xFF
|
||||||
.with_guessed_format()
|
&& data[1] == 0xD8
|
||||||
{
|
&& let Ok(reader) =
|
||||||
if let Ok((w, h)) = reader.into_dimensions() {
|
image::ImageReader::new(std::io::Cursor::new(&data)).with_guessed_format()
|
||||||
if w <= max_dim && h <= max_dim {
|
&& let Ok((w, h)) = reader.into_dimensions()
|
||||||
// Already JPEG at correct size — zero-copy store
|
&& w <= max_dim
|
||||||
return Ok(data.to_vec());
|
&& h <= max_dim
|
||||||
}
|
{
|
||||||
}
|
// Already JPEG at correct size — zero-copy store
|
||||||
}
|
return Ok(data.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slow path: decode, resize, re-encode to JPEG ─────────
|
// ── Slow path: decode, resize, re-encode to JPEG ─────────
|
||||||
@@ -637,11 +633,7 @@ impl ThumbnailPort for ThumbnailService {
|
|||||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_thumbnail(
|
async fn get_cached_thumbnail(&self, file_id: &str, size: PortThumbnailSize) -> Option<Bytes> {
|
||||||
&self,
|
|
||||||
file_id: &str,
|
|
||||||
size: PortThumbnailSize,
|
|
||||||
) -> Option<Bytes> {
|
|
||||||
self.get_cached_thumbnail(file_id, size.into()).await
|
self.get_cached_thumbnail(file_id, size.into()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((Uuid::parse_str(&claims.sub).map_err(|_| AppError::internal_error("Invalid user ID in token"))?, claims.role))
|
Ok((
|
||||||
|
Uuid::parse_str(&claims.sub)
|
||||||
|
.map_err(|_| AppError::internal_error("Invalid user ID in token"))?,
|
||||||
|
claims.role,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ async fn revoke_app_password(
|
|||||||
|
|
||||||
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
let response = service
|
let response = service.revoke(user.id, id).await.map_err(AppError::from)?;
|
||||||
.revoke(user.id, id)
|
|
||||||
.await
|
|
||||||
.map_err(AppError::from)?;
|
|
||||||
|
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,13 +299,12 @@ async fn handle_propfind(
|
|||||||
} else {
|
} else {
|
||||||
// Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/)
|
// Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/)
|
||||||
// List all calendars for this user
|
// List all calendars for this user
|
||||||
let calendars =
|
let calendars = calendar_service
|
||||||
calendar_service
|
.list_my_calendars(user.id)
|
||||||
.list_my_calendars(user.id)
|
.await
|
||||||
.await
|
.map_err(|e| {
|
||||||
.map_err(|e| {
|
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
||||||
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let base_href = &format!("/caldav/{}/", first_segment);
|
let base_href = &format!("/caldav/{}/", first_segment);
|
||||||
let mut response_body = Vec::new();
|
let mut response_body = Vec::new();
|
||||||
|
|||||||
@@ -190,13 +190,7 @@ impl ChunkedUploadHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
match chunked_service
|
match chunked_service
|
||||||
.upload_chunk(
|
.upload_chunk(&upload_id, auth_user.id, params.chunk_index, body, checksum)
|
||||||
&upload_id,
|
|
||||||
auth_user.id,
|
|
||||||
params.chunk_index,
|
|
||||||
body,
|
|
||||||
checksum,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ impl DedupHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only reveal whether THIS user has the blob — no global oracle
|
// Only reveal whether THIS user has the blob — no global oracle
|
||||||
let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await;
|
let user_has_it = dedup
|
||||||
|
.user_owns_blob_reference(&hash, &auth_user.id.to_string())
|
||||||
|
.await;
|
||||||
|
|
||||||
if user_has_it {
|
if user_has_it {
|
||||||
// Fetch size from metadata (safe — user owns a reference)
|
// Fetch size from metadata (safe — user owns a reference)
|
||||||
@@ -346,7 +348,10 @@ impl DedupHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the user owns at least one file referencing this blob
|
// Verify the user owns at least one file referencing this blob
|
||||||
if !dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await {
|
if !dedup
|
||||||
|
.user_owns_blob_reference(&hash, &auth_user.id.to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
|||||||
@@ -203,7 +203,8 @@ async fn revoke_device(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let device_service = get_device_service(&state)?;
|
let device_service = get_device_service(&state)?;
|
||||||
|
|
||||||
let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?;
|
let device_id =
|
||||||
|
Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?;
|
||||||
|
|
||||||
device_service
|
device_service
|
||||||
.revoke_device(device_id, auth_user.id)
|
.revoke_device(device_id, auth_user.id)
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ impl FileHandler {
|
|||||||
if let Some(ref fid) = folder_id {
|
if let Some(ref fid) = folder_id {
|
||||||
use crate::application::ports::inbound::FolderUseCase;
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = &state.applications.folder_service;
|
||||||
if folder_service.get_folder_owned(fid, auth_user.id).await.is_err() {
|
if folder_service
|
||||||
|
.get_folder_owned(fid, auth_user.id)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
|
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
|
||||||
auth_user.username,
|
auth_user.username,
|
||||||
@@ -336,18 +340,17 @@ impl FileHandler {
|
|||||||
// (file_id, size) pair. If the browser already has it, return 304
|
// (file_id, size) pair. If the browser already has it, return 304
|
||||||
// with zero I/O or DB work.
|
// with zero I/O or DB work.
|
||||||
let etag = format!("\"thumb-{}-{:?}\"", id, thumb_size);
|
let etag = format!("\"thumb-{}-{:?}\"", id, thumb_size);
|
||||||
if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) {
|
if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH)
|
||||||
if let Ok(val) = if_none_match.to_str() {
|
&& let Ok(val) = if_none_match.to_str()
|
||||||
if val == etag || val == "*" {
|
&& (val == etag || val == "*")
|
||||||
return Response::builder()
|
{
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
return Response::builder()
|
||||||
.header(header::ETAG, &etag)
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
|
.header(header::ETAG, &etag)
|
||||||
.body(Body::empty())
|
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
|
||||||
.unwrap()
|
.body(Body::empty())
|
||||||
.into_response();
|
.unwrap()
|
||||||
}
|
.into_response();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cache-first path (Solution A) ────────────────────────────
|
// ── Cache-first path (Solution A) ────────────────────────────
|
||||||
@@ -409,21 +412,17 @@ impl FileHandler {
|
|||||||
.get_thumbnail(&id, thumb_size.into(), &file_path)
|
.get_thumbnail(&id, thumb_size.into(), &file_path)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(data) => {
|
Ok(data) => Response::builder()
|
||||||
Response::builder()
|
.status(StatusCode::OK)
|
||||||
.status(StatusCode::OK)
|
.header(header::CONTENT_TYPE, "image/jpeg")
|
||||||
.header(header::CONTENT_TYPE, "image/jpeg")
|
.header(header::CONTENT_LENGTH, data.len())
|
||||||
.header(header::CONTENT_LENGTH, data.len())
|
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
|
||||||
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
|
.header(header::ETAG, &etag)
|
||||||
.header(header::ETAG, &etag)
|
.body(Body::from(data))
|
||||||
.body(Body::from(data))
|
.unwrap()
|
||||||
.unwrap()
|
.into_response(),
|
||||||
.into_response()
|
Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err))
|
||||||
}
|
.into_response(),
|
||||||
Err(err) => {
|
|
||||||
AppError::internal_error(format!("Thumbnail generation failed: {}", err))
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,10 +486,8 @@ impl FileHandler {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => StatusCode::CREATED.into_response(),
|
Ok(_) => StatusCode::CREATED.into_response(),
|
||||||
Err(err) => {
|
Err(err) => AppError::internal_error(format!("Failed to store thumbnail: {}", err))
|
||||||
AppError::internal_error(format!("Failed to store thumbnail: {}", err))
|
.into_response(),
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,9 +660,7 @@ impl FileHandler {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.into_response(),
|
.into_response(),
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => AppError::from(err).into_response(),
|
||||||
AppError::from(err).into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,9 +710,7 @@ impl FileHandler {
|
|||||||
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
|
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => AppError::from(err).into_response(),
|
||||||
AppError::from(err).into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,8 +743,7 @@ impl FileHandler {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
tracing::info!("🖼️ Generating thumbnails for: {}", file_id);
|
tracing::info!("🖼️ Generating thumbnails for: {}", file_id);
|
||||||
thumbnail_service
|
thumbnail_service.generate_all_sizes_background(file_id, file_path);
|
||||||
.generate_all_sizes_background(file_id, file_path);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,7 +824,7 @@ impl FileHandler {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||||
Err(err) => AppError::from(err).into_response()
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,7 +856,7 @@ impl FileHandler {
|
|||||||
let mgmt = &state.applications.file_management_service;
|
let mgmt = &state.applications.file_management_service;
|
||||||
match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await {
|
match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await {
|
||||||
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response()
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,7 +876,7 @@ impl FileHandler {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response()
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,7 +895,7 @@ impl FileHandler {
|
|||||||
let mgmt = &state.applications.file_management_service;
|
let mgmt = &state.applications.file_management_service;
|
||||||
match mgmt.move_file_owned(&id, auth_user.id, folder_id).await {
|
match mgmt.move_file_owned(&id, auth_user.id, folder_id).await {
|
||||||
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response()
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,9 +954,7 @@ impl FileHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
format!(
|
format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}")
|
||||||
"{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a 201 Created JSON response.
|
/// Build a 201 Created JSON response.
|
||||||
|
|||||||
@@ -1251,7 +1251,11 @@ async fn handle_move(
|
|||||||
&& let Ok(parent) =
|
&& let Ok(parent) =
|
||||||
folder_service.get_folder_by_path(dest_parent_path).await
|
folder_service.get_folder_by_path(dest_parent_path).await
|
||||||
{
|
{
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
file_management_service
|
file_management_service
|
||||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||||
@@ -1281,7 +1285,11 @@ async fn handle_move(
|
|||||||
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
if let Ok(folder) = folder_result {
|
||||||
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
|
assert_owner(
|
||||||
|
folder.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
&source_path,
|
||||||
|
)?;
|
||||||
let dest_folder_name = destination_path
|
let dest_folder_name = destination_path
|
||||||
.split('/')
|
.split('/')
|
||||||
.next_back()
|
.next_back()
|
||||||
@@ -1299,7 +1307,11 @@ async fn handle_move(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1352,7 +1364,11 @@ async fn handle_move(
|
|||||||
if !dest_parent_path.is_empty()
|
if !dest_parent_path.is_empty()
|
||||||
&& let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await
|
&& let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await
|
||||||
{
|
{
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
file_management_service
|
file_management_service
|
||||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||||
@@ -1480,7 +1496,11 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1528,7 +1548,11 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1553,7 +1577,11 @@ async fn handle_copy(
|
|||||||
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
if let Ok(folder) = folder_result {
|
||||||
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
|
assert_owner(
|
||||||
|
folder.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
&source_path,
|
||||||
|
)?;
|
||||||
let recursive = depth != "0";
|
let recursive = depth != "0";
|
||||||
|
|
||||||
let dest_folder_name = destination_path
|
let dest_folder_name = destination_path
|
||||||
@@ -1572,7 +1600,11 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1627,7 +1659,11 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
|
assert_owner(
|
||||||
|
parent.owner_id.as_deref(),
|
||||||
|
&user.id.to_string(),
|
||||||
|
dest_parent_path,
|
||||||
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
|
|||||||
@@ -459,17 +459,18 @@ pub async fn get_editor_url(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate WOPI access token
|
// Generate WOPI access token
|
||||||
let (access_token, access_token_ttl) =
|
let (access_token, access_token_ttl) = match state.token_service.generate_token(
|
||||||
match state
|
¶ms.file_id,
|
||||||
.token_service
|
&user_id.to_string(),
|
||||||
.generate_token(¶ms.file_id, &user_id.to_string(), &username, can_write)
|
username,
|
||||||
{
|
can_write,
|
||||||
Ok(t) => t,
|
) {
|
||||||
Err(e) => {
|
Ok(t) => t,
|
||||||
tracing::error!("Failed to generate WOPI token: {}", e);
|
Err(e) => {
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
tracing::error!("Failed to generate WOPI token: {}", e);
|
||||||
}
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
axum::Json(EditorUrlResponse {
|
axum::Json(EditorUrlResponse {
|
||||||
editor_url,
|
editor_url,
|
||||||
|
|||||||
@@ -145,7 +145,10 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
|||||||
.route("/", get(FileHandler::list_files_query))
|
.route("/", get(FileHandler::list_files_query))
|
||||||
.route("/upload", post(FileHandler::upload_file_with_thumbnails))
|
.route("/upload", post(FileHandler::upload_file_with_thumbnails))
|
||||||
.route("/{id}", get(FileHandler::download_file))
|
.route("/{id}", get(FileHandler::download_file))
|
||||||
.route("/{id}/thumbnail/{size}", get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail))
|
.route(
|
||||||
|
"/{id}/thumbnail/{size}",
|
||||||
|
get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail),
|
||||||
|
)
|
||||||
.route("/{id}/metadata", get(FileHandler::get_file_metadata))
|
.route("/{id}/metadata", get(FileHandler::get_file_metadata))
|
||||||
.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10 GB for file uploads
|
.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10 GB for file uploads
|
||||||
.with_state(app_state.clone());
|
.with_state(app_state.clone());
|
||||||
|
|||||||
@@ -91,16 +91,11 @@ where
|
|||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
Ok(OptionalUserId(
|
Ok(OptionalUserId(
|
||||||
parts
|
parts.extensions.get::<Arc<CurrentUser>>().map(|cu| cu.id),
|
||||||
.extensions
|
|
||||||
.get::<Arc<CurrentUser>>()
|
|
||||||
.map(|cu| cu.id),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Error for authentication operations
|
// Error for authentication operations
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ async fn user_provisioning_response(
|
|||||||
|
|
||||||
// Fetch quota from storage usage service
|
// Fetch quota from storage usage service
|
||||||
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
|
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
|
||||||
Some(service) => match service.get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()).await {
|
Some(service) => match service
|
||||||
|
.get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok((used, total)) => (used, total),
|
Ok((used, total)) => (used, total),
|
||||||
Err(_) => (0, 0),
|
Err(_) => (0, 0),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ pub fn create_web_routes() -> Router<Arc<AppState>> {
|
|||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."))
|
.unwrap_or(std::path::Path::new("."))
|
||||||
.join("static-dist");
|
.join("static-dist");
|
||||||
if dist.exists() { dist } else { config.static_path.clone() }
|
if dist.exists() {
|
||||||
|
dist
|
||||||
|
} else {
|
||||||
|
config.static_path.clone()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
config.static_path.clone()
|
config.static_path.clone()
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user