Add bucket config function and further improve storage api

This commit is contained in:
Harrison Burt 2022-03-27 12:17:17 +01:00
parent d1a4d158cb
commit 845500f689
8 changed files with 190 additions and 17 deletions

View File

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

View File

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

View File

@ -3,6 +3,7 @@ mod storage;
mod routes;
mod pipelines;
mod controller;
mod utils;
use std::path::PathBuf;
use std::sync::Arc;

View File

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

View File

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

View File

@ -1,3 +1,4 @@
mod register;
mod filesystem;
pub use register::BackendConfigs;

View File

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

7
src/utils.rs Normal file
View File

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