use crate::attachments;
use crate::blob::{self, Blob};
use crate::config;
use crate::service::{Service, UrlOptions};
use crate::signing::{Disposition, Signer};
use doido_core::Result;
use doido_model::sea_orm::DatabaseConnection;
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
const PURPOSE_BLOB: &str = "blob";
pub(crate) const PURPOSE_DISK_UPLOAD: &str = "disk_upload";
#[derive(Clone)]
pub struct Storage {
conn: DatabaseConnection,
service: Arc<dyn Service>,
signer: Signer,
prefix: String,
expires_in: Duration,
}
impl Storage {
pub fn new(conn: DatabaseConnection, service: Arc<dyn Service>, signer: Signer) -> Self {
Self {
conn,
service,
signer,
prefix: "/doido/storage".to_string(),
expires_in: Duration::from_secs(300),
}
}
pub async fn from_config(conn: DatabaseConnection) -> Result<Self> {
let service = config::load().build().await?;
Ok(Self::new(conn, service, Signer::from_env()))
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_expiry(mut self, expires_in: Duration) -> Self {
self.expires_in = expires_in;
self
}
pub fn conn(&self) -> &DatabaseConnection {
&self.conn
}
pub fn service(&self) -> &Arc<dyn Service> {
&self.service
}
pub fn prefix(&self) -> &str {
&self.prefix
}
pub(crate) fn signer(&self) -> &Signer {
&self.signer
}
pub(crate) fn expires_in(&self) -> Duration {
self.expires_in
}
pub async fn ensure_tables(&self) -> Result<()> {
crate::schema::ensure_tables(&self.conn).await
}
pub async fn create_and_upload(
&self,
filename: &str,
data: Vec<u8>,
metadata: Option<Value>,
) -> Result<Blob> {
let blob = blob::build(filename, &data, self.service.name(), metadata);
self.service
.upload(&blob.key, data, blob.content_type.as_deref())
.await?;
blob::insert(&self.conn, &blob).await?;
Ok(blob)
}
pub async fn find_blob(&self, key: &str) -> Result<Option<Blob>> {
blob::find(&self.conn, key).await
}
pub async fn download(&self, key: &str) -> Result<Vec<u8>> {
self.service.download(key).await
}
pub async fn purge(&self, key: &str) -> Result<()> {
self.service.delete(key).await?;
attachments::delete_for_blob(&self.conn, key).await?;
blob::delete(&self.conn, key).await?;
Ok(())
}
pub async fn attach_upload(
&self,
record_type: &str,
record_id: &str,
name: &str,
filename: &str,
data: Vec<u8>,
) -> Result<Blob> {
let blob = self.create_and_upload(filename, data, None).await?;
attachments::attach(&self.conn, name, record_type, record_id, &blob.key).await?;
Ok(blob)
}
pub async fn attach(
&self,
record_type: &str,
record_id: &str,
name: &str,
blob_key: &str,
) -> Result<()> {
attachments::attach(&self.conn, name, record_type, record_id, blob_key).await
}
pub async fn one(
&self,
record_type: &str,
record_id: &str,
name: &str,
) -> Result<Option<Blob>> {
attachments::one(&self.conn, name, record_type, record_id).await
}
pub async fn many(&self, record_type: &str, record_id: &str, name: &str) -> Result<Vec<Blob>> {
attachments::many(&self.conn, name, record_type, record_id).await
}
pub async fn detach(&self, record_type: &str, record_id: &str, name: &str) -> Result<()> {
attachments::detach(&self.conn, name, record_type, record_id).await
}
pub async fn purge_for_record(&self, record_type: &str, record_id: &str) -> Result<()> {
let keys = attachments::all_keys_for_record(&self.conn, record_type, record_id).await?;
for key in keys {
self.purge(&key).await?;
}
Ok(())
}
pub fn signed_id(&self, key: &str) -> String {
self.signer.sign(key, PURPOSE_BLOB, None)
}
pub fn verify_signed_id(&self, signed_id: &str) -> Result<String> {
self.signer.verify(signed_id, PURPOSE_BLOB)
}
pub fn redirect_path(&self, blob: &Blob) -> String {
format!(
"{}/blobs/redirect/{}/{}",
self.prefix,
self.signed_id(&blob.key),
encode_filename(&blob.filename)
)
}
pub fn proxy_path(&self, blob: &Blob) -> String {
format!(
"{}/blobs/proxy/{}/{}",
self.prefix,
self.signed_id(&blob.key),
encode_filename(&blob.filename)
)
}
pub async fn url_for(&self, blob: &Blob, disposition: Disposition) -> Result<String> {
let opts = UrlOptions {
expires_in: self.expires_in,
disposition,
filename: Some(blob.filename.clone()),
content_type: blob.content_type.clone(),
};
match self.service.url(&blob.key, &opts).await? {
Some(url) => Ok(url),
None => Ok(self.proxy_path(blob)),
}
}
}
fn encode_filename(filename: &str) -> String {
filename.replace(['/', '\\'], "_")
}