diff --git a/Cargo.lock b/Cargo.lock index 73243dd..6d6cc44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -1180,6 +1186,10 @@ dependencies = [ "sha2", "sqlx", "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", "uuid", ] @@ -1276,6 +1286,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1304,6 +1323,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1380,6 +1409,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "nuid" version = "0.5.0" @@ -2048,6 +2086,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2428,6 +2475,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -2535,9 +2591,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2583,6 +2639,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2625,6 +2707,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2643,6 +2755,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -2706,6 +2824,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..76b105a --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + nats: + image: nats:alpine + container_name: libertas_nats + ports: + - "4222:4222" + - "6222:6222" + - "8222:8222" + restart: unless-stopped diff --git a/libertas_api/Cargo.toml b/libertas_api/Cargo.toml index 6a6bf82..b4e8c50 100644 --- a/libertas_api/Cargo.toml +++ b/libertas_api/Cargo.toml @@ -32,3 +32,7 @@ sha2 = "0.10.9" futures = "0.3.31" bytes = "1.10.1" async-nats = "0.44.2" +tower = { version = "0.5.2", features = ["util"] } +tower-http = { version = "0.6.6", features = ["fs", "trace"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } diff --git a/libertas_api/src/factory.rs b/libertas_api/src/factory.rs index 66de256..d2c1d9e 100644 --- a/libertas_api/src/factory.rs +++ b/libertas_api/src/factory.rs @@ -46,5 +46,6 @@ pub async fn build_app_state(config: Config) -> CoreResult { album_service, token_generator: tokenizer, nats_client, + config, }) } diff --git a/libertas_api/src/handlers/media_handlers.rs b/libertas_api/src/handlers/media_handlers.rs index a9644a2..9981067 100644 --- a/libertas_api/src/handlers/media_handlers.rs +++ b/libertas_api/src/handlers/media_handlers.rs @@ -1,14 +1,18 @@ use axum::{ Router, - extract::{DefaultBodyLimit, Multipart, State}, + extract::{DefaultBodyLimit, Multipart, Path, Request, State}, http::StatusCode, - response::Json, - routing::post, + response::{IntoResponse, Json}, + routing::{get, post}, }; use futures::TryStreamExt; use libertas_core::{error::CoreError, models::Media, schema::UploadMediaData}; use serde::Serialize; -use std::io; +use std::{io, path::PathBuf}; + +use tower::ServiceExt; +use tower_http::services::ServeFile; +use uuid::Uuid; use crate::{error::ApiError, middleware::auth::UserId, state::AppState}; @@ -36,6 +40,7 @@ impl From for MediaResponse { pub fn media_routes() -> Router { Router::new() .route("/", post(upload_media)) + .route("/{media_id}/file", get(get_media_file)) .layer(DefaultBodyLimit::max(250 * 1024 * 1024)) } @@ -75,3 +80,27 @@ async fn upload_media( Ok((StatusCode::CREATED, Json(media.into()))) } + +async fn get_media_file( + State(state): State, + UserId(user_id): UserId, + Path(media_id): Path, + request: Request, +) -> Result { + let storage_path = state + .media_service + .get_media_filepath(media_id, user_id) + .await?; + + let full_path = PathBuf::from(&state.config.media_library_path).join(&storage_path); + + ServeFile::new(full_path) + .oneshot(request) + .await + .map_err(|e| { + ApiError::from(CoreError::Io(io::Error::new( + io::ErrorKind::NotFound, + format!("File not found: {}", e), + ))) + }) +} diff --git a/libertas_api/src/main.rs b/libertas_api/src/main.rs index f8e257d..bcb4be2 100644 --- a/libertas_api/src/main.rs +++ b/libertas_api/src/main.rs @@ -1,5 +1,7 @@ use std::net::SocketAddr; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + pub mod config; pub mod error; pub mod factory; @@ -13,6 +15,16 @@ pub mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { let config = config::load_config()?; + + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + format!("{}=debug,tower_http=debug", env!("CARGO_CRATE_NAME")).into() + }), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + let addr: SocketAddr = config.server_address.parse()?; let app_state = factory::build_app_state(config).await?; diff --git a/libertas_api/src/services/media_service.rs b/libertas_api/src/services/media_service.rs index e40bb7f..e451353 100644 --- a/libertas_api/src/services/media_service.rs +++ b/libertas_api/src/services/media_service.rs @@ -124,4 +124,18 @@ impl MediaService for MediaServiceImpl { async fn list_user_media(&self, user_id: Uuid) -> CoreResult> { self.repo.list_by_user(user_id).await } + + async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult { + let media = self + .repo + .find_by_id(id) + .await? + .ok_or(CoreError::NotFound("Media".to_string(), id))?; + + if media.owner_id != user_id { + return Err(CoreError::Auth("Access denied".to_string())); + } + + Ok(media.storage_path) + } } diff --git a/libertas_api/src/state.rs b/libertas_api/src/state.rs index 4f2a7bd..17bb5e3 100644 --- a/libertas_api/src/state.rs +++ b/libertas_api/src/state.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use libertas_core::services::{AlbumService, MediaService, UserService}; +use libertas_core::{ + config::Config, + services::{AlbumService, MediaService, UserService}, +}; use crate::security::TokenGenerator; @@ -11,4 +14,5 @@ pub struct AppState { pub album_service: Arc, pub token_generator: Arc, pub nats_client: async_nats::Client, + pub config: Config, } diff --git a/libertas_core/src/services.rs b/libertas_core/src/services.rs index 60c1cee..1ff6753 100644 --- a/libertas_core/src/services.rs +++ b/libertas_core/src/services.rs @@ -15,6 +15,7 @@ pub trait MediaService: Send + Sync { async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult; async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult; async fn list_user_media(&self, user_id: Uuid) -> CoreResult>; + async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult; } #[async_trait]