Added basic image handling

main
Joey Hines 2024-02-01 19:34:38 -07:00
parent be25ac0c5f
commit e7cdb44714
No known key found for this signature in database
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",
"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"

View File

@ -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"

View File

@ -0,0 +1 @@

View File

@ -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<FileStoreConfig>
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()
}
}
}

View File

@ -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<Context>;
#[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<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);
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))
}
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();
(StatusCode::OK, Json(album))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AlbumQuery {
pub album_name: Option<String>
async fn add_image(
State(context): State<PicContext>,
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 {
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<AlbumQuery>,
State(context): State<PicContext>,
) -> 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();
}
}

View File

@ -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<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 {
@ -46,5 +57,3 @@ impl JdbModel for Album {
other.album_name != self.album_name
}
}

View File

@ -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<u64>
id: Option<u64>,
}
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 {
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<u8>),
Link(String)
}
Link(String),
}

View File

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

View File

@ -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
}
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 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
}
}
}

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 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<tokio::io::Error> 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<Image, StoreError> {
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<Image, StoreError> {
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<FileStoreConfig>,
}
#[derive(Debug, Clone)]
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())
}
}