diff --git a/Cargo.lock b/Cargo.lock index a59c0ec..c968f95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "rustversion", @@ -415,6 +416,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "0.1.0" @@ -880,6 +890,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -1093,7 +1121,9 @@ dependencies = [ "hex", "j_db", "log", + "multer", "serde", + "serde_json", "sha2", "structopt", "tokio", @@ -1393,6 +1423,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index cd39fc1..6ec4504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] j_db = { version = "0.1.0", registry = "jojo-dev" } -axum = "0.7.4" axum-macros = "0.4.1" serde = "1.0.195" config = "0.13.4" @@ -21,6 +20,12 @@ url = "2.5.0" structopt = "0.3.26" log = { version = "0.4.20", features = [] } env_logger = "0.11.0" +multer = "3.0.0" +serde_json = "1.0.111" + +[dependencies.axum] +version = "0.7.4" +features = ["multipart"] [dependencies.tokio] version = "1.35.1" diff --git a/src/api/mod.rs b/src/api/mod.rs index e69de29..8b13789 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -0,0 +1 @@ + diff --git a/src/config/mod.rs b/src/config/mod.rs index 52efa39..cccf8ea 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,23 +1,26 @@ -use std::path::PathBuf; +use crate::storage_manager::StorageManagerConfig; use config::Config; use serde::{Deserialize, Serialize}; -use crate::storage_manager::file_store::FileStoreConfig; -use crate::storage_manager::StorageTypes; +use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PicOxConfig { pub host: String, - pub default_storage_method: StorageTypes, pub db_path: PathBuf, - pub file_store_config: Option + pub storage_config: StorageManagerConfig, } impl PicOxConfig { pub fn new(config: PathBuf) -> PicOxConfig { - let pic_ox_config = Config::builder() - .add_source(config::File::new(config.to_str().unwrap(), config::FileFormat::Toml)).build().unwrap(); + let pic_ox_config = Config::builder() + .add_source(config::File::new( + config.to_str().unwrap(), + config::FileFormat::Toml, + )) + .build() + .unwrap(); pic_ox_config.try_deserialize().unwrap() } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 3f64505..cdcf6e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,55 @@ mod api; -mod model; -mod storage_manager; mod config; +mod model; mod state; +mod storage_manager; -use std::path::PathBuf; -use std::sync::Arc; -use axum::{Json, Router}; -use axum::extract::{Path, Query, State}; +use crate::config::PicOxConfig; +use crate::model::album::Album; +use crate::model::image::ImageData; +use crate::state::Context; +use crate::storage_manager::StorageManager; +use axum::extract::{Multipart, Path, Query, State}; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{get, post}; -use axum_macros::debug_handler; +use axum::{Json, Router}; use j_db::database::Database; -use serde::{Deserialize, Serialize}; -use structopt::StructOpt; +use j_db::model::JdbModel; use log::info; -use crate::config::PicOxConfig; -use crate::model::album::Album; -use crate::state::Context; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Arc; +use structopt::StructOpt; +use tokio::sync::RwLock; + +type PicContext = Arc; #[derive(Debug, Clone, StructOpt)] struct Args { - config: PathBuf + pub config: PathBuf, } #[derive(Debug, Clone, Serialize, Deserialize)] struct CreateAlbum { - pub album_name: String + pub album_name: String, } -async fn create_album(State(context): State>, Json(album): Json) -> impl IntoResponse { +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AlbumQuery { + pub album_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AddImage { + pub album: AlbumQuery, + pub image_name: String, +} + +async fn create_album( + State(context): State, + Json(album): Json, +) -> impl IntoResponse { info!("Creating new album '{}'", album.album_name); let new_album = Album::new(&album.album_name, Vec::new(), 0); @@ -40,24 +59,57 @@ async fn create_album(State(context): State>, Json(album): Json, State(context): State>) -> impl IntoResponse { +async fn get_album(album_id: Path, State(context): State) -> impl IntoResponse { let album = context.db.get::(*album_id).unwrap(); (StatusCode::OK, Json(album)) } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct AlbumQuery { - pub album_name: Option +async fn add_image( + State(context): State, + mut img_data: Multipart, +) -> impl IntoResponse { + let mut data: Vec = Vec::new(); + let mut metadata: Option = None; + while let Some(field) = img_data.next_field().await.unwrap() { + let field_name = field.name().clone(); + if let Some(field_name) = field_name { + if field_name == "metadata" { + let metadata_json = field.text().await.unwrap(); + metadata = Some(serde_json::from_str(&metadata_json).unwrap()) + } else if field_name == "img_data" { + data.extend_from_slice(field.bytes().await.unwrap().as_ref()); + } + } + } + + let mut album = + Album::find_album_by_query(&context.db, metadata.clone().unwrap().album).unwrap(); + + let mut store_manager = context.store_manager.write().await; + let img = store_manager + .store_img( + &context.db, + None, + ImageData::Bytes(data), + &metadata.unwrap().image_name, + 0, + ) + .await + .unwrap(); + + album.images.push(img.id().unwrap()); + + context.db.insert::(album).unwrap(); + + StatusCode::OK } -async fn query_album(album_query: Query, State(context): State>) -> impl IntoResponse { - let resp = if let Some(album_name) = &album_query.album_name { - context.db.filter(|_, album: &Album| {album.album_name == *album_name}).unwrap().next() } - else { - None - }; - +async fn query_album( + album_query: Query, + State(context): State, +) -> impl IntoResponse { + let resp = Album::find_album_by_query(&context.db, album_query.0); (StatusCode::OK, Json(resp)) } @@ -69,13 +121,15 @@ async fn main() { let db = Database::new(&config.db_path).unwrap(); + let store_manager = StorageManager::new(config.storage_config.clone()); + let context = Context { db, - config, + config: config.clone(), + store_manager: RwLock::new(store_manager), }; - let context = Arc::new(context); - + let context = Arc::new(context); // initialize tracing tracing_subscriber::fmt::init(); @@ -85,10 +139,11 @@ async fn main() { .route("/api/album/create", post(create_album)) .route("/api/album/:id", get(get_album)) .route("/api/album/", get(query_album)) - .with_state(context.clone()); + .route("/api/image/", post(add_image)) + .with_state(context); // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind(&context.config.host).await.unwrap(); - info!("Serving at {}", context.config.host); + let listener = tokio::net::TcpListener::bind(&config.host).await.unwrap(); + info!("Serving at {}", config.host); axum::serve(listener, app).await.unwrap(); -} \ No newline at end of file +} diff --git a/src/model/album.rs b/src/model/album.rs index 8f4039c..fb01051 100644 --- a/src/model/album.rs +++ b/src/model/album.rs @@ -1,6 +1,7 @@ +use crate::AlbumQuery; use chrono::{DateTime, Utc}; +use j_db::database::Database; use j_db::model::JdbModel; -use j_db::query::QueryBuilder; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -27,6 +28,16 @@ impl Album { id: None, } } + + pub fn find_album_by_query(db: &Database, album_query: AlbumQuery) -> Option { + if let Some(album_name) = &album_query.album_name { + db.filter(|_, album: &Album| album.album_name == *album_name) + .unwrap() + .next() + } else { + None + } + } } impl JdbModel for Album { @@ -46,5 +57,3 @@ impl JdbModel for Album { other.album_name != self.album_name } } - - diff --git a/src/model/image.rs b/src/model/image.rs index 7e2b238..6b9d923 100644 --- a/src/model/image.rs +++ b/src/model/image.rs @@ -1,12 +1,12 @@ -use std::path::PathBuf; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum StorageLocation { - FileStore {path: PathBuf}, - Link + FileStore { path: PathBuf }, + Link, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -18,11 +18,17 @@ pub struct Image { pub created_by: u64, pub storage_location: StorageLocation, - id: Option + id: Option, } impl Image { - pub fn new(filename: &str, tags: Vec, link: Url, created_by: u64, storage_location: StorageLocation) -> Self { + pub fn new( + filename: &str, + tags: Vec, + link: Url, + created_by: u64, + storage_location: StorageLocation, + ) -> Self { Self { filename: filename.to_string(), tags, @@ -52,5 +58,5 @@ impl j_db::model::JdbModel for Image { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ImageData { Bytes(Vec), - Link(String) -} \ No newline at end of file + Link(String), +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 7d77481..2519aa0 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,2 @@ +pub mod album; pub mod image; -pub mod album; \ No newline at end of file diff --git a/src/state.rs b/src/state.rs index 9d25cc7..c308186 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,10 @@ -use j_db::database::Database; use crate::config::PicOxConfig; +use crate::storage_manager::StorageManager; +use j_db::database::Database; +use tokio::sync::RwLock; pub struct Context { pub db: Database, - pub config: PicOxConfig -} \ No newline at end of file + pub config: PicOxConfig, + pub store_manager: RwLock, +} diff --git a/src/storage_manager/file_store.rs b/src/storage_manager/file_store.rs index a1feaf3..7b3d9f3 100644 --- a/src/storage_manager/file_store.rs +++ b/src/storage_manager/file_store.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; +use crate::model::image::{Image, ImageData, StorageLocation}; +use crate::storage_manager::{Store, StoreError}; use async_trait::async_trait; use j_db::database::Database; use serde::{Deserialize, Serialize}; use sha2::Digest; +use std::path::PathBuf; use url::Url; -use crate::model::image::{Image, ImageData, StorageLocation}; -use crate::storage_manager::{Store, StoreError}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileStoreConfig { @@ -18,23 +18,25 @@ pub struct FileStoreConfig { #[derive(Debug, Clone)] pub struct FileStore { - pub config: FileStoreConfig + pub config: FileStoreConfig, } impl FileStore { pub fn new(config: FileStoreConfig) -> Self { - Self { - config - } + Self { config } } } #[async_trait] impl Store for FileStore { - async fn store_img(&mut self, img_data: ImageData, file_name: &str) -> Result<(Url, StorageLocation), StoreError> { + async fn store_img( + &mut self, + img_data: ImageData, + file_name: &str, + ) -> Result<(Url, StorageLocation), StoreError> { let img_data = match img_data { ImageData::Bytes(b) => b, - ImageData::Link(_) => unimplemented!("No link support") + ImageData::Link(_) => unimplemented!("No link support"), }; let hash = sha2::Sha256::digest(&img_data); let disk_file_name = hex::encode(hash); @@ -48,9 +50,12 @@ impl Store for FileStore { tokio::fs::write(&path, img_data).await?; - let img_link = Url::parse(&self.config.base_url).unwrap().join(&disk_file_name).unwrap(); + let img_link = Url::parse(&self.config.base_url) + .unwrap() + .join(&disk_file_name) + .unwrap(); - let storage_location = StorageLocation::FileStore {path}; + let storage_location = StorageLocation::FileStore { path }; Ok((img_link, storage_location)) } @@ -70,4 +75,4 @@ impl Store for FileStore { fn current_store_size(&self, _db: &Database) -> usize { 0 } -} \ No newline at end of file +} diff --git a/src/storage_manager/mod.rs b/src/storage_manager/mod.rs index ad7a67e..720746e 100644 --- a/src/storage_manager/mod.rs +++ b/src/storage_manager/mod.rs @@ -1,9 +1,10 @@ -use std::fmt::{Display, Formatter}; +use crate::model::image::{Image, ImageData, StorageLocation}; +use crate::storage_manager::file_store::{FileStore, FileStoreConfig}; use axum::async_trait; use j_db::database::Database; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; use url::Url; -use crate::model::image::{Image, ImageData, StorageLocation}; pub mod file_store; @@ -13,7 +14,7 @@ pub enum StoreError { ImageNotFound, OutOfStorage, ImageTooBig, - IOError(tokio::io::Error) + IOError(tokio::io::Error), } impl Display for StoreError { @@ -23,7 +24,7 @@ impl Display for StoreError { StoreError::ImageNotFound => write!(f, "Image not found"), StoreError::IOError(err) => write!(f, "IO Error: {}", err), StoreError::OutOfStorage => write!(f, "Underlying store full"), - StoreError::ImageTooBig => write!(f, "Image too big for store") + StoreError::ImageTooBig => write!(f, "Image too big for store"), } } } @@ -37,14 +38,28 @@ impl From for StoreError { } #[async_trait] -pub trait Store { - async fn store_img(&mut self, img_data: ImageData, file_name: &str) -> Result<(Url, StorageLocation), StoreError>; - - async fn create_img(&mut self, img_data: ImageData, file_name: &str, created_by: u64) -> Result { +pub trait Store: Send { + async fn store_img( + &mut self, + img_data: ImageData, + file_name: &str, + ) -> Result<(Url, StorageLocation), StoreError>; + async fn create_img( + &mut self, + img_data: ImageData, + file_name: &str, + created_by: u64, + ) -> Result { let (url, storage_location) = self.store_img(img_data, file_name).await?; - Ok(Image::new(file_name, Vec::new(), url, created_by, storage_location)) + Ok(Image::new( + file_name, + Vec::new(), + url, + created_by, + storage_location, + )) } async fn delete_img(&mut self, img: Image) -> StoreError; @@ -56,18 +71,71 @@ pub trait Store { fn current_store_size(&self, db: &Database) -> usize; } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum StorageTypes { FileStore, - LinkStore + LinkStore, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageManagerConfig { - + pub default_storage_method: StorageTypes, + pub file_store_config: Option, } #[derive(Debug, Clone)] pub struct StorageManager { + config: StorageManagerConfig, -} \ No newline at end of file + file_store: Option, +} + +impl StorageManager { + pub fn new(storage_manager_config: StorageManagerConfig) -> Self { + let file_store = + if let Some(file_store_config) = storage_manager_config.file_store_config.clone() { + Some(FileStore::new(file_store_config)) + } else { + None + }; + + Self { + config: storage_manager_config, + file_store, + } + } + + fn get_default_storage_manager(&mut self) -> Box<&mut dyn Store> { + self.get_storage_manager(self.config.default_storage_method) + } + + fn get_storage_manager(&mut self, storage_type: StorageTypes) -> Box<&mut dyn Store> { + match storage_type { + StorageTypes::FileStore => Box::new(self.file_store.as_mut().unwrap()), + StorageTypes::LinkStore => { + unimplemented!() + } + } + } + + pub async fn store_img( + &mut self, + db: &Database, + store: Option, + img_data: ImageData, + file_name: &str, + created_by: u64, + ) -> Result { + let store_type = if let Some(store_type) = store { + self.get_storage_manager(store_type) + } else { + self.get_default_storage_manager() + }; + + let img = store_type + .create_img(img_data, file_name, created_by) + .await?; + + Ok(db.insert::(img).unwrap()) + } +}