Added basic image handling
parent
be25ac0c5f
commit
e7cdb44714
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
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();
|
||||
.add_source(config::File::new(
|
||||
config.to_str().unwrap(),
|
||||
config::FileFormat::Toml,
|
||||
))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
pic_ox_config.try_deserialize().unwrap()
|
||||
}
|
||||
|
|
117
src/main.rs
117
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<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,14 +121,16 @@ 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);
|
||||
|
||||
|
||||
// initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
// build our application with a route
|
||||
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
pub mod image;
|
||||
pub mod album;
|
||||
pub mod image;
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue