Initial commit

+ Album handling
+ Mostly untested
main
Joey Hines 2024-01-21 12:53:30 -07:00
commit be25ac0c5f
Signed by: joeyahines
GPG Key ID: 995E531F7A569DDB
15 changed files with 2365 additions and 0 deletions

View File

@ -0,0 +1,2 @@
[registries.jojo-dev]
index = "https://git.jojodev.com/joeyahines/_cargo-index.git"

4
.gitignore vendored 100644
View File

@ -0,0 +1,4 @@
/target
/db
config.toml
.idea/

1943
Cargo.lock generated 100644

File diff suppressed because it is too large Load Diff

27
Cargo.toml 100644
View File

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

9
LICENSE 100644
View File

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

2
README.md 100644
View File

@ -0,0 +1,2 @@
# PicOx (Name WIP)
Self-hosted image hosting because h*ck imgur.

0
src/api/mod.rs 100644
View File

23
src/config/mod.rs 100644
View File

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

94
src/main.rs 100644
View File

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

50
src/model/album.rs 100644
View File

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

56
src/model/image.rs 100644
View File

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

2
src/model/mod.rs 100644
View File

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

7
src/state.rs 100644
View File

@ -0,0 +1,7 @@
use j_db::database::Database;
use crate::config::PicOxConfig;
pub struct Context {
pub db: Database,
pub config: PicOxConfig
}

View File

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

View File

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