commit
be25ac0c5f
|
@ -0,0 +1,2 @@
|
|||
[registries.jojo-dev]
|
||||
index = "https://git.jojodev.com/joeyahines/_cargo-index.git"
|
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/db
|
||||
config.toml
|
||||
.idea/
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "pic_ox"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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"
|
||||
tracing-subscriber = "0.3.18"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
chrono-tz = "0.8.5"
|
||||
async-trait = "0.1.77"
|
||||
sha2 = "0.10.8"
|
||||
hex = "0.4.3"
|
||||
url = "2.5.0"
|
||||
structopt = "0.3.26"
|
||||
log = { version = "0.4.20", features = [] }
|
||||
env_logger = "0.11.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.35.1"
|
||||
features = ["macros", "rt-multi-thread", "fs"]
|
|
@ -0,0 +1,9 @@
|
|||
Copyright 2023 Joey Hines
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Join Us
|
|
@ -0,0 +1,2 @@
|
|||
# PicOx (Name WIP)
|
||||
Self-hosted image hosting because h*ck imgur.
|
|
@ -0,0 +1,23 @@
|
|||
use std::path::PathBuf;
|
||||
use config::Config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::storage_manager::file_store::FileStoreConfig;
|
||||
use crate::storage_manager::StorageTypes;
|
||||
|
||||
#[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>
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
pic_ox_config.try_deserialize().unwrap()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
mod api;
|
||||
mod model;
|
||||
mod storage_manager;
|
||||
mod config;
|
||||
mod state;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use axum::{Json, Router};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum_macros::debug_handler;
|
||||
use j_db::database::Database;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
use log::info;
|
||||
use crate::config::PicOxConfig;
|
||||
use crate::model::album::Album;
|
||||
use crate::state::Context;
|
||||
|
||||
#[derive(Debug, Clone, StructOpt)]
|
||||
struct Args {
|
||||
config: PathBuf
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CreateAlbum {
|
||||
pub album_name: String
|
||||
}
|
||||
|
||||
async fn create_album(State(context): State<Arc<Context>>, Json(album): Json<CreateAlbum>) -> impl IntoResponse {
|
||||
info!("Creating new album '{}'", album.album_name);
|
||||
|
||||
let new_album = Album::new(&album.album_name, Vec::new(), 0);
|
||||
|
||||
let new_album = context.db.insert(new_album).unwrap();
|
||||
|
||||
(StatusCode::OK, Json(new_album))
|
||||
}
|
||||
|
||||
async fn get_album(album_id: Path<u64>, State(context): State<Arc<Context>>) -> 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 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
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(resp))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::from_args();
|
||||
|
||||
let config = PicOxConfig::new(args.config);
|
||||
|
||||
let db = Database::new(&config.db_path).unwrap();
|
||||
|
||||
let context = Context {
|
||||
db,
|
||||
config,
|
||||
};
|
||||
|
||||
let context = Arc::new(context);
|
||||
|
||||
|
||||
// initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// `GET /` goes to `root`
|
||||
.route("/api/album/create", post(create_album))
|
||||
.route("/api/album/:id", get(get_album))
|
||||
.route("/api/album/", get(query_album))
|
||||
.with_state(context.clone());
|
||||
|
||||
// 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);
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use j_db::model::JdbModel;
|
||||
use j_db::query::QueryBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Album {
|
||||
pub album_name: String,
|
||||
pub images: Vec<u64>,
|
||||
pub aliases: Vec<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub modified_at: DateTime<Utc>,
|
||||
pub owner: u64,
|
||||
|
||||
id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Album {
|
||||
pub fn new(name: &str, aliases: Vec<String>, owner: u64) -> Album {
|
||||
Album {
|
||||
album_name: name.to_string(),
|
||||
images: vec![],
|
||||
aliases,
|
||||
created_at: Utc::now(),
|
||||
modified_at: Utc::now(),
|
||||
owner,
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JdbModel for Album {
|
||||
fn id(&self) -> Option<u64> {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: u64) {
|
||||
self.id = Some(id);
|
||||
}
|
||||
|
||||
fn tree() -> String {
|
||||
"Album".to_string()
|
||||
}
|
||||
|
||||
fn check_unique(&self, other: &Self) -> bool {
|
||||
other.album_name != self.album_name
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
use std::path::PathBuf;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum StorageLocation {
|
||||
FileStore {path: PathBuf},
|
||||
Link
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub filename: String,
|
||||
pub tags: Vec<String>,
|
||||
pub create_date: DateTime<Utc>,
|
||||
pub link: String,
|
||||
pub created_by: u64,
|
||||
pub storage_location: StorageLocation,
|
||||
|
||||
id: Option<u64>
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(filename: &str, tags: Vec<String>, link: Url, created_by: u64, storage_location: StorageLocation) -> Self {
|
||||
Self {
|
||||
filename: filename.to_string(),
|
||||
tags,
|
||||
create_date: Utc::now(),
|
||||
link: link.to_string(),
|
||||
created_by,
|
||||
storage_location,
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl j_db::model::JdbModel for Image {
|
||||
fn id(&self) -> Option<u64> {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: u64) {
|
||||
self.id = Some(id)
|
||||
}
|
||||
|
||||
fn tree() -> String {
|
||||
"Image".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ImageData {
|
||||
Bytes(Vec<u8>),
|
||||
Link(String)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod image;
|
||||
pub mod album;
|
|
@ -0,0 +1,7 @@
|
|||
use j_db::database::Database;
|
||||
use crate::config::PicOxConfig;
|
||||
|
||||
pub struct Context {
|
||||
pub db: Database,
|
||||
pub config: PicOxConfig
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use j_db::database::Database;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
use url::Url;
|
||||
use crate::model::image::{Image, ImageData, StorageLocation};
|
||||
use crate::storage_manager::{Store, StoreError};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileStoreConfig {
|
||||
pub base_path: PathBuf,
|
||||
pub base_url: String,
|
||||
|
||||
pub max_file_size: usize,
|
||||
pub max_total_storage: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileStore {
|
||||
pub config: FileStoreConfig
|
||||
}
|
||||
|
||||
impl FileStore {
|
||||
pub fn new(config: FileStoreConfig) -> Self {
|
||||
Self {
|
||||
config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Store for FileStore {
|
||||
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")
|
||||
};
|
||||
let hash = sha2::Sha256::digest(&img_data);
|
||||
let disk_file_name = hex::encode(hash);
|
||||
|
||||
let file = PathBuf::from(file_name);
|
||||
let ext = file.extension().unwrap().to_str().unwrap();
|
||||
|
||||
let disk_file_name = format!("{}.{}", disk_file_name, ext);
|
||||
|
||||
let path = self.config.base_path.join(&disk_file_name);
|
||||
|
||||
tokio::fs::write(&path, img_data).await?;
|
||||
|
||||
let img_link = Url::parse(&self.config.base_url).unwrap().join(&disk_file_name).unwrap();
|
||||
|
||||
let storage_location = StorageLocation::FileStore {path};
|
||||
|
||||
Ok((img_link, storage_location))
|
||||
}
|
||||
|
||||
async fn delete_img(&mut self, img: Image) -> StoreError {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn max_image_size(&self) -> usize {
|
||||
self.config.max_file_size
|
||||
}
|
||||
|
||||
fn max_total_storage(&self) -> usize {
|
||||
self.config.max_total_storage
|
||||
}
|
||||
|
||||
fn current_store_size(&self, _db: &Database) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use axum::async_trait;
|
||||
use j_db::database::Database;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use crate::model::image::{Image, ImageData, StorageLocation};
|
||||
|
||||
pub mod file_store;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoreError {
|
||||
InvalidFile,
|
||||
ImageNotFound,
|
||||
OutOfStorage,
|
||||
ImageTooBig,
|
||||
IOError(tokio::io::Error)
|
||||
}
|
||||
|
||||
impl Display for StoreError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StoreError::InvalidFile => write!(f, "Invalid file"),
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StoreError {}
|
||||
|
||||
impl From<tokio::io::Error> for StoreError {
|
||||
fn from(value: tokio::io::Error) -> Self {
|
||||
Self::IOError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
|
||||
let (url, storage_location) = self.store_img(img_data, file_name).await?;
|
||||
|
||||
Ok(Image::new(file_name, Vec::new(), url, created_by, storage_location))
|
||||
}
|
||||
|
||||
async fn delete_img(&mut self, img: Image) -> StoreError;
|
||||
|
||||
fn max_image_size(&self) -> usize;
|
||||
|
||||
fn max_total_storage(&self) -> usize;
|
||||
|
||||
fn current_store_size(&self, db: &Database) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum StorageTypes {
|
||||
FileStore,
|
||||
LinkStore
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageManagerConfig {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
|
||||
}
|
Loading…
Reference in New Issue