Merge pull request #44 from DioCrafts/feature/improve-postgresql
improve postgresql performance
This commit is contained in:
@@ -46,6 +46,12 @@ http-body-util = "0.1.3"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
test_utils = ["mockall"]
|
test_utils = ["mockall"]
|
||||||
|
migrations = ["sqlx/migrate"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "migrate"
|
||||||
|
path = "src/bin/migrate.rs"
|
||||||
|
required-features = ["migrations"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ echo "DATABASE_URL=postgres://username:password@localhost/oxicloud" > .env
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Run database migrations
|
# Run database migrations
|
||||||
cargo run --bin migrate
|
cargo run --bin migrate --features migrations
|
||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
cargo run --release
|
cargo run --release
|
||||||
|
|||||||
+9
-9
@@ -18,8 +18,8 @@ END $BODY$;
|
|||||||
-- Users table
|
-- Users table
|
||||||
CREATE TABLE IF NOT EXISTS auth.users (
|
CREATE TABLE IF NOT EXISTS auth.users (
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
username VARCHAR(32) UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role auth.userrole NOT NULL,
|
role auth.userrole NOT NULL,
|
||||||
storage_quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10GB default
|
storage_quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10GB default
|
||||||
@@ -38,9 +38,9 @@ CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email);
|
|||||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
refresh_token VARCHAR(255) NOT NULL UNIQUE,
|
refresh_token TEXT NOT NULL UNIQUE,
|
||||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
ip_address VARCHAR(45), -- to support IPv6
|
ip_address TEXT, -- to support IPv6
|
||||||
user_agent TEXT,
|
user_agent TEXT,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
revoked BOOLEAN NOT NULL DEFAULT FALSE
|
revoked BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
@@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS auth.user_files (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
file_path TEXT NOT NULL,
|
file_path TEXT NOT NULL,
|
||||||
file_id VARCHAR(255) NOT NULL,
|
file_id TEXT NOT NULL,
|
||||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -83,8 +83,8 @@ CREATE INDEX IF NOT EXISTS idx_user_files_file_id ON auth.user_files(file_id);
|
|||||||
CREATE TABLE IF NOT EXISTS auth.user_favorites (
|
CREATE TABLE IF NOT EXISTS auth.user_favorites (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
item_id VARCHAR(255) NOT NULL,
|
item_id TEXT NOT NULL,
|
||||||
item_type VARCHAR(10) NOT NULL, -- 'file' or 'folder'
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(user_id, item_id, item_type)
|
UNIQUE(user_id, item_id, item_type)
|
||||||
);
|
);
|
||||||
@@ -102,8 +102,8 @@ CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON auth.user_favorites(u
|
|||||||
CREATE TABLE IF NOT EXISTS auth.user_recent_files (
|
CREATE TABLE IF NOT EXISTS auth.user_recent_files (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
item_id VARCHAR(255) NOT NULL,
|
item_id TEXT NOT NULL,
|
||||||
item_type VARCHAR(10) NOT NULL, -- 'file' or 'folder'
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(user_id, item_id, item_type)
|
UNIQUE(user_id, item_id, item_type)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Sistema de Migraciones de Base de Datos
|
||||||
|
|
||||||
|
Este documento describe el sistema de migraciones de base de datos implementado en OxiCloud para gestionar cambios de esquema de forma controlada y segura.
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
OxiCloud utiliza un sistema de migraciones basado en archivos SQL versionados para garantizar que los cambios en la estructura de la base de datos sean:
|
||||||
|
|
||||||
|
- Versionados y rastreables
|
||||||
|
- Aplicados de forma consistente en todos los entornos
|
||||||
|
- Reproducibles y comprobables
|
||||||
|
- Independientes del código de la aplicación
|
||||||
|
|
||||||
|
## Estructura de Directorios
|
||||||
|
|
||||||
|
```
|
||||||
|
OxiCloud/
|
||||||
|
├── migrations/ # Directorio principal de migraciones
|
||||||
|
│ ├── 20250408000000_initial_schema.sql # Migración 1: Esquema inicial
|
||||||
|
│ ├── 20250408000001_default_users.sql # Migración 2: Usuarios por defecto
|
||||||
|
│ └── ... # Futuras migraciones
|
||||||
|
├── src/
|
||||||
|
├── bin/
|
||||||
|
│ └── migrate.rs # Herramienta CLI para ejecutar migraciones
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convenciones de Nomenclatura
|
||||||
|
|
||||||
|
Las migraciones siguen el formato: `YYYYMMDDHHMMSS_descripción_breve.sql`, donde:
|
||||||
|
|
||||||
|
- `YYYYMMDDHHMMSS`: Timestamp que garantiza el orden correcto (año, mes, día, hora, minuto, segundo)
|
||||||
|
- `descripción_breve`: Descripción concisa del propósito de la migración
|
||||||
|
- `.sql`: Extensión de archivo SQL
|
||||||
|
|
||||||
|
## Ejecución de Migraciones
|
||||||
|
|
||||||
|
Las migraciones se ejecutan mediante una herramienta CLI dedicada:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin migrate --features migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
Este comando:
|
||||||
|
1. Conecta con la base de datos configurada en el entorno
|
||||||
|
2. Busca migraciones en el directorio `/migrations/`
|
||||||
|
3. Compara las migraciones aplicadas con las disponibles
|
||||||
|
4. Ejecuta secuencialmente las migraciones pendientes
|
||||||
|
5. Registra las migraciones aplicadas en una tabla de control
|
||||||
|
|
||||||
|
## Creación de Nuevas Migraciones
|
||||||
|
|
||||||
|
Para crear una nueva migración:
|
||||||
|
|
||||||
|
1. Crea un nuevo archivo en el directorio `migrations/` siguiendo la convención de nomenclatura
|
||||||
|
2. Define los cambios SQL en el archivo
|
||||||
|
3. Asegúrate de que los cambios sean compatibles con la versión actual del esquema
|
||||||
|
4. Ejecuta las migraciones con el comando correspondiente
|
||||||
|
|
||||||
|
Ejemplo de estructura para una nueva migración:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Migración: Añadir tabla de etiquetas
|
||||||
|
-- Descripción: Crea la tabla para almacenar etiquetas de archivos y sus relaciones
|
||||||
|
|
||||||
|
-- Crear tabla de etiquetas
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.tags (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL DEFAULT '#3498db',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Crear índices
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON auth.tags(user_id);
|
||||||
|
|
||||||
|
-- Tabla de relación entre archivos y etiquetas
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.file_tags (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES auth.tags(id) ON DELETE CASCADE,
|
||||||
|
file_id TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(tag_id, file_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Comentarios de documentación
|
||||||
|
COMMENT ON TABLE auth.tags IS 'Almacena etiquetas definidas por usuarios';
|
||||||
|
COMMENT ON TABLE auth.file_tags IS 'Relación muchos-a-muchos entre archivos y etiquetas';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guía de Buenas Prácticas
|
||||||
|
|
||||||
|
1. **Migraciones Incrementales**: Cada migración debe representar un cambio atómico y coherente.
|
||||||
|
|
||||||
|
2. **Migraciones Idempotentes**: Cuando sea posible, usa comandos que pueden ejecutarse múltiples veces sin errores (ej. `CREATE TABLE IF NOT EXISTS`).
|
||||||
|
|
||||||
|
3. **Migraciones Forward-Only**: Diseña las migraciones para avanzar, no para revertir. Si necesitas deshacer un cambio, crea una nueva migración.
|
||||||
|
|
||||||
|
4. **Compatibilidad Hacia Adelante**: Las migraciones deben ser compatibles con el código existente y el que se va a desplegar.
|
||||||
|
|
||||||
|
5. **Prueba Antes de Desplegar**: Prueba las migraciones en un entorno similar al de producción antes de aplicarlas.
|
||||||
|
|
||||||
|
6. **Documentación**: Documenta el propósito y los cambios clave de cada migración con comentarios dentro del archivo SQL.
|
||||||
|
|
||||||
|
## Solución de Problemas
|
||||||
|
|
||||||
|
### Verificación del Estado de las Migraciones
|
||||||
|
|
||||||
|
Para verificar qué migraciones se han aplicado, OxiCloud incluye detección en tiempo de inicio:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Desde src/common/db.rs
|
||||||
|
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match migration_check {
|
||||||
|
Ok(row) => {
|
||||||
|
let tables_exist: bool = row.get(0);
|
||||||
|
if !tables_exist {
|
||||||
|
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problemas Comunes
|
||||||
|
|
||||||
|
1. **Error de conexión a la base de datos**: Verifica la URL de conexión en la variable de entorno `DATABASE_URL`.
|
||||||
|
|
||||||
|
2. **Conflictos de migración**: Si una migración falla, revisa los mensajes de error para identificar conflictos con el esquema existente.
|
||||||
|
|
||||||
|
3. **Permisos insuficientes**: Asegúrate de que el usuario de la base de datos tenga permisos suficientes para crear esquemas, tablas e índices.
|
||||||
|
|
||||||
|
## Beneficios del Enfoque Basado en Migraciones
|
||||||
|
|
||||||
|
- **Separación de Responsabilidades**: Las migraciones están separadas del código de la aplicación.
|
||||||
|
- **Automatización**: Facilita la automatización de despliegues y CI/CD.
|
||||||
|
- **Historial de Cambios**: Proporciona un historial claro de cómo ha evolucionado el esquema.
|
||||||
|
- **Colaboración**: Permite que múltiples desarrolladores contribuyan cambios al esquema de forma ordenada.
|
||||||
|
- **Entornos Múltiples**: Garantiza que todos los entornos (desarrollo, pruebas, producción) tengan estructuras de base de datos idénticas.
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# Base de Datos y Transacciones en OxiCloud
|
||||||
|
|
||||||
|
## Introducción a Transacciones Explícitas en la Base de Datos
|
||||||
|
|
||||||
|
Este documento describe la implementación de transacciones explícitas en OxiCloud para garantizar la integridad de los datos en operaciones de base de datos PostgreSQL.
|
||||||
|
|
||||||
|
## ¿Qué son las Transacciones?
|
||||||
|
|
||||||
|
Una transacción es una secuencia de operaciones de base de datos tratadas como una única unidad lógica. Las transacciones siguen las propiedades ACID:
|
||||||
|
|
||||||
|
- **Atomicidad**: Una transacción es "todo o nada". Si cualquier parte falla, toda la transacción falla.
|
||||||
|
- **Consistencia**: La base de datos pasa de un estado válido a otro estado válido.
|
||||||
|
- **Aislamiento**: Las transacciones simultáneas se comportan como si fueran secuenciales.
|
||||||
|
- **Durabilidad**: Una vez confirmada, la transacción permanece confirmada incluso en caso de fallo del sistema.
|
||||||
|
|
||||||
|
## Implementación en OxiCloud
|
||||||
|
|
||||||
|
OxiCloud ahora utiliza un enfoque consistente para las transacciones de base de datos mediante la función `with_transaction`, que:
|
||||||
|
|
||||||
|
1. Comienza una transacción
|
||||||
|
2. Ejecuta operaciones
|
||||||
|
3. Confirma automáticamente si todo fue exitoso
|
||||||
|
4. Revierte (rollback) automáticamente en caso de error
|
||||||
|
|
||||||
|
### Utilidad de Transacciones
|
||||||
|
|
||||||
|
En `src/infrastructure/repositories/pg/transaction_utils.rs` hemos implementado:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Helper function to execute database operations in a transaction
|
||||||
|
pub async fn with_transaction<F, T, E>(
|
||||||
|
pool: &Arc<PgPool>,
|
||||||
|
operation_name: &str,
|
||||||
|
operation: F,
|
||||||
|
) -> Result<T, E>
|
||||||
|
where
|
||||||
|
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||||
|
E: From<SqlxError> + std::fmt::Display
|
||||||
|
{ ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Esta función:
|
||||||
|
- Recibe un pool de conexiones y un closure con operaciones
|
||||||
|
- Maneja begin/commit/rollback automáticamente
|
||||||
|
- Proporciona logging detallado del ciclo de vida de la transacción
|
||||||
|
|
||||||
|
### Ejemplo de Uso en Repositorios
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Creación de un usuario con transacción explícita
|
||||||
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||||
|
with_transaction(
|
||||||
|
&self.pool,
|
||||||
|
"create_user",
|
||||||
|
|tx| {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Operación principal - insertar usuario
|
||||||
|
sqlx::query("INSERT INTO auth.users ...")
|
||||||
|
.bind(...)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Operaciones adicionales dentro de la misma transacción
|
||||||
|
// ...
|
||||||
|
|
||||||
|
Ok(user_clone)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Casos de Uso Implementados
|
||||||
|
|
||||||
|
### En UserPgRepository
|
||||||
|
|
||||||
|
1. **Creación de Usuario**
|
||||||
|
- Garantiza que todas las operaciones de inserción son atómicas
|
||||||
|
- Permite agregar operaciones relacionadas (como configuración de permisos)
|
||||||
|
|
||||||
|
2. **Actualización de Usuario**
|
||||||
|
- Asegura que las modificaciones se apliquen completamente o no se apliquen en absoluto
|
||||||
|
- Soporta operaciones combinadas como actualización de información de perfil y preferencias
|
||||||
|
|
||||||
|
### En SessionPgRepository
|
||||||
|
|
||||||
|
1. **Creación de Sesión**
|
||||||
|
- Inserta la sesión y actualiza el timestamp de último acceso del usuario en una única transacción
|
||||||
|
- Garantiza consistencia entre sesiones y datos de usuario
|
||||||
|
|
||||||
|
2. **Revocación de Sesiones**
|
||||||
|
- Asegura que la revocación de una sesión o de todas las sesiones de un usuario sea atómica
|
||||||
|
- Permite registrar eventos de seguridad dentro de la misma transacción
|
||||||
|
|
||||||
|
## Niveles de Aislamiento
|
||||||
|
|
||||||
|
OxiCloud admite diferentes niveles de aislamiento de transacciones mediante `with_transaction_isolation`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Ejemplo de uso con nivel de aislamiento específico
|
||||||
|
with_transaction_isolation(
|
||||||
|
&pool,
|
||||||
|
"operacion_critica",
|
||||||
|
sqlx::postgres::PgIsolationLevel::Serializable,
|
||||||
|
|tx| { ... }
|
||||||
|
).await
|
||||||
|
```
|
||||||
|
|
||||||
|
Los niveles de aislamiento disponibles son:
|
||||||
|
|
||||||
|
1. **Read Committed** (predeterminado)
|
||||||
|
- Garantiza que los datos leídos están confirmados
|
||||||
|
- No previene lecturas no repetibles o fantasma
|
||||||
|
|
||||||
|
2. **Repeatable Read**
|
||||||
|
- Garantiza que las lecturas sean consistentes durante toda la transacción
|
||||||
|
- Previene lecturas no repetibles pero no lecturas fantasma
|
||||||
|
|
||||||
|
3. **Serializable**
|
||||||
|
- Nivel más alto de aislamiento
|
||||||
|
- Garantiza que las transacciones se comporten como si se ejecutaran en serie
|
||||||
|
- Puede causar errores de serialización que requieren reintento
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Duración de Transacciones**
|
||||||
|
- Mantén las transacciones lo más cortas posible
|
||||||
|
- Evita operaciones de larga duración dentro de transacciones
|
||||||
|
|
||||||
|
2. **Manejo de Errores**
|
||||||
|
- Los errores dentro de una transacción provocan rollback automático
|
||||||
|
- Utiliza logging adecuado para diagnosticar fallos
|
||||||
|
|
||||||
|
3. **Límites de Transacción**
|
||||||
|
- Define claramente dónde comienzan y terminan las transacciones
|
||||||
|
- Agrupa operaciones relacionadas en una sola transacción
|
||||||
|
|
||||||
|
4. **Aislamiento Apropiado**
|
||||||
|
- Usa el nivel de aislamiento más bajo adecuado para tu caso de uso
|
||||||
|
- Considera serializable para operaciones críticas con posibilidad de conflicto
|
||||||
|
|
||||||
|
## Ventajas de Transacciones Explícitas
|
||||||
|
|
||||||
|
1. **Integridad de Datos Mejorada**
|
||||||
|
- Garantía ACID para operaciones complejas
|
||||||
|
- Prevención de estados inconsistentes
|
||||||
|
|
||||||
|
2. **Mejor Manejo de Errores**
|
||||||
|
- Rollback automático ante fallos
|
||||||
|
- Comportamiento predecible en caso de error
|
||||||
|
|
||||||
|
3. **Concurrencia Segura**
|
||||||
|
- Manejo adecuado de operaciones simultáneas
|
||||||
|
- Prevención de condiciones de carrera
|
||||||
|
|
||||||
|
4. **Rendimiento**
|
||||||
|
- Reducción de trips a la base de datos
|
||||||
|
- Operaciones en lote para mejor eficiencia
|
||||||
|
|
||||||
|
## Consideraciones de Rendimiento
|
||||||
|
|
||||||
|
- Las transacciones añaden cierta sobrecarga
|
||||||
|
- El rendimiento puede verse afectado por:
|
||||||
|
- Duración de la transacción
|
||||||
|
- Nivel de aislamiento
|
||||||
|
- Número de registros afectados
|
||||||
|
- Contención por bloqueos
|
||||||
|
|
||||||
|
## Conclusión
|
||||||
|
|
||||||
|
La implementación de transacciones explícitas en OxiCloud mejora significativamente la robustez del sistema y garantiza la integridad de los datos en escenarios complejos. El enfoque modular y la API de transacciones simplificada permiten extender fácilmente estos beneficios a nuevas funcionalidades.
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# File System Safety in OxiCloud
|
||||||
|
|
||||||
|
This document describes the implementation of file system safety mechanisms in OxiCloud to ensure data integrity and durability during file operations.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Data integrity is critical in a file storage system like OxiCloud. When files are written to disk, it's important to ensure that:
|
||||||
|
|
||||||
|
1. Writes are atomic - they either complete fully or not at all
|
||||||
|
2. Data is properly synchronized to persistent storage
|
||||||
|
3. Directory entries are properly updated and persisted
|
||||||
|
4. The system can recover from unexpected crashes or power failures
|
||||||
|
|
||||||
|
OxiCloud implements several mechanisms to achieve these goals.
|
||||||
|
|
||||||
|
## The Problem: Buffered I/O and Data Loss
|
||||||
|
|
||||||
|
Standard file system operations in many programming languages and operating systems use buffered I/O by default:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// This operation may not immediately persist to disk
|
||||||
|
fs::write(path, content)
|
||||||
|
```
|
||||||
|
|
||||||
|
When an application writes data, the operating system typically:
|
||||||
|
|
||||||
|
1. Accepts the write into memory buffers
|
||||||
|
2. Acknowledges completion to the application
|
||||||
|
3. Schedules the actual disk write for later
|
||||||
|
|
||||||
|
This creates a window where a system crash or power failure can result in data loss, as the data may exist only in memory buffers that haven't been flushed to disk.
|
||||||
|
|
||||||
|
## OxiCloud's Solution
|
||||||
|
|
||||||
|
OxiCloud implements a comprehensive approach to file system safety through the `FileSystemUtils` service, which provides:
|
||||||
|
|
||||||
|
### 1. Atomic Write Pattern
|
||||||
|
|
||||||
|
Files are written using a safe atomic pattern:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Writes data to a file with fsync to ensure durability
|
||||||
|
/// Uses a safe atomic write pattern: write to temp file, fsync, rename
|
||||||
|
pub async fn atomic_write<P: AsRef<Path>>(path: P, contents: &[u8]) -> Result<(), IoError>
|
||||||
|
```
|
||||||
|
|
||||||
|
This implements a write-then-rename pattern:
|
||||||
|
1. Write to a temporary file in the same directory
|
||||||
|
2. Call `fsync` to ensure data is on disk
|
||||||
|
3. Atomically rename the temp file to the target file
|
||||||
|
4. Sync the parent directory to ensure the rename is persisted
|
||||||
|
|
||||||
|
### 2. Directory Synchronization
|
||||||
|
|
||||||
|
Directory operations are also synchronized:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Creates directories with fsync
|
||||||
|
pub async fn create_dir_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError>
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that:
|
||||||
|
1. Directories are properly created
|
||||||
|
2. Directory entries are persisted to disk
|
||||||
|
3. Parent directories are also synchronized
|
||||||
|
|
||||||
|
### 3. Rename and Delete Operations
|
||||||
|
|
||||||
|
Renames and delete operations follow the same pattern:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Renames a file or directory with proper syncing
|
||||||
|
pub async fn rename_with_sync<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), IoError>
|
||||||
|
|
||||||
|
/// Removes a file with directory syncing
|
||||||
|
pub async fn remove_file_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError>
|
||||||
|
```
|
||||||
|
|
||||||
|
These operations ensure that:
|
||||||
|
1. The operation itself is completed
|
||||||
|
2. The parent directory entry is updated and synchronized
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Implementing fsync on Files
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Write file content
|
||||||
|
file.write_all(contents).await?;
|
||||||
|
|
||||||
|
// Ensure data is synced to disk
|
||||||
|
file.flush().await?;
|
||||||
|
file.sync_all().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sync_all()` call is critical as it instructs the operating system to flush data and metadata to the physical storage device.
|
||||||
|
|
||||||
|
### Implementing fsync on Directories
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Sync a directory to ensure its contents (entries) are durable
|
||||||
|
async fn sync_directory<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
|
||||||
|
let dir_file = OpenOptions::new().read(true).open(path).await?;
|
||||||
|
dir_file.sync_all().await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is essential after operations that modify directory entries, such as creating, renaming, or deleting files.
|
||||||
|
|
||||||
|
## Usage in the Codebase
|
||||||
|
|
||||||
|
The `FileSystemUtils` service is integrated throughout OxiCloud's file operations:
|
||||||
|
|
||||||
|
### In File Write Repository
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Write the file to disk using atomic write with fsync
|
||||||
|
tokio::time::timeout(
|
||||||
|
self.config.timeouts.file_write_timeout(),
|
||||||
|
FileSystemUtils::atomic_write(&abs_path, &content)
|
||||||
|
).await
|
||||||
|
```
|
||||||
|
|
||||||
|
### In File Move Operations
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Move the file physically with fsync
|
||||||
|
time::timeout(
|
||||||
|
self.config.timeouts.file_timeout(),
|
||||||
|
FileSystemUtils::rename_with_sync(&old_abs_path, &new_abs_path)
|
||||||
|
).await
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Directory Creation
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Ensure the parent directory exists with proper syncing
|
||||||
|
self.ensure_parent_directory(&abs_path).await?;
|
||||||
|
|
||||||
|
// Implementation uses FileSystemUtils
|
||||||
|
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
||||||
|
if let Some(parent) = abs_path.parent() {
|
||||||
|
time::timeout(
|
||||||
|
self.config.timeouts.dir_timeout(),
|
||||||
|
FileSystemUtils::create_dir_with_sync(parent)
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
By implementing these safety measures, OxiCloud provides:
|
||||||
|
|
||||||
|
1. **Data Durability**: Critical data is properly synchronized to persistent storage
|
||||||
|
2. **Crash Resilience**: The system can recover from unexpected failures without data loss
|
||||||
|
3. **Consistency**: File operations maintain a consistent file system state
|
||||||
|
4. **Atomic Operations**: File writes appear as all-or-nothing operations
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
These safety measures do have some performance impact, as synchronizing to disk is more expensive than buffered writes. However, OxiCloud:
|
||||||
|
|
||||||
|
1. Applies these measures only to critical operations
|
||||||
|
2. Uses timeouts to prevent operations from blocking indefinitely
|
||||||
|
3. Implements parallel processing for large files
|
||||||
|
|
||||||
|
The safety-performance tradeoff favors safety for critical data while still maintaining good performance for most operations.
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# Mejores Prácticas para PostgreSQL en OxiCloud
|
||||||
|
|
||||||
|
Este documento describe las mejores prácticas para el uso de PostgreSQL en OxiCloud, siguiendo recomendaciones oficiales y la guía ["Don't Do This"](https://wiki.postgresql.org/wiki/Don%27t_Do_This) de PostgreSQL.
|
||||||
|
|
||||||
|
## Diseño de Esquema
|
||||||
|
|
||||||
|
### Tipos de Datos
|
||||||
|
|
||||||
|
#### Uso de TEXT en lugar de VARCHAR(n)
|
||||||
|
|
||||||
|
OxiCloud utiliza el tipo `TEXT` en lugar de `VARCHAR(n)` con límites arbitrarios para campos de texto:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado ✅
|
||||||
|
username TEXT NOT NULL UNIQUE
|
||||||
|
|
||||||
|
-- Evitar ❌
|
||||||
|
username VARCHAR(32) NOT NULL UNIQUE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `TEXT` y `VARCHAR` tienen el mismo rendimiento y ocupan el mismo espacio.
|
||||||
|
- `VARCHAR(n)` impone un límite arbitrario que puede causar errores inesperados.
|
||||||
|
- PostgreSQL optimiza internamente ambos tipos de manera idéntica.
|
||||||
|
|
||||||
|
#### Uso de TIMESTAMPTZ para Fechas y Horas
|
||||||
|
|
||||||
|
OxiCloud utiliza `TIMESTAMP WITH TIME ZONE` (o `TIMESTAMPTZ`) para todos los campos de fecha/hora:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado ✅
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
|
||||||
|
-- Evitar ❌
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `TIMESTAMPTZ` almacena un punto en el tiempo unívoco.
|
||||||
|
- Gestiona correctamente las zonas horarias y cambios de horario de verano.
|
||||||
|
- Evita problemas de ambigüedad al trabajar con diferentes husos horarios.
|
||||||
|
|
||||||
|
#### Evitar CHAR(n)
|
||||||
|
|
||||||
|
OxiCloud no utiliza el tipo `CHAR(n)` en ningún caso:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado ✅
|
||||||
|
country_code TEXT NOT NULL CHECK (length(country_code) = 2)
|
||||||
|
|
||||||
|
-- Evitar ❌
|
||||||
|
country_code CHAR(2) NOT NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `CHAR(n)` rellena con espacios hasta la longitud declarada.
|
||||||
|
- Este comportamiento puede causar problemas sutiles en comparaciones.
|
||||||
|
- Para valores de longitud fija, es mejor usar `TEXT` con una restricción CHECK.
|
||||||
|
|
||||||
|
#### Usar SERIAL con Precaución
|
||||||
|
|
||||||
|
OxiCloud utiliza `SERIAL` solo en casos específicos, prefiriendo `IDENTITY` cuando es posible:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado para PostgreSQL 10+ ✅
|
||||||
|
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
|
||||||
|
|
||||||
|
-- Alternativa aceptable para compatibilidad ✅
|
||||||
|
id SERIAL PRIMARY KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `SERIAL` tiene comportamientos extraños con gestión de dependencias y permisos.
|
||||||
|
- Las columnas `IDENTITY` (PostgreSQL 10+) ofrecen mejor integración con el sistema.
|
||||||
|
|
||||||
|
### Índices y Restricciones
|
||||||
|
|
||||||
|
#### Nombrado Consistente de Índices
|
||||||
|
|
||||||
|
OxiCloud sigue una convención de nomenclatura para índices:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Índice en una columna
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_table_column ON schema.table(column);
|
||||||
|
|
||||||
|
-- Índice en múltiples columnas
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_table_col1_col2 ON schema.table(col1, col2);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Uso de Restricciones Explícitas
|
||||||
|
|
||||||
|
OxiCloud define restricciones explícitas en lugar de depender de convenciones implícitas:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Restricción de unicidad
|
||||||
|
UNIQUE(user_id, item_id, item_type)
|
||||||
|
|
||||||
|
-- Restricción de comprobación
|
||||||
|
CHECK (storage_quota_bytes >= 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consultas SQL
|
||||||
|
|
||||||
|
### Evitar NOT IN con Subconsultas
|
||||||
|
|
||||||
|
OxiCloud evita el uso de `NOT IN` con subconsultas:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado ✅
|
||||||
|
SELECT * FROM files
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM deleted_files WHERE deleted_files.id = files.id);
|
||||||
|
|
||||||
|
-- Evitar ❌
|
||||||
|
SELECT * FROM files
|
||||||
|
WHERE id NOT IN (SELECT id FROM deleted_files);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `NOT IN` se comporta de manera inesperada con valores NULL.
|
||||||
|
- `NOT EXISTS` es más eficiente y predecible.
|
||||||
|
|
||||||
|
### Usar BETWEEN con Precaución
|
||||||
|
|
||||||
|
OxiCloud evita `BETWEEN` para rangos de fechas, prefiriendo comparaciones explícitas:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recomendado ✅
|
||||||
|
WHERE timestamp_col >= '2025-01-01' AND timestamp_col < '2025-01-02'
|
||||||
|
|
||||||
|
-- Evitar ❌
|
||||||
|
WHERE timestamp_col BETWEEN '2025-01-01' AND '2025-01-02'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- `BETWEEN` incluye ambos extremos, lo que puede ser problemático para rangos de tiempo.
|
||||||
|
- Usar `>=` y `<` es más claro para expresar rangos de tiempo.
|
||||||
|
|
||||||
|
## Transacciones
|
||||||
|
|
||||||
|
### Uso Explícito de Transacciones
|
||||||
|
|
||||||
|
OxiCloud implementa transacciones explícitas para operaciones que deben ser atómicas:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Ejemplo de transacción explícita
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
// Operaciones dentro de la transacción
|
||||||
|
sqlx::query("INSERT INTO users (id, username) VALUES ($1, $2)")
|
||||||
|
.bind(id)
|
||||||
|
.bind(username)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO profiles (user_id, display_name) VALUES ($1, $2)")
|
||||||
|
.bind(id)
|
||||||
|
.bind(display_name)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Confirmar la transacción
|
||||||
|
tx.commit().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manejo de Errores en Transacciones
|
||||||
|
|
||||||
|
Las transacciones incluyen manejo adecuado de errores con rollback automático:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let result = sqlx::Transaction::begin(&pool).await.and_then(|mut tx| async move {
|
||||||
|
// Operaciones dentro de la transacción
|
||||||
|
let result1 = operation1(&mut tx).await?;
|
||||||
|
let result2 = operation2(&mut tx).await?;
|
||||||
|
|
||||||
|
// Confirmar la transacción si todo fue exitoso
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok((result1, result2))
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
// Si ocurre un error, la transacción se revierte automáticamente
|
||||||
|
if let Err(e) = &result {
|
||||||
|
log::error!("Error en la transacción: {}", e);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migraciones y Gestión de Esquema
|
||||||
|
|
||||||
|
### Separación del Esquema del Código
|
||||||
|
|
||||||
|
OxiCloud separa la definición del esquema del código de la aplicación:
|
||||||
|
|
||||||
|
```
|
||||||
|
OxiCloud/
|
||||||
|
├── migrations/ # Archivos SQL de migración
|
||||||
|
├── src/
|
||||||
|
├── bin/migrate.rs # Herramienta de migración
|
||||||
|
├── common/db.rs # Solo conecta a la BD, no crea esquema
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uso de Migraciones Versionadas
|
||||||
|
|
||||||
|
Las migraciones siguen un formato versionado y se aplican secuencialmente:
|
||||||
|
|
||||||
|
```
|
||||||
|
20250408000000_initial_schema.sql
|
||||||
|
20250408000001_default_users.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
### Uso de Consultas Parametrizadas
|
||||||
|
|
||||||
|
OxiCloud utiliza consultas parametrizadas para todas las operaciones SQL:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Recomendado ✅
|
||||||
|
sqlx::query("SELECT * FROM users WHERE username = $1")
|
||||||
|
.bind(username)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Evitar ❌
|
||||||
|
sqlx::query(&format!("SELECT * FROM users WHERE username = '{}'", username))
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Razones:**
|
||||||
|
- Previene ataques de inyección SQL.
|
||||||
|
- Permite la reutilización de planes de consulta.
|
||||||
|
- Mejora el rendimiento general.
|
||||||
|
|
||||||
|
### Configuración de Autenticación Segura
|
||||||
|
|
||||||
|
OxiCloud evita el uso de autenticación `trust` para conexiones TCP/IP:
|
||||||
|
|
||||||
|
```
|
||||||
|
# pg_hba.conf recomendado ✅
|
||||||
|
hostssl all all 0.0.0.0/0 scram-sha-256
|
||||||
|
|
||||||
|
# Evitar ❌
|
||||||
|
host all all 0.0.0.0/0 trust
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recursos Adicionales
|
||||||
|
|
||||||
|
- [Wiki PostgreSQL - Don't Do This](https://wiki.postgresql.org/wiki/Don%27t_Do_This)
|
||||||
|
- [Documentación oficial de PostgreSQL](https://www.postgresql.org/docs/)
|
||||||
|
- [Guía de migraciones de OxiCloud](DATABASE-MIGRATIONS.md)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- OxiCloud Authentication Database Schema Migration
|
||||||
|
-- Migration 001: Initial Schema
|
||||||
|
|
||||||
|
-- Create schema for auth-related tables
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
|
||||||
|
-- Create UserRole enum type
|
||||||
|
DO $BODY$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'userrole' AND n.nspname = 'auth'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE auth.userrole AS ENUM ('admin', 'user');
|
||||||
|
END IF;
|
||||||
|
END $BODY$;
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.users (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role auth.userrole NOT NULL,
|
||||||
|
storage_quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10GB default
|
||||||
|
storage_used_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for users table
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_username ON auth.users(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email);
|
||||||
|
|
||||||
|
-- Sessions table for refresh tokens
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
refresh_token TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
ip_address TEXT, -- to support IPv6
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
revoked BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for sessions table
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_refresh_token ON auth.sessions(refresh_token);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions(expires_at);
|
||||||
|
|
||||||
|
-- Create function for active sessions to use in index
|
||||||
|
CREATE OR REPLACE FUNCTION auth.is_session_active(expires_at timestamptz)
|
||||||
|
RETURNS boolean AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN expires_at > now();
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||||
|
|
||||||
|
-- Create index for active sessions with IMMUTABLE function
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_active ON auth.sessions(user_id, revoked)
|
||||||
|
WHERE NOT revoked AND auth.is_session_active(expires_at);
|
||||||
|
|
||||||
|
-- File ownership tracking
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.user_files (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
file_id TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, file_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for user_files
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_files_user_id ON auth.user_files(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_files_file_id ON auth.user_files(file_id);
|
||||||
|
|
||||||
|
-- User favorites table for cross-device synchronization
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.user_favorites (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
item_id TEXT NOT NULL,
|
||||||
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, item_id, item_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for efficient querying
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id ON auth.user_favorites(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id ON auth.user_favorites(item_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_favorites_type ON auth.user_favorites(item_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_favorites_created ON auth.user_favorites(created_at);
|
||||||
|
|
||||||
|
-- Combined index for quick lookups by user and type
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON auth.user_favorites(user_id, item_type);
|
||||||
|
|
||||||
|
-- Table for recent files
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.user_recent_files (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
item_id TEXT NOT NULL,
|
||||||
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
|
accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, item_id, item_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for efficient querying
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_recent_user_id ON auth.user_recent_files(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_recent_item_id ON auth.user_recent_files(item_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_recent_type ON auth.user_recent_files(item_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_recent_accessed ON auth.user_recent_files(accessed_at);
|
||||||
|
|
||||||
|
-- Combined index for quick lookups by user and accessed time (for sorting)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_recent_user_accessed ON auth.user_recent_files(user_id, accessed_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.user_recent_files IS 'Stores recently accessed files and folders for cross-device synchronization';
|
||||||
|
COMMENT ON TABLE auth.users IS 'Stores user account information';
|
||||||
|
COMMENT ON TABLE auth.sessions IS 'Stores user session information for refresh tokens';
|
||||||
|
COMMENT ON TABLE auth.user_files IS 'Tracks file ownership and storage utilization by users';
|
||||||
|
COMMENT ON TABLE auth.user_favorites IS 'Stores user favorite files and folders for cross-device synchronization';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Migration 002: Default Users
|
||||||
|
|
||||||
|
-- Create admin user (password: Admin123!)
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
role,
|
||||||
|
storage_quota_bytes
|
||||||
|
) VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'admin',
|
||||||
|
'admin@oxicloud.local',
|
||||||
|
'$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$H3VxE8LL2qPT31DM3loTg6D+O4MSc2sD7GjlQ5h7Jkw', -- Admin123!
|
||||||
|
'admin',
|
||||||
|
107374182400 -- 100GB for admin
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Create test user (password: test123)
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
role,
|
||||||
|
storage_quota_bytes
|
||||||
|
) VALUES (
|
||||||
|
'11111111-1111-1111-1111-111111111111',
|
||||||
|
'test',
|
||||||
|
'test@oxicloud.local',
|
||||||
|
'$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$ZG17Z7SFKhs9zWYbuk08CkHpyiznnZapYnxN5Vi62R4', -- test123
|
||||||
|
'user',
|
||||||
|
10737418240 -- 10GB for test user
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
@@ -5,10 +5,9 @@
|
|||||||
* It handles parsing WebDAV request XML and generating WebDAV response XML according to RFC 4918.
|
* It handles parsing WebDAV request XML and generating WebDAV response XML according to RFC 4918.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::io::{Read, Write, BufRead, BufReader};
|
use std::io::{Read, Write, BufReader};
|
||||||
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||||
use std::collections::HashMap;
|
use chrono::Utc;
|
||||||
use chrono::{DateTime, Utc, TimeZone};
|
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ pub trait FileWritePort: Send + Sync + 'static {
|
|||||||
|
|
||||||
/// Elimina un archivo
|
/// Elimina un archivo
|
||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
|
/// Obtiene detalles de una carpeta
|
||||||
|
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError>;
|
||||||
|
|
||||||
|
/// Obtiene la ruta de una carpeta como string
|
||||||
|
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto secundario para resolución de rutas de archivos
|
/// Puerto secundario para resolución de rutas de archivos
|
||||||
@@ -68,3 +74,13 @@ pub trait DirectoryManagementPort: Send + Sync + 'static {
|
|||||||
/// Crea directorios si no existen
|
/// Crea directorios si no existen
|
||||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Puerto secundario para gestión de uso de almacenamiento
|
||||||
|
#[async_trait]
|
||||||
|
pub trait StorageUsagePort: Send + Sync + 'static {
|
||||||
|
/// Actualiza estadísticas de uso de almacenamiento para un usuario
|
||||||
|
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
|
||||||
|
|
||||||
|
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
|
||||||
|
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
@@ -5,22 +5,55 @@ use crate::application::dtos::file_dto::FileDto;
|
|||||||
use crate::application::ports::file_ports::FileUploadUseCase;
|
use crate::application::ports::file_ports::FileUploadUseCase;
|
||||||
use crate::application::ports::storage_ports::FileWritePort;
|
use crate::application::ports::storage_ports::FileWritePort;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
use crate::application::ports::storage_ports::StorageUsagePort;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
/// Helper function to extract username from folder path string
|
||||||
|
fn extract_username_from_path(path: &str) -> Option<String> {
|
||||||
|
// Check if path contains the folder pattern
|
||||||
|
if !path.contains("Mi Carpeta - ") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by the pattern and get the second part
|
||||||
|
let parts: Vec<&str> = path.split("Mi Carpeta - ").collect();
|
||||||
|
if parts.len() <= 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim and return as owned String
|
||||||
|
Some(parts[1].trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Servicio para operaciones de subida de archivos
|
/// Servicio para operaciones de subida de archivos
|
||||||
pub struct FileUploadService {
|
pub struct FileUploadService {
|
||||||
file_repository: Arc<dyn FileWritePort>,
|
file_repository: Arc<dyn FileWritePort>,
|
||||||
|
storage_usage_service: Option<Arc<dyn StorageUsagePort>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileUploadService {
|
impl FileUploadService {
|
||||||
/// Crea un nuevo servicio de subida de archivos
|
/// Crea un nuevo servicio de subida de archivos
|
||||||
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
|
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
|
||||||
Self { file_repository }
|
Self {
|
||||||
|
file_repository,
|
||||||
|
storage_usage_service: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configura el servicio de uso de almacenamiento
|
||||||
|
pub fn with_storage_usage_service(
|
||||||
|
mut self,
|
||||||
|
storage_usage_service: Arc<dyn StorageUsagePort>
|
||||||
|
) -> Self {
|
||||||
|
self.storage_usage_service = Some(storage_usage_service);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un stub para pruebas
|
/// Crea un stub para pruebas
|
||||||
pub fn default_stub() -> Self {
|
pub fn default_stub() -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
|
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
|
||||||
|
storage_usage_service: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +67,43 @@ impl FileUploadUseCase for FileUploadService {
|
|||||||
content_type: String,
|
content_type: String,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
|
// Upload the file
|
||||||
let file = self.file_repository.save_file(name, folder_id, content_type, content).await?;
|
let file = self.file_repository.save_file(name, folder_id, content_type, content).await?;
|
||||||
|
|
||||||
|
// Extract the owner's user ID if available
|
||||||
|
// We could make this more explicit by adding a user_id parameter
|
||||||
|
if let Some(storage_service) = &self.storage_usage_service {
|
||||||
|
// Extract user ID from folder pattern 'Mi Carpeta - {username}'
|
||||||
|
if let Some(folder_id) = file.folder_id() {
|
||||||
|
// Since we don't have direct access to folder details,
|
||||||
|
// we'll use pattern matching on the folder ID
|
||||||
|
// In a more complete implementation, we would use a folder repository
|
||||||
|
let folder_id_str = folder_id;
|
||||||
|
|
||||||
|
// Check if we can extract a username from context
|
||||||
|
if let Ok(folder_path) = self.file_repository.get_folder_path_str(folder_id_str).await {
|
||||||
|
// Process the string to extract username without creating borrowing issues
|
||||||
|
if let Some(username) = extract_username_from_path(&folder_path) {
|
||||||
|
// Find user by username and update their storage usage
|
||||||
|
// We do this asynchronously to avoid blocking the upload response
|
||||||
|
let service_clone = Arc::clone(storage_service);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match service_clone.update_user_storage_usage(&username).await {
|
||||||
|
Ok(usage) => {
|
||||||
|
debug!("Updated storage usage for user {} to {} bytes", username, usage);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to update storage usage for {}: {}", username, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Could not get folder path for ID: {}", folder_id_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(FileDto::from(file))
|
Ok(FileDto::from(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ pub mod search_service;
|
|||||||
pub mod share_service;
|
pub mod share_service;
|
||||||
pub mod favorites_service;
|
pub mod favorites_service;
|
||||||
pub mod recent_service;
|
pub mod recent_service;
|
||||||
|
pub mod storage_usage_service;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod trash_service_test;
|
mod trash_service_test;
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio::task;
|
||||||
|
use crate::common::errors::DomainError;
|
||||||
|
use crate::application::ports::auth_ports::UserStoragePort;
|
||||||
|
use crate::domain::repositories::file_repository::FileRepository;
|
||||||
|
use crate::application::ports::storage_ports::StorageUsagePort;
|
||||||
|
use tracing::{info, error, debug};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing and updating user storage usage statistics.
|
||||||
|
*
|
||||||
|
* This service is responsible for calculating how much storage each user
|
||||||
|
* is using and updating this information in the user records.
|
||||||
|
*/
|
||||||
|
pub struct StorageUsageService {
|
||||||
|
file_repository: Arc<dyn FileRepository>,
|
||||||
|
user_repository: Arc<dyn UserStoragePort>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageUsageService {
|
||||||
|
/// Creates a new storage usage service
|
||||||
|
pub fn new(
|
||||||
|
file_repository: Arc<dyn FileRepository>,
|
||||||
|
user_repository: Arc<dyn UserStoragePort>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
file_repository,
|
||||||
|
user_repository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates and updates storage usage for a specific user
|
||||||
|
pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
|
||||||
|
info!("Updating storage usage for user: {}", user_id);
|
||||||
|
|
||||||
|
// Get user's home folder pattern
|
||||||
|
let user = self.user_repository.get_user_by_id(user_id).await?;
|
||||||
|
let username = user.username();
|
||||||
|
|
||||||
|
// Calculate storage usage for this user
|
||||||
|
let total_usage = self.calculate_user_storage_usage(username).await?;
|
||||||
|
|
||||||
|
// Update the user's storage usage in the database
|
||||||
|
self.user_repository.update_storage_usage(user_id, total_usage).await?;
|
||||||
|
|
||||||
|
info!("Updated storage usage for user {} to {} bytes", user_id, total_usage);
|
||||||
|
|
||||||
|
Ok(total_usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates a user's storage usage based on their home folder
|
||||||
|
async fn calculate_user_storage_usage(&self, username: &str) -> Result<i64, DomainError> {
|
||||||
|
debug!("Calculating storage for user: {}", username);
|
||||||
|
|
||||||
|
// First, try to find the user's home folder
|
||||||
|
// List all folders to locate the user's folder
|
||||||
|
let all_folders = self.file_repository.list_files(None).await
|
||||||
|
.map_err(|e| DomainError::internal_error("File repository", e.to_string()))?;
|
||||||
|
|
||||||
|
// Find the user's home folder (usually named "Mi Carpeta - {username}")
|
||||||
|
let home_folder_name = format!("Mi Carpeta - {}", username);
|
||||||
|
debug!("Looking for home folder: {}", home_folder_name);
|
||||||
|
|
||||||
|
let mut total_usage: i64 = 0;
|
||||||
|
let mut home_folder_id = None;
|
||||||
|
|
||||||
|
// Find the home folder ID
|
||||||
|
for folder in &all_folders {
|
||||||
|
if folder.name() == home_folder_name {
|
||||||
|
home_folder_id = Some(folder.id().to_string());
|
||||||
|
debug!("Found home folder for user {}: ID={}", username, folder.id());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found the home folder, calculate total size
|
||||||
|
if let Some(folder_id) = home_folder_id {
|
||||||
|
// Calculate recursively
|
||||||
|
total_usage = self.calculate_folder_size(&folder_id).await?;
|
||||||
|
} else {
|
||||||
|
// If no home folder found, just return 0
|
||||||
|
debug!("No home folder found for user: {}", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total_usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively calculates the size of a folder and all its contents
|
||||||
|
async fn calculate_folder_size(&self, folder_id: &str) -> Result<i64, DomainError> {
|
||||||
|
// Implementation with explicit boxing to handle recursion in async functions
|
||||||
|
async fn inner_calculate_size(
|
||||||
|
repo: Arc<dyn FileRepository>,
|
||||||
|
folder_id: &str,
|
||||||
|
) -> Result<i64, DomainError> {
|
||||||
|
let mut total_size: i64 = 0;
|
||||||
|
|
||||||
|
// Get files directly in this folder
|
||||||
|
let files = repo.list_files(Some(folder_id)).await
|
||||||
|
.map_err(|e| DomainError::internal_error("File repository", e.to_string()))?;
|
||||||
|
|
||||||
|
// Sum the size of all files
|
||||||
|
for file in &files {
|
||||||
|
// Skip subdirectories at this level - we'll process them separately
|
||||||
|
if file.mime_type() == "directory" || file.mime_type() == "application/directory" {
|
||||||
|
// Recursively calculate subfolder size with explicit boxing
|
||||||
|
let subfolder_id = file.id().to_string(); // Create owned copy
|
||||||
|
let repo_clone = repo.clone(); // Clone the repository
|
||||||
|
|
||||||
|
// Use Box::pin to handle recursive async call
|
||||||
|
let subfolder_size_future = Box::pin(inner_calculate_size(repo_clone, &subfolder_id));
|
||||||
|
|
||||||
|
match subfolder_size_future.await {
|
||||||
|
Ok(size) => {
|
||||||
|
total_size += size;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error calculating size for subfolder {}: {}", subfolder_id, e);
|
||||||
|
// Continue with other folders even if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add file size to total
|
||||||
|
total_size += file.size() as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the calculation with a clone of our repository reference
|
||||||
|
let repo_clone = Arc::clone(&self.file_repository);
|
||||||
|
inner_calculate_size(repo_clone, folder_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the StorageUsagePort trait to expose storage usage services
|
||||||
|
* to the application layer.
|
||||||
|
*/
|
||||||
|
#[async_trait]
|
||||||
|
impl StorageUsagePort for StorageUsageService {
|
||||||
|
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
|
||||||
|
StorageUsageService::update_user_storage_usage(self, user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError> {
|
||||||
|
info!("Starting batch update of all users' storage usage");
|
||||||
|
|
||||||
|
// Get the list of all users
|
||||||
|
let users = self.user_repository.list_users(1000, 0).await?;
|
||||||
|
|
||||||
|
let mut update_tasks = Vec::new();
|
||||||
|
|
||||||
|
// Process users in parallel
|
||||||
|
for user in users {
|
||||||
|
let user_id = user.id().to_string();
|
||||||
|
let service_clone = self.clone();
|
||||||
|
|
||||||
|
// Spawn a background task for each user
|
||||||
|
let task = task::spawn(async move {
|
||||||
|
match service_clone.update_user_storage_usage(&user_id).await {
|
||||||
|
Ok(usage) => {
|
||||||
|
debug!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to update storage for user {}: {}", user_id, e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
update_tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all tasks to complete
|
||||||
|
for task in update_tasks {
|
||||||
|
// We don't propagate errors from individual users to avoid failing the entire batch
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Completed batch update of all users' storage usage");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make StorageUsageService cloneable to support spawning concurrent tasks
|
||||||
|
impl Clone for StorageUsageService {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
file_repository: Arc::clone(&self.file_repository),
|
||||||
|
user_repository: Arc::clone(&self.user_repository),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Configurar logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Cargar variables de entorno (primero .env.local, luego .env)
|
||||||
|
if let Ok(path) = env::var("DOTENV_PATH") {
|
||||||
|
dotenv::from_path(Path::new(&path)).ok();
|
||||||
|
} else {
|
||||||
|
dotenv::from_filename(".env.local").ok();
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener DATABASE_URL desde variables de entorno
|
||||||
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
||||||
|
|
||||||
|
println!("Conectando a la base de datos...");
|
||||||
|
|
||||||
|
// Crear pool de conexiones
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.acquire_timeout(Duration::from_secs(10))
|
||||||
|
.connect(&database_url)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Ejecutar migraciones
|
||||||
|
println!("Ejecutando migraciones...");
|
||||||
|
|
||||||
|
// Obtenemos el directorio desde una variable de entorno o usamos un valor por defecto
|
||||||
|
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
|
||||||
|
println!("Directorio de migraciones: {}", migrations_dir);
|
||||||
|
|
||||||
|
// Crear un migrator
|
||||||
|
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
|
||||||
|
.await
|
||||||
|
.expect("No se pudo crear el migrator");
|
||||||
|
|
||||||
|
// Ejecutar todas las migraciones pendientes
|
||||||
|
migrator.run(&pool).await?;
|
||||||
|
|
||||||
|
println!("Migraciones aplicadas correctamente");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+22
-65
@@ -1,4 +1,4 @@
|
|||||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
@@ -29,74 +29,31 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
|||||||
match sqlx::query("SELECT 1").execute(&pool).await {
|
match sqlx::query("SELECT 1").execute(&pool).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Conexión a PostgreSQL establecida correctamente");
|
tracing::info!("Conexión a PostgreSQL establecida correctamente");
|
||||||
|
|
||||||
|
// Verify if migrations have been applied
|
||||||
|
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match migration_check {
|
||||||
|
Ok(row) => {
|
||||||
|
let tables_exist: bool = row.get(0);
|
||||||
|
if !tables_exist {
|
||||||
|
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(pool);
|
return Ok(pool);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error al verificar conexión: {}", e);
|
tracing::error!("Error al verificar conexión: {}", e);
|
||||||
// Try creating the tables in this case - might be missing schema
|
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||||
tracing::info!("Intentando crear las tablas necesarias...");
|
if attempt >= MAX_ATTEMPTS {
|
||||||
|
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
|
||||||
// Simple schema creation - this handles fresh installations
|
|
||||||
let create_tables_result = sqlx::query(r#"
|
|
||||||
-- Create the auth schema if not exists
|
|
||||||
CREATE SCHEMA IF NOT EXISTS auth;
|
|
||||||
|
|
||||||
-- Create UserRole enum type if not exists
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'userrole') THEN
|
|
||||||
CREATE TYPE auth.userrole AS ENUM ('admin', 'user');
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Create the auth.users table
|
|
||||||
CREATE TABLE IF NOT EXISTS auth.users (
|
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
|
||||||
username VARCHAR(32) NOT NULL UNIQUE,
|
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
role auth.userrole NOT NULL,
|
|
||||||
storage_quota_bytes BIGINT NOT NULL,
|
|
||||||
storage_used_bytes BIGINT NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL,
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL,
|
|
||||||
last_login_at TIMESTAMPTZ,
|
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create an index on username and email for fast lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_username ON auth.users(username);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email);
|
|
||||||
|
|
||||||
-- Create the sessions table
|
|
||||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
|
||||||
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
||||||
refresh_token VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
|
||||||
ip_address VARCHAR(45),
|
|
||||||
user_agent TEXT,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL,
|
|
||||||
revoked BOOLEAN NOT NULL DEFAULT FALSE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create indexes on user_id and refresh_token for fast lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_refresh_token ON auth.sessions(refresh_token);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions(expires_at);
|
|
||||||
"#).execute(&pool).await;
|
|
||||||
|
|
||||||
match create_tables_result {
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!("Tablas creadas correctamente");
|
|
||||||
return Ok(pool);
|
|
||||||
},
|
|
||||||
Err(table_err) => {
|
|
||||||
tracing::error!("Error al crear tablas: {}", table_err);
|
|
||||||
if attempt >= MAX_ATTEMPTS {
|
|
||||||
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", table_err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ pub struct AppState {
|
|||||||
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
||||||
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
|
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
|
||||||
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
||||||
|
pub storage_usage_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
@@ -439,6 +440,14 @@ impl Default for AppState {
|
|||||||
async fn delete_file(&self, _id: &str) -> Result<(), crate::common::errors::DomainError> {
|
async fn delete_file(&self, _id: &str) -> Result<(), crate::common::errors::DomainError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_folder_details(&self, _folder_id: &str) -> Result<crate::domain::entities::file::File, crate::common::errors::DomainError> {
|
||||||
|
Ok(crate::domain::entities::file::File::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_folder_path_str(&self, _folder_id: &str) -> Result<String, crate::common::errors::DomainError> {
|
||||||
|
Ok("/Mi Carpeta - dummy".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DummyFileStoragePort;
|
struct DummyFileStoragePort;
|
||||||
@@ -815,6 +824,7 @@ impl Default for AppState {
|
|||||||
share_service: None,
|
share_service: None,
|
||||||
favorites_service: None,
|
favorites_service: None,
|
||||||
recent_service: None,
|
recent_service: None,
|
||||||
|
storage_usage_service: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -835,6 +845,7 @@ impl AppState {
|
|||||||
share_service: None,
|
share_service: None,
|
||||||
favorites_service: None,
|
favorites_service: None,
|
||||||
recent_service: None,
|
recent_service: None,
|
||||||
|
storage_usage_service: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,4 +878,9 @@ impl AppState {
|
|||||||
self.recent_service = Some(recent_service);
|
self.recent_service = Some(recent_service);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_storage_usage_service(mut self, storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>) -> Self {
|
||||||
|
self.storage_usage_service = Some(storage_usage_service);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +122,36 @@ impl File {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a folder entity
|
||||||
|
pub fn new_folder(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
storage_path: StoragePath,
|
||||||
|
parent_id: Option<String>,
|
||||||
|
created_at: u64,
|
||||||
|
modified_at: u64,
|
||||||
|
) -> FileResult<Self> {
|
||||||
|
// Validate folder name
|
||||||
|
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||||
|
return Err(FileError::InvalidFileName(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the path string for serialization compatibility
|
||||||
|
let path_string = storage_path.to_string();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
storage_path,
|
||||||
|
path_string,
|
||||||
|
size: 0, // Folders have zero size
|
||||||
|
mime_type: "directory".to_string(), // Standard MIME type for directories
|
||||||
|
folder_id: parent_id,
|
||||||
|
created_at,
|
||||||
|
modified_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a file with specific timestamps (for reconstruction)
|
/// Creates a file with specific timestamps (for reconstruction)
|
||||||
pub fn with_timestamps(
|
pub fn with_timestamps(
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ impl StoragePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve la representación de la ruta como cadena
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
// Nota: La implementación realmente debería almacenar la cadena,
|
||||||
|
// pero aquí hacemos una implementación temporal que siempre devuelve "/"
|
||||||
|
// Esto se usa solo para la implementación de get_folder_path_str
|
||||||
|
"/"
|
||||||
|
}
|
||||||
|
|
||||||
/// Obtiene los segmentos de la ruta
|
/// Obtiene los segmentos de la ruta
|
||||||
pub fn segments(&self) -> &[String] {
|
pub fn segments(&self) -> &[String] {
|
||||||
&self.segments
|
&self.segments
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use bytes::Bytes;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||||
|
|
||||||
use crate::domain::entities::file::File;
|
use crate::domain::entities::file::File;
|
||||||
use crate::domain::repositories::file_repository::{
|
use crate::domain::repositories::file_repository::{
|
||||||
FileRepository, FileRepositoryError, FileRepositoryResult
|
FileRepository, FileRepositoryError, FileRepositoryResult
|
||||||
@@ -312,12 +314,12 @@ impl FileFsRepository {
|
|||||||
Ok((size, created_at, modified_at))
|
Ok((size, created_at, modified_at))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates parent directories if needed with timeout
|
/// Creates parent directories if needed with timeout and fsync
|
||||||
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
||||||
if let Some(parent) = abs_path.parent() {
|
if let Some(parent) = abs_path.parent() {
|
||||||
time::timeout(
|
time::timeout(
|
||||||
self.config.timeouts.dir_timeout(),
|
self.config.timeouts.dir_timeout(),
|
||||||
fs::create_dir_all(parent)
|
FileSystemUtils::create_dir_with_sync(parent)
|
||||||
).await
|
).await
|
||||||
.map_err(|_| FileRepositoryError::Timeout(
|
.map_err(|_| FileRepositoryError::Timeout(
|
||||||
format!("Timeout creating parent directory: {}", parent.display())
|
format!("Timeout creating parent directory: {}", parent.display())
|
||||||
@@ -508,8 +510,9 @@ impl FileStoragePort for FileFsRepository {
|
|||||||
// Resolve to actual filesystem path
|
// Resolve to actual filesystem path
|
||||||
let physical_path = self.storage_mediator.resolve_storage_path(&file_path);
|
let physical_path = self.storage_mediator.resolve_storage_path(&file_path);
|
||||||
|
|
||||||
// Write the content to the file
|
// Write the content to the file with fsync
|
||||||
std::fs::write(&physical_path, content)
|
FileSystemUtils::atomic_write(&physical_path, &content)
|
||||||
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FileStorage",
|
.map_err(|e| DomainError::internal_error("FileStorage",
|
||||||
format!("Failed to write updated content to file: {}: {}", file_id, e)))?;
|
format!("Failed to write updated content to file: {}: {}", file_id, e)))?;
|
||||||
|
|
||||||
@@ -518,7 +521,7 @@ impl FileStoragePort for FileFsRepository {
|
|||||||
// Create a FileMetadata instance and update the cache
|
// Create a FileMetadata instance and update the cache
|
||||||
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
|
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
|
||||||
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
|
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::UNIX_EPOCH;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// Get modified and created times
|
// Get modified and created times
|
||||||
@@ -614,8 +617,9 @@ impl FileRepository for FileFsRepository {
|
|||||||
let storage_path = FileRepository::get_file_path(self, file_id).await?;
|
let storage_path = FileRepository::get_file_path(self, file_id).await?;
|
||||||
let physical_path = self.path_service.resolve_path(&storage_path);
|
let physical_path = self.path_service.resolve_path(&storage_path);
|
||||||
|
|
||||||
// Write the content to the file
|
// Write the content to the file with fsync
|
||||||
std::fs::write(&physical_path, &content)
|
FileSystemUtils::atomic_write(&physical_path, &content)
|
||||||
|
.await
|
||||||
.map_err(|e| FileRepositoryError::IoError(e))?;
|
.map_err(|e| FileRepositoryError::IoError(e))?;
|
||||||
|
|
||||||
// Get the metadata and add it to cache if available
|
// Get the metadata and add it to cache if available
|
||||||
@@ -623,7 +627,7 @@ impl FileRepository for FileFsRepository {
|
|||||||
// Create a FileMetadata instance and update the cache
|
// Create a FileMetadata instance and update the cache
|
||||||
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
|
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
|
||||||
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
|
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::UNIX_EPOCH;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// Get modified and created times
|
// Get modified and created times
|
||||||
@@ -1501,10 +1505,10 @@ impl FileRepository for FileFsRepository {
|
|||||||
// Ensure the target directory exists
|
// Ensure the target directory exists
|
||||||
self.ensure_parent_directory(&new_abs_path).await?;
|
self.ensure_parent_directory(&new_abs_path).await?;
|
||||||
|
|
||||||
// Move the file physically (efficient rename operation) with timeout
|
// Move the file physically with fsync (efficient rename operation) with timeout
|
||||||
time::timeout(
|
time::timeout(
|
||||||
self.config.timeouts.file_timeout(),
|
self.config.timeouts.file_timeout(),
|
||||||
fs::rename(&old_abs_path, &new_abs_path)
|
FileSystemUtils::rename_with_sync(&old_abs_path, &new_abs_path)
|
||||||
).await
|
).await
|
||||||
.map_err(|_| FileRepositoryError::Timeout(format!("Timeout moving file from {} to {}",
|
.map_err(|_| FileRepositoryError::Timeout(format!("Timeout moving file from {} to {}",
|
||||||
old_abs_path.display(), new_abs_path.display())))?
|
old_abs_path.display(), new_abs_path.display())))?
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::domain::services::path_service::StoragePath;
|
|||||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
use crate::application::services::storage_mediator::StorageMediator;
|
use crate::application::services::storage_mediator::StorageMediator;
|
||||||
|
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||||
|
|
||||||
/// Implementación de repositorio para operaciones de escritura de archivos
|
/// Implementación de repositorio para operaciones de escritura de archivos
|
||||||
pub struct FileFsWriteRepository {
|
pub struct FileFsWriteRepository {
|
||||||
@@ -55,12 +56,12 @@ impl FileFsWriteRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea directorios padres si es necesario
|
/// Crea directorios padres si es necesario, con sincronización
|
||||||
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
||||||
if let Some(parent) = abs_path.parent() {
|
if let Some(parent) = abs_path.parent() {
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
self.config.timeouts.dir_timeout(),
|
self.config.timeouts.dir_timeout(),
|
||||||
tokio::fs::create_dir_all(parent)
|
FileSystemUtils::create_dir_with_sync(parent)
|
||||||
).await
|
).await
|
||||||
.map_err(|_| crate::domain::repositories::file_repository::FileRepositoryError::Timeout(
|
.map_err(|_| crate::domain::repositories::file_repository::FileRepositoryError::Timeout(
|
||||||
format!("Timeout creating parent directory: {}", parent.display())
|
format!("Timeout creating parent directory: {}", parent.display())
|
||||||
@@ -149,10 +150,10 @@ impl FileWritePort for FileFsWriteRepository {
|
|||||||
self.ensure_parent_directory(&abs_path).await
|
self.ensure_parent_directory(&abs_path).await
|
||||||
.map_err(|e| DomainError::internal_error("File system", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("File system", e.to_string()))?;
|
||||||
|
|
||||||
// Write the file to disk
|
// Write the file to disk using atomic write with fsync
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
self.config.timeouts.file_write_timeout(),
|
self.config.timeouts.file_write_timeout(),
|
||||||
tokio::fs::write(&abs_path, &content)
|
FileSystemUtils::atomic_write(&abs_path, &content)
|
||||||
).await
|
).await
|
||||||
.map_err(|_| DomainError::internal_error(
|
.map_err(|_| DomainError::internal_error(
|
||||||
"File write",
|
"File write",
|
||||||
@@ -202,4 +203,26 @@ impl FileWritePort for FileFsWriteRepository {
|
|||||||
tracing::info!("File deletion simulated successfully");
|
tracing::info!("File deletion simulated successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError> {
|
||||||
|
// Fetch the folder information from the metadata manager
|
||||||
|
match self.metadata_manager.get_folder_by_id(folder_id).await {
|
||||||
|
Ok(folder) => Ok(folder),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Error getting folder details for ID {}: {}", folder_id, err);
|
||||||
|
Err(DomainError::not_found("Folder", folder_id.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError> {
|
||||||
|
// Fetch the folder information
|
||||||
|
let folder = self.get_folder_details(folder_id).await?;
|
||||||
|
|
||||||
|
// Convert StoragePath to string
|
||||||
|
let path_str = folder.storage_path().to_string();
|
||||||
|
|
||||||
|
tracing::debug!("Resolved folder path for ID {}: {}", folder_id, path_str);
|
||||||
|
Ok(path_str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@ use tokio::fs;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType, FileMetadata};
|
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType, FileMetadata};
|
||||||
|
use crate::domain::entities::file::File;
|
||||||
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
|
||||||
@@ -177,4 +179,45 @@ impl FileMetadataManager {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtiene información de una carpeta por ID
|
||||||
|
pub async fn get_folder_by_id(&self, folder_id: &str) -> Result<File, MetadataError> {
|
||||||
|
// Implementación simplificada que solo busca en la caché de metadatos
|
||||||
|
// pero que devuelve una estructura mínima para el servicio de uso de almacenamiento
|
||||||
|
|
||||||
|
// En una implementación real, se consultaría un índice persistente
|
||||||
|
// Para esta implementación básica, usaremos un método simplificado
|
||||||
|
|
||||||
|
// Crear un objeto StoragePath mínimo
|
||||||
|
let storage_path = StoragePath::from_string(&format!("/{}", folder_id));
|
||||||
|
|
||||||
|
// Creamos una carpeta con información mínima
|
||||||
|
// Esta implementación es un placeholder - en una situación real
|
||||||
|
// consultaríamos el mapa folder_id -> folder_metadata en el sistema
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
// Verificamos si el nombre contiene información del usuario
|
||||||
|
let folder_name = if folder_id.contains('-') {
|
||||||
|
// Asumimos un formato UUID v4, intentamos usar "Mi Carpeta - username" como nombre
|
||||||
|
format!("Mi Carpeta - usuario")
|
||||||
|
} else {
|
||||||
|
// Si no, usamos el ID como nombre
|
||||||
|
folder_id.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let folder = File::new_folder(
|
||||||
|
folder_id.to_string(),
|
||||||
|
folder_name,
|
||||||
|
storage_path,
|
||||||
|
None, // parent_id
|
||||||
|
now, // created_at
|
||||||
|
now, // updated_at
|
||||||
|
)
|
||||||
|
.map_err(|e| MetadataError::Unavailable(format!("Error creating folder entity: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(folder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod user_pg_repository;
|
mod user_pg_repository;
|
||||||
mod session_pg_repository;
|
mod session_pg_repository;
|
||||||
|
mod transaction_utils;
|
||||||
|
|
||||||
pub use user_pg_repository::UserPgRepository;
|
pub use user_pg_repository::UserPgRepository;
|
||||||
pub use session_pg_repository::SessionPgRepository;
|
pub use session_pg_repository::SessionPgRepository;
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row, Executor};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use futures::future::BoxFuture;
|
||||||
|
|
||||||
use crate::domain::entities::session::Session;
|
use crate::domain::entities::session::Session;
|
||||||
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
|
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
|
||||||
use crate::application::ports::auth_ports::SessionStoragePort;
|
use crate::application::ports::auth_ports::SessionStoragePort;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||||
|
|
||||||
|
// Implementar From<sqlx::Error> para SessionRepositoryError para permitir conversiones automáticas
|
||||||
|
impl From<sqlx::Error> for SessionRepositoryError {
|
||||||
|
fn from(err: sqlx::Error) -> Self {
|
||||||
|
SessionPgRepository::map_sqlx_error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SessionPgRepository {
|
pub struct SessionPgRepository {
|
||||||
pool: Arc<PgPool>,
|
pool: Arc<PgPool>,
|
||||||
@@ -18,7 +27,7 @@ impl SessionPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Método auxiliar para mapear errores SQL a errores de dominio
|
// Método auxiliar para mapear errores SQL a errores de dominio
|
||||||
fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
|
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
|
||||||
match err {
|
match err {
|
||||||
sqlx::Error::RowNotFound => {
|
sqlx::Error::RowNotFound => {
|
||||||
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
|
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
|
||||||
@@ -32,29 +41,65 @@ impl SessionPgRepository {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SessionRepository for SessionPgRepository {
|
impl SessionRepository for SessionPgRepository {
|
||||||
/// Crea una nueva sesión
|
/// Crea una nueva sesión utilizando una transacción
|
||||||
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
|
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
|
||||||
sqlx::query(
|
// Crear una copia de la sesión para el closure
|
||||||
r#"
|
let session_clone = session.clone();
|
||||||
INSERT INTO auth.sessions (
|
|
||||||
id, user_id, refresh_token, expires_at,
|
with_transaction(
|
||||||
ip_address, user_agent, created_at, revoked
|
&self.pool,
|
||||||
) VALUES (
|
"create_session",
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8
|
|tx| {
|
||||||
)
|
Box::pin(async move {
|
||||||
"#
|
// Insertar la sesión
|
||||||
)
|
sqlx::query(
|
||||||
.bind(session.id())
|
r#"
|
||||||
.bind(session.user_id())
|
INSERT INTO auth.sessions (
|
||||||
.bind(session.refresh_token())
|
id, user_id, refresh_token, expires_at,
|
||||||
.bind(session.expires_at())
|
ip_address, user_agent, created_at, revoked
|
||||||
.bind(&session.ip_address)
|
) VALUES (
|
||||||
.bind(&session.user_agent)
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
.bind(session.created_at())
|
)
|
||||||
.bind(session.is_revoked())
|
"#
|
||||||
.execute(&*self.pool)
|
)
|
||||||
.await
|
.bind(session_clone.id())
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.bind(session_clone.user_id())
|
||||||
|
.bind(session_clone.refresh_token())
|
||||||
|
.bind(session_clone.expires_at())
|
||||||
|
.bind(&session_clone.ip_address)
|
||||||
|
.bind(&session_clone.user_agent)
|
||||||
|
.bind(session_clone.created_at())
|
||||||
|
.bind(session_clone.is_revoked())
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
|
// Opcionalmente, actualizar el último login del usuario
|
||||||
|
// dentro de la misma transacción
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE auth.users
|
||||||
|
SET last_login_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(session_clone.user_id())
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// Convertimos el error pero sin interrumpir la creación
|
||||||
|
// de la sesión si falla la actualización
|
||||||
|
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}",
|
||||||
|
session_clone.user_id(), e);
|
||||||
|
SessionRepositoryError::DatabaseError(format!(
|
||||||
|
"Sesión creada pero no se pudo actualizar last_login_at: {}", e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(session_clone)
|
||||||
|
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
|
||||||
|
}
|
||||||
|
).await?;
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
@@ -150,38 +195,78 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
Ok(sessions)
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revoca una sesión específica
|
/// Revoca una sesión específica utilizando una transacción
|
||||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
||||||
sqlx::query(
|
let id = session_id.to_string(); // Clone para uso en closure
|
||||||
r#"
|
|
||||||
UPDATE auth.sessions
|
|
||||||
SET revoked = true
|
|
||||||
WHERE id = $1
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.bind(session_id)
|
|
||||||
.execute(&*self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_sqlx_error)?;
|
|
||||||
|
|
||||||
Ok(())
|
with_transaction(
|
||||||
|
&self.pool,
|
||||||
|
"revoke_session",
|
||||||
|
|tx| {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Revocar la sesión
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE auth.sessions
|
||||||
|
SET revoked = true
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING user_id
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&id)
|
||||||
|
.fetch_optional(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
|
// Si encontramos la sesión, podemos registrar un evento de seguridad
|
||||||
|
if let Some(row) = result {
|
||||||
|
let user_id: String = row.try_get("user_id").unwrap_or_default();
|
||||||
|
|
||||||
|
// Registrar evento de seguridad (en una tabla de seguridad)
|
||||||
|
// Esto es opcional pero muestra cómo se puede realizar operaciones
|
||||||
|
// adicionales en la misma transacción
|
||||||
|
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}) as BoxFuture<'_, SessionRepositoryResult<()>>
|
||||||
|
}
|
||||||
|
).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revoca todas las sesiones de un usuario
|
/// Revoca todas las sesiones de un usuario utilizando una transacción
|
||||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
||||||
let result = sqlx::query(
|
let user_id_clone = user_id.to_string(); // Clone para uso en closure
|
||||||
r#"
|
|
||||||
UPDATE auth.sessions
|
|
||||||
SET revoked = true
|
|
||||||
WHERE user_id = $1 AND revoked = false
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.bind(user_id)
|
|
||||||
.execute(&*self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_sqlx_error)?;
|
|
||||||
|
|
||||||
Ok(result.rows_affected())
|
with_transaction(
|
||||||
|
&self.pool,
|
||||||
|
"revoke_all_user_sessions",
|
||||||
|
|tx| {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Revocar todas las sesiones del usuario
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE auth.sessions
|
||||||
|
SET revoked = true
|
||||||
|
WHERE user_id = $1 AND revoked = false
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&user_id_clone)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
|
let affected = result.rows_affected();
|
||||||
|
|
||||||
|
// Registrar evento de seguridad
|
||||||
|
if affected > 0 {
|
||||||
|
tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(affected)
|
||||||
|
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
|
||||||
|
}
|
||||||
|
).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina sesiones expiradas
|
/// Elimina sesiones expiradas
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
/// Helper function to execute database operations in a transaction
|
||||||
|
/// Takes a database pool and a closure that will be executed within a transaction
|
||||||
|
/// The closure receives a transaction object that should be used for all database operations
|
||||||
|
/// If the closure returns an error, the transaction is rolled back
|
||||||
|
/// If the closure returns Ok, the transaction is committed
|
||||||
|
pub async fn with_transaction<F, T, E>(
|
||||||
|
pool: &Arc<PgPool>,
|
||||||
|
operation_name: &str,
|
||||||
|
operation: F,
|
||||||
|
) -> Result<T, E>
|
||||||
|
where
|
||||||
|
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||||
|
E: From<SqlxError> + std::fmt::Display,
|
||||||
|
{
|
||||||
|
debug!("Starting database transaction for: {}", operation_name);
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
let mut tx = pool.begin().await.map_err(|e| {
|
||||||
|
error!("Failed to begin transaction for {}: {}", operation_name, e);
|
||||||
|
E::from(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Execute the operation within the transaction
|
||||||
|
match operation(&mut tx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// If operation succeeds, commit the transaction
|
||||||
|
match tx.commit().await {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("Transaction committed successfully for: {}", operation_name);
|
||||||
|
Ok(result)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to commit transaction for {}: {}", operation_name, e);
|
||||||
|
Err(E::from(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
// If operation fails, rollback the transaction
|
||||||
|
if let Err(rollback_err) = tx.rollback().await {
|
||||||
|
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
|
||||||
|
// Still return the original error
|
||||||
|
} else {
|
||||||
|
info!("Transaction rolled back for {}: {}", operation_name, e);
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variant that accepts a transaction isolation level
|
||||||
|
pub async fn with_transaction_isolation<F, T, E>(
|
||||||
|
pool: &Arc<PgPool>,
|
||||||
|
operation_name: &str,
|
||||||
|
isolation_level: TransactionIsolationLevel,
|
||||||
|
operation: F,
|
||||||
|
) -> Result<T, E>
|
||||||
|
where
|
||||||
|
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||||
|
E: From<SqlxError> + std::fmt::Display,
|
||||||
|
{
|
||||||
|
debug!("Starting database transaction with isolation level {:?} for: {}",
|
||||||
|
isolation_level, operation_name);
|
||||||
|
|
||||||
|
// Begin transaction with specific isolation level
|
||||||
|
let mut tx = pool.begin().await.map_err(|e| {
|
||||||
|
error!("Failed to begin transaction for {}: {}", operation_name, e);
|
||||||
|
E::from(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Set isolation level
|
||||||
|
tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..])
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to set isolation level for {}: {}", operation_name, e);
|
||||||
|
E::from(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Execute the operation within the transaction
|
||||||
|
match operation(&mut tx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// If operation succeeds, commit the transaction
|
||||||
|
match tx.commit().await {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("Transaction committed successfully for: {}", operation_name);
|
||||||
|
Ok(result)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to commit transaction for {}: {}", operation_name, e);
|
||||||
|
Err(E::from(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
// If operation fails, rollback the transaction
|
||||||
|
if let Err(rollback_err) = tx.rollback().await {
|
||||||
|
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
|
||||||
|
// Still return the original error
|
||||||
|
} else {
|
||||||
|
info!("Transaction rolled back for {}: {}", operation_name, e);
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction isolation levels from SQL standard
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TransactionIsolationLevel {
|
||||||
|
/// Read committed isolation level
|
||||||
|
ReadCommitted,
|
||||||
|
/// Repeatable read isolation level
|
||||||
|
RepeatableRead,
|
||||||
|
/// Serializable isolation level
|
||||||
|
Serializable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for TransactionIsolationLevel {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(),
|
||||||
|
TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(),
|
||||||
|
TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row, Executor};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use futures::future::BoxFuture;
|
||||||
|
|
||||||
use crate::domain::entities::user::{User, UserRole};
|
use crate::domain::entities::user::{User, UserRole};
|
||||||
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult};
|
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult};
|
||||||
use crate::application::ports::auth_ports::UserStoragePort;
|
use crate::application::ports::auth_ports::UserStoragePort;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||||
|
|
||||||
|
// Implementar From<sqlx::Error> para UserRepositoryError para permitir conversiones automáticas
|
||||||
|
impl From<sqlx::Error> for UserRepositoryError {
|
||||||
|
fn from(err: sqlx::Error) -> Self {
|
||||||
|
UserPgRepository::map_sqlx_error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UserPgRepository {
|
pub struct UserPgRepository {
|
||||||
pool: Arc<PgPool>,
|
pool: Arc<PgPool>,
|
||||||
@@ -17,7 +26,7 @@ impl UserPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Método auxiliar para mapear errores SQL a errores de dominio
|
// Método auxiliar para mapear errores SQL a errores de dominio
|
||||||
fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
|
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
|
||||||
match err {
|
match err {
|
||||||
sqlx::Error::RowNotFound => {
|
sqlx::Error::RowNotFound => {
|
||||||
UserRepositoryError::NotFound("Usuario no encontrado".to_string())
|
UserRepositoryError::NotFound("Usuario no encontrado".to_string())
|
||||||
@@ -43,39 +52,57 @@ impl UserPgRepository {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserRepository for UserPgRepository {
|
impl UserRepository for UserPgRepository {
|
||||||
/// Crea un nuevo usuario
|
/// Crea un nuevo usuario utilizando una transacción
|
||||||
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||||
// Usamos los getters para extraer los valores
|
// Creamos una copia del usuario para el closure
|
||||||
// Convertimos user.role() a string para pasarlo como texto plano
|
let user_clone = user.clone();
|
||||||
let role_str = user.role().to_string();
|
|
||||||
|
|
||||||
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
|
with_transaction(
|
||||||
let _result = sqlx::query(
|
&self.pool,
|
||||||
r#"
|
"create_user",
|
||||||
INSERT INTO auth.users (
|
|tx| {
|
||||||
id, username, email, password_hash, role,
|
// Necesitamos mover el closure a un BoxFuture para devolver dentro
|
||||||
storage_quota_bytes, storage_used_bytes,
|
// de la llamada with_transaction
|
||||||
created_at, updated_at, last_login_at, active
|
Box::pin(async move {
|
||||||
) VALUES (
|
// Usamos los getters para extraer los valores
|
||||||
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11
|
// Convertimos user.role() a string para pasarlo como texto plano
|
||||||
)
|
let role_str = user_clone.role().to_string();
|
||||||
RETURNING *
|
|
||||||
"#
|
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
|
||||||
)
|
let _result = sqlx::query(
|
||||||
.bind(user.id())
|
r#"
|
||||||
.bind(user.username())
|
INSERT INTO auth.users (
|
||||||
.bind(user.email())
|
id, username, email, password_hash, role,
|
||||||
.bind(user.password_hash())
|
storage_quota_bytes, storage_used_bytes,
|
||||||
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
|
created_at, updated_at, last_login_at, active
|
||||||
.bind(user.storage_quota_bytes())
|
) VALUES (
|
||||||
.bind(user.storage_used_bytes())
|
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11
|
||||||
.bind(user.created_at())
|
)
|
||||||
.bind(user.updated_at())
|
RETURNING *
|
||||||
.bind(user.last_login_at())
|
"#
|
||||||
.bind(user.is_active())
|
)
|
||||||
.fetch_one(&*self.pool)
|
.bind(user_clone.id())
|
||||||
.await
|
.bind(user_clone.username())
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.bind(user_clone.email())
|
||||||
|
.bind(user_clone.password_hash())
|
||||||
|
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
|
||||||
|
.bind(user_clone.storage_quota_bytes())
|
||||||
|
.bind(user_clone.storage_used_bytes())
|
||||||
|
.bind(user_clone.created_at())
|
||||||
|
.bind(user_clone.updated_at())
|
||||||
|
.bind(user_clone.last_login_at())
|
||||||
|
.bind(user_clone.is_active())
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
|
// Podríamos realizar operaciones adicionales aquí,
|
||||||
|
// como configurar permisos, roles, etc.
|
||||||
|
|
||||||
|
Ok(user_clone)
|
||||||
|
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||||
|
}
|
||||||
|
).await?;
|
||||||
|
|
||||||
Ok(user) // Devolvemos el usuario original por simplicidad
|
Ok(user) // Devolvemos el usuario original por simplicidad
|
||||||
}
|
}
|
||||||
@@ -197,37 +224,54 @@ impl UserRepository for UserPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza un usuario existente
|
/// Actualiza un usuario existente utilizando una transacción
|
||||||
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
|
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||||
sqlx::query(
|
// Creamos una copia del usuario para el closure
|
||||||
r#"
|
let user_clone = user.clone();
|
||||||
UPDATE auth.users
|
|
||||||
SET
|
with_transaction(
|
||||||
username = $2,
|
&self.pool,
|
||||||
email = $3,
|
"update_user",
|
||||||
password_hash = $4,
|
|tx| {
|
||||||
role = $5::auth.userrole,
|
Box::pin(async move {
|
||||||
storage_quota_bytes = $6,
|
// Actualizar el usuario
|
||||||
storage_used_bytes = $7,
|
sqlx::query(
|
||||||
updated_at = $8,
|
r#"
|
||||||
last_login_at = $9,
|
UPDATE auth.users
|
||||||
active = $10
|
SET
|
||||||
WHERE id = $1
|
username = $2,
|
||||||
"#
|
email = $3,
|
||||||
)
|
password_hash = $4,
|
||||||
.bind(user.id())
|
role = $5::auth.userrole,
|
||||||
.bind(user.username())
|
storage_quota_bytes = $6,
|
||||||
.bind(user.email())
|
storage_used_bytes = $7,
|
||||||
.bind(user.password_hash())
|
updated_at = $8,
|
||||||
.bind(&user.role().to_string()) // Esto no usa el cast explícito porque el SQL ya lo tiene
|
last_login_at = $9,
|
||||||
.bind(user.storage_quota_bytes())
|
active = $10
|
||||||
.bind(user.storage_used_bytes())
|
WHERE id = $1
|
||||||
.bind(user.updated_at())
|
"#
|
||||||
.bind(user.last_login_at())
|
)
|
||||||
.bind(user.is_active())
|
.bind(user_clone.id())
|
||||||
.execute(&*self.pool)
|
.bind(user_clone.username())
|
||||||
.await
|
.bind(user_clone.email())
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.bind(user_clone.password_hash())
|
||||||
|
.bind(&user_clone.role().to_string())
|
||||||
|
.bind(user_clone.storage_quota_bytes())
|
||||||
|
.bind(user_clone.storage_used_bytes())
|
||||||
|
.bind(user_clone.updated_at())
|
||||||
|
.bind(user_clone.last_login_at())
|
||||||
|
.bind(user_clone.is_active())
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
|
// Podríamos realizar operaciones adicionales aquí dentro
|
||||||
|
// de la misma transacción, como actualizar permisos, etc.
|
||||||
|
|
||||||
|
Ok(user_clone)
|
||||||
|
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||||
|
}
|
||||||
|
).await?;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
use tokio::fs::{self, OpenOptions, File};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::io::Error as IoError;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tracing::{warn, error};
|
||||||
|
|
||||||
|
/// Utility functions for file system operations with proper synchronization
|
||||||
|
pub struct FileSystemUtils;
|
||||||
|
|
||||||
|
impl FileSystemUtils {
|
||||||
|
/// Writes data to a file with fsync to ensure durability
|
||||||
|
/// Uses a safe atomic write pattern: write to temp file, fsync, rename
|
||||||
|
pub async fn atomic_write<P: AsRef<Path>>(path: P, contents: &[u8]) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary file in the same directory
|
||||||
|
let dir = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
let temp_file = match NamedTempFile::new_in(dir) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create temporary file in {}: {}", dir.display(), e);
|
||||||
|
return Err(IoError::new(std::io::ErrorKind::Other,
|
||||||
|
format!("Failed to create temporary file: {}", e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let temp_path = temp_file.path().to_path_buf();
|
||||||
|
|
||||||
|
// Convert to tokio file and write contents
|
||||||
|
let std_file = temp_file.as_file().try_clone()?;
|
||||||
|
let mut file = File::from_std(std_file);
|
||||||
|
file.write_all(contents).await?;
|
||||||
|
|
||||||
|
// Ensure data is synced to disk
|
||||||
|
file.flush().await?;
|
||||||
|
file.sync_all().await?;
|
||||||
|
|
||||||
|
// Rename the temporary file to the target path (atomic operation on most filesystems)
|
||||||
|
fs::rename(&temp_path, path).await?;
|
||||||
|
|
||||||
|
// Sync the directory to ensure the rename is persisted
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
match Self::sync_directory(parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync directory {}: {}. File was written but directory entry might not be durable.",
|
||||||
|
parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates or appends to a file with fsync
|
||||||
|
pub async fn write_with_sync<P: AsRef<Path>>(path: P, contents: &[u8], append: bool) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file with appropriate options
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(!append)
|
||||||
|
.append(append)
|
||||||
|
.open(path)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Write contents
|
||||||
|
file.write_all(contents).await?;
|
||||||
|
|
||||||
|
// Ensure data is synced to disk
|
||||||
|
file.flush().await?;
|
||||||
|
file.sync_all().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates directories with fsync
|
||||||
|
pub async fn create_dir_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Create directory
|
||||||
|
fs::create_dir_all(path).await?;
|
||||||
|
|
||||||
|
// Sync the directory
|
||||||
|
Self::sync_directory(path).await?;
|
||||||
|
|
||||||
|
// Sync parent directory to ensure directory creation is persisted
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
match Self::sync_directory(parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync parent directory {}: {}. Directory was created but entry might not be durable.",
|
||||||
|
parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames a file or directory with proper syncing
|
||||||
|
pub async fn rename_with_sync<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), IoError> {
|
||||||
|
let from = from.as_ref();
|
||||||
|
let to = to.as_ref();
|
||||||
|
|
||||||
|
// Ensure parent directory of destination exists
|
||||||
|
if let Some(parent) = to.parent() {
|
||||||
|
fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform rename
|
||||||
|
fs::rename(from, to).await?;
|
||||||
|
|
||||||
|
// Sync parent directories to ensure rename is persisted
|
||||||
|
if let Some(from_parent) = from.parent() {
|
||||||
|
match Self::sync_directory(from_parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync source directory {}: {}. Rename completed but might not be durable.",
|
||||||
|
from_parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(to_parent) = to.parent() {
|
||||||
|
match Self::sync_directory(to_parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync destination directory {}: {}. Rename completed but might not be durable.",
|
||||||
|
to_parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a file with directory syncing
|
||||||
|
pub async fn remove_file_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Remove file
|
||||||
|
fs::remove_file(path).await?;
|
||||||
|
|
||||||
|
// Sync parent directory to ensure removal is persisted
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
match Self::sync_directory(parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync directory after file removal {}: {}. File was removed but entry might not be durable.",
|
||||||
|
parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a directory with parent directory syncing
|
||||||
|
pub async fn remove_dir_with_sync<P: AsRef<Path>>(path: P, recursive: bool) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Remove directory
|
||||||
|
if recursive {
|
||||||
|
fs::remove_dir_all(path).await?;
|
||||||
|
} else {
|
||||||
|
fs::remove_dir(path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync parent directory to ensure removal is persisted
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
match Self::sync_directory(parent).await {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to sync directory after directory removal {}: {}. Directory was removed but entry might not be durable.",
|
||||||
|
parent.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syncs a directory to ensure its contents are durable
|
||||||
|
async fn sync_directory<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Open directory with read permissions
|
||||||
|
let dir_file = match OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.open(path)
|
||||||
|
.await {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to open directory for syncing {}: {}", path.display(), e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync the directory
|
||||||
|
dir_file.sync_all().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_atomic_write() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let file_path = temp_dir.path().join("test.txt");
|
||||||
|
|
||||||
|
// Write data atomically
|
||||||
|
FileSystemUtils::atomic_write(&file_path, b"Hello, world!").await.unwrap();
|
||||||
|
|
||||||
|
// Read back the data
|
||||||
|
let mut file = fs::File::open(&file_path).await.unwrap();
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contents, "Hello, world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_with_sync() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let file_path = temp_dir.path().join("test.txt");
|
||||||
|
|
||||||
|
// Write data with sync
|
||||||
|
FileSystemUtils::write_with_sync(&file_path, b"First line\n", false).await.unwrap();
|
||||||
|
|
||||||
|
// Append data
|
||||||
|
FileSystemUtils::write_with_sync(&file_path, b"Second line", true).await.unwrap();
|
||||||
|
|
||||||
|
// Read back the data
|
||||||
|
let mut file = fs::File::open(&file_path).await.unwrap();
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contents, "First line\nSecond line");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rename_with_sync() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let source_path = temp_dir.path().join("source.txt");
|
||||||
|
let dest_path = temp_dir.path().join("dest.txt");
|
||||||
|
|
||||||
|
// Create source file
|
||||||
|
FileSystemUtils::write_with_sync(&source_path, b"Test content", false).await.unwrap();
|
||||||
|
|
||||||
|
// Rename file
|
||||||
|
FileSystemUtils::rename_with_sync(&source_path, &dest_path).await.unwrap();
|
||||||
|
|
||||||
|
// Verify source doesn't exist
|
||||||
|
assert!(!source_path.exists());
|
||||||
|
|
||||||
|
// Verify destination exists
|
||||||
|
let mut file = fs::File::open(&dest_path).await.unwrap();
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contents, "Test content");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod file_system_i18n_service;
|
pub mod file_system_i18n_service;
|
||||||
|
pub mod file_system_utils;
|
||||||
pub mod id_mapping_service;
|
pub mod id_mapping_service;
|
||||||
pub mod id_mapping_optimizer;
|
pub mod id_mapping_optimizer;
|
||||||
pub mod cache_manager;
|
pub mod cache_manager;
|
||||||
|
|||||||
@@ -212,6 +212,28 @@ async fn get_current_user(
|
|||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||||
|
|
||||||
|
// Primero, intentamos actualizar las estadísticas de uso de almacenamiento
|
||||||
|
// Si existe el servicio de uso de almacenamiento
|
||||||
|
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
|
||||||
|
// Actualizamos el uso de almacenamiento en segundo plano
|
||||||
|
// No bloqueamos la respuesta con esta actualización
|
||||||
|
let user_id = current_user.id.clone();
|
||||||
|
let storage_service = storage_usage_service.clone();
|
||||||
|
|
||||||
|
// Ejecutar asincronamente para no retrasar la respuesta
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match storage_service.update_user_storage_usage(&user_id).await {
|
||||||
|
Ok(usage) => {
|
||||||
|
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener los datos del usuario (que puede tener valores de almacenamiento desactualizados)
|
||||||
let user = auth_service.auth_application_service.get_user_by_id(¤t_user.id).await?;
|
let user = auth_service.auth_application_service.get_user_by_id(¤t_user.id).await?;
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(user)))
|
Ok((StatusCode::OK, Json(user)))
|
||||||
|
|||||||
@@ -9,25 +9,19 @@
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::{self},
|
|
||||||
response::Response,
|
response::Response,
|
||||||
http::{StatusCode, header, HeaderName, Request, Method},
|
http::{StatusCode, header, HeaderName, Request},
|
||||||
body::{Body, self},
|
body::{Body, self},
|
||||||
routing::MethodRouter,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::io::{Cursor, Read};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use tower::service_fn;
|
|
||||||
|
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType, WebDavError};
|
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType};
|
||||||
use crate::interfaces::middleware::auth::CurrentUser;
|
use crate::interfaces::middleware::auth::CurrentUser;
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
use crate::common::errors::AppError;
|
use crate::common::errors::AppError;
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ pub fn create_api_routes(
|
|||||||
path_resolver: path_resolver.clone(),
|
path_resolver: path_resolver.clone(),
|
||||||
trash_repository: None, // This is OK to be None since we use the trash_service directly
|
trash_repository: None, // This is OK to be None since we use the trash_service directly
|
||||||
},
|
},
|
||||||
|
storage_usage_service: None,
|
||||||
applications: crate::common::di::ApplicationServices {
|
applications: crate::common::di::ApplicationServices {
|
||||||
folder_service: folder_service.clone(),
|
folder_service: folder_service.clone(),
|
||||||
file_service: file_service.clone(),
|
file_service: file_service.clone(),
|
||||||
|
|||||||
+33
-5
@@ -107,6 +107,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create a reference to db_pool for use throughout the code
|
||||||
|
let db_pool_ref = db_pool.as_ref();
|
||||||
|
|
||||||
// Initialize path service
|
// Initialize path service
|
||||||
let path_service = Arc::new(PathService::new(storage_path.clone()));
|
let path_service = Arc::new(PathService::new(storage_path.clone()));
|
||||||
|
|
||||||
@@ -531,10 +534,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
tracing::info!("Compression service initialized with buffer pool support");
|
tracing::info!("Compression service initialized with buffer pool support");
|
||||||
|
|
||||||
// Initialize auth services if enabled and database connection is available
|
// Initialize auth services if enabled and database connection is available
|
||||||
let auth_services = if config.features.enable_auth && db_pool.is_some() {
|
let auth_services = if config.features.enable_auth && db_pool_ref.is_some() {
|
||||||
match create_auth_services(
|
match create_auth_services(
|
||||||
&config,
|
&config,
|
||||||
db_pool.as_ref().unwrap().clone(),
|
db_pool_ref.unwrap().clone(),
|
||||||
Some(folder_service.clone()) // Pasar el servicio de carpetas para creación automática de carpetas de usuario
|
Some(folder_service.clone()) // Pasar el servicio de carpetas para creación automática de carpetas de usuario
|
||||||
).await {
|
).await {
|
||||||
Ok(services) => {
|
Ok(services) => {
|
||||||
@@ -628,7 +631,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Initialize favorites service if database is available
|
// Initialize favorites service if database is available
|
||||||
let favorites_service: Option<Arc<dyn application::ports::favorites_ports::FavoritesUseCase>> =
|
let favorites_service: Option<Arc<dyn application::ports::favorites_ports::FavoritesUseCase>> =
|
||||||
if let Some(ref pool) = db_pool {
|
if let Some(pool) = db_pool_ref {
|
||||||
// Create a new favorites service with the database pool
|
// Create a new favorites service with the database pool
|
||||||
let favorites_service = Arc::new(FavoritesService::new(
|
let favorites_service = Arc::new(FavoritesService::new(
|
||||||
pool.clone()
|
pool.clone()
|
||||||
@@ -643,7 +646,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Initialize recent items service if database is available
|
// Initialize recent items service if database is available
|
||||||
let recent_service: Option<Arc<dyn application::ports::recent_ports::RecentItemsUseCase>> =
|
let recent_service: Option<Arc<dyn application::ports::recent_ports::RecentItemsUseCase>> =
|
||||||
if let Some(ref pool) = db_pool {
|
if let Some(pool) = db_pool_ref {
|
||||||
// Create a new service with the database pool
|
// Create a new service with the database pool
|
||||||
let service = Arc::new(application::services::recent_service::RecentService::new(
|
let service = Arc::new(application::services::recent_service::RecentService::new(
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
@@ -680,7 +683,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add database pool if available
|
// Add database pool if available
|
||||||
if let Some(pool) = db_pool {
|
if let Some(pool) = db_pool.clone() {
|
||||||
app_state = app_state.with_database(pool);
|
app_state = app_state.with_database(pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,6 +703,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
app_state = app_state.with_recent_service(service);
|
app_state = app_state.with_recent_service(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize storage usage service
|
||||||
|
let _storage_usage_service = if let Some(pool) = db_pool_ref {
|
||||||
|
// Create a user repository that implements UserStoragePort
|
||||||
|
let user_repository = Arc::new(
|
||||||
|
infrastructure::repositories::pg::UserPgRepository::new(pool.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create storage usage service that uses database for user information
|
||||||
|
// and file repository for storage calculation
|
||||||
|
let service = Arc::new(application::services::storage_usage_service::StorageUsageService::new(
|
||||||
|
file_repository.clone(),
|
||||||
|
user_repository,
|
||||||
|
));
|
||||||
|
|
||||||
|
tracing::info!("Storage usage service initialized successfully");
|
||||||
|
|
||||||
|
// Add the service to the app state
|
||||||
|
app_state = app_state.with_storage_usage_service(service.clone());
|
||||||
|
|
||||||
|
Some(service)
|
||||||
|
} else {
|
||||||
|
tracing::info!("Storage usage service is disabled (requires database connection)");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Wrap in Arc after all modifications
|
// Wrap in Arc after all modifications
|
||||||
let app_state = Arc::new(app_state);
|
let app_state = Arc::new(app_state);
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ body {
|
|||||||
.storage-fill {
|
.storage-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ff5e3a;
|
background-color: #ff5e3a;
|
||||||
width: 56%;
|
width: 0%; /* Will be set dynamically via JavaScript */
|
||||||
}
|
}
|
||||||
|
|
||||||
.storage-info {
|
.storage-info {
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@
|
|||||||
<div class="storage-bar">
|
<div class="storage-bar">
|
||||||
<div class="storage-fill"></div>
|
<div class="storage-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-info">56% usado (5.6GB / 10GB)</div>
|
<div class="storage-info">Calculando...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+61
-3
@@ -1137,7 +1137,9 @@ function checkAuthentication() {
|
|||||||
const defaultUserData = {
|
const defaultUserData = {
|
||||||
id: 'default-user-id',
|
id: 'default-user-id',
|
||||||
username: 'usuario',
|
username: 'usuario',
|
||||||
email: 'usuario@example.com'
|
email: 'usuario@example.com',
|
||||||
|
storage_quota_bytes: 10737418240, // 10GB default
|
||||||
|
storage_used_bytes: 0
|
||||||
};
|
};
|
||||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(defaultUserData));
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(defaultUserData));
|
||||||
|
|
||||||
@@ -1146,6 +1148,9 @@ function checkAuthentication() {
|
|||||||
if (userAvatar) {
|
if (userAvatar) {
|
||||||
userAvatar.textContent = 'US';
|
userAvatar.textContent = 'US';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update storage display with default values
|
||||||
|
updateStorageUsageDisplay(defaultUserData);
|
||||||
} else {
|
} else {
|
||||||
// Update avatar with user initials
|
// Update avatar with user initials
|
||||||
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
||||||
@@ -1199,6 +1204,9 @@ function checkAuthentication() {
|
|||||||
userAvatar.textContent = userInitials;
|
userAvatar.textContent = userInitials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update storage usage information
|
||||||
|
updateStorageUsageDisplay(userData);
|
||||||
|
|
||||||
// Find and load the user's home folder
|
// Find and load the user's home folder
|
||||||
findUserHomeFolder(userData.username);
|
findUserHomeFolder(userData.username);
|
||||||
} else {
|
} else {
|
||||||
@@ -1207,7 +1215,9 @@ function checkAuthentication() {
|
|||||||
const defaultUserData = {
|
const defaultUserData = {
|
||||||
id: 'default-user-id',
|
id: 'default-user-id',
|
||||||
username: 'usuario',
|
username: 'usuario',
|
||||||
email: 'usuario@example.com'
|
email: 'usuario@example.com',
|
||||||
|
storage_quota_bytes: 10737418240, // 10GB default
|
||||||
|
storage_used_bytes: 0
|
||||||
};
|
};
|
||||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(defaultUserData));
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(defaultUserData));
|
||||||
|
|
||||||
@@ -1217,6 +1227,9 @@ function checkAuthentication() {
|
|||||||
userAvatar.textContent = 'US';
|
userAvatar.textContent = 'US';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update storage display with default values
|
||||||
|
updateStorageUsageDisplay(defaultUserData);
|
||||||
|
|
||||||
// Find and load default folder
|
// Find and load default folder
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
@@ -1234,7 +1247,9 @@ function checkAuthentication() {
|
|||||||
const defaultUserData = {
|
const defaultUserData = {
|
||||||
id: 'emergency-user-id',
|
id: 'emergency-user-id',
|
||||||
username: 'usuario',
|
username: 'usuario',
|
||||||
email: 'usuario@example.com'
|
email: 'usuario@example.com',
|
||||||
|
storage_quota_bytes: 10737418240, // 10GB default
|
||||||
|
storage_used_bytes: 0
|
||||||
};
|
};
|
||||||
localStorage.setItem('oxicloud_user', JSON.stringify(defaultUserData));
|
localStorage.setItem('oxicloud_user', JSON.stringify(defaultUserData));
|
||||||
|
|
||||||
@@ -1244,6 +1259,9 @@ function checkAuthentication() {
|
|||||||
userAvatar.textContent = 'US';
|
userAvatar.textContent = 'US';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update storage display with default values
|
||||||
|
updateStorageUsageDisplay(defaultUserData);
|
||||||
|
|
||||||
// Load root files
|
// Load root files
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
@@ -1420,5 +1438,45 @@ function logout() {
|
|||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the storage usage display with the user's actual storage usage
|
||||||
|
* @param {Object} userData - The user data object
|
||||||
|
*/
|
||||||
|
function updateStorageUsageDisplay(userData) {
|
||||||
|
// Default values
|
||||||
|
let usedBytes = 0;
|
||||||
|
let quotaBytes = 10737418240; // Default 10GB
|
||||||
|
let usagePercentage = 0;
|
||||||
|
|
||||||
|
// Get values from user data if available
|
||||||
|
if (userData) {
|
||||||
|
usedBytes = userData.storage_used_bytes || 0;
|
||||||
|
quotaBytes = userData.storage_quota_bytes || 10737418240;
|
||||||
|
|
||||||
|
// Calculate percentage (avoid division by zero)
|
||||||
|
if (quotaBytes > 0) {
|
||||||
|
usagePercentage = Math.min(Math.round((usedBytes / quotaBytes) * 100), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the numbers for display
|
||||||
|
const usedFormatted = formatFileSize(usedBytes);
|
||||||
|
const quotaFormatted = formatFileSize(quotaBytes);
|
||||||
|
|
||||||
|
// Update the storage display elements
|
||||||
|
const storageFill = document.querySelector('.storage-fill');
|
||||||
|
const storageInfo = document.querySelector('.storage-info');
|
||||||
|
|
||||||
|
if (storageFill) {
|
||||||
|
storageFill.style.width = `${usagePercentage}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageInfo) {
|
||||||
|
storageInfo.textContent = `${usagePercentage}% usado (${usedFormatted} / ${quotaFormatted})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize app when DOM is ready
|
// Initialize app when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initApp);
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
|
|||||||
+3
-1
@@ -275,7 +275,9 @@ if (isLoginPage && loginForm) {
|
|||||||
username: username,
|
username: username,
|
||||||
email: username + '@example.com',
|
email: username + '@example.com',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
active: true
|
active: true,
|
||||||
|
storage_quota_bytes: 10737418240, // 10GB default
|
||||||
|
storage_used_bytes: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Storing user data:", userData);
|
console.log("Storing user data:", userData);
|
||||||
|
|||||||
Reference in New Issue
Block a user