mirror of https://github.com/ChillFish8/lust.git
Add bucket config function and further improve storage api
This commit is contained in:
parent
d1a4d158cb
commit
845500f689
|
@ -1,6 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Deserialize;
|
||||
use poem_openapi::Enum;
|
||||
|
@ -9,11 +10,16 @@ use crate::pipelines::ProcessingMode;
|
|||
use crate::storage::backends::BackendConfigs;
|
||||
|
||||
static CONFIG: OnceCell<RuntimeConfig> = OnceCell::new();
|
||||
static BUCKET_CONFIGS: OnceCell<hashbrown::HashMap<u32, BucketConfig>> = OnceCell::new();
|
||||
|
||||
pub fn config() -> &'static RuntimeConfig {
|
||||
CONFIG.get().expect("config init")
|
||||
}
|
||||
|
||||
pub fn config_for_bucket(bucket_id: u32) -> Option<&'static BucketConfig> {
|
||||
BUCKET_CONFIGS.get_or_init(hashbrown::HashMap::new).get(&bucket_id)
|
||||
}
|
||||
|
||||
pub async fn init(config_file: &Path) -> Result<()> {
|
||||
let file = tokio::fs::read(config_file).await?;
|
||||
|
||||
|
@ -26,6 +32,14 @@ pub async fn init(config_file: &Path) -> Result<()> {
|
|||
_ => return Err(anyhow!("Config file must have an extension of either `.json`,`.yaml` or `.yml`"))
|
||||
};
|
||||
|
||||
let bucket_configs: hashbrown::HashMap<u32, BucketConfig> = cfg.buckets
|
||||
.iter()
|
||||
.map(|(name, cfg)| {
|
||||
(crate::utils::crc_hash(name), cfg.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = BUCKET_CONFIGS.set(bucket_configs);
|
||||
let _ = CONFIG.set(cfg);
|
||||
Ok(())
|
||||
} else {
|
||||
|
@ -137,6 +151,14 @@ pub struct BucketConfig {
|
|||
pub max_concurrency: Option<usize>,
|
||||
}
|
||||
|
||||
impl BucketConfig {
|
||||
#[inline]
|
||||
pub fn sizing_preset_ids(&self) -> Vec<u32> {
|
||||
self.presets.keys()
|
||||
.map(crate::utils::crc_hash)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Enum, Deserialize, strum::AsRefStr)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
@ -170,13 +192,26 @@ impl ImageKind {
|
|||
}
|
||||
|
||||
pub fn as_content_type(&self) -> String {
|
||||
format!("image/{}", self.as_file_extension())
|
||||
}
|
||||
|
||||
pub fn as_file_extension(&self) -> &'static str {
|
||||
match self {
|
||||
ImageKind::Png => "image/png".to_string(),
|
||||
ImageKind::Jpeg => "image/jpeg".to_string(),
|
||||
ImageKind::Webp => "image/webp".to_string(),
|
||||
ImageKind::Gif => "image/gif".to_string(),
|
||||
ImageKind::Png => "png",
|
||||
ImageKind::Jpeg => "jpeg",
|
||||
ImageKind::Webp => "webp",
|
||||
ImageKind::Gif => "gif",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variants() -> &'static [Self] {
|
||||
&[
|
||||
Self::Png,
|
||||
Self::Jpeg,
|
||||
Self::Gif,
|
||||
Self::Webp,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use poem_openapi::Object;
|
||||
|
@ -100,7 +99,7 @@ impl BucketController {
|
|||
) -> anyhow::Result<Option<StoreEntry>> {
|
||||
let _permit = get_optional_permit(&self.global_limiter, &self.limiter).await?;
|
||||
|
||||
let sizing_id = size_preset.map(crc_hash).unwrap_or(0);
|
||||
let sizing_id = size_preset.map(crate::utils::crc_hash).unwrap_or(0);
|
||||
let data = match self.storage.fetch(image_id, kind, sizing_id).await? {
|
||||
None => return Ok(None),
|
||||
Some(d) => d,
|
||||
|
@ -130,9 +129,3 @@ impl BucketController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn crc_hash<H: Hash>(v: H) -> u32 {
|
||||
let mut hasher = crc32fast::Hasher::default();
|
||||
v.hash(&mut hasher);
|
||||
hasher.finalize()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ mod storage;
|
|||
mod routes;
|
||||
mod pipelines;
|
||||
mod controller;
|
||||
mod utils;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt::Display;
|
||||
use bytes::Bytes;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use hashbrown::HashMap;
|
||||
use poem_openapi::OpenApi;
|
||||
use poem::{Body, Result};
|
||||
use poem_openapi::{ApiResponse, Object};
|
||||
|
@ -16,6 +16,7 @@ use crate::pipelines::ProcessingMode;
|
|||
|
||||
#[derive(Debug, Object)]
|
||||
pub struct Detail {
|
||||
/// Additional information regarding the response.
|
||||
detail: String,
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,20 @@ pub enum UploadResponse {
|
|||
Unauthorized,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
pub enum DeleteResponse {
|
||||
#[oai(status = 200)]
|
||||
Ok,
|
||||
|
||||
/// You do not have permission to execute this action.
|
||||
#[oai(status = 401)]
|
||||
UnAuthorized,
|
||||
|
||||
/// Bucket does not exist.
|
||||
#[oai(status = 404)]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
pub enum FetchResponse {
|
||||
#[oai(status = 200)]
|
||||
|
@ -204,6 +219,28 @@ impl LustApi {
|
|||
Some(img) => Ok(FetchResponse::Ok(Binary(img.data), img.kind.as_content_type()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete Image
|
||||
///
|
||||
/// Delete the given image.
|
||||
/// This will purge all variants of the image including sizing presets and formats.
|
||||
///
|
||||
/// Images that do not exist already will be ignored and will not return a 404.
|
||||
#[oai(path = "/:image_id", method = "delete")]
|
||||
pub async fn delete_image(
|
||||
&self,
|
||||
bucket: Path<String>,
|
||||
image_id: Path<Uuid>,
|
||||
) -> Result<DeleteResponse> {
|
||||
let bucket = match self.buckets.get(&*bucket) {
|
||||
None => return Ok(DeleteResponse::NotFound),
|
||||
Some(b) => b,
|
||||
};
|
||||
|
||||
bucket.delete(*image_id).await?;
|
||||
|
||||
Ok(DeleteResponse::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -212,7 +249,7 @@ fn get_image_kind(direct_format: Option<ImageKind>, accept: Option<String>, buck
|
|||
Some(kind) => kind,
|
||||
None => match accept {
|
||||
Some(accept) => {
|
||||
let parts = accept.split(",");
|
||||
let parts = accept.split(',');
|
||||
for accepted in parts {
|
||||
if let Some(kind) = ImageKind::from_content_type(accepted) {
|
||||
return kind;
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::{BucketConfig, config, config_for_bucket, ImageKind};
|
||||
use crate::StorageBackend;
|
||||
|
||||
pub struct FileSystemBackend {
|
||||
directory: PathBuf,
|
||||
}
|
||||
|
||||
impl FileSystemBackend {
|
||||
pub fn new(dir: PathBuf) -> Self {
|
||||
Self {
|
||||
directory: dir,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_path(&self, bucket_id: u32, sizing_id: u32) -> PathBuf {
|
||||
self.directory
|
||||
.join(bucket_id.to_string())
|
||||
.join(sizing_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageBackend for FileSystemBackend {
|
||||
async fn store(
|
||||
&self,
|
||||
bucket_id: u32,
|
||||
image_id: Uuid,
|
||||
kind: ImageKind,
|
||||
sizing_id: u32,
|
||||
data: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
let store_in = self.format_path(bucket_id, sizing_id);
|
||||
let path = store_in.join(format!("{}.{}", image_id, kind.as_file_extension()));
|
||||
|
||||
tokio::fs::write(&path, data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
&self,
|
||||
bucket_id: u32,
|
||||
image_id: Uuid,
|
||||
kind: ImageKind,
|
||||
sizing_id: u32,
|
||||
) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let store_in = self.format_path(bucket_id, sizing_id);
|
||||
let path = store_in.join(format!("{}.{}", image_id, kind.as_file_extension()));
|
||||
|
||||
match tokio::fs::read(&path).await {
|
||||
Ok(data) => Ok(Some(data)),
|
||||
Err(ref e) if e.kind() == ErrorKind::NotFound => Ok(None),
|
||||
Err(other) => Err(other.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
bucket_id: u32,
|
||||
image_id: Uuid,
|
||||
) -> anyhow::Result<()> {
|
||||
let bucket = config_for_bucket(bucket_id)
|
||||
.ok_or_else(|| anyhow!("Bucket does not exist."))?;
|
||||
|
||||
for sizing_id in bucket.sizing_preset_ids().iter().copied() {
|
||||
for kind in ImageKind::variants() {
|
||||
let store_in = self.format_path(bucket_id, sizing_id);
|
||||
let path = store_in.join(format!("{}.{}", image_id, kind.as_file_extension()));
|
||||
|
||||
match tokio::fs::remove_file(&path).await {
|
||||
Ok(()) => continue,
|
||||
Err(ref e) if e.kind() == ErrorKind::NotFound => continue,
|
||||
Err(other) => return Err(other.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod register;
|
||||
mod filesystem;
|
||||
|
||||
pub use register::BackendConfigs;
|
|
@ -1,17 +1,26 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::StorageBackend;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BackendConfigs {
|
||||
Scylla {
|
||||
nodes: Vec<String>,
|
||||
// Scylla {
|
||||
// nodes: Vec<String>,
|
||||
// },
|
||||
FileSystem {
|
||||
directory: PathBuf,
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendConfigs {
|
||||
pub async fn connect(&self) -> anyhow::Result<Arc<dyn StorageBackend>> {
|
||||
todo!()
|
||||
match self {
|
||||
Self::FileSystem { directory } => {
|
||||
super::filesystem::FileSystemBackend::new(directory.clone(), )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
use std::hash::Hash;
|
||||
|
||||
pub fn crc_hash<H: Hash>(v: H) -> u32 {
|
||||
let mut hasher = crc32fast::Hasher::default();
|
||||
v.hash(&mut hasher);
|
||||
hasher.finalize()
|
||||
}
|
Loading…
Reference in New Issue