diff --git a/Cargo.lock b/Cargo.lock index 1767f85..c666d74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,6 +1791,7 @@ name = "media" version = "0.6.0" dependencies = [ "axum", + "bytes", "common", "database", "hyper", diff --git a/Cargo.toml b/Cargo.toml index 1e8e1e3..e2413a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,8 @@ axum = { version = "0.6.20", features = ["ws", "headers"] } axum-test = "12.1.0" anyhow = "1.0.72" +bytes = "1.4.0" + core_extensions = { version = "1.5.2", default_features = false, features = ["std"] } chrono = { version = "0.4.26", features = ["serde"] } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 55b58aa..b4a7b3e 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -34,6 +34,7 @@ axum = { workspace = true, features = ["multipart"] } hyper = { workspace = true, features = ["full"] } tower-http.workspace = true mime.workspace = true +bytes.workspace = true # persistency uuid = { workspace = true, features = ["serde"] } diff --git a/crates/media/src/api/routes/post_media.rs b/crates/media/src/api/routes/post_media.rs index b8a2185..8a209f3 100644 --- a/crates/media/src/api/routes/post_media.rs +++ b/crates/media/src/api/routes/post_media.rs @@ -20,15 +20,14 @@ use axum::{ extract::{Multipart, State}, http::StatusCode, + response::{IntoResponse, Redirect}, Json, }; use common::auth::user::User; -use hyper::header::LOCATION; -use hyper::HeaderMap; use serde::{Deserialize, Serialize}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; -use tracing::debug; +use tracing::{debug, info}; use uuid::Uuid; use crate::{data::error::DataAccessError, repository::MediaRepositoryState}; @@ -42,10 +41,11 @@ pub(crate) async fn post_media( State(repo): State, user: User, mut multipart: Multipart, -) -> Result<(StatusCode, Json), StatusCode> { +) -> Result { + info!("POST /media"); + let mut name = None; let mut date_taken = None; - let mut headers = HeaderMap::new(); while let Some(field) = multipart.next_field().await.unwrap() { if let Some(field_name) = field.name() { @@ -88,20 +88,15 @@ pub(crate) async fn post_media( Json(ResponseId { id: uuid.hyphenated().to_string(), }), - )) + ) + .into_response()) } - Err(error) => { - match error { - DataAccessError::AlreadyExist(id) => { - // TODO: use Redirect::permanent to add a Location header to the already existing item - let location = format!("/media/{}", id); - headers.insert(LOCATION, location.parse().unwrap()); - - Err(StatusCode::SEE_OTHER) - } - _ => Err(StatusCode::INTERNAL_SERVER_ERROR), + Err(error) => match error { + DataAccessError::AlreadyExist(id) => { + Ok(Redirect::to(&format!("/media/{id}")).into_response()) } - } + _ => Err(StatusCode::INTERNAL_SERVER_ERROR), + }, } } diff --git a/crates/media/src/api/routes/post_media_id.rs b/crates/media/src/api/routes/post_media_id.rs index a67e330..54373a9 100644 --- a/crates/media/src/api/routes/post_media_id.rs +++ b/crates/media/src/api/routes/post_media_id.rs @@ -20,16 +20,12 @@ use axum::extract::{Multipart, Path, State}; use axum::http::StatusCode; +use axum::response::{IntoResponse, Redirect}; use common::auth::user::User; -use hyper::header::LOCATION; -use hyper::HeaderMap; -use tempfile::tempfile; -use tokio::fs::File; -use tracing::{debug, error}; +use tracing::{debug, info}; use uuid::Uuid; -use std::io::SeekFrom; -use tokio::io::{AsyncSeekExt, AsyncWriteExt}; +use bytes::Bytes; use crate::data::error::DataAccessError; use crate::repository::MediaRepositoryState; @@ -39,13 +35,12 @@ pub(crate) async fn post_media_id( Path(media_id): Path, user: User, mut multipart: Multipart, -) -> Result { - error!("POST /media/{} user={}", media_id, user); - let mut headers = HeaderMap::new(); - let tempfile = tempfile().unwrap(); - let mut tempfile = File::from_std(tempfile); +) -> Result { + info!("POST /media/.."); + let mut name: String = "".to_string(); - while let Some(mut field) = multipart.next_field().await.unwrap() { + let mut bytes = Bytes::new(); + while let Some(field) = multipart.next_field().await.unwrap() { if let Some(field_name) = field.name() { match field_name { "name" => { @@ -53,32 +48,22 @@ pub(crate) async fn post_media_id( debug!("name={}", name.clone()); } "file" => { - while let Some(chunk) = field - .chunk() - .await - .expect("Could not read file from multipart upload!") - { - tempfile - .write_all(&chunk) - .await - .expect("Could not write reference file to tmp!") - } - tempfile.seek(SeekFrom::Start(0)).await.unwrap(); - - // TODO: wrap bytes and write to persistence - debug!("filesize={}", field.chunk().await.unwrap().unwrap().len()); + bytes = field.bytes().await.unwrap(); + debug!("{} bytes received", bytes.clone().len()); } _ => continue, } } } + debug!("{} bytes received", bytes.clone().len()); + let result = repo .add_reference_for_media_item( Uuid::parse_str(user.uuid.as_str()).unwrap(), - media_id, + &media_id, name, - tempfile, + bytes, // tempfile, ) .await; @@ -86,20 +71,13 @@ pub(crate) async fn post_media_id( Ok(uuid) => { debug!("reference added. uuid={}", uuid.hyphenated().to_string()); - Ok(uuid.hyphenated().to_string()) + Ok(uuid.hyphenated().to_string().into_response()) } - Err(error) => { - match error { - DataAccessError::AlreadyExist(id) => { - // TODO: use Redirect::permanent to add a Location header to the already existing item - - let location = format!("/media/{}", id); - headers.insert(LOCATION, location.parse().unwrap()); - - Err(StatusCode::SEE_OTHER) - } - _ => Err(StatusCode::INTERNAL_SERVER_ERROR), + Err(error) => match error { + DataAccessError::AlreadyExist(id) => { + Ok(Redirect::to(&format!("/media/{media_id}/{id}")).into_response()) } - } + _ => Err(StatusCode::INTERNAL_SERVER_ERROR), + }, } } diff --git a/crates/media/src/repository.rs b/crates/media/src/repository.rs index 33bd9f3..9d414f0 100644 --- a/crates/media/src/repository.rs +++ b/crates/media/src/repository.rs @@ -18,15 +18,17 @@ use crate::data::error::DataAccessError; use crate::data::media_item::MediaItem; use axum::async_trait; +use bytes::Bytes; use common::config::configuration::Configuration; use common::database::reference::Reference; use common::database::Database; use database::sqlite::SqliteDatabase; +use std::fs; use std::path::Path; use std::sync::Arc; use time::OffsetDateTime; -use tokio::fs::File; -use tracing::info; +use tracing::log::warn; +use tracing::{debug, info}; use uuid::Uuid; #[allow(dead_code)] @@ -58,9 +60,9 @@ pub trait MediaRepositoryTrait { async fn add_reference_for_media_item( &self, user_id: Uuid, - media_id: String, + media_id: &str, name: String, - file: File, + bytes: Bytes, ) -> Result; } @@ -128,29 +130,36 @@ impl MediaRepositoryTrait for MediaRepository { async fn add_reference_for_media_item( &self, user_id: Uuid, - media_id: String, + media_id: &str, name: String, - mut tmp_file: File, + bytes: Bytes, ) -> Result { let path = Path::new("data/files/") .join(user_id.hyphenated().to_string()) - .join(media_id.clone()) - .join(name.clone()); + .join(media_id); - let mut dest_file = File::create(path.clone()).await.unwrap(); + let file_path = path.join(&name); + let _ = fs::create_dir_all(&path); - let num_bytes = tokio::io::copy(&mut tmp_file, &mut dest_file) - .await - .expect("Coudl not copy tmp file to path!"); - println!( - "{} bytes copied to path {}", - num_bytes, - path.clone().to_string_lossy() - ); + info!("target {}", path.clone().to_str().unwrap().to_string()); + debug!("got {} bytes to handle", bytes.len()); + + let file_result = tokio::fs::write(&path.join(&name), &bytes).await; + match file_result { + Ok(_) => { + info!("wrote to {}", file_path.to_str().unwrap().to_string()); + } + Err(_) => { + warn!( + "Could not write file to path {}", + path.clone().to_str().unwrap().to_string() + ); + } + } let reference = Reference { uuid: Uuid::new_v4().hyphenated().to_string(), - filepath: path.to_str().unwrap().to_string(), + filepath: path.clone().to_str().unwrap().to_string(), filename: name.to_string(), size: 0u64, description: "", @@ -159,7 +168,7 @@ impl MediaRepositoryTrait for MediaRepository { }; let _ = &self .database - .add_reference(media_id.as_str(), name.as_str(), &reference) + .add_reference(media_id, name.as_str(), &reference) .await; Err(DataAccessError::OtherError) } diff --git a/documentation/http/DSC_1234.NEF b/documentation/http/DSC_1234.NEF new file mode 100755 index 0000000..4df7b1c Binary files /dev/null and b/documentation/http/DSC_1234.NEF differ diff --git a/documentation/http/post_media_id.hurl b/documentation/http/post_media_id.hurl new file mode 100644 index 0000000..66f6fd6 --- /dev/null +++ b/documentation/http/post_media_id.hurl @@ -0,0 +1,9 @@ +POST http://127.0.0.1:7777/media/75889665-7df3-4edd-8b15-d80819f7a17 +Authorization: FakeToken +Connection: Keep-Alive + +[MultipartFormData] +name: DSC_1234.NEF +file: file,DSC_1234.NEF; + +HTTP 200