201 lines
5.3 KiB
Rust
201 lines
5.3 KiB
Rust
use std::fmt;
|
|
|
|
use async_trait::async_trait;
|
|
use bytes::Bytes;
|
|
use loco_rs::app::{AppContext, Initializer};
|
|
use once_cell::sync::OnceCell;
|
|
use opendal::{layers::LoggingLayer, services::Fs, Buffer, Operator};
|
|
use quirks_path::{Path, PathBuf};
|
|
use serde::{Deserialize, Serialize};
|
|
use url::Url;
|
|
use uuid::Uuid;
|
|
|
|
use super::AppDalConfig;
|
|
use crate::{app::App, config::AppConfigExt};
|
|
|
|
// TODO: wait app-context-trait to integrate
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum DalContentCategory {
|
|
Image,
|
|
}
|
|
|
|
impl AsRef<str> for DalContentCategory {
|
|
fn as_ref(&self) -> &str {
|
|
match self {
|
|
Self::Image => "image",
|
|
}
|
|
}
|
|
}
|
|
|
|
static APP_DAL_CLIENT: OnceCell<AppDalClient> = OnceCell::new();
|
|
|
|
pub enum DalStoredUrl {
|
|
RelativePath { path: String },
|
|
Absolute { url: Url },
|
|
}
|
|
|
|
impl AsRef<str> for DalStoredUrl {
|
|
fn as_ref(&self) -> &str {
|
|
match &self {
|
|
Self::Absolute { url } => url.as_str(),
|
|
Self::RelativePath { path } => path,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DalStoredUrl {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.as_ref())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AppDalClient {
|
|
pub data_dir: String,
|
|
}
|
|
|
|
impl AppDalClient {
|
|
pub fn new(config: AppDalConfig) -> Self {
|
|
Self {
|
|
data_dir: App::get_working_root()
|
|
.join(config.data_dir.as_deref().unwrap_or("./data"))
|
|
.to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn app_instance() -> &'static AppDalClient {
|
|
APP_DAL_CLIENT
|
|
.get()
|
|
.expect("AppDalClient is not initialized")
|
|
}
|
|
|
|
pub fn get_fs(&self) -> Fs {
|
|
Fs::default().root(&self.data_dir)
|
|
}
|
|
|
|
pub fn create_filename(extname: &str) -> String {
|
|
format!("{}{}", Uuid::new_v4(), extname)
|
|
}
|
|
|
|
pub async fn store_object(
|
|
&self,
|
|
content_category: DalContentCategory,
|
|
subscriber_pid: &str,
|
|
bucket: Option<&str>,
|
|
filename: &str,
|
|
data: Bytes,
|
|
) -> color_eyre::eyre::Result<DalStoredUrl> {
|
|
match content_category {
|
|
DalContentCategory::Image => {
|
|
let fullname = [
|
|
subscriber_pid,
|
|
content_category.as_ref(),
|
|
bucket.unwrap_or_default(),
|
|
filename,
|
|
]
|
|
.into_iter()
|
|
.map(Path::new)
|
|
.collect::<PathBuf>();
|
|
|
|
let fs_op = Operator::new(self.get_fs())?
|
|
.layer(LoggingLayer::default())
|
|
.finish();
|
|
|
|
if let Some(dirname) = fullname.parent() {
|
|
let dirname = dirname.join("/");
|
|
fs_op.create_dir(dirname.as_str()).await?;
|
|
}
|
|
|
|
fs_op.write(fullname.as_str(), data).await?;
|
|
|
|
Ok(DalStoredUrl::RelativePath {
|
|
path: fullname.to_string(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn exists_object(
|
|
&self,
|
|
content_category: DalContentCategory,
|
|
subscriber_pid: &str,
|
|
bucket: Option<&str>,
|
|
filename: &str,
|
|
) -> color_eyre::eyre::Result<Option<DalStoredUrl>> {
|
|
match content_category {
|
|
DalContentCategory::Image => {
|
|
let fullname = [
|
|
subscriber_pid,
|
|
content_category.as_ref(),
|
|
bucket.unwrap_or_default(),
|
|
filename,
|
|
]
|
|
.into_iter()
|
|
.map(Path::new)
|
|
.collect::<PathBuf>();
|
|
|
|
let fs_op = Operator::new(self.get_fs())?
|
|
.layer(LoggingLayer::default())
|
|
.finish();
|
|
|
|
if fs_op.exists(fullname.as_str()).await? {
|
|
Ok(Some(DalStoredUrl::RelativePath {
|
|
path: fullname.to_string(),
|
|
}))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn load_object(
|
|
&self,
|
|
content_category: DalContentCategory,
|
|
subscriber_pid: &str,
|
|
bucket: Option<&str>,
|
|
filename: &str,
|
|
) -> color_eyre::eyre::Result<Buffer> {
|
|
match content_category {
|
|
DalContentCategory::Image => {
|
|
let fullname = [
|
|
subscriber_pid,
|
|
content_category.as_ref(),
|
|
bucket.unwrap_or_default(),
|
|
filename,
|
|
]
|
|
.into_iter()
|
|
.map(Path::new)
|
|
.collect::<PathBuf>();
|
|
|
|
let fs_op = Operator::new(self.get_fs())?
|
|
.layer(LoggingLayer::default())
|
|
.finish();
|
|
|
|
let data = fs_op.read(fullname.as_str()).await?;
|
|
|
|
Ok(data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AppDalInitalizer;
|
|
|
|
#[async_trait]
|
|
impl Initializer for AppDalInitalizer {
|
|
fn name(&self) -> String {
|
|
String::from("AppDalInitalizer")
|
|
}
|
|
|
|
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
|
let config = &app_context.config;
|
|
let app_dal_conf = config.get_app_conf()?.dal;
|
|
|
|
APP_DAL_CLIENT.get_or_init(|| AppDalClient::new(app_dal_conf));
|
|
|
|
Ok(())
|
|
}
|
|
}
|