Added basic image handling

main
Joey Hines 2024-02-01 19:34:38 -07:00
parent be25ac0c5f
commit e7cdb44714
Signed by: joeyahines
GPG Key ID: 995E531F7A569DDB
11 changed files with 272 additions and 81 deletions

36
Cargo.lock generated
View File

@ -156,6 +156,7 @@ dependencies = [
"matchit", "matchit",
"memchr", "memchr",
"mime", "mime",
"multer",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustversion", "rustversion",
@ -415,6 +416,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" 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]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.0" version = "0.1.0"
@ -880,6 +890,24 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1093,7 +1121,9 @@ dependencies = [
"hex", "hex",
"j_db", "j_db",
"log", "log",
"multer",
"serde", "serde",
"serde_json",
"sha2", "sha2",
"structopt", "structopt",
"tokio", "tokio",
@ -1393,6 +1423,12 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"

View File

@ -7,7 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
j_db = { version = "0.1.0", registry = "jojo-dev" } j_db = { version = "0.1.0", registry = "jojo-dev" }
axum = "0.7.4"
axum-macros = "0.4.1" axum-macros = "0.4.1"
serde = "1.0.195" serde = "1.0.195"
config = "0.13.4" config = "0.13.4"
@ -21,6 +20,12 @@ url = "2.5.0"
structopt = "0.3.26" structopt = "0.3.26"
log = { version = "0.4.20", features = [] } log = { version = "0.4.20", features = [] }
env_logger = "0.11.0" env_logger = "0.11.0"
multer = "3.0.0"
serde_json = "1.0.111"
[dependencies.axum]
version = "0.7.4"
features = ["multipart"]
[dependencies.tokio] [dependencies.tokio]
version = "1.35.1" version = "1.35.1"

View File

@ -0,0 +1 @@

View File

@ -1,22 +1,25 @@
use std::path::PathBuf; use crate::storage_manager::StorageManagerConfig;
use config::Config; use config::Config;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::storage_manager::file_store::FileStoreConfig; use std::path::PathBuf;
use crate::storage_manager::StorageTypes;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PicOxConfig { pub struct PicOxConfig {
pub host: String, pub host: String,
pub default_storage_method: StorageTypes,
pub db_path: PathBuf, pub db_path: PathBuf,
pub file_store_config: Option<FileStoreConfig> pub storage_config: StorageManagerConfig,
} }
impl PicOxConfig { impl PicOxConfig {
pub fn new(config: PathBuf) -> PicOxConfig { pub fn new(config: PathBuf) -> PicOxConfig {
let pic_ox_config = Config::builder() let pic_ox_config = Config::builder()
.add_source(config::File::new(config.to_str().unwrap(), config::FileFormat::Toml)).build().unwrap(); .add_source(config::File::new(
config.to_str().unwrap(),
config::FileFormat::Toml,
))
.build()
.unwrap();
pic_ox_config.try_deserialize().unwrap() pic_ox_config.try_deserialize().unwrap()
} }

View File

@ -1,36 +1,55 @@
mod api; mod api;
mod model;
mod storage_manager;
mod config; mod config;
mod model;
mod state; mod state;
mod storage_manager;
use std::path::PathBuf; use crate::config::PicOxConfig;
use std::sync::Arc; use crate::model::album::Album;
use axum::{Json, Router}; use crate::model::image::ImageData;
use axum::extract::{Path, Query, State}; use crate::state::Context;
use crate::storage_manager::StorageManager;
use axum::extract::{Multipart, Path, Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum_macros::debug_handler; use axum::{Json, Router};
use j_db::database::Database; use j_db::database::Database;
use serde::{Deserialize, Serialize}; use j_db::model::JdbModel;
use structopt::StructOpt;
use log::info; use log::info;
use crate::config::PicOxConfig; use serde::{Deserialize, Serialize};
use crate::model::album::Album; use std::path::PathBuf;
use crate::state::Context; use std::sync::Arc;
use structopt::StructOpt;
use tokio::sync::RwLock;
type PicContext = Arc<Context>;
#[derive(Debug, Clone, StructOpt)] #[derive(Debug, Clone, StructOpt)]
struct Args { struct Args {
config: PathBuf pub config: PathBuf,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct CreateAlbum { struct CreateAlbum {
pub album_name: String pub album_name: String,
} }
async fn create_album(State(context): State<Arc<Context>>, Json(album): Json<CreateAlbum>) -> impl IntoResponse { #[derive(Debug, Clone, Serialize, Deserialize)]
struct AlbumQuery {
pub album_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AddImage {
pub album: AlbumQuery,
pub image_name: String,
}
async fn create_album(
State(context): State<PicContext>,
Json(album): Json<CreateAlbum>,
) -> impl IntoResponse {
info!("Creating new album '{}'", album.album_name); info!("Creating new album '{}'", album.album_name);
let new_album = Album::new(&album.album_name, Vec::new(), 0); let new_album = Album::new(&album.album_name, Vec::new(), 0);
@ -40,24 +59,57 @@ async fn create_album(State(context): State<Arc<Context>>, Json(album): Json<Cre
(StatusCode::OK, Json(new_album)) (StatusCode::OK, Json(new_album))
} }
async fn get_album(album_id: Path<u64>, State(context): State<Arc<Context>>) -> impl IntoResponse { async fn get_album(album_id: Path<u64>, State(context): State<PicContext>) -> impl IntoResponse {
let album = context.db.get::<Album>(*album_id).unwrap(); let album = context.db.get::<Album>(*album_id).unwrap();
(StatusCode::OK, Json(album)) (StatusCode::OK, Json(album))
} }
#[derive(Debug, Clone, Serialize, Deserialize)] async fn add_image(
struct AlbumQuery { State(context): State<PicContext>,
pub album_name: Option<String> mut img_data: Multipart,
) -> impl IntoResponse {
let mut data: Vec<u8> = Vec::new();
let mut metadata: Option<AddImage> = 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>(album).unwrap();
StatusCode::OK
} }
async fn query_album(album_query: Query<AlbumQuery>, State(context): State<Arc<Context>>) -> impl IntoResponse { async fn query_album(
let resp = if let Some(album_name) = &album_query.album_name { album_query: Query<AlbumQuery>,
context.db.filter(|_, album: &Album| {album.album_name == *album_name}).unwrap().next() } State(context): State<PicContext>,
else { ) -> impl IntoResponse {
None let resp = Album::find_album_by_query(&context.db, album_query.0);
};
(StatusCode::OK, Json(resp)) (StatusCode::OK, Json(resp))
} }
@ -69,14 +121,16 @@ async fn main() {
let db = Database::new(&config.db_path).unwrap(); let db = Database::new(&config.db_path).unwrap();
let store_manager = StorageManager::new(config.storage_config.clone());
let context = Context { let context = Context {
db, db,
config, config: config.clone(),
store_manager: RwLock::new(store_manager),
}; };
let context = Arc::new(context); let context = Arc::new(context);
// initialize tracing // initialize tracing
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// build our application with a route // build our application with a route
@ -85,10 +139,11 @@ async fn main() {
.route("/api/album/create", post(create_album)) .route("/api/album/create", post(create_album))
.route("/api/album/:id", get(get_album)) .route("/api/album/:id", get(get_album))
.route("/api/album/", get(query_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 // run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind(&context.config.host).await.unwrap(); let listener = tokio::net::TcpListener::bind(&config.host).await.unwrap();
info!("Serving at {}", context.config.host); info!("Serving at {}", config.host);
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }

View File

@ -1,6 +1,7 @@
use crate::AlbumQuery;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use j_db::database::Database;
use j_db::model::JdbModel; use j_db::model::JdbModel;
use j_db::query::QueryBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -27,6 +28,16 @@ impl Album {
id: None, id: None,
} }
} }
pub fn find_album_by_query(db: &Database, album_query: AlbumQuery) -> Option<Self> {
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 { impl JdbModel for Album {
@ -46,5 +57,3 @@ impl JdbModel for Album {
other.album_name != self.album_name other.album_name != self.album_name
} }
} }

View File

@ -1,12 +1,12 @@
use std::path::PathBuf;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use url::Url; use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StorageLocation { pub enum StorageLocation {
FileStore {path: PathBuf}, FileStore { path: PathBuf },
Link Link,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -18,11 +18,17 @@ pub struct Image {
pub created_by: u64, pub created_by: u64,
pub storage_location: StorageLocation, pub storage_location: StorageLocation,
id: Option<u64> id: Option<u64>,
} }
impl Image { impl Image {
pub fn new(filename: &str, tags: Vec<String>, link: Url, created_by: u64, storage_location: StorageLocation) -> Self { pub fn new(
filename: &str,
tags: Vec<String>,
link: Url,
created_by: u64,
storage_location: StorageLocation,
) -> Self {
Self { Self {
filename: filename.to_string(), filename: filename.to_string(),
tags, tags,
@ -52,5 +58,5 @@ impl j_db::model::JdbModel for Image {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ImageData { pub enum ImageData {
Bytes(Vec<u8>), Bytes(Vec<u8>),
Link(String) Link(String),
} }

View File

@ -1,2 +1,2 @@
pub mod image;
pub mod album; pub mod album;
pub mod image;

View File

@ -1,7 +1,10 @@
use j_db::database::Database;
use crate::config::PicOxConfig; use crate::config::PicOxConfig;
use crate::storage_manager::StorageManager;
use j_db::database::Database;
use tokio::sync::RwLock;
pub struct Context { pub struct Context {
pub db: Database, pub db: Database,
pub config: PicOxConfig pub config: PicOxConfig,
pub store_manager: RwLock<StorageManager>,
} }

View File

@ -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 async_trait::async_trait;
use j_db::database::Database; use j_db::database::Database;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Digest; use sha2::Digest;
use std::path::PathBuf;
use url::Url; use url::Url;
use crate::model::image::{Image, ImageData, StorageLocation};
use crate::storage_manager::{Store, StoreError};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileStoreConfig { pub struct FileStoreConfig {
@ -18,23 +18,25 @@ pub struct FileStoreConfig {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileStore { pub struct FileStore {
pub config: FileStoreConfig pub config: FileStoreConfig,
} }
impl FileStore { impl FileStore {
pub fn new(config: FileStoreConfig) -> Self { pub fn new(config: FileStoreConfig) -> Self {
Self { Self { config }
config
}
} }
} }
#[async_trait] #[async_trait]
impl Store for FileStore { 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 { let img_data = match img_data {
ImageData::Bytes(b) => b, ImageData::Bytes(b) => b,
ImageData::Link(_) => unimplemented!("No link support") ImageData::Link(_) => unimplemented!("No link support"),
}; };
let hash = sha2::Sha256::digest(&img_data); let hash = sha2::Sha256::digest(&img_data);
let disk_file_name = hex::encode(hash); let disk_file_name = hex::encode(hash);
@ -48,9 +50,12 @@ impl Store for FileStore {
tokio::fs::write(&path, img_data).await?; 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)) Ok((img_link, storage_location))
} }

View File

@ -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 axum::async_trait;
use j_db::database::Database; use j_db::database::Database;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use url::Url; use url::Url;
use crate::model::image::{Image, ImageData, StorageLocation};
pub mod file_store; pub mod file_store;
@ -13,7 +14,7 @@ pub enum StoreError {
ImageNotFound, ImageNotFound,
OutOfStorage, OutOfStorage,
ImageTooBig, ImageTooBig,
IOError(tokio::io::Error) IOError(tokio::io::Error),
} }
impl Display for StoreError { impl Display for StoreError {
@ -23,7 +24,7 @@ impl Display for StoreError {
StoreError::ImageNotFound => write!(f, "Image not found"), StoreError::ImageNotFound => write!(f, "Image not found"),
StoreError::IOError(err) => write!(f, "IO Error: {}", err), StoreError::IOError(err) => write!(f, "IO Error: {}", err),
StoreError::OutOfStorage => write!(f, "Underlying store full"), 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<tokio::io::Error> for StoreError {
} }
#[async_trait] #[async_trait]
pub trait Store { pub trait Store: Send {
async fn store_img(&mut self, img_data: ImageData, file_name: &str) -> Result<(Url, StorageLocation), StoreError>; async fn store_img(
&mut self,
async fn create_img(&mut self, img_data: ImageData, file_name: &str, created_by: u64) -> Result<Image, StoreError> { 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<Image, StoreError> {
let (url, storage_location) = self.store_img(img_data, file_name).await?; 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; async fn delete_img(&mut self, img: Image) -> StoreError;
@ -56,18 +71,71 @@ pub trait Store {
fn current_store_size(&self, db: &Database) -> usize; fn current_store_size(&self, db: &Database) -> usize;
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum StorageTypes { pub enum StorageTypes {
FileStore, FileStore,
LinkStore LinkStore,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageManagerConfig { pub struct StorageManagerConfig {
pub default_storage_method: StorageTypes,
pub file_store_config: Option<FileStoreConfig>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StorageManager { pub struct StorageManager {
config: StorageManagerConfig,
file_store: Option<FileStore>,
}
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<StorageTypes>,
img_data: ImageData,
file_name: &str,
created_by: u64,
) -> Result<Image, StoreError> {
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::<Image>(img).unwrap())
}
} }