adding comments, moving technical documentation, improve Dockerfile, delete unuseful files

This commit is contained in:
dionidev
2025-03-30 14:17:09 +00:00
parent b2d989fbe3
commit e79ca8304b
30 changed files with 461 additions and 1537 deletions
-55
View File
@@ -1,55 +0,0 @@
# OxiCloud Development Guide
## Build Commands
```bash
# Core development workflow
cargo build # Build the project
cargo run # Run the project locally (server at http://127.0.0.1:8085)
cargo check # Quick check for compilation errors without building
# Testing commands
cargo test # Run all tests
cargo test -- --nocapture # Run tests with output displayed
cargo test <test_name> # Run a specific test (e.g., cargo test file_service)
cargo test domain::entities::file::tests::test_create_file # Run a specific test function
RUST_LOG=debug cargo test # Run tests with debug-level logging
RUST_LOG=trace cargo test # Run tests with trace-level logging
# Code quality tools
cargo clippy # Run linter to catch common mistakes
cargo clippy --fix # Fix auto-fixable linting issues
cargo fmt --check # Check code formatting without changing files
cargo fmt # Format code according to Rust conventions
# Debugging
RUST_LOG=debug cargo run # Run with detailed logging for debugging
RUST_BACKTRACE=1 cargo run # Run with full backtrace for better error diagnostics
```
## Code Style Guidelines
- **Architecture**: Follow Clean Architecture with clear layer separation (domain → application → infrastructure → interfaces)
- **Naming**: Use `snake_case` for files, modules, functions, variables; `PascalCase` for types/structs/enums; getters without `get_` prefix
- **Modules**: Use mod.rs files for explicit exports with visibility modifiers (pub, pub(crate))
- **Error Handling**: Use Result<T, E> with thiserror for custom error types; propagate errors with ? operator; include context in error messages
- **Documentation**: Document public APIs with /// doc comments, explain "why" not "what"; both English and Spanish comments are acceptable
- **Imports**: Group imports: 1) std, 2) external crates, 3) internal modules (with blank lines between)
- **Async**: Use async-trait for repository interfaces; handle futures with .await and tokio runtime; implement timeouts for I/O operations
- **Testing**: Write unit tests in the same file as implementation (bottom of file, in a tests module with #[cfg(test)])
- **Dependencies**: Use axum for web API, tower-http for middleware, serde for serialization; share dependencies with Arc
- **Logging**: Use tracing with appropriate levels (debug, info, warn, error) and structured contexts for detailed diagnostics
- **Repository Pattern**: Define interfaces in domain layer, implement in infrastructure layer; use traits with dynamic dispatch (Box<dyn Trait>)
- **I18n**: Store translations in JSON files under static/locales/, use i18n service for text lookups
- **Type Safety**: Prefer strong typing with domain-specific types over primitive types; validate at construction time
- **Error Messages**: Provide clear, actionable error messages that help diagnose the issue
- **Immutability**: Prefer immutable data structures; use with_* methods to return modified copies rather than mutating in place
- **Performance**: Implement caching with proper invalidation; use parallel processing for large file operations; optimize based on file sizes
## Project Structure
OxiCloud is a NextCloud-like file storage system built in Rust with a focus on performance and security. It provides a clean REST API and web interface for file management using a layered architecture approach:
- **Domain Layer**: Core business logic and entities (src/domain/)
- **Application Layer**: Use cases and application services (src/application/)
- **Infrastructure Layer**: External systems and implementations (src/infrastructure/)
- **Interfaces Layer**: API and web controllers (src/interfaces/)
The roadmap in TODO-LIST.md outlines planned features including enhanced folder support, file previews, user authentication, sharing, and a sync client.
Generated
+9 -9
View File
@@ -153,9 +153,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.1"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [
"axum-core",
"axum-macros",
@@ -189,12 +189,12 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-util",
"futures-core",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -1466,9 +1466,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.2"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
@@ -2197,9 +2197,9 @@ checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "socket2"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
-3
View File
@@ -3,9 +3,6 @@ name = "oxicloud"
version = "0.1.0"
edition = "2021"
[[bench]]
name = "file_operations"
harness = true
[dependencies]
axum = { version = "0.8.1", features = ["multipart", "http1", "tokio", "macros"] }
+53 -11
View File
@@ -1,23 +1,65 @@
FROM alpine:3.21.3 AS builder
# Stage 1: Builder - compile the application
FROM rust:1.82-alpine AS builder
COPY . /Oxicloud
# Install build dependencies
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
WORKDIR /Oxicloud
# Create a non-root user for better security
RUN adduser -D -u 10001 oxicloud
RUN apk update && \
apk add cargo pkgconfig openssl-dev
# Create a new empty project and copy only dependency files first
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# Create empty source files to trick cargo into caching dependencies
RUN mkdir -p src && \
echo "fn main() {}" > src/main.rs && \
touch src/lib.rs
# Build dependencies only (this will be cached if dependencies don't change)
RUN cargo build --release
# /Oxicloud/target/release/oxicloud
# Remove the fake source files
RUN rm -rf src
# Copy the actual source code
COPY src ./src
COPY db ./db
# Build the actual application
RUN cargo build --release && \
strip target/release/oxicloud
# Stage 2: Runtime - only include what's necessary for running
FROM alpine:3.21.3
COPY . /Oxicloud
# Install runtime dependencies only
RUN apk add --no-cache libgcc openssl ca-certificates tzdata && \
rm -rf /var/cache/apk/*
COPY --from=builder /Oxicloud/target/release/ /Oxicloud
COPY --from=builder /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
# Create a non-root user
RUN adduser -D -u 10001 oxicloud
WORKDIR /Oxicloud
# Create app directories with proper permissions
WORKDIR /app
RUN mkdir -p /app/static /app/storage && \
chown -R oxicloud:oxicloud /app
CMD ["./oxicloud","--release"]
# Copy only the compiled binary from the builder stage
COPY --from=builder /app/target/release/oxicloud /app/oxicloud
# Copy static files and necessary runtime config
COPY static ./static
COPY db ./db
# Set permissions
RUN chown -R oxicloud:oxicloud /app
# Set the user to run the application
USER oxicloud
# Expose the port the application runs on
EXPOSE 3000
# Run the binary
CMD ["./oxicloud", "--release"]
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="static/oxicloud-logo.svg" alt="OxiCloud" width="400" />
<img src="static/oxicloud-logo.svg" alt="OxiCloud" width="375" />
</p>
<div align="center">
+3 -3
View File
@@ -38,9 +38,9 @@ This document contains the task list for the development of OxiCloud, a minimali
## Phase 2: Authentication and Multi-User
### User System
- [ ] Design data model for users
- [x] Design data model for users
- [ ] Implement user registration
- [ ] Create login system
- [x] Create login system
- [ ] Add user profile page
- [ ] Implement password recovery
- [ ] Separate storage by user
@@ -63,7 +63,7 @@ This document contains the task list for the development of OxiCloud, a minimali
## Phase 3: Collaboration Features
### File Sharing
- [ ] Implement shared link generation
- [x] Implement shared link generation
- [ ] Add permission configuration for links
- [ ] Implement password protection for links
- [ ] Add expiration dates for shared links
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
# Definir variables de conexión por defecto
DB_HOST=${PGHOST:-"localhost"}
DB_PORT=${PGPORT:-"5432"}
DB_USER=${PGUSER:-"postgres"}
DB_PASS=${PGPASSWORD:-"postgres"}
DB_NAME=${PGDATABASE:-"postgres"}
# Intentar usar variables de entorno de OxiCloud si están definidas
if [ -n "$OXICLOUD_DB_CONNECTION" ]; then
# Parse postgres:// connection string
if [[ $OXICLOUD_DB_CONNECTION =~ postgres://([^:]+):([^@]+)@([^:]+):([0-9]+)/([^?]+) ]]; then
DB_USER="${BASH_REMATCH[1]}"
DB_PASS="${BASH_REMATCH[2]}"
DB_HOST="${BASH_REMATCH[3]}"
DB_PORT="${BASH_REMATCH[4]}"
DB_NAME="${BASH_REMATCH[5]}"
fi
fi
echo "Applying database migrations..."
echo "Using database: postgres://$DB_USER:***@$DB_HOST:$DB_PORT/$DB_NAME"
# Exportar variable PGPASSWORD para psql
export PGPASSWORD="$DB_PASS"
# Ejecutar el script SQL de migración
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f fix-userrole.sql
# Comprobar si fue exitoso
if [ $? -eq 0 ]; then
echo "Migration applied successfully!"
else
echo "Error applying migration."
exit 1
fi
echo "Database is now ready for use."
-62
View File
@@ -1,62 +0,0 @@
#!/bin/bash
# This script is used to apply all migrations in order
# It can be run manually or as part of container initialization
# Determine run mode
if [ -z "$POSTGRES_DB" ]; then
# Manual mode - script was run from command line
# Verify we are in the right environment
if [ ! -f "Cargo.toml" ]; then
echo "Error: This script must be run from the OxiCloud project root directory"
exit 1
fi
# Check if postgres container is running
POSTGRES_ID=$(docker-compose ps -q postgres 2>/dev/null)
if [ -z "$POSTGRES_ID" ]; then
echo "Error: PostgreSQL container is not running. Start it with 'docker-compose up -d postgres'"
exit 1
fi
# Set parameters for manual mode
DB_NAME="oxicloud"
MIGRATIONS_DIR="./migrations"
MIGRATIONS_CMD="docker-compose exec -T postgres psql -U postgres"
else
# Auto mode - script is running inside postgres container during initialization
echo "Running in PostgreSQL container initialization mode"
# Set parameters for auto mode
DB_NAME="$POSTGRES_DB"
MIGRATIONS_DIR="/docker-entrypoint-initdb.d/migrations"
MIGRATIONS_CMD="psql -U $POSTGRES_USER"
fi
echo "Applying migrations from $MIGRATIONS_DIR to database $DB_NAME"
# Ensure schema is created (fallback)
$MIGRATIONS_CMD -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS auth;" 2>/dev/null
# Get each SQL file from migrations directory in alphabetical order
for sql_file in $(find $MIGRATIONS_DIR -name "*.sql" | sort); do
echo "Applying migration: $(basename $sql_file)"
# Run the migration
if [ -z "$POSTGRES_DB" ]; then
# Manual mode - run through docker-compose
docker-compose exec -T postgres psql -U postgres -d "$DB_NAME" -f "/docker-entrypoint-initdb.d/migrations/$(basename $sql_file)"
else
# Auto mode - run directly
psql -U "$POSTGRES_USER" -d "$DB_NAME" -f "$sql_file"
fi
# Check if migration was successful
if [ $? -ne 0 ]; then
echo "Error: Failed to apply migration $sql_file"
exit 1
fi
done
echo "All migrations applied successfully"
-53
View File
@@ -1,53 +0,0 @@
//! Benchmarks for file operations in OxiCloud
#![feature(test)]
extern crate test;
use test::{black_box, Bencher};
use oxicloud::application::services::file_service::{FileCreationOptions, FileService};
use oxicloud::domain::entities::file::File;
use std::sync::Arc;
use tokio::runtime::Runtime;
use uuid::Uuid;
/// Benchmark for creating files
#[bench]
fn bench_create_file(b: &mut Bencher) {
let rt = Runtime::new().unwrap();
// Initialize services - this would need adaptation based on actual application structure
let file_service = Arc::new(get_file_service());
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
let filename = "benchmark_test.txt";
let content = "This is a test file for benchmarking".as_bytes().to_vec();
let folder_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
b.iter(|| {
black_box(rt.block_on(async {
let options = FileCreationOptions {
overwrite: true,
..Default::default()
};
// Create a file with the service
file_service.create_file(
user_id,
folder_id,
filename.to_string(),
content.clone(),
options,
).await
}))
});
}
/// Mock implementation for getting a file service instance for benchmarking
fn get_file_service() -> FileService {
// This is a simplified mock implementation
// In a real benchmark, you would use actual dependencies
FileService::new(
// Add required repositories/services as needed
// For illustration only - will need adaptation for actual implementation
)
}
-22
View File
@@ -1,22 +0,0 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./fix-userrole.sql:/docker-entrypoint-initdb.d/fix-userrole.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
-883
View File
@@ -1,883 +0,0 @@
warning: unused import: `UseCaseFactory`
--> src/common/di.rs:20:70
|
20 | use crate::application::ports::inbound::{FileUseCase, FolderUseCase, UseCaseFactory};
| ^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused import: `FilePathResolutionPort`
--> src/common/di.rs:23:77
|
23 | use crate::application::ports::storage_ports::{FileReadPort, FileWritePort, FilePathResolutionPort};
| ^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::domain::repositories::folder_repository::FolderRepository`
--> src/common/di.rs:29:5
|
29 | use crate::domain::repositories::folder_repository::FolderRepository;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unnecessary trailing semicolon
--> src/common/di.rs:677:46
|
677 | struct DummyI18nApplicationService {};
| ^ help: remove this semicolon
|
= note: `#[warn(redundant_semicolons)]` on by default
warning: unused import: `DateTime`
--> src/domain/services/auth_service.rs:4:19
|
4 | use chrono::{Utc, DateTime};
| ^^^^^^^^
warning: unused import: `UserRole`
--> src/domain/services/auth_service.rs:6:43
|
6 | use crate::domain::entities::user::{User, UserRole};
| ^^^^^^^^
warning: unused import: `UserRole`
--> src/application/dtos/user_dto.rs:3:43
|
3 | use crate::domain::entities::user::{User, UserRole};
| ^^^^^^^^
warning: unused imports: `Path` and `middleware`
--> src/interfaces/api/handlers/auth_handler.rs:5:28
|
5 | extract::{State, Json, Path, Extension},
| ^^^^
...
8 | middleware,
| ^^^^^^^^^^
warning: unused imports: `AuthResponseDto` and `UserDto`
--> src/interfaces/api/handlers/auth_handler.rs:13:28
|
13 | LoginDto, RegisterDto, UserDto, ChangePasswordDto, RefreshTokenDto, AuthResponseDto
| ^^^^^^^ ^^^^^^^^^^^^^^^
warning: unused import: `middleware`
--> src/interfaces/api/routes.rs:6:5
|
6 | middleware,
| ^^^^^^^^^^
warning: unused import: `crate::interfaces::middleware::auth::auth_middleware`
--> src/interfaces/api/routes.rs:13:5
|
13 | use crate::interfaces::middleware::auth::auth_middleware;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `std::path::PathBuf`
--> src/interfaces/web/mod.rs:7:5
|
7 | use std::path::PathBuf;
| ^^^^^^^^^^^^^^^^^^
warning: unused imports: `FromRequestParts`, `RequestPartsExt`, `body::Body`, and `request::Parts`
--> src/interfaces/middleware/auth.rs:3:31
|
3 | extract::{State, Request, FromRequestParts},
| ^^^^^^^^^^^^^^^^
4 | http::{StatusCode, request::Parts, HeaderMap, header},
| ^^^^^^^^^^^^^^
...
7 | body::Body,
| ^^^^^^^^^^
8 | RequestPartsExt,
| ^^^^^^^^^^^^^^^
warning: unused import: `async_trait::async_trait`
--> src/interfaces/middleware/auth.rs:10:5
|
10 | use async_trait::async_trait;
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `futures::future::BoxFuture`
--> src/interfaces/middleware/auth.rs:11:5
|
11 | use futures::future::BoxFuture;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::common::errors::AppError`
--> src/interfaces/middleware/auth.rs:14:5
|
14 | use crate::common::errors::AppError;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::domain::entities::user::UserRole`
--> src/interfaces/middleware/auth.rs:15:5
|
15 | use crate::domain::entities::user::UserRole;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused variable: `state`
--> src/interfaces/middleware/auth.rs:65:11
|
65 | State(state): State<Arc<AppState>>,
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_state`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `token_str`
--> src/interfaces/middleware/auth.rs:71:17
|
71 | if let Some(token_str) = headers
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_token_str`
warning: unused import: `crate::application::ports::outbound::IdMappingPort`
--> src/infrastructure/repositories/file_fs_repository.rs:19:5
|
19 | use crate::application::ports::outbound::IdMappingPort;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::application::ports::outbound::IdMappingPort`
--> src/infrastructure/repositories/folder_fs_repository.rs:13:5
|
13 | use crate::application::ports::outbound::IdMappingPort;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::application::ports::outbound::IdMappingPort`
--> src/infrastructure/repositories/file_path_resolver.rs:7:5
|
7 | use crate::application::ports::outbound::IdMappingPort;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused variable: `folder_repository`
--> src/application/services/storage_mediator.rs:118:9
|
118 | folder_repository: Arc<RwLock<Option<Arc<dyn FolderRepository>>>>,
| ^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_folder_repository`
warning: unused variable: `folder_id`
--> src/infrastructure/repositories/file_fs_read_repository.rs:166:32
|
166 | async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_folder_id`
warning: unused variable: `abs_path`
--> src/infrastructure/repositories/file_fs_read_repository.rs:183:13
|
183 | let abs_path = self.path_resolver.resolve_storage_path(file.storage_path());
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_abs_path`
warning: unused variable: `id`
--> src/infrastructure/repositories/file_fs_read_repository.rs:190:37
|
190 | async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Sen...
| ^^ help: if this is intentional, prefix it with an underscore: `_id`
warning: unused variable: `result`
--> src/infrastructure/repositories/pg/user_pg_repository.rs:49:13
|
49 | let result = sqlx::query(
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_result`
warning: unused variable: `config`
--> src/interfaces/api/routes.rs:144:9
|
144 | let config = AppConfig::from_env();
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_config`
warning: struct `DummyFilePathResolutionPort` is never constructed
--> src/common/di.rs:497:16
|
497 | struct DummyFilePathResolutionPort;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `root_path`, `config`, and `parallel_processor` are never read
--> src/infrastructure/repositories/file_fs_read_repository.rs:19:5
|
18 | pub struct FileFsReadRepository {
| -------------------- fields in this struct
19 | root_path: PathBuf,
| ^^^^^^^^^
...
22 | config: AppConfig,
| ^^^^^^
23 | parallel_processor: Option<Arc<ParallelFileProcessor>>,
| ^^^^^^^^^^^^^^^^^^
warning: fields `root_path`, `storage_mediator`, and `parallel_processor` are never read
--> src/infrastructure/repositories/file_fs_write_repository.rs:18:5
|
17 | pub struct FileFsWriteRepository {
| --------------------- fields in this struct
18 | root_path: PathBuf,
| ^^^^^^^^^
...
21 | storage_mediator: Arc<dyn StorageMediator>,
| ^^^^^^^^^^^^^^^^
22 | config: AppConfig,
23 | parallel_processor: Option<Arc<ParallelFileProcessor>>,
| ^^^^^^^^^^^^^^^^^^
warning: method `delete_file_non_blocking` is never used
--> src/infrastructure/repositories/file_fs_write_repository.rs:112:14
|
26 | impl FileFsWriteRepository {
| -------------------------- method in this implementation
...
112 | async fn delete_file_non_blocking(&self, _abs_path: PathBuf) -> FileRepositoryResult<()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: `oxicloud` (lib) generated 32 warnings (run `cargo fix --lib -p oxicloud` to apply 16 suggestions)
warning: unused import: `ports::inbound::FolderUseCase`
--> src/application/mod.rs:7:9
|
7 | pub use ports::inbound::FolderUseCase;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused imports: `FileManagementUseCase`, `FileRetrievalUseCase`, `FileUploadUseCase`, and `FileUseCaseFactory`
--> src/application/mod.rs:8:29
|
8 | pub use ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory};
| ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
warning: unused imports: `FolderStoragePort` and `IdMappingPort`
--> src/application/mod.rs:9:27
|
9 | pub use ports::outbound::{FolderStoragePort, IdMappingPort};
| ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
warning: unused imports: `DirectoryManagementPort`, `FilePathResolutionPort`, `FileReadPort`, `FileWritePort`, and `StorageVerificationPort`
--> src/application/mod.rs:10:32
|
10 | ...ts::{FileReadPort, FileWritePort, FilePathResolutionPort, StorageVerificationPort, DirectoryManagementPort};
| ^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
warning: value assigned to `app` is never read
--> src/main.rs:285:9
|
285 | app = app.nest("/api/auth", auth_router);
| ^^^
|
= help: maybe it is overwritten before being read?
= note: `#[warn(unused_assignments)]` on by default
warning: unused variable: `path`
--> src/main.rs:580:45
|
580 | ... let path = entry.path();
| ^^^^ help: if this is intentional, prefix it with an underscore: `_path`
warning: variant `NotImplemented` is never constructed
--> src/common/errors.rs:21:5
|
7 | pub enum ErrorKind {
| --------- variant in this enum
...
21 | NotImplemented,
| ^^^^^^^^^^^^^^
|
= note: `ErrorKind` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
= note: `#[warn(dead_code)]` on by default
warning: associated function `not_implemented` is never used
--> src/common/errors.rs:140:12
|
55 | impl DomainError {
| ---------------- associated function in this implementation
...
140 | pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
| ^^^^^^^^^^^^^^^
warning: associated functions `bad_request`, `forbidden`, and `not_found` are never used
--> src/common/errors.rs:252:12
|
243 | impl AppError {
| ------------- associated functions in this implementation
...
252 | pub fn bad_request(message: impl Into<String>) -> Self {
| ^^^^^^^^^^^
...
260 | pub fn forbidden(message: impl Into<String>) -> Self {
| ^^^^^^^^^
...
264 | pub fn not_found(message: impl Into<String>) -> Self {
| ^^^^^^^^^
warning: fields `file_ttl_ms`, `directory_ttl_ms`, and `max_entries` are never read
--> src/common/config.rs:9:9
|
7 | pub struct CacheConfig {
| ----------- fields in this struct
8 | /// TTL para entradas de archivos en caché (ms)
9 | pub file_ttl_ms: u64,
| ^^^^^^^^^^^
10 | /// TTL para entradas de directorios en caché (ms)
11 | pub directory_ttl_ms: u64,
| ^^^^^^^^^^^^^^^^
12 | /// Máximo número de entradas en caché
13 | pub max_entries: usize,
| ^^^^^^^^^^^
|
= note: `CacheConfig` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
warning: methods `file_read_timeout` and `file_delete_timeout` are never used
--> src/common/config.rs:63:12
|
51 | impl TimeoutConfig {
| ------------------ methods in this implementation
...
63 | pub fn file_read_timeout(&self) -> Duration {
| ^^^^^^^^^^^^^^^^^
...
68 | pub fn file_delete_timeout(&self) -> Duration {
| ^^^^^^^^^^^^^^^^^^^
warning: fields `hash_memory_cost` and `hash_time_cost` are never read
--> src/common/config.rs:228:9
|
224 | pub struct AuthConfig {
| ---------- fields in this struct
...
228 | pub hash_memory_cost: u32,
| ^^^^^^^^^^^^^^^^
229 | pub hash_time_cost: u32,
| ^^^^^^^^^^^^^^
|
= note: `AuthConfig` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
warning: field `enable_file_sharing` is never read
--> src/common/config.rs:249:9
|
246 | pub struct FeaturesConfig {
| -------------- field in this struct
...
249 | pub enable_file_sharing: bool,
| ^^^^^^^^^^^^^^^^^^^
|
= note: `FeaturesConfig` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
warning: field `cache` is never read
--> src/common/config.rs:274:9
|
264 | pub struct AppConfig {
| --------- field in this struct
...
274 | pub cache: CacheConfig,
| ^^^^^
|
= note: `AppConfig` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
warning: methods `with_features`, `db_enabled`, and `auth_enabled` are never used
--> src/common/config.rs:386:12
|
307 | impl AppConfig {
| -------------- methods in this implementation
...
386 | pub fn with_features(mut self, features: FeaturesConfig) -> Self {
| ^^^^^^^^^^^^^
...
391 | pub fn db_enabled(&self) -> bool {
| ^^^^^^^^^^
...
395 | pub fn auth_enabled(&self) -> bool {
| ^^^^^^^^^^^^
warning: fields `core`, `repositories`, and `applications` are never read
--> src/common/di.rs:269:9
|
268 | pub struct AppState {
| -------- fields in this struct
269 | pub core: CoreServices,
| ^^^^
270 | pub repositories: RepositoryServices,
| ^^^^^^^^^^^^
271 | pub applications: ApplicationServices,
| ^^^^^^^^^^^^
warning: struct `DummyFilePathResolutionPort` is never constructed
--> src/common/di.rs:497:16
|
497 | struct DummyFilePathResolutionPort;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: methods `update_storage_used`, `deactivate`, and `activate` are never used
--> src/domain/entities/user.rs:209:12
|
57 | impl User {
| --------- methods in this implementation
...
209 | pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
| ^^^^^^^^^^^^^^^^^^^
...
222 | pub fn deactivate(&mut self) {
| ^^^^^^^^^^
...
228 | pub fn activate(&mut self) {
| ^^^^^^^^
warning: method `revoke` is never used
--> src/domain/entities/session.rs:67:12
|
17 | impl Session {
| ------------ method in this implementation
...
67 | pub fn revoke(&mut self) {
| ^^^^^^
warning: variants `ValidationError`, `Timeout`, and `OperationNotAllowed` are never constructed
--> src/domain/repositories/user_repository.rs:17:5
|
6 | pub enum UserRepositoryError {
| ------------------- variants in this enum
...
17 | ValidationError(String),
| ^^^^^^^^^^^^^^^
...
20 | Timeout(String),
| ^^^^^^^
...
23 | OperationNotAllowed(String),
| ^^^^^^^^^^^^^^^^^^^
|
= note: `UserRepositoryError` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
warning: methods `update_last_login`, `set_user_active_status`, `change_role`, and `delete_user` are never used
--> src/domain/repositories/user_repository.rs:75:14
|
55 | pub trait UserRepository: Send + Sync + 'static {
| -------------- methods in this trait
...
75 | async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
| ^^^^^^^^^^^^^^^^^
...
81 | async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>;
| ^^^^^^^^^^^^^^^^^^^^^^
...
87 | async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
| ^^^^^^^^^^^
...
90 | async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
| ^^^^^^^^^^^
warning: variant `Timeout` is never constructed
--> src/domain/repositories/session_repository.rs:14:5
|
6 | pub enum SessionRepositoryError {
| ---------------------- variant in this enum
...
14 | Timeout(String),
| ^^^^^^^
|
= note: `SessionRepositoryError` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
warning: methods `get_session_by_id`, `get_sessions_by_user_id`, and `delete_expired_sessions` are never used
--> src/domain/repositories/session_repository.rs:42:14
|
37 | pub trait SessionRepository: Send + Sync + 'static {
| ----------------- methods in this trait
...
42 | async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
| ^^^^^^^^^^^^^^^^^
...
48 | async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>;
| ^^^^^^^^^^^^^^^^^^^^^^^
...
57 | async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
| ^^^^^^^^^^^^^^^^^^^^^^^
warning: variants `InvalidCredentials`, `TokenExpired`, `InvalidToken`, `AccessDenied`, and `OperationNotAllowed` are never constructed
--> src/domain/services/auth_service.rs:24:5
|
22 | pub enum AuthError {
| --------- variants in this enum
23 | #[error("Credenciales inválidas")]
24 | InvalidCredentials,
| ^^^^^^^^^^^^^^^^^^
...
27 | TokenExpired,
| ^^^^^^^^^^^^
...
30 | InvalidToken(String),
| ^^^^^^^^^^^^
...
33 | AccessDenied(String),
| ^^^^^^^^^^^^
...
36 | OperationNotAllowed(String),
| ^^^^^^^^^^^^^^^^^^^
|
= note: `AuthError` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
warning: method `validate_token` is never used
--> src/domain/services/auth_service.rs:107:12
|
73 | impl AuthService {
| ---------------- method in this implementation
...
107 | pub fn validate_token(&self, token: &str) -> Result<TokenClaims, AuthError> {
| ^^^^^^^^^^^^^^
warning: multiple methods are never used
--> src/application/ports/inbound.rs:14:14
|
12 | pub trait FileUseCase: Send + Sync + 'static {
| ----------- methods in this trait
13 | /// Sube un nuevo archivo desde bytes
14 | async fn upload_file(
| ^^^^^^^^^^^
...
23 | async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
| ^^^^^^^^
...
26 | async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
| ^^^^^^^^^^
...
29 | async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
| ^^^^^^^^^^^
...
32 | async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
| ^^^^^^^^^^^^^^^^
...
35 | async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send...
| ^^^^^^^^^^^^^^^
...
38 | async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
| ^^^^^^^^^
warning: method `get_folder_by_path` is never used
--> src/application/ports/inbound.rs:51:14
|
43 | pub trait FolderUseCase: Send + Sync + 'static {
| ------------- method in this trait
...
51 | async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
| ^^^^^^^^^^^^^^^^^^
warning: trait `UseCaseFactory` is never used
--> src/application/ports/inbound.rs:74:11
|
74 | pub trait UseCaseFactory {
| ^^^^^^^^^^^^^^
warning: methods `resolve_path`, `ensure_directory`, `file_exists`, and `directory_exists` are never used
--> src/application/ports/outbound.rs:15:8
|
13 | pub trait StoragePort: Send + Sync + 'static {
| ----------- methods in this trait
14 | /// Resuelve una ruta de dominio a una ruta física
15 | fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
| ^^^^^^^^^^^^
...
18 | async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
| ^^^^^^^^^^^^^^^^
...
21 | async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
| ^^^^^^^^^^^
...
24 | async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
| ^^^^^^^^^^^^^^^^
warning: method `get_file_path` is never used
--> src/application/ports/outbound.rs:58:14
|
29 | pub trait FileStoragePort: Send + Sync + 'static {
| --------------- method in this trait
...
58 | async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
| ^^^^^^^^^^^^^
warning: methods `folder_exists` and `get_folder_path` are never used
--> src/application/ports/outbound.rs:95:14
|
63 | pub trait FolderStoragePort: Send + Sync + 'static {
| ----------------- methods in this trait
...
95 | async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
| ^^^^^^^^^^^^^
...
98 | async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
| ^^^^^^^^^^^^^^^
warning: method `upload_file` is never used
--> src/application/ports/file_ports.rs:13:14
|
11 | pub trait FileUploadUseCase: Send + Sync + 'static {
| ----------------- method in this trait
12 | /// Sube un nuevo archivo desde bytes
13 | async fn upload_file(
| ^^^^^^^^^^^
warning: methods `get_file`, `list_files`, `get_file_content`, and `get_file_stream` are never used
--> src/application/ports/file_ports.rs:26:14
|
24 | pub trait FileRetrievalUseCase: Send + Sync + 'static {
| -------------------- methods in this trait
25 | /// Obtiene un archivo por su ID
26 | async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
| ^^^^^^^^
...
29 | async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
| ^^^^^^^^^^
...
32 | async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
| ^^^^^^^^^^^^^^^^
...
35 | async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send...
| ^^^^^^^^^^^^^^^
warning: methods `move_file` and `delete_file` are never used
--> src/application/ports/file_ports.rs:42:14
|
40 | pub trait FileManagementUseCase: Send + Sync + 'static {
| --------------------- methods in this trait
41 | /// Mueve un archivo a otra carpeta
42 | async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
| ^^^^^^^^^
...
45 | async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
| ^^^^^^^^^^^
warning: methods `create_file_upload_use_case`, `create_file_retrieval_use_case`, and `create_file_management_use_case` are never used
--> src/application/ports/file_ports.rs:50:8
|
49 | pub trait FileUseCaseFactory: Send + Sync + 'static {
| ------------------ methods in this trait
50 | fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
51 | fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: methods `get_file_path` and `resolve_path` are never used
--> src/application/ports/storage_ports.rs:49:14
|
47 | pub trait FilePathResolutionPort: Send + Sync + 'static {
| ---------------------- methods in this trait
48 | /// Obtiene la ruta de almacenamiento de un archivo
49 | async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
| ^^^^^^^^^^^^^
...
52 | fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
| ^^^^^^^^^^^^
warning: trait `StorageVerificationPort` is never used
--> src/application/ports/storage_ports.rs:57:11
|
57 | pub trait StorageVerificationPort: Send + Sync + 'static {
| ^^^^^^^^^^^^^^^^^^^^^^^
warning: trait `DirectoryManagementPort` is never used
--> src/application/ports/storage_ports.rs:67:11
|
67 | pub trait DirectoryManagementPort: Send + Sync + 'static {
| ^^^^^^^^^^^^^^^^^^^^^^^
warning: methods `update_storage_usage`, `list_users`, and `change_password` are never used
--> src/application/ports/auth_ports.rs:24:14
|
7 | pub trait UserStoragePort: Send + Sync + 'static {
| --------------- methods in this trait
...
24 | async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>;
| ^^^^^^^^^^^^^^^^^^^^
...
27 | async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
| ^^^^^^^^^^
...
30 | async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
| ^^^^^^^^^^^^^^^
warning: associated function `new_stub` is never used
--> src/application/services/file_service.rs:83:12
|
76 | impl FileService {
| ---------------- associated function in this implementation
...
83 | pub fn new_stub() -> impl FileUseCase {
| ^^^^^^^^
warning: associated function `new_stub` is never used
--> src/application/services/folder_service.rs:22:12
|
15 | impl FolderService {
| ------------------ associated function in this implementation
...
22 | pub fn new_stub() -> impl FolderUseCase {
| ^^^^^^^^
warning: multiple methods are never used
--> src/application/services/storage_mediator.rs:69:14
|
64 | pub trait StorageMediator: Send + Sync + 'static {
| --------------- methods in this trait
...
69 | async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
| ^^^^^^^^^^^^^^^^^^^^^^^
...
72 | async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
| ^^^^^^^^^^
...
75 | async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
| ^^^^^^^^^^^^^^^^^^^
...
78 | async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
81 | async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
| ^^^^^^^^^^^^^^^^^^^^^
...
84 | async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
93 | async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
| ^^^^^^^^^^^^^^^^
...
96 | async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: methods `logout_all` and `list_users` are never used
--> src/application/services/auth_application_service.rs:263:18
|
18 | impl AuthApplicationService {
| --------------------------- methods in this implementation
...
263 | pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
| ^^^^^^^^^^
...
317 | pub async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<UserDto>, DomainError> {
| ^^^^^^^^^^
warning: variant `Unavailable` is never constructed
--> src/infrastructure/repositories/file_metadata_manager.rs:26:5
|
18 | pub enum MetadataError {
| ------------- variant in this enum
...
26 | Unavailable(String),
| ^^^^^^^^^^^
|
= note: `MetadataError` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
warning: methods `invalidate` and `invalidate_directory` are never used
--> src/infrastructure/repositories/file_metadata_manager.rs:158:18
|
39 | impl FileMetadataManager {
| ------------------------ methods in this implementation
...
158 | pub async fn invalidate(&self, abs_path: &PathBuf) {
| ^^^^^^^^^^
...
163 | pub async fn invalidate_directory(&self, dir_path: &PathBuf) {
| ^^^^^^^^^^^^^^^^^^^^
warning: field `storage_mediator` is never read
--> src/infrastructure/repositories/file_path_resolver.rs:15:5
|
13 | pub struct FilePathResolver {
| ---------------- field in this struct
14 | path_service: Arc<PathService>,
15 | storage_mediator: Arc<dyn StorageMediator>,
| ^^^^^^^^^^^^^^^^
warning: methods `resolve_legacy_path`, `update_path`, `get_or_create_id`, `remove_id`, and `save_changes` are never used
--> src/infrastructure/repositories/file_path_resolver.rs:80:12
|
19 | impl FilePathResolver {
| --------------------- methods in this implementation
...
80 | pub fn resolve_legacy_path(&self, relative_path: &std::path::Path) -> PathBuf {
| ^^^^^^^^^^^^^^^^^^^
...
91 | pub async fn update_path(&self, id: &str, storage_path: &StoragePath) -> Result<(), FileRepositoryError> {
| ^^^^^^^^^^^
...
97 | pub async fn get_or_create_id(&self, storage_path: &StoragePath) -> Result<String, FileRepositoryError> {
| ^^^^^^^^^^^^^^^^
...
103 | pub async fn remove_id(&self, id: &str) -> Result<(), FileRepositoryError> {
| ^^^^^^^^^
...
109 | pub async fn save_changes(&self) -> Result<(), FileRepositoryError> {
| ^^^^^^^^^^^^
warning: associated function `new_in_memory` is never used
--> src/infrastructure/services/id_mapping_service.rs:100:12
|
84 | impl IdMappingService {
| --------------------- associated function in this implementation
...
100 | pub fn new_in_memory() -> Self {
| ^^^^^^^^^^^^^
warning: fields `username`, `email`, and `role` are never read
--> src/interfaces/middleware/auth.rs:21:9
|
19 | pub struct CurrentUser {
| ----------- fields in this struct
20 | pub id: String,
21 | pub username: String,
| ^^^^^^^^
22 | pub email: String,
| ^^^^^
23 | pub role: String,
| ^^^^
|
= note: `CurrentUser` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis
warning: variants `TokenNotProvided`, `InvalidToken`, `TokenExpired`, `UserNotFound`, and `AccessDenied` are never constructed
--> src/interfaces/middleware/auth.rs:30:5
|
28 | pub enum AuthError {
| --------- variants in this enum
29 | #[error("Token no proporcionado")]
30 | TokenNotProvided,
| ^^^^^^^^^^^^^^^^
...
33 | InvalidToken(String),
| ^^^^^^^^^^^^
...
36 | TokenExpired,
| ^^^^^^^^^^^^
...
39 | UserNotFound,
| ^^^^^^^^^^^^
...
42 | AccessDenied(String),
| ^^^^^^^^^^^^
|
= note: `AuthError` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
warning: function `auth_middleware` is never used
--> src/interfaces/middleware/auth.rs:64:14
|
64 | pub async fn auth_middleware(
| ^^^^^^^^^^^^^^^
warning: function `require_admin` is never used
--> src/interfaces/middleware/auth.rs:94:14
|
94 | pub async fn require_admin(
| ^^^^^^^^^^^^^
warning: `oxicloud` (bin "oxicloud") generated 83 warnings (31 duplicates) (run `cargo fix --bin "oxicloud"` to apply 4 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/oxicloud`
2025-03-23T17:05:10.052229Z  INFO oxicloud::infrastructure::services::id_mapping_service: Loaded ID map with 0 entries (version: 0)
2025-03-23T17:05:10.052304Z  INFO oxicloud: ID mapping optimizer initialized with batch processing and caching
2025-03-23T17:05:10.052341Z  INFO oxicloud: Buffer pool initialized with 50 buffers of 256KB each
2025-03-23T17:05:10.052362Z  INFO oxicloud::infrastructure::services::file_system_i18n_service: Loading translations for locale en from "./static/locales/en.json"
2025-03-23T17:05:10.052497Z  INFO oxicloud::infrastructure::services::file_system_i18n_service: Translations loaded for locale en
2025-03-23T17:05:10.052506Z  INFO oxicloud::infrastructure::services::file_system_i18n_service: Loading translations for locale es from "./static/locales/es.json"
2025-03-23T17:05:10.052595Z  INFO oxicloud::infrastructure::services::file_system_i18n_service: Translations loaded for locale es
2025-03-23T17:05:10.052602Z  INFO oxicloud: Compression service initialized with buffer pool support
2025-03-23T17:05:10.053417Z  INFO oxicloud: Preloading common directories to warm up cache...
2025-03-23T17:05:10.054362Z  INFO oxicloud: Preloaded 4 directory entries into cache
2025-03-23T17:05:10.054377Z  INFO oxicloud: Starting OxiCloud server on http://127.0.0.1:8085
2025-03-23T17:05:10.054382Z  INFO oxicloud: Authentication system initialized successfully
2025-03-23T17:05:10.054387Z  INFO oxicloud: Server binding to http://127.0.0.1:8085
2025-03-23T17:05:10.054405Z DEBUG oxicloud::interfaces::middleware::cache: HttpCache cleanup: removed 0 expired entries
thread 'main' panicked at src/main.rs:308:47:
Failed to bind to address: Os { code: 98, kind: AddrInUse, message: "Address already in use" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2025-03-23T17:05:10.054424Z  INFO oxicloud::interfaces::middleware::cache: HTTP Cache cleanup: removed 0, current: 0/0
+1 -1
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::domain::entities::share::{Share, ShareItemType, SharePermissions};
use crate::domain::entities::share::{Share, SharePermissions};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareDto {
@@ -6,18 +6,18 @@ use crate::application::ports::file_ports::FileManagementUseCase;
use crate::application::ports::storage_ports::FileWritePort;
use crate::common::errors::DomainError;
/// Servicio para operaciones de gestión de archivos
/// Service for file management operations
pub struct FileManagementService {
file_repository: Arc<dyn FileWritePort>,
}
impl FileManagementService {
/// Crea un nuevo servicio de gestión de archivos
/// Creates a new file management service
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
}
/// Crea un stub para pruebas
/// Creates a stub for testing
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
@@ -28,15 +28,15 @@ impl FileManagementService {
#[async_trait]
impl FileManagementUseCase for FileManagementService {
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError> {
tracing::info!("Moviendo archivo con ID: {} a carpeta: {:?}", file_id, folder_id);
tracing::info!("Moving file with ID: {} to folder: {:?}", file_id, folder_id);
let moved_file = self.file_repository.move_file(file_id, folder_id).await
.map_err(|e| {
tracing::error!("Error al mover archivo (ID: {}): {}", file_id, e);
tracing::error!("Error moving file (ID: {}): {}", file_id, e);
e
})?;
tracing::info!("Archivo movido exitosamente: {} (ID: {}) a carpeta: {:?}",
tracing::info!("File moved successfully: {} (ID: {}) to folder: {:?}",
moved_file.name(), moved_file.id(), moved_file.folder_id());
Ok(FileDto::from(moved_file))
+10 -10
View File
@@ -19,23 +19,23 @@ use bytes::Bytes;
#[derive(Debug, Error)]
pub enum FileServiceError {
/// Returned when a requested file cannot be found
#[error("Archivo no encontrado: {0}")]
#[error("File not found: {0}")]
NotFound(String),
/// Returned when a file operation conflicts with existing files
#[error("Archivo ya existe: {0}")]
#[error("File already exists: {0}")]
Conflict(String),
/// Returned when file access fails due to permissions or I/O issues
#[error("Error de acceso al archivo: {0}")]
#[error("File access error: {0}")]
AccessError(String),
/// Returned when a file path is invalid
#[error("Ruta de archivo inválida: {0}")]
#[error("Invalid file path: {0}")]
InvalidPath(String),
/// Generic internal error for unexpected failures
#[error("Error interno: {0}")]
#[error("Internal error: {0}")]
InternalError(String),
}
@@ -52,7 +52,7 @@ impl From<FileRepositoryError> for FileServiceError {
FileRepositoryError::AlreadyExists(path) => FileServiceError::Conflict(path),
FileRepositoryError::InvalidPath(path) => FileServiceError::InvalidPath(path),
FileRepositoryError::IoError(e) => FileServiceError::AccessError(e.to_string()),
FileRepositoryError::Timeout(msg) => FileServiceError::AccessError(format!("Operación expiró: {}", msg)),
FileRepositoryError::Timeout(msg) => FileServiceError::AccessError(format!("Operation timed out: {}", msg)),
_ => FileServiceError::InternalError(err.to_string()),
}
}
@@ -213,16 +213,16 @@ impl FileService {
/// Moves a file to a new folder using filesystem operations directly
pub async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> FileServiceResult<FileDto> {
tracing::info!("Moviendo archivo con ID: {} a carpeta: {:?}", file_id, folder_id);
tracing::info!("Moving file with ID: {} to folder: {:?}", file_id, folder_id);
// Usar la implementación eficiente del repositorio que utiliza rename
// Use the efficient repository implementation that uses rename
let moved_file = self.file_repository.move_file(file_id, folder_id).await
.map_err(|e| {
tracing::error!("Error al mover archivo (ID: {}): {}", file_id, e);
tracing::error!("Error moving file (ID: {}): {}", file_id, e);
FileServiceError::from(e)
})?;
tracing::info!("Archivo movido exitosamente: {} (ID: {}) a carpeta: {:?}",
tracing::info!("File moved successfully: {} (ID: {}) to folder: {:?}",
moved_file.name(), moved_file.id(), moved_file.folder_id());
Ok(FileDto::from(moved_file))
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::{
application::{
dtos::{
pagination::PaginatedResponseDto,
share_dto::{CreateShareDto, ShareDto, SharePermissionsDto, UpdateShareDto},
share_dto::{CreateShareDto, ShareDto, UpdateShareDto},
},
ports::{
outbound::{FileStoragePort, FolderStoragePort},
+54 -54
View File
@@ -53,9 +53,9 @@ impl TrashService {
}
}
/// Convierte una entidad TrashedItem a un DTO
/// Converts a TrashedItem entity to a DTO
fn to_dto(&self, item: TrashedItem) -> TrashedItemDto {
// Calcular days_until_deletion antes de mover item.original_path
// Calculate days_until_deletion before moving item.original_path
let days_until_deletion = item.days_until_deletion();
TrashedItemDto {
@@ -72,12 +72,12 @@ impl TrashService {
}
}
/// Valida los permisos del usuario sobre un elemento
/// Validates user permissions over an item
#[instrument(skip(self))]
async fn validate_user_ownership(&self, _item_id: &str, _user_id: &str) -> Result<()> {
// Aquí implementaríamos la validación de permisos
// Por ahora, simplemente devolvemos Ok ya que no tenemos una implementación completa
// de permisos por usuario
// Here we would implement permission validation
// For now, we simply return Ok since we don't have a complete
// implementation of user permissions
Ok(())
}
}
@@ -86,7 +86,7 @@ impl TrashService {
impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
debug!("Obteniendo elementos en papelera para usuario: {}", user_id);
debug!("Getting trash items for user: {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
@@ -102,52 +102,52 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
info!("Moviendo a papelera: tipo={}, id={}, usuario={}", item_type, item_id, user_id);
info!("Moving to trash: type={}, id={}, user={}", item_type, item_id, user_id);
debug!("User UUID validation: {}", user_id);
// Validate user ownership
debug!("Validando permisos de usuario");
debug!("Validating user permissions");
self.validate_user_ownership(item_id, user_id).await?;
debug!("Permisos de usuario validados");
debug!("User permissions validated");
// Parse UUIDs with detailed error handling
debug!("Validando UUID del item: {}", item_id);
debug!("Validating item UUID: {}", item_id);
let item_uuid = match Uuid::parse_str(item_id) {
Ok(uuid) => {
debug!("UUID del item válido: {}", uuid);
debug!("Valid item UUID: {}", uuid);
uuid
},
Err(e) => {
error!("UUID del item inválido: {} - Error: {}", item_id, e);
error!("Invalid item UUID: {} - Error: {}", item_id, e);
return Err(DomainError::validation_error("Item", format!("Invalid item ID: {}", e)));
}
};
debug!("Validando UUID del usuario: {}", user_id);
debug!("Validating user UUID: {}", user_id);
let user_uuid = match Uuid::parse_str(user_id) {
Ok(uuid) => {
debug!("UUID del usuario válido: {}", uuid);
debug!("Valid user UUID: {}", uuid);
uuid
},
Err(e) => {
error!("UUID del usuario inválido: {} - Error: {}", user_id, e);
error!("Invalid user UUID: {} - Error: {}", user_id, e);
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
}
};
match item_type {
"file" => {
info!("Procesando archivo para mover a papelera: {}", item_id);
info!("Processing file to move to trash: {}", item_id);
// Obtener el archivo para verificar que existe y capturar sus datos
debug!("Obteniendo datos del archivo: {}", item_id);
// Get the file to verify it exists and capture its data
debug!("Getting file data: {}", item_id);
let file = match self.file_repository.get_file_by_id(item_id).await {
Ok(file) => {
debug!("Archivo encontrado: {} ({})", file.name(), item_id);
debug!("File found: {} ({})", file.name(), item_id);
file
},
Err(e) => {
error!("Error al obtener archivo: {} - {}", item_id, e);
error!("Error getting file: {} - {}", item_id, e);
return Err(DomainError::new(
ErrorKind::NotFound,
"File",
@@ -157,10 +157,10 @@ impl TrashUseCase for TrashService {
};
let original_path = file.storage_path().to_string();
debug!("Ruta original del archivo: {}", original_path);
debug!("Original file path: {}", original_path);
// Crear el elemento de papelera
debug!("Creando objeto TrashedItem para el archivo");
// Create the trash item
debug!("Creating TrashedItem object for the file");
let trashed_item = TrashedItem::new(
item_uuid,
user_uuid,
@@ -169,28 +169,28 @@ impl TrashUseCase for TrashService {
original_path,
self.retention_days,
);
debug!("TrashedItem creado con éxito: {} -> {}", file.name(), trashed_item.id);
debug!("TrashedItem created successfully: {} -> {}", file.name(), trashed_item.id);
// Primero añadimos a la papelera para registrar el elemento
info!("Añadiendo archivo {} a índice de papelera", item_id);
// First add to trash index to register the item
info!("Adding file {} to trash index", item_id);
match self.trash_repository.add_to_trash(&trashed_item).await {
Ok(_) => {
debug!("Archivo añadido al índice de papelera con éxito");
debug!("File added to trash index successfully");
},
Err(e) => {
error!("Error al añadir archivo al índice de papelera: {}", e);
error!("Error adding file to trash index: {}", e);
return Err(DomainError::internal_error("TrashRepository", format!("Failed to add file to trash: {}", e)));
}
};
// Luego movemos el archivo físicamente a la papelera
info!("Moviendo archivo físicamente a la papelera: {}", item_id);
// Then physically move the file to trash
info!("Physically moving file to trash: {}", item_id);
match self.file_repository.move_to_trash(item_id).await {
Ok(_) => {
debug!("Archivo movido físicamente a papelera con éxito: {}", item_id);
debug!("File physically moved to trash successfully: {}", item_id);
},
Err(e) => {
error!("Error al mover archivo físicamente a papelera: {} - {}", item_id, e);
error!("Error physically moving file to trash: {} - {}", item_id, e);
return Err(DomainError::new(
ErrorKind::InternalError,
"File",
@@ -199,11 +199,11 @@ impl TrashUseCase for TrashService {
}
}
info!("Archivo movido a papelera completamente: {}", item_id);
info!("File completely moved to trash: {}", item_id);
Ok(())
},
"folder" => {
// Obtener la carpeta para verificar que existe y capturar sus datos
// Get the folder to verify it exists and capture its data
let folder = self.folder_repository.get_folder_by_id(item_id).await
.map_err(|e| DomainError::new(
ErrorKind::NotFound,
@@ -213,7 +213,7 @@ impl TrashUseCase for TrashService {
let original_path = folder.storage_path().to_string();
// Crear el elemento de papelera
// Create the trash item
let trashed_item = TrashedItem::new(
item_uuid,
user_uuid,
@@ -223,7 +223,7 @@ impl TrashUseCase for TrashService {
self.retention_days,
);
// Primero añadimos a la papelera para registrar el elemento
// First add to trash index to register the item
debug!("Adding folder {} to trash repository", item_id);
match self.trash_repository.add_to_trash(&trashed_item).await {
Ok(_) => debug!("Successfully added folder to trash repository"),
@@ -233,7 +233,7 @@ impl TrashUseCase for TrashService {
}
};
// Luego movemos la carpeta físicamente a la papelera
// Then physically move the folder to trash
self.folder_repository.move_to_trash(item_id).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
@@ -241,7 +241,7 @@ impl TrashUseCase for TrashService {
format!("Error moving folder {} to trash: {}", item_id, e)
))?;
debug!("Carpeta movida a papelera: {}", item_id);
debug!("Folder moved to trash: {}", item_id);
Ok(())
},
_ => Err(DomainError::validation_error("Item", format!("Invalid item type: {}", item_type))),
@@ -250,7 +250,7 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
info!("Restaurando elemento {} para usuario {}", trash_id, user_id);
info!("Restoring item {} for user {}", trash_id, user_id);
let trash_uuid = match Uuid::parse_str(trash_id) {
Ok(id) => {
@@ -283,10 +283,10 @@ impl TrashUseCase for TrashService {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id, item.item_type, item.original_id);
// Restaurar según tipo
// Restore based on type
match item.item_type {
TrashedItemType::File => {
// Restaurar el archivo a su ubicación original
// Restore the file to its original location
let file_id = item.original_id.to_string();
let original_path = item.original_path.clone();
@@ -313,7 +313,7 @@ impl TrashUseCase for TrashService {
}
},
TrashedItemType::Folder => {
// Restaurar la carpeta a su ubicación original
// Restore the folder to its original location
let folder_id = item.original_id.to_string();
let original_path = item.original_path.clone();
@@ -408,7 +408,7 @@ impl TrashUseCase for TrashService {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id, item.item_type, item.original_id);
// Eliminar permanentemente según tipo
// Permanently delete based on type
match item.item_type {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
@@ -463,7 +463,7 @@ impl TrashUseCase for TrashService {
}
}
// Eliminar el item de la papelera siempre, para mantener consistencia
// Always remove the item from trash index to maintain consistency
info!("Removing entry from trash index: {}", trash_id);
match self.trash_repository.delete_permanently(&trash_uuid, &user_uuid).await {
Ok(_) => {
@@ -497,38 +497,38 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn empty_trash(&self, user_id: &str) -> Result<()> {
info!("Vaciando papelera para usuario {}", user_id);
info!("Emptying trash for user {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
// Obtener todos los elementos en la papelera del usuario
// Get all items in the user's trash
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
// Eliminar permanentemente cada elemento
// Permanently delete each item
for item in items {
match item.item_type {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
// Permanently delete the file
let file_id = item.original_id.to_string();
if let Err(e) = self.file_repository.delete_file_permanently(&file_id).await {
error!("Error al eliminar archivo {} permanentemente: {}", file_id, e);
error!("Error permanently deleting file {}: {}", file_id, e);
}
},
TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente
// Permanently delete the folder
let folder_id = item.original_id.to_string();
if let Err(e) = self.folder_repository.delete_folder_permanently(&folder_id).await {
error!("Error al eliminar carpeta {} permanentemente: {}", folder_id, e);
error!("Error permanently deleting folder {}: {}", folder_id, e);
}
}
}
}
// Limpiar todos los registros de la papelera para este usuario
// Clear all trash records for this user
self.trash_repository.clear_trash(&user_uuid).await?;
info!("Papelera vaciada completamente para usuario {}", user_id);
info!("Trash completely emptied for user {}", user_id);
Ok(())
}
}
+24 -24
View File
@@ -10,11 +10,11 @@ use crate::domain::services::path_service::StoragePath;
#[derive(Debug, thiserror::Error)]
pub enum FileError {
/// Occurs when a file name contains invalid characters or is empty.
#[error("Nombre de archivo inválido: {0}")]
#[error("Invalid file name: {0}")]
InvalidFileName(String),
/// Occurs when validation fails for any file entity attribute.
#[error("Error en la validación: {0}")]
#[error("Validation error: {0}")]
#[allow(dead_code)]
ValidationError(String),
}
@@ -68,7 +68,7 @@ pub struct File {
modified_at: u64,
}
// Ya no necesitamos este módulo, ahora usamos un String directamente
// We no longer need this module, now we use a String directly
impl Default for File {
fn default() -> Self {
@@ -87,7 +87,7 @@ impl Default for File {
}
impl File {
/// Crea un nuevo archivo con validación
/// Creates a new file with validation
pub fn new(
id: String,
name: String,
@@ -96,7 +96,7 @@ impl File {
mime_type: String,
folder_id: Option<String>,
) -> FileResult<Self> {
// Validar nombre de archivo
// Validate file name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
@@ -106,7 +106,7 @@ impl File {
.unwrap_or_default()
.as_secs();
// Almacenamos el string de la ruta para compatibilidad con serialización
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
@@ -122,7 +122,7 @@ impl File {
})
}
/// Crea un archivo con timestamps específicos (para reconstrucción)
/// Creates a file with specific timestamps (for reconstruction)
pub fn with_timestamps(
id: String,
name: String,
@@ -133,12 +133,12 @@ impl File {
created_at: u64,
modified_at: u64,
) -> FileResult<Self> {
// Validar nombre de archivo
// Validate file name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
// Almacenamos el string de la ruta para compatibilidad con serialización
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
@@ -191,8 +191,8 @@ impl File {
self.modified_at
}
/// Crea una nueva instancia de File desde un DTO
/// Esta función es principalmente para conversiones en los batch handlers
/// Creates a new File instance from a DTO
/// This function is primarily for conversions in batch handlers
pub fn from_dto(
id: String,
name: String,
@@ -203,10 +203,10 @@ impl File {
created_at: u64,
modified_at: u64,
) -> Self {
// Crear storage_path desde el string
// Create storage_path from string
let storage_path = StoragePath::from_string(&path);
// Crear directamente sin validación para evitar errores en conversiones DTO
// Create directly without validation to avoid errors in DTO conversions
Self {
id,
name,
@@ -220,24 +220,24 @@ impl File {
}
}
// Métodos para crear nuevas versiones del archivo (inmutable)
// Methods to create new versions of the file (immutable)
/// Crea una nueva versión del archivo con nombre actualizado
/// Creates a new version of the file with updated name
#[allow(dead_code)]
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
// Validar nombre de archivo
// Validate file name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FileError::InvalidFileName(new_name));
}
// Actualizar ruta basada en el nombre
// Update path based on name
let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name),
};
// Actualizar representación en string
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
@@ -258,15 +258,15 @@ impl File {
})
}
/// Crea una nueva versión del archivo con carpeta actualizada
/// Creates a new version of the file with updated folder
pub fn with_folder(&self, folder_id: Option<String>, folder_path: Option<StoragePath>) -> FileResult<Self> {
// Necesitamos una ruta de carpeta para actualizar la ruta del archivo
// We need a folder path to update the file path
let new_storage_path = match folder_path {
Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Raíz
None => StoragePath::from_string(&self.name), // Root
};
// Actualizar representación en string
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
@@ -287,7 +287,7 @@ impl File {
})
}
/// Crea una nueva versión del archivo con tamaño actualizado
/// Creates a new version of the file with updated size
#[allow(dead_code)]
pub fn with_size(&self, new_size: u64) -> Self {
let now = std::time::SystemTime::now()
@@ -333,7 +333,7 @@ mod tests {
let storage_path = StoragePath::from_string("/test/invalid/file.txt");
let file = File::new(
"123".to_string(),
"file/with/slash.txt".to_string(), // Nombre inválido
"file/with/slash.txt".to_string(), // Invalid name
storage_path,
100,
"text/plain".to_string(),
+22 -22
View File
@@ -1,18 +1,18 @@
use serde::{Serialize, Deserialize};
use crate::domain::services::path_service::StoragePath;
/// Error en la creación o manipulación de entidades de carpeta
/// Error in the creation or manipulation of folder entities
#[derive(Debug, thiserror::Error)]
pub enum FolderError {
#[error("Nombre de carpeta inválido: {0}")]
#[error("Invalid folder name: {0}")]
InvalidFolderName(String),
#[error("Error en la validación: {0}")]
#[error("Validation error: {0}")]
#[allow(dead_code)]
ValidationError(String),
}
/// Tipo de resultado para operaciones con entidades de carpeta
/// Result type for folder entity operations
pub type FolderResult<T> = Result<T, FolderError>;
/// Represents a folder entity in the domain
@@ -42,7 +42,7 @@ pub struct Folder {
modified_at: u64,
}
// Ya no necesitamos este módulo, ahora usamos un String directamente
// We no longer need this module, now we use a String directly
impl Default for Folder {
fn default() -> Self {
@@ -66,7 +66,7 @@ impl Folder {
storage_path: StoragePath,
parent_id: Option<String>,
) -> FolderResult<Self> {
// Validar nombre de carpeta
// Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name));
}
@@ -76,7 +76,7 @@ impl Folder {
.unwrap_or_default()
.as_secs();
// Almacenamos el string de la ruta para compatibilidad con serialización
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
@@ -99,12 +99,12 @@ impl Folder {
created_at: u64,
modified_at: u64,
) -> FolderResult<Self> {
// Validar nombre de carpeta
// Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name));
}
// Almacenamos el string de la ruta para compatibilidad con serialización
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
@@ -147,8 +147,8 @@ impl Folder {
self.modified_at
}
/// Crea una nueva instancia de Folder desde un DTO
/// Esta función es principalmente para conversiones en los batch handlers
/// Creates a new Folder instance from a DTO
/// This function is primarily for conversions in batch handlers
pub fn from_dto(
id: String,
name: String,
@@ -157,10 +157,10 @@ impl Folder {
created_at: u64,
modified_at: u64,
) -> Self {
// Crear storage_path desde el string
// Create storage_path from the string
let storage_path = StoragePath::from_string(&path);
// Crear directamente sin validación para evitar errores en conversiones DTO
// Create directly without validation to avoid errors in DTO conversions
Self {
id,
name,
@@ -172,23 +172,23 @@ impl Folder {
}
}
// Métodos para crear nuevas versiones de la carpeta (inmutable)
// Methods to create new versions of the folder (immutable)
/// Creates a new version of the folder with updated name
pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
// Validar nombre de carpeta
// Validate folder name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FolderError::InvalidFolderName(new_name));
}
// Actualizar ruta basada en el nombre
// Update path based on the name
let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name),
};
// Actualizar representación en string
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
@@ -209,13 +209,13 @@ impl Folder {
/// Creates a new version of the folder with updated parent
pub fn with_parent(&self, parent_id: Option<String>, parent_path: Option<StoragePath>) -> FolderResult<Self> {
// Necesitamos una ruta de carpeta para actualizar la ruta
// We need a folder path to update the path
let new_storage_path = match parent_path {
Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Raíz
None => StoragePath::from_string(&self.name), // Root
};
// Actualizar representación en string
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
@@ -276,7 +276,7 @@ mod tests {
let storage_path = StoragePath::from_string("/test/invalid/folder");
let folder = Folder::new(
"123".to_string(),
"folder/with/slash".to_string(), // Nombre inválido
"folder/with/slash".to_string(), // Invalid name
storage_path,
None,
);
@@ -302,6 +302,6 @@ mod tests {
assert!(renamed.is_ok());
let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "new_name");
assert_eq!(renamed.id(), "123"); // El ID no cambia
assert_eq!(renamed.id(), "123"); // The ID doesn't change
}
}
@@ -1,4 +1,3 @@
use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error;
@@ -45,8 +45,8 @@ use crate::infrastructure::repositories::parallel_file_processor::ParallelFilePr
* filesystem-specific details.
*/
// Usar constantes de la configuración centralizada en lugar de valores fijos
// Esto se reemplaza con self.config.concurrency.max_concurrent_files más adelante
// Use constants from centralized configuration instead of fixed values
// This is replaced with self.config.concurrency.max_concurrent_files later
/// Filesystem implementation of the FileRepository interface
pub struct FileFsRepository {
@@ -130,16 +130,16 @@ impl FileFsRepository {
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> FileRepositoryResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
// Intentar obtener del caché avanzado primero
// Try to get from advanced cache first
if let Some(is_file) = self.metadata_cache.is_file(&abs_path).await {
tracing::debug!("Metadata cache hit for existence check: {} - path: {}", is_file, abs_path.display());
return Ok(is_file);
}
// Si no está en caché, verificar directamente y actualizar caché
// If not in cache, verify directly and update cache
tracing::debug!("Metadata cache miss for existence check: {}", abs_path.display());
// Utilizar timeout para evitar bloqueo
// Use timeout to avoid blocking
match time::timeout(
self.config.timeouts.file_timeout(),
fs::metadata(&abs_path)
@@ -147,7 +147,7 @@ impl FileFsRepository {
Ok(Ok(metadata)) => {
let is_file = metadata.is_file();
// Actualizar la caché con información fresca
// Update cache with fresh information
if let Err(e) = self.metadata_cache.refresh_metadata(&abs_path).await {
tracing::warn!("Failed to update cache for {}: {}", abs_path.display(), e);
}
@@ -163,7 +163,7 @@ impl FileFsRepository {
Ok(Err(e)) => {
tracing::warn!("File check failed: {} - {}", abs_path.display(), e);
// Añadir a caché como no existente
// Add to cache as non-existent
let entry_type = CacheEntryType::Unknown;
let file_metadata = crate::infrastructure::services::file_metadata_cache::FileMetadata::new(
abs_path.clone(),
@@ -191,13 +191,13 @@ impl FileFsRepository {
pub async fn file_exists(&self, path: &std::path::Path) -> FileRepositoryResult<bool> {
let abs_path = self.resolve_legacy_path(path);
// Intentar obtener del caché avanzado primero
// Try to get from advanced cache first
if let Some(is_file) = self.metadata_cache.is_file(&abs_path).await {
tracing::debug!("Metadata cache hit for legacy existence check: {} - path: {}", is_file, abs_path.display());
return Ok(is_file);
}
// Si no está en caché, verificar directamente
// If not in cache, verify directly
tracing::info!("Checking if file exists: {} - path: {}", abs_path.exists(), abs_path.display());
match time::timeout(
@@ -207,7 +207,7 @@ impl FileFsRepository {
Ok(Ok(metadata)) => {
let is_file = metadata.is_file();
// Actualizar la caché con información fresca
// Update cache with fresh information
if let Err(e) = self.metadata_cache.refresh_metadata(&abs_path).await {
tracing::warn!("Failed to update cache for {}: {}", abs_path.display(), e);
}
@@ -271,7 +271,7 @@ impl FileFsRepository {
/// Extracts file metadata from a physical path with timeout and cache
async fn get_file_metadata(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
// Intentar obtener de caché primero
// Try to get from cache first
if let Some(cached_metadata) = self.metadata_cache.get_metadata(abs_path).await {
if let (Some(size), Some(created_at), Some(modified_at)) =
(cached_metadata.size, cached_metadata.created_at, cached_metadata.modified_at) {
@@ -280,7 +280,7 @@ impl FileFsRepository {
}
}
// Si no está en caché o metadatos incompletos, cargar desde sistema de archivos
// If not in cache or incomplete metadata, load from filesystem
let metadata = match time::timeout(
self.config.timeouts.file_timeout(),
fs::metadata(&abs_path)
@@ -304,7 +304,7 @@ impl FileFsRepository {
.map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
.unwrap_or_else(|_| 0);
// Actualizar caché si es posible
// Update cache if possible
if let Err(e) = self.metadata_cache.refresh_metadata(abs_path).await {
tracing::warn!("Failed to update metadata cache for {}: {}", abs_path.display(), e);
}
@@ -340,7 +340,7 @@ impl FileFsRepository {
.map_err(|_| FileRepositoryError::Timeout(format!("Timeout checking file size: {}", abs_path.display())))?
.map_err(FileRepositoryError::IoError)?;
// Utiliza el método del ResourceConfig para determinar si es un archivo grande
// Use the ResourceConfig method to determine if it's a large file
Ok(self.config.resources.is_large_file(metadata.len()))
}
@@ -395,7 +395,7 @@ impl FileRepositoryError {
}
}
// Los errores ya están definidos por la interfaz FileRepositoryError
// Errors are already defined by the FileRepositoryError interface
// Enable cloning for concurrent operations
impl Clone for FileFsRepository {
@@ -5,19 +5,19 @@ use tracing::{debug, error, instrument};
use crate::domain::repositories::file_repository::FileRepositoryResult;
use crate::infrastructure::repositories::file_fs_repository::FileFsRepository;
// Este archivo contiene la implementación de los métodos relacionados con la papelera
// para el repositorio de archivos FileFsRepository
// This file contains the implementation of trash-related methods
// for the FileFsRepository file repository
// Implementación de métodos de papelera para el repositorio de archivos
// Implementation of trash methods for the file repository
impl FileFsRepository {
// Obtiene la ruta completa a la papelera
// Gets the complete path to the trash directory
fn get_trash_dir(&self) -> PathBuf {
let trash_dir = self.get_root_path().join(".trash").join("files");
debug!("Base trash directory: {}", trash_dir.display());
trash_dir
}
// Obtiene la ruta de la papelera para un usuario específico (si se proporciona)
// Gets the trash directory path for a specific user (if provided)
fn get_user_trash_dir(&self, user_id: Option<&str>) -> PathBuf {
let base_trash_dir = self.get_trash_dir();
@@ -33,7 +33,7 @@ impl FileFsRepository {
}
}
// Crea una ruta única en la papelera para el archivo
// Creates a unique path in the trash for the file
async fn create_trash_file_path(&self, file_id: &str) -> FileRepositoryResult<PathBuf> {
debug!("Creating trash file path for file ID: {}", file_id);
@@ -62,7 +62,7 @@ impl FileFsRepository {
}
}
// Implementación de los métodos públicos del trait FileRepository relacionados con la papelera
// Implementation of the public methods of the FileRepository trait related to trash
// Note: The FileRepository trait implementation has been moved to file_fs_repository.rs
// to avoid duplicate implementations
@@ -70,101 +70,101 @@ impl FileFsRepository {
impl FileFsRepository {
/// Helper method that will be used for trash functionality
pub(crate) async fn _trash_move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()> {
debug!("Moviendo archivo a la papelera: {}", file_id);
debug!("Moving file to trash: {}", file_id);
// Obtener la ruta física del archivo
// Creamos un método independiente para acceder al servicio de mapeo de IDs
debug!("Obteniendo ruta del archivo con ID: {}", file_id);
// Get the physical path of the file
// We create an independent method to access the ID mapping service
debug!("Getting file path with ID: {}", file_id);
let file_path = match self.id_mapping_service().get_file_path(file_id).await {
Ok(path) => {
debug!("Ruta del archivo obtenida: {}", path.display());
debug!("File path obtained: {}", path.display());
path
},
Err(e) => {
error!("Error obteniendo ruta del archivo {}: {:?}", file_id, e);
error!("Error getting file path {}: {:?}", file_id, e);
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
}
};
// Verificamos que el archivo existe
debug!("Verificando que el archivo existe: {}", file_path.display());
// Verify that the file exists
debug!("Verifying that the file exists: {}", file_path.display());
if !self.file_exists(&file_path).await? {
error!("Archivo no encontrado en la ruta especificada: {}", file_path.display());
error!("File not found at the specified path: {}", file_path.display());
return Err(FileRepositoryError::NotFound(format!("File not found: {}", file_id)));
}
debug!("Archivo encontrado, continuando con la operación");
debug!("File found, continuing with the operation");
// Crear directorio en la papelera si no existe
debug!("Creando path para archivo en papelera");
// Create directory in trash if it doesn't exist
debug!("Creating path for file in trash");
let trash_file_path = self.create_trash_file_path(file_id).await?;
debug!("Path en papelera: {}", trash_file_path.display());
debug!("Path in trash: {}", trash_file_path.display());
// Mover el archivo físicamente a la papelera (no actualiza mappings)
debug!("Moviendo archivo físicamente a papelera: {} -> {}", file_path.display(), trash_file_path.display());
// Physically move the file to trash (doesn't update mappings)
debug!("Physically moving file to trash: {} -> {}", file_path.display(), trash_file_path.display());
match fs::rename(&file_path, &trash_file_path).await {
Ok(_) => {
debug!("Archivo movido a papelera exitosamente: {} -> {}", file_path.display(), trash_file_path.display());
debug!("File successfully moved to trash: {} -> {}", file_path.display(), trash_file_path.display());
// Invalidar la caché del archivo original
debug!("Invalidando caché para: {}", file_path.display());
// Invalidate the cache for the original file
debug!("Invalidating cache for: {}", file_path.display());
self.metadata_cache().invalidate(&file_path).await;
// Actualizar el mapeo al nuevo path en la papelera
debug!("Actualizando mapeo de ID a nuevo path en papelera");
// Update the mapping to the new path in trash
debug!("Updating ID mapping to new path in trash");
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &trash_file_path).await {
error!("Error actualizando mapeo de archivo en papelera: {}", e);
error!("Error updating file mapping in trash: {}", e);
return Err(FileRepositoryError::MappingError(format!("Failed to update mapping: {}", e)));
}
debug!("Mapeo actualizado exitosamente");
debug!("Mapping successfully updated");
debug!("Operación de mover a papelera completada con éxito para el archivo: {}", file_id);
debug!("Move to trash operation completed successfully for file: {}", file_id);
Ok(())
},
Err(e) => {
error!("Error moviendo archivo a papelera: {} -> {}: {}",
error!("Error moving file to trash: {} -> {}: {}",
file_path.display(), trash_file_path.display(), e);
Err(FileRepositoryError::IoError(e))
}
}
}
/// Restaura un archivo desde la papelera a su ubicación original
/// Restores a file from trash to its original location
#[instrument(skip(self))]
pub(crate) async fn _trash_restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()> {
debug!("Restaurando archivo {} a {}", file_id, original_path);
debug!("Restoring file {} to {}", file_id, original_path);
// Try to get the current path from the ID mapping service
let current_path_result = self.id_mapping_service().get_file_path(file_id).await;
match current_path_result {
Ok(current_path) => {
debug!("Ruta actual en papelera: {}", current_path.display());
debug!("Current path in trash: {}", current_path.display());
// Check if the file exists in the trash
let file_exists = match fs::metadata(&current_path).await {
Ok(_) => {
debug!("Archivo existe en papelera");
debug!("File exists in trash");
true
},
Err(e) => {
debug!("Archivo no existe en papelera: {} - {}", current_path.display(), e);
debug!("File does not exist in trash: {} - {}", current_path.display(), e);
false
}
};
if !file_exists {
error!("El archivo no existe físicamente en la papelera: {}", current_path.display());
error!("The file does not physically exist in the trash: {}", current_path.display());
return Err(FileRepositoryError::NotFound(format!("File not found in trash: {}", file_id)));
}
// Parse the original path to a PathBuf
let original_path_buf = PathBuf::from(original_path);
debug!("Ruta original para restauración: {}", original_path_buf.display());
debug!("Original path for restoration: {}", original_path_buf.display());
// Check if a file already exists at the destination
let target_exists = fs::metadata(&original_path_buf).await.is_ok();
if target_exists {
debug!("Ya existe un archivo en la ruta de destino, generando ruta alternativa");
debug!("A file already exists at the destination path, generating alternative path");
// Generate a unique path by adding a suffix
// Extract filename and extension
@@ -187,16 +187,16 @@ impl FileFsRepository {
// Create the alternative path
let alternative_path = parent_dir.join(new_name);
debug!("Ruta alternativa para restauración: {}", alternative_path.display());
debug!("Alternative path for restoration: {}", alternative_path.display());
// Ensure the parent directory exists
if let Some(parent) = alternative_path.parent() {
if !parent.exists() {
debug!("Creando directorio padre para restauración: {}", parent.display());
debug!("Creating parent directory for restoration: {}", parent.display());
match fs::create_dir_all(parent).await {
Ok(_) => debug!("Directorio padre creado exitosamente"),
Ok(_) => debug!("Parent directory created successfully"),
Err(e) => {
error!("Error creando directorio padre: {} - {}", parent.display(), e);
error!("Error creating parent directory: {} - {}", parent.display(), e);
return Err(FileRepositoryError::IoError(e));
}
}
@@ -204,30 +204,30 @@ impl FileFsRepository {
}
// Move the file from trash to the alternative location
debug!("Moviendo archivo de papelera a ubicación alternativa: {} -> {}",
debug!("Moving file from trash to alternative location: {} -> {}",
current_path.display(), alternative_path.display());
match fs::rename(&current_path, &alternative_path).await {
Ok(_) => {
debug!("Archivo restaurado exitosamente a ubicación alternativa");
debug!("File successfully restored to alternative location");
// Invalidate cache entries
debug!("Invalidando caché para archivo en papelera");
debug!("Invalidating cache for file in trash");
self.metadata_cache().invalidate(&current_path).await;
// Update the ID mapping
debug!("Actualizando mapeo de ID a nueva ubicación");
debug!("Updating ID mapping to new location");
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &alternative_path).await {
error!("Error actualizando mapeo de archivo restaurado: {}", e);
error!("Error updating mapping of restored file: {}", e);
return Err(FileRepositoryError::MappingError(
format!("Failed to update mapping: {}", e)
));
}
debug!("Restauración a ubicación alternativa completada con éxito");
debug!("Restoration to alternative location completed successfully");
Ok(())
},
Err(e) => {
error!("Error restaurando archivo a ubicación alternativa: {}", e);
error!("Error restoring file to alternative location: {}", e);
Err(FileRepositoryError::IoError(e))
}
}
@@ -235,11 +235,11 @@ impl FileFsRepository {
// Ensure the parent directory exists
if let Some(parent) = original_path_buf.parent() {
if !parent.exists() {
debug!("Creando directorio padre para restauración: {}", parent.display());
debug!("Creating parent directory for restoration: {}", parent.display());
match fs::create_dir_all(parent).await {
Ok(_) => debug!("Directorio padre creado exitosamente"),
Ok(_) => debug!("Parent directory created successfully"),
Err(e) => {
error!("Error creando directorio padre: {} - {}", parent.display(), e);
error!("Error creating parent directory: {} - {}", parent.display(), e);
return Err(FileRepositoryError::IoError(e));
}
}
@@ -247,41 +247,41 @@ impl FileFsRepository {
}
// Move the file from trash to its original location
debug!("Moviendo archivo de papelera a ubicación original: {} -> {}",
debug!("Moving file from trash to original location: {} -> {}",
current_path.display(), original_path_buf.display());
match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => {
debug!("Archivo restaurado exitosamente a ubicación original");
debug!("File successfully restored to original location");
// Invalidate cache entries
debug!("Invalidando caché para archivo en papelera");
debug!("Invalidating cache for file in trash");
self.metadata_cache().invalidate(&current_path).await;
// Update the ID mapping
debug!("Actualizando mapeo de ID a ubicación original");
debug!("Updating ID mapping to original location");
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &original_path_buf).await {
error!("Error actualizando mapeo de archivo restaurado: {}", e);
error!("Error updating mapping of restored file: {}", e);
return Err(FileRepositoryError::MappingError(
format!("Failed to update mapping: {}", e)
));
}
debug!("Restauración a ubicación original completada con éxito");
debug!("Restoration to original location completed successfully");
Ok(())
},
Err(e) => {
error!("Error restaurando archivo a ubicación original: {}", e);
error!("Error restoring file to original location: {}", e);
Err(FileRepositoryError::IoError(e))
}
}
}
},
Err(e) => {
error!("Error obteniendo ruta actual del archivo {}: {:?}", file_id, e);
error!("Error getting current path of file {}: {:?}", file_id, e);
// Check if the error is because the ID was not found
if format!("{}", e).contains("not found") {
debug!("ID no encontrado en mapeo, archivo ya no existe en papelera");
debug!("ID not found in mapping, file no longer exists in trash");
return Err(FileRepositoryError::NotFound(format!("File not found in trash: {}", file_id)));
}
@@ -292,48 +292,48 @@ impl FileFsRepository {
}
}
/// Elimina un archivo permanentemente (usado por la papelera)
/// Permanently deletes a file (used by trash)
#[instrument(skip(self))]
pub(crate) async fn _trash_delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()> {
debug!("Eliminando archivo permanentemente: {}", file_id);
debug!("Permanently deleting file: {}", file_id);
// Get the file path using the ID mapping service
let file_path_result = self.id_mapping_service().get_file_path(file_id).await;
match file_path_result {
Ok(file_path) => {
debug!("Encontrada ruta para archivo: {} -> {}", file_id, file_path.display());
debug!("Found path for file: {} -> {}", file_id, file_path.display());
// Check if the file physically exists before attempting to delete
let file_exists = fs::metadata(&file_path).await.is_ok();
if file_exists {
debug!("Archivo existe físicamente, eliminando: {}", file_path.display());
debug!("File exists physically, deleting: {}", file_path.display());
// Delete the file physically
if let Err(e) = fs::remove_file(&file_path).await {
error!("Error eliminando archivo permanentemente: {} - {}", file_path.display(), e);
error!("Error permanently deleting file: {} - {}", file_path.display(), e);
// Don't report error if the file already doesn't exist
if e.kind() != std::io::ErrorKind::NotFound {
return Err(FileRepositoryError::IoError(e));
}
} else {
debug!("Archivo eliminado físicamente con éxito");
debug!("File physically deleted successfully");
}
// Invalidate cache for this file
debug!("Invalidando caché para el archivo: {}", file_path.display());
debug!("Invalidating cache for file: {}", file_path.display());
self.metadata_cache().invalidate(&file_path).await;
} else {
debug!("Archivo no existe físicamente, solo limpiando mapeos: {}", file_path.display());
debug!("File does not exist physically, only cleaning mappings: {}", file_path.display());
}
// Always remove the ID mapping regardless of whether the file exists
debug!("Eliminando mapeo de ID: {}", file_id);
debug!("Removing ID mapping: {}", file_id);
match self.id_mapping_service().remove_id(file_id).await {
Ok(_) => debug!("Mapeo de ID eliminado con éxito"),
Ok(_) => debug!("ID mapping successfully removed"),
Err(e) => {
error!("Error eliminando mapeo del archivo: {}", e);
error!("Error removing file mapping: {}", e);
// Only return error for critical mapping errors, otherwise continue
if format!("{}", e).contains("not found") {
debug!("ID mapping not found, ignoring this error for deletion");
@@ -343,16 +343,16 @@ impl FileFsRepository {
}
};
debug!("Archivo eliminado permanentemente con éxito: {}", file_id);
debug!("File permanently deleted successfully: {}", file_id);
Ok(())
},
Err(e) => {
// This could happen if the file is already deleted or wasn't properly indexed
error!("Error obteniendo ruta del archivo {}: {:?}", file_id, e);
error!("Error getting file path {}: {:?}", file_id, e);
// Check if the error is because the ID was not found
if format!("{}", e).contains("not found") {
debug!("ID no encontrado en mapeo, considerando borrado exitoso: {}", file_id);
debug!("ID not found in mapping, considering deletion successful: {}", file_id);
// In this case, we consider the file already deleted
return Ok(());
}
@@ -363,5 +363,5 @@ impl FileFsRepository {
}
}
// Re-exportaciones necesarias para el compilador
// Re-exports needed for the compiler
use crate::domain::repositories::file_repository::FileRepositoryError;
@@ -17,7 +17,7 @@ use crate::application::services::storage_mediator::StorageMediator;
use crate::application::ports::outbound::FolderStoragePort;
use crate::common::errors::DomainError;
// Para poder usar streams en la función list_folders
// To be able to use streams in the list_folders function
use tokio_stream;
/// Filesystem implementation of the FolderRepository interface
@@ -79,7 +79,7 @@ impl FolderFsRepository {
async fn count_directory_items(&self, directory_path: &Path) -> FolderRepositoryResult<usize> {
use tokio::fs::read_dir;
// Timeout para evitar bloqueos
// Timeout to avoid blocking
let read_dir_timeout = Duration::from_secs(30);
let read_dir_result = timeout(
read_dir_timeout,
@@ -91,7 +91,7 @@ impl FolderFsRepository {
let mut entries = result.map_err(FolderRepositoryError::IoError)?;
let mut count = 0;
// Contar entradas manualmente
// Count entries manually
while let Ok(Some(_)) = entries.next_entry().await {
count += 1;
}
@@ -261,10 +261,10 @@ impl From<FolderRepositoryError> for DomainError {
}
}
// Implementar Clone para poder usar en procesamiento concurrente
// Implement Clone to use in concurrent processing
impl Clone for FolderFsRepository {
fn clone(&self) -> Self {
// Clonamos los Arc, lo que solo incrementa el contador de referencias
// Clone the Arcs, which only increments the reference counter
Self {
root_path: self.root_path.clone(),
storage_mediator: self.storage_mediator.clone(),
@@ -13,18 +13,18 @@ use crate::common::config::AppConfig;
use crate::domain::repositories::file_repository::FileRepositoryError;
use crate::infrastructure::services::buffer_pool::BufferPool;
/// Estructura para el rango de bytes a procesar
/// Structure for the byte range to process
#[derive(Debug, Clone, Copy)]
pub struct ChunkRange {
/// Índice del chunk
/// Chunk index
pub index: usize,
/// Posición de inicio en bytes
/// Start position in bytes
pub start: u64,
/// Tamaño del chunk en bytes
/// Chunk size in bytes
pub size: usize,
}
/// Buffer pooling específico para BytesMut
/// Specific buffer pooling for BytesMut
pub struct BytesBufferPool {
buffers: Mutex<Vec<BytesMut>>,
buffer_size: usize,
@@ -40,53 +40,53 @@ impl BytesBufferPool {
}
}
/// Obtener un buffer del pool o crear uno nuevo
/// Get a buffer from the pool or create a new one
pub async fn get_buffer(&self) -> BytesMut {
let mut buffers = self.buffers.lock().await;
if let Some(mut buffer) = buffers.pop() {
// Reutilizar buffer existente
buffer.clear(); // Mantener capacidad, limpiar contenido
// Reuse existing buffer
buffer.clear(); // Keep capacity, clear content
buffer
} else {
// Crear nuevo buffer si el pool está vacío
// Create new buffer if the pool is empty
BytesMut::with_capacity(self.buffer_size)
}
}
/// Devolver un buffer al pool para reutilización
/// Return a buffer to the pool for reuse
pub async fn return_buffer(&self, mut buffer: BytesMut) {
// Restablece el buffer para reutilización
// Reset the buffer for reuse
buffer.clear();
let mut buffers = self.buffers.lock().await;
// Solo mantener hasta max_buffers
// Only keep up to max_buffers
if buffers.len() < self.max_buffers {
buffers.push(buffer);
}
// Si ya tenemos suficientes buffers, este se descartará
// If we already have enough buffers, this one will be discarded
}
}
/// Procesador paralelo de archivos para operaciones IO intensivas
/// Parallel file processor for IO-intensive operations
pub struct ParallelFileProcessor {
/// Configuración de la aplicación
/// Application configuration
config: AppConfig,
/// Semáforo para limitar concurrencia global
/// Semaphore to limit global concurrency
concurrency_limiter: Arc<Semaphore>,
/// Pool de buffers para optimizar memoria
/// Buffer pool to optimize memory
buffer_pool: Option<Arc<BufferPool>>,
/// Pool de buffers BytesMut para operaciones zero-copy
/// BytesMut buffer pool for zero-copy operations
bytes_pool: Arc<BytesBufferPool>,
}
impl ParallelFileProcessor {
/// Crea una nueva instancia del procesador
/// Creates a new processor instance
pub fn new(config: AppConfig) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Crear pool de BytesMut para operaciones eficientes
// Create BytesMut pool for efficient operations
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
@@ -99,11 +99,11 @@ impl ParallelFileProcessor {
}
}
/// Crea una nueva instancia del procesador con un pool de buffers
/// Creates a new processor instance with a buffer pool
pub fn new_with_buffer_pool(config: AppConfig, buffer_pool: Arc<BufferPool>) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Crear pool de BytesMut para operaciones eficientes
// Create BytesMut pool for efficient operations
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
@@ -116,15 +116,15 @@ impl ParallelFileProcessor {
}
}
/// Divide un archivo en chunks para procesamiento paralelo
/// Divides a file into chunks for parallel processing
pub fn calculate_chunks(&self, file_size: u64) -> Vec<ChunkRange> {
// Determinar si el archivo necesita procesamiento paralelo
// Determine if the file needs parallel processing
let needs_parallel = self.config.resources.needs_parallel_processing(
file_size, &self.config.concurrency
);
if !needs_parallel {
// Para archivos pequeños, usar un solo chunk
// For small files, use a single chunk
return vec![ChunkRange {
index: 0,
start: 0,
@@ -132,21 +132,21 @@ impl ParallelFileProcessor {
}];
}
// Calcular número óptimo de chunks
// Calculate optimal number of chunks
let chunk_count = self.config.resources.calculate_optimal_chunks(
file_size, &self.config.concurrency
);
// Calcular tamaño de cada chunk
// Calculate size of each chunk
let chunk_size = self.config.resources.calculate_chunk_size(file_size, chunk_count);
// Crear los rangos de chunks
// Create chunk ranges
let mut chunks = Vec::with_capacity(chunk_count);
let mut start = 0;
for i in 0..chunk_count {
let current_chunk_size = if i == chunk_count - 1 {
// Último chunk puede ser más pequeño
// Last chunk might be smaller
(file_size - start) as usize
} else {
chunk_size
@@ -167,16 +167,16 @@ impl ParallelFileProcessor {
chunks
}
/// Lee un archivo en paralelo y devuelve el contenido completo
/// Implementación optimizada usando BytesMut para reducir copias de memoria
/// Reads a file in parallel and returns the complete content
/// Optimized implementation using BytesMut to reduce memory copies
pub async fn read_file_parallel(&self, file_path: &PathBuf) -> Result<Vec<u8>, FileRepositoryError> {
// Obtener tamaño del archivo
// Get file size
let metadata = tokio::fs::metadata(file_path).await
.map_err(FileRepositoryError::IoError)?;
let file_size = metadata.len();
// Verificar si el archivo es demasiado grande para memoria
// Check if the file is too large for memory
if !self.config.resources.can_load_in_memory(file_size) {
return Err(FileRepositoryError::Other(
format!("File too large to load in memory: {} MB (max: {} MB)",
@@ -185,19 +185,19 @@ impl ParallelFileProcessor {
));
}
// Calcular chunks
// Calculate chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// Para un solo chunk, usar lectura simple con buffer pool si está disponible
// For a single chunk, use simple reading with buffer pool if available
info!("Reading file with size {}MB as a single chunk", file_size / (1024 * 1024));
if let Some(pool) = &self.buffer_pool {
// Usar buffer del pool para lectura eficiente
// Use buffer from the pool for efficient reading
debug!("Using buffer pool for single chunk read");
let mut buffer = pool.get_buffer().await;
// Si el buffer es demasiado pequeño, revertir a la implementación estándar
// If the buffer is too small, revert to standard implementation
if buffer.capacity() < file_size as usize {
debug!("Buffer from pool too small ({}), using standard read", buffer.capacity());
let content = tokio::fs::read(file_path).await
@@ -206,7 +206,7 @@ impl ParallelFileProcessor {
return Ok(content);
}
// Usar el buffer de memoria del pool
// Use memory buffer from the pool
let mut file = File::open(file_path).await
.map_err(FileRepositoryError::IoError)?;
@@ -215,11 +215,11 @@ impl ParallelFileProcessor {
buffer.set_used(read_size);
// Convertir en Vec<u8>
// Convert to Vec<u8>
let content = buffer.into_vec();
return Ok(content);
} else {
// Implementación estándar sin pool
// Standard implementation without pool
let content = tokio::fs::read(file_path).await
.map_err(FileRepositoryError::IoError)?;
@@ -227,51 +227,51 @@ impl ParallelFileProcessor {
}
}
// Para múltiples chunks, usar lectura paralela
// For multiple chunks, use parallel reading
info!("Reading file with size {}MB in {} parallel chunks using BytesMut",
file_size / (1024 * 1024), chunks.len());
// Crear buffer de resultado final (pre-allocated)
// Create final result buffer (pre-allocated)
let mut result = BytesMut::with_capacity(file_size as usize);
result.resize(file_size as usize, 0);
let result_mutex = Arc::new(Mutex::new(result));
// Crear tareas para cada chunk
// Create tasks for each chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Abrir archivo una sola vez y compartirlo
// Open file once and share it
let file = Arc::new(File::open(file_path).await
.map_err(FileRepositoryError::IoError)?);
// Referencia al pool de BytesMut
// Reference to BytesMut pool
let bytes_pool = self.bytes_pool.clone();
// Procesar chunks en paralelo
// Process chunks in parallel
for chunk in chunks {
let file_clone = file.clone();
let result_clone = result_mutex.clone();
let semaphore_clone = self.concurrency_limiter.clone();
let bytes_pool_clone = bytes_pool.clone();
// Spawn task para este chunk - no hay necesidad de copiar los datos originales
// Spawn task for this chunk - no need to copy the original data
let task = task::spawn(async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let _permit = semaphore_clone.acquire().await.unwrap();
// Obtener un buffer reusable del pool de BytesMut
// Get a reusable buffer from the BytesMut pool
let mut chunk_buffer = bytes_pool_clone.get_buffer().await;
// Asegurar que tenga suficiente capacidad
// Ensure it has sufficient capacity
if chunk_buffer.capacity() < chunk.size {
chunk_buffer = BytesMut::with_capacity(chunk.size);
}
// Resize al tamaño exacto necesario
// Resize to the exact size needed
chunk_buffer.resize(chunk.size, 0);
// Crear un descriptor de archivo duplicado para uso independiente
// Create a duplicate file descriptor for independent use
let mut file_handle = file_clone.try_clone().await?;
// Posicionar y leer directamente en el BytesMut
// Position and read directly into the BytesMut
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
let bytes_read = file_handle.read_exact(&mut chunk_buffer[..chunk.size]).await?;
@@ -282,18 +282,18 @@ impl ParallelFileProcessor {
));
}
// Escribir en resultado final
// Write to final result
let mut result_lock = result_clone.lock().await;
let start_pos = chunk.start as usize;
let end_pos = start_pos + chunk.size;
// Usar copy_from_slice para copiar desde BytesMut al buffer de resultado
// Use copy_from_slice to copy from BytesMut to result buffer
result_lock[start_pos..end_pos].copy_from_slice(&chunk_buffer[..chunk.size]);
// Devolver el buffer al pool para su reutilización
// Return the buffer to the pool for reuse
bytes_pool_clone.return_buffer(chunk_buffer).await;
// Registrar progreso
// Log progress
debug!("Chunk {} processed: {} bytes from offset {}",
chunk.index, chunk.size, chunk.start);
@@ -303,10 +303,10 @@ impl ParallelFileProcessor {
tasks.push(task);
}
// Esperar a que todas las tareas terminen
// Wait for all tasks to complete
let results = join_all(tasks).await;
// Verificar errores
// Check for errors
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
@@ -321,7 +321,7 @@ impl ParallelFileProcessor {
}
}
// Obtener el resultado final y convertir a Vec<u8>
// Get the final result and convert to Vec<u8>
let result_buffer = result_mutex.lock().await;
let result_vec = result_buffer.to_vec();
@@ -329,8 +329,8 @@ impl ParallelFileProcessor {
Ok(result_vec)
}
/// Escribe un archivo en paralelo desde un buffer
/// Implementación optimizada usando BytesMut/Bytes para reducir copias de memoria
/// Writes a file in parallel from a buffer
/// Optimized implementation using BytesMut/Bytes to reduce memory copies
pub async fn write_file_parallel(
&self,
file_path: &PathBuf,
@@ -338,56 +338,56 @@ impl ParallelFileProcessor {
) -> Result<(), FileRepositoryError> {
let file_size = content.len() as u64;
// Calcular chunks
// Calculate chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// Para un solo chunk, usar escritura simple
// For a single chunk, use simple writing
info!("Writing file with size {}MB as a single chunk", file_size / (1024 * 1024));
// Implementación estándar (el buffer pooling no ofrece ventajas para escritura simple)
// Standard implementation (buffer pooling offers no advantages for simple writing)
tokio::fs::write(file_path, content).await
.map_err(FileRepositoryError::IoError)?;
return Ok(());
}
// Para múltiples chunks, usar escritura paralela
// For multiple chunks, use parallel writing
info!("Writing file with size {}MB in {} parallel chunks using Bytes",
file_size / (1024 * 1024), chunks.len());
// Crear archivo (no usamos Mutex para reducir contención)
// Create file (we don't use Mutex to reduce contention)
let file = File::create(file_path).await
.map_err(FileRepositoryError::IoError)?;
// Convertir contenido a Bytes (un solo paso de copia)
// Convert content to Bytes (single copy step)
let content_bytes = Bytes::copy_from_slice(content);
// Crear tareas para cada chunk
// Create tasks for each chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Procesar chunks en paralelo
// Process chunks in parallel
for chunk in chunks {
let file_clone = file.try_clone().await
.map_err(FileRepositoryError::IoError)?;
let semaphore_clone = self.concurrency_limiter.clone();
// Crear slice de Bytes (no copia datos, solo referencia)
// Create Bytes slice (doesn't copy data, only references)
let start_idx = chunk.start as usize;
let end_idx = start_idx + chunk.size;
let chunk_data = content_bytes.slice(start_idx..end_idx);
// Crear y lanzar tarea
// Create and launch task
let task = task::spawn(async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let _permit = semaphore_clone.acquire().await.unwrap();
// Posicionar y escribir
// Position and write
let mut file_handle = file_clone;
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
file_handle.write_all(&chunk_data).await?;
// Registrar progreso
// Log progress
debug!("Chunk {} written: {} bytes at offset {}",
chunk.index, chunk.size, chunk.start);
@@ -397,10 +397,10 @@ impl ParallelFileProcessor {
tasks.push(task);
}
// Esperar a que todas las tareas terminen
// Wait for all tasks to complete
let results = join_all(tasks).await;
// Verificar errores
// Check for errors
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
@@ -415,7 +415,7 @@ impl ParallelFileProcessor {
}
}
// Garantizar que todo se ha escrito correctamente
// Ensure everything has been written correctly
let mut file_handle = file;
file_handle.flush().await.map_err(FileRepositoryError::IoError)?;
@@ -423,17 +423,17 @@ impl ParallelFileProcessor {
Ok(())
}
/// Escribe un chunk en un archivo en una posición específica
/// Writes a chunk to a file at a specific position
#[allow(dead_code)]
async fn write_chunk_optimized(
file: &mut File,
offset: u64,
data: Bytes
) -> Result<(), std::io::Error> {
// Preparar la escritura en la posición correcta
// Prepare writing at the correct position
file.seek(SeekFrom::Start(offset)).await?;
// Escribir datos sin copias adicionales
// Write data without additional copies
file.write_all(&data).await?;
Ok(())
@@ -447,59 +447,59 @@ mod tests {
#[tokio::test]
async fn test_parallel_read_write() {
// Crear configuración con umbral bajo para testing
// Create configuration with low threshold for testing
let mut config = AppConfig::default();
config.concurrency.min_size_for_parallel_chunks_mb = 1; // 1MB para testing
config.concurrency.min_size_for_parallel_chunks_mb = 1; // 1MB for testing
config.concurrency.max_parallel_chunks = 4;
let processor = ParallelFileProcessor::new(config);
// Crear directorio temporal
// Create temporary directory
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.bin");
// Crear datos de prueba (2MB)
// Create test data (2MB)
let size = 2 * 1024 * 1024;
let mut test_data = Vec::with_capacity(size);
for i in 0..size {
test_data.push((i % 256) as u8);
}
// Escribir archivo en paralelo
// Write file in parallel
processor.write_file_parallel(&file_path, &test_data).await.unwrap();
// Leer archivo en paralelo
// Read file in parallel
let read_data = processor.read_file_parallel(&file_path).await.unwrap();
// Verificar que los datos son idénticos
// Verify that the data is identical
assert_eq!(test_data.len(), read_data.len());
assert_eq!(test_data, read_data);
}
#[tokio::test]
async fn test_bytesmut_pool() {
// Crear pool
// Create pool
let pool = BytesBufferPool::new(1024, 5);
// Obtener buffer
// Get buffer
let mut buffer1 = pool.get_buffer().await;
buffer1.put_slice(b"test data");
assert_eq!(&buffer1[..9], b"test data");
// Devolver buffer al pool
// Return buffer to the pool
pool.return_buffer(buffer1).await;
// Obtener otro buffer (debería ser el mismo)
// Get another buffer (should be the same one)
let buffer2 = pool.get_buffer().await;
assert_eq!(buffer2.capacity(), 1024);
// El buffer debería estar vacío (clear)
// The buffer should be empty (cleared)
assert_eq!(buffer2.len(), 0);
}
#[test]
fn test_chunk_calculation() {
// Crear configuración de prueba
// Create test configuration
let mut config = AppConfig::default();
config.concurrency.min_size_for_parallel_chunks_mb = 100; // 100MB
config.concurrency.max_parallel_chunks = 4;
@@ -507,18 +507,18 @@ mod tests {
let processor = ParallelFileProcessor::new(config);
// Archivo pequeño (10MB)
// Small file (10MB)
let small_file_size = 10 * 1024 * 1024;
let chunks = processor.calculate_chunks(small_file_size);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].size as u64, small_file_size);
// Archivo grande (300MB)
// Large file (300MB)
let large_file_size = 300 * 1024 * 1024;
let chunks = processor.calculate_chunks(large_file_size);
assert_eq!(chunks.len(), 4); // Limitado a max_parallel_chunks
assert_eq!(chunks.len(), 4); // Limited to max_parallel_chunks
// Verificar que todos los chunks suman el tamaño total
// Verify that all chunks add up to the total size
let total_size: u64 = chunks.iter().map(|c| c.size as u64).sum();
assert_eq!(total_size, large_file_size);
}
@@ -10,60 +10,60 @@ use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMa
use crate::common::errors::DomainError;
use crate::application::ports::outbound::IdMappingPort;
/// Tamaño máximo de entradas en el caché
/// Maximum number of entries in the cache
const MAX_CACHE_SIZE: usize = 10_000;
/// Tiempo de vida del caché (en segundos)
const CACHE_TTL_SECONDS: u64 = 60 * 5; // 5 minutos
/// Cache time-to-live (in seconds)
const CACHE_TTL_SECONDS: u64 = 60 * 5; // 5 minutes
/// Optimizador para operaciones masivas de mapeo de IDs
/// Optimizer for batch ID mapping operations
pub struct IdMappingOptimizer {
/// Servicio base de mapeo de IDs
/// Base ID mapping service
base_service: Arc<IdMappingService>,
/// Caché de ID por ruta (path -> id)
/// Path to ID cache (path -> id)
path_to_id_cache: RwLock<HashMap<String, (String, Instant)>>,
/// Caché de ruta por ID (id -> path)
/// ID to path cache (id -> path)
id_to_path_cache: RwLock<HashMap<String, (String, Instant)>>,
/// Contador de hits
/// Hit counter
stats: RwLock<OptimizerStats>,
/// Semáforo para limitar operaciones de batch
/// Semaphore to limit batch operations
batch_limiter: Semaphore,
/// Cola de batch pendientes
/// Pending batch queue
pending_batch: Mutex<BatchQueue>,
}
/// Estadísticas del optimizador
/// Optimizer statistics
#[derive(Debug, Default, Clone)]
pub struct OptimizerStats {
/// Número total de consultas get_path_by_id
/// Total number of get_path_by_id queries
pub path_by_id_queries: usize,
/// Número de hits en caché get_path_by_id
/// Number of cache hits for get_path_by_id
pub path_by_id_hits: usize,
/// Número total de consultas get_or_create_id
/// Total number of get_or_create_id queries
pub get_id_queries: usize,
/// Número de hits en caché get_or_create_id
/// Number of cache hits for get_or_create_id
pub get_id_hits: usize,
/// Número de batch realizados
/// Number of batch operations performed
pub batch_operations: usize,
/// Número total de IDs procesados en batch
/// Total number of IDs processed in batch
pub batch_items_processed: usize,
/// Último momento de limpieza de caché
/// Last cache cleanup timestamp
pub last_cleanup: Option<Instant>,
}
/// Cola para operaciones batch
/// Queue for batch operations
struct BatchQueue {
/// Rutas pendientes para obtener/crear ID
/// Pending paths to get/create ID
path_to_id_requests: HashSet<String>,
/// IDs pendientes para obtener ruta
/// Pending IDs to get path
id_to_path_requests: HashSet<String>,
}
@@ -76,43 +76,43 @@ impl Default for BatchQueue {
}
}
/// Resultado de una operación batch
/// Result of a batch operation
struct BatchResult {
/// Mapeo de ruta a ID
/// Path to ID mapping
path_to_id: HashMap<String, String>,
/// Mapeo de ID a ruta
/// ID to path mapping
id_to_path: HashMap<String, String>,
}
impl IdMappingOptimizer {
/// Crea un nuevo optimizador para el servicio de mapeo de IDs
/// Creates a new optimizer for the ID mapping service
pub fn new(base_service: Arc<IdMappingService>) -> Self {
Self {
base_service,
path_to_id_cache: RwLock::new(HashMap::with_capacity(1000)),
id_to_path_cache: RwLock::new(HashMap::with_capacity(1000)),
stats: RwLock::new(OptimizerStats::default()),
batch_limiter: Semaphore::new(2), // Limitar a 2 operaciones batch concurrentes
batch_limiter: Semaphore::new(2), // Limit to 2 concurrent batch operations
pending_batch: Mutex::new(BatchQueue::default()),
}
}
/// Obtiene estadísticas del optimizador
/// Gets optimizer statistics
pub async fn get_stats(&self) -> OptimizerStats {
self.stats.read().await.clone()
}
/// Limpia entradas expiradas del caché
/// Cleans expired cache entries
pub async fn cleanup_cache(&self) {
let now = Instant::now();
let ttl = Duration::from_secs(CACHE_TTL_SECONDS);
// Limpiar caché path_to_id
// Clean path_to_id cache
{
let mut cache = self.path_to_id_cache.write().await;
let initial_size = cache.len();
// Retener solo entradas no expiradas
// Retain only non-expired entries
cache.retain(|_, (_, timestamp)| {
now.duration_since(*timestamp) < ttl
});
@@ -123,12 +123,12 @@ impl IdMappingOptimizer {
}
}
// Limpiar caché id_to_path
// Clean id_to_path cache
{
let mut cache = self.id_to_path_cache.write().await;
let initial_size = cache.len();
// Retener solo entradas no expiradas
// Retain only non-expired entries
cache.retain(|_, (_, timestamp)| {
now.duration_since(*timestamp) < ttl
});
@@ -139,14 +139,14 @@ impl IdMappingOptimizer {
}
}
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.last_cleanup = Some(now);
}
}
/// Inicia tarea de limpieza periódica
/// Starts periodic cleanup task
pub fn start_cleanup_task(optimizer: Arc<Self>) {
tokio::spawn(async move {
let cleanup_interval = Duration::from_secs(CACHE_TTL_SECONDS / 2);
@@ -155,7 +155,7 @@ impl IdMappingOptimizer {
tokio::time::sleep(cleanup_interval).await;
optimizer.cleanup_cache().await;
// Loguear estadísticas periódicamente
// Log statistics periodically
let stats = optimizer.get_stats().await;
info!("ID Mapping Optimizer stats - Path queries: {}, hits: {} ({}%), ID queries: {}, hits: {} ({}%), Batch ops: {}, items: {}",
stats.path_by_id_queries,
@@ -171,11 +171,11 @@ impl IdMappingOptimizer {
});
}
/// Agrega una solicitud a la cola pendiente para procesamiento batch
/// Adds a request to the pending queue for batch processing
async fn queue_path_to_id_request(&self, path: &StoragePath) -> Result<Option<String>, IdMappingError> {
let path_str = path.to_string();
// Verificar primero en el caché
// Check first in the cache
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
@@ -204,7 +204,7 @@ impl IdMappingOptimizer {
// Adquirir permiso para operación batch
let _permit = self.batch_limiter.acquire().await.unwrap();
// Obtener las solicitudes pendientes
// Get pending requests
let (path_requests, id_requests) = {
let mut batch_queue = self.pending_batch.lock().await;
@@ -250,7 +250,7 @@ impl IdMappingOptimizer {
}
}
// Actualizar caché con los resultados del batch
// Update cache with batch results
{
let mut path_cache = self.path_to_id_cache.write().await;
let mut id_cache = self.id_to_path_cache.write().await;
@@ -397,7 +397,7 @@ impl IdMappingPort for IdMappingOptimizer {
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.get_id_hits += 1;
@@ -407,7 +407,7 @@ impl IdMappingPort for IdMappingOptimizer {
}
}
// Si no está en caché, intentar agregar a cola de batch primero
// If not in cache, try adding to batch queue first
let queued_result = self.queue_path_to_id_request(path).await?;
if let Some(id) = queued_result {
return Ok(id);
@@ -416,17 +416,17 @@ impl IdMappingPort for IdMappingOptimizer {
// Trigger batch processing if enough items accumulated
self.trigger_batch_if_needed(20).await?;
// Intentar obtener del servicio base
// Try to get from the base service
let id = self.base_service.get_or_create_id(path).await?;
// Actualizar caché con el nuevo ID
// Update cache with the new ID
{
let mut path_cache = self.path_to_id_cache.write().await;
let mut id_cache = self.id_to_path_cache.write().await;
let now = Instant::now();
// Controlar tamaño del caché
// Control cache size
if path_cache.len() >= MAX_CACHE_SIZE {
warn!("Path-to-ID cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
path_cache.clear();
@@ -451,11 +451,11 @@ impl IdMappingPort for IdMappingOptimizer {
stats.path_by_id_queries += 1;
}
// Verificar primero en el caché
// Check first in the cache
{
let cache = self.id_to_path_cache.read().await;
if let Some((path_str, _)) = cache.get(id) {
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.path_by_id_hits += 1;
@@ -465,10 +465,10 @@ impl IdMappingPort for IdMappingOptimizer {
}
}
// Obtener del servicio base
// Get from the base service
let path = self.base_service.get_path_by_id(id).await?;
// Actualizar caché
// Update cache
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
@@ -476,7 +476,7 @@ impl IdMappingPort for IdMappingOptimizer {
let now = Instant::now();
let path_str = path.to_string();
// Controlar tamaño del caché
// Control cache size
if id_cache.len() >= MAX_CACHE_SIZE {
warn!("ID-to-path cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
id_cache.clear();
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::{
dtos::share_dto::{CreateShareDto, UpdateShareDto},
ports::share_ports::ShareUseCase
},
common::errors::{DomainError, ErrorKind},
common::errors::ErrorKind,
};
#[derive(Debug, Deserialize)]
+32 -32
View File
@@ -1,4 +1,4 @@
/* Reset y estilos base */
/* Reset and base styles */
* {
box-sizing: border-box;
margin: 0;
@@ -13,7 +13,7 @@ body {
overflow: hidden;
}
/* Barra lateral */
/* Sidebar */
.sidebar {
width: 250px;
background-color: #2a3042;
@@ -86,7 +86,7 @@ body {
text-align: center;
}
/* Indicador de almacenamiento */
/* Storage indicator */
.storage-container {
margin: 20px 15px;
background-color: #374e65;
@@ -120,7 +120,7 @@ body {
color: #f5f5f5;
}
/* Contenido principal */
/* Main content */
.main-content {
flex-grow: 1;
display: flex;
@@ -128,7 +128,7 @@ body {
overflow: hidden;
}
/* Barra superior */
/* Top bar */
.top-bar {
height: 70px;
background-color: white;
@@ -186,7 +186,7 @@ body {
background-color: #e64a29;
}
/* Estilos para resultados de búsqueda */
/* Styles for search results */
.search-results-header {
display: flex;
justify-content: space-between;
@@ -261,7 +261,7 @@ body {
font-weight: bold;
}
/* Área de contenido */
/* Content area */
.content-area {
flex-grow: 1;
padding: 20px;
@@ -358,11 +358,11 @@ body {
border: 2px dashed #ffc107;
}
/* Estilos para las carpetas como en el mockup */
/* Styles for folders as in the mockup */
.file-icon.folder-icon {
width: 100px;
height: 70px;
background-color: #ffeaa7; /* Color amarillo claro */
background-color: #ffeaa7; /* Light yellow color */
border-radius: 8px;
position: relative;
margin-bottom: 10px;
@@ -378,15 +378,15 @@ body {
left: 0;
right: 0;
height: 20px;
background-color: #fdcb6e; /* Color amarillo más oscuro para la pestaña */
background-color: #fdcb6e; /* Darker yellow color for the tab */
border-radius: 8px 8px 0 0;
}
.file-icon.folder-icon i {
display: none; /* Ocultar el icono Font Awesome */
display: none; /* Hide Font Awesome icon */
}
/* Estilo para documentos */
/* Style for documents */
.file-icon.doc-icon {
width: 100px;
height: 70px;
@@ -418,11 +418,11 @@ body {
border-radius: 2px;
}
/* Estilo para imágenes */
/* Style for images */
.file-icon.image-icon {
width: 100px;
height: 70px;
background-color: #74b9ff; /* Fondo azul claro */
background-color: #74b9ff; /* Light blue background */
border-radius: 4px;
position: relative;
margin-bottom: 10px;
@@ -436,15 +436,15 @@ body {
left: 15px;
width: 20px;
height: 20px;
background-color: #ffda79; /* Círculo amarillo como un sol */
background-color: #ffda79; /* Yellow circle like a sun */
border-radius: 50%;
}
/* Estilo para videos */
/* Style for videos */
.file-icon.video-icon {
width: 100px;
height: 70px;
background-color: #111; /* Fondo negro */
background-color: #111; /* Black background */
border-radius: 4px;
position: relative;
margin-bottom: 10px;
@@ -459,10 +459,10 @@ body {
height: 0;
border-top: 15px solid transparent;
border-bottom: 15px solid transparent;
border-left: 20px solid white; /* Triángulo de reproducción */
border-left: 20px solid white; /* Play triangle */
}
/* Estilos para archivos de código */
/* Styles for code files */
.file-icon.code-icon {
width: 100px;
height: 70px;
@@ -470,11 +470,11 @@ body {
border-radius: 4px;
position: relative;
margin-bottom: 10px;
border-top: 3px solid #556ee6; /* Borde superior azul */
border-top: 3px solid #556ee6; /* Blue top border */
overflow: hidden;
}
/* Líneas que simulan código */
/* Lines that simulate code */
.file-icon.code-icon::before,
.file-icon.code-icon::after {
content: "";
@@ -482,7 +482,7 @@ body {
left: 10px;
right: 10px;
height: 2px;
background-color: #556ee6; /* Color azul para el código */
background-color: #556ee6; /* Blue color for code */
}
.file-icon.code-icon::before {
@@ -495,14 +495,14 @@ body {
width: 60%;
}
/* Agregar líneas adicionales para simular código */
/* Add additional lines to simulate code */
.file-icon.code-icon .code-line-1,
.file-icon.code-icon .code-line-2,
.file-icon.code-icon .code-line-3 {
position: absolute;
left: 10px;
height: 2px;
background-color: #a0aec0; /* Color gris para el código */
background-color: #a0aec0; /* Gray color for code */
}
.file-icon.code-icon .code-line-1 {
@@ -520,9 +520,9 @@ body {
width: 50%;
}
/* Colores específicos para distintos tipos de archivo de código */
/* Specific colors for different code file types */
.file-icon.json-icon {
border-top-color: #ffb86c; /* Naranja para JSON */
border-top-color: #ffb86c; /* Orange for JSON */
}
.file-icon.json-icon::before,
@@ -531,7 +531,7 @@ body {
}
.file-icon.js-icon {
border-top-color: #ffd43b; /* Amarillo para JavaScript */
border-top-color: #ffd43b; /* Yellow for JavaScript */
}
.file-icon.js-icon::before,
@@ -540,7 +540,7 @@ body {
}
.file-icon.html-icon {
border-top-color: #e34c26; /* Rojo para HTML */
border-top-color: #e34c26; /* Red for HTML */
}
.file-icon.html-icon::before,
@@ -549,7 +549,7 @@ body {
}
.file-icon.css-icon {
border-top-color: #2965f1; /* Azul para CSS */
border-top-color: #2965f1; /* Blue for CSS */
}
.file-icon.css-icon::before,
@@ -558,7 +558,7 @@ body {
}
.file-icon.py-icon {
border-top-color: #3776ab; /* Azul oscuro para Python */
border-top-color: #3776ab; /* Dark blue for Python */
}
.file-icon.py-icon::before,
@@ -1570,7 +1570,7 @@ header {
color: #ffc107;
}
/* Estilos para la papelera */
/* Styles for trash */
.trash-item {
position: relative;
}
@@ -1634,7 +1634,7 @@ header {
grid-column: 1 / -1;
}
/* Botón de peligro para vaciar papelera */
/* Danger button for emptying trash */
.btn-danger {
background-color: #f44336;
color: white;