use bytes::Bytes;
use chrono::Utc;
use crate::config::StorageConfig;
use crate::error::Error;
use crate::img;
use crate::traits::StorageBackend;
use crate::types::*;
pub struct StorageService<B: StorageBackend> {
backend: B,
config: StorageConfig,
}
impl<B: StorageBackend> StorageService<B> {
pub fn new(backend: B, config: StorageConfig) -> Self {
Self { backend, config }
}
pub fn backend(&self) -> &B {
&self.backend
}
pub fn config(&self) -> &StorageConfig {
&self.config
}
fn validate_output_dimensions(&self, width: u32, height: u32) -> Result<(), Error> {
let max = self.config.max_image_dimension;
if width == 0 || height == 0 || width > max || height > max {
return Err(Error::InvalidDimensions(width, height, max));
}
Ok(())
}
fn make_put_options(&self, content_disposition: Option<&str>) -> PutOptions {
PutOptions {
cache_control: Some(self.config.default_cache_control.clone()),
content_disposition: content_disposition.map(String::from),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn upload_image_with_key(
&self,
data: &[u8],
content_type: &str,
key: &str,
width: u32,
height: u32,
thumbnail_width: u32,
thumbnail_height: u32,
) -> Result<UploadResult, Error> {
validate_content_type(content_type)?;
validate_file_size(data.len(), self.config.max_file_size)?;
self.validate_output_dimensions(width, height)?;
self.validate_output_dimensions(thumbnail_width, thumbnail_height)?;
let original = img::load_image(
data,
self.config.max_image_alloc,
self.config.max_image_dimension,
)?;
let format = img::format_from_content_type(content_type);
let thumbnail_key = generate_thumbnail_key(key);
let resized = img::resize_image(&original, width, height, false);
let thumbnail = img::resize_image(&original, thumbnail_width, thumbnail_height, false);
let main_bytes = img::encode_image(&resized, format)?;
let thumb_bytes = img::encode_image(&thumbnail, format)?;
let options = self.make_put_options(None);
log::debug!("Uploading main image: {key}");
self.backend
.put_object(key, Bytes::from(main_bytes), content_type, &options)
.await?;
log::debug!("Uploading thumbnail: {thumbnail_key}");
if let Err(e) = self
.backend
.put_object(
&thumbnail_key,
Bytes::from(thumb_bytes),
content_type,
&options,
)
.await
{
log::warn!("Thumbnail upload failed, cleaning up main image: {key}");
let _ = self.backend.delete_object(key).await;
return Err(e);
}
log::info!("Successfully uploaded image: {key}");
Ok(UploadResult {
url: self.backend.public_url(key),
thumbnail_url: self.backend.public_url(&thumbnail_key),
key: key.to_string(),
thumbnail_key,
})
}
pub async fn upload_image_with_config(
&self,
data: &[u8],
content_type: &str,
config: &ImageUploadConfig,
) -> Result<UploadResult, Error> {
validate_content_type(content_type)?;
validate_file_size(data.len(), self.config.max_file_size)?;
self.validate_output_dimensions(config.width, config.height)?;
self.validate_output_dimensions(config.thumbnail_width, config.thumbnail_height)?;
let original = img::load_image(
data,
self.config.max_image_alloc,
self.config.max_image_dimension,
)?;
let format = img::format_from_content_type(content_type);
let extension = img::extension_from_content_type(content_type);
let resized = img::resize_image(
&original,
config.width,
config.height,
config.maintain_aspect_ratio,
);
let thumbnail = img::resize_image(
&original,
config.thumbnail_width,
config.thumbnail_height,
config.maintain_aspect_ratio,
);
let main_bytes = img::encode_image(&resized, format)?;
let thumb_bytes = img::encode_image(&thumbnail, format)?;
let key = generate_storage_key(&config.folder, extension);
let thumbnail_key = generate_thumbnail_key(&key);
let options = self.make_put_options(None);
log::debug!("Uploading main image: {key}");
self.backend
.put_object(&key, Bytes::from(main_bytes), content_type, &options)
.await?;
log::debug!("Uploading thumbnail: {thumbnail_key}");
if let Err(e) = self
.backend
.put_object(
&thumbnail_key,
Bytes::from(thumb_bytes),
content_type,
&options,
)
.await
{
log::warn!("Thumbnail upload failed, cleaning up main image: {key}");
let _ = self.backend.delete_object(&key).await;
return Err(e);
}
log::info!("Successfully uploaded image: {key}");
Ok(UploadResult {
url: self.backend.public_url(&key),
thumbnail_url: self.backend.public_url(&thumbnail_key),
key,
thumbnail_key,
})
}
pub async fn upload_file(
&self,
data: &[u8],
content_type: &str,
folder: &str,
extension: &str,
) -> Result<FileUploadResult, Error> {
validate_file_size(data.len(), self.config.max_file_size)?;
let key = generate_storage_key(folder, extension);
let options = self.make_put_options(Some("attachment"));
log::debug!("Uploading file: {key}");
self.backend
.put_object(&key, Bytes::copy_from_slice(data), content_type, &options)
.await?;
log::info!("Successfully uploaded file: {key}");
Ok(FileUploadResult {
url: self.backend.public_url(&key),
key,
content_type: content_type.to_string(),
})
}
pub async fn download_object(&self, key: &str) -> Result<DownloadResult, Error> {
log::debug!("Downloading object: {key}");
let raw = self.backend.get_object(key).await?;
let max_size = self.config.max_download_size;
if let Some(len) = raw.content_length
&& len > 0
&& (len as u64) > max_size
{
return Err(Error::FileTooLarge(len as usize, max_size as usize));
}
if (raw.data.len() as u64) > max_size {
return Err(Error::FileTooLarge(raw.data.len(), max_size as usize));
}
log::info!("Downloaded object: {key} ({} bytes)", raw.data.len());
Ok(DownloadResult {
data: raw.data,
content_type: raw.content_type,
content_length: raw.content_length,
})
}
pub async fn delete_file(&self, key: &str) -> Result<(), Error> {
log::debug!("Deleting file: {key}");
self.backend.delete_object(key).await?;
log::info!("Deleted file: {key}");
Ok(())
}
pub async fn delete_image_with_thumbnail(&self, key: &str) -> Result<(), Error> {
let thumbnail_key = generate_thumbnail_key(key);
let (main_result, thumb_result) = tokio::join!(
self.backend.delete_object(key),
self.backend.delete_object(&thumbnail_key),
);
if main_result.is_err() {
if let Err(ref te) = thumb_result {
log::warn!("Thumbnail deletion also failed for {thumbnail_key}: {te}");
}
return main_result;
}
thumb_result
}
pub async fn file_exists(&self, key: &str) -> Result<bool, Error> {
self.backend.exists(key).await
}
pub async fn presigned_get_url(
&self,
key: &str,
expires_in_secs: u64,
) -> Result<String, Error> {
validate_expiry(expires_in_secs)?;
self.backend.presigned_get_url(key, expires_in_secs).await
}
pub async fn presigned_upload_url(
&self,
key: &str,
content_type: &str,
expires_in_secs: u64,
) -> Result<PresignedUploadResult, Error> {
validate_expiry(expires_in_secs)?;
let presigned_url = self
.backend
.presigned_put_url(key, content_type, expires_in_secs)
.await?;
let public_url = self.backend.public_url(key);
let expires_at = Utc::now().timestamp() + expires_in_secs as i64;
Ok(PresignedUploadResult {
presigned_url,
key: key.to_string(),
public_url,
expires_at,
})
}
pub fn public_url(&self, key: &str) -> String {
self.backend.public_url(key)
}
pub async fn test_connection(&self) -> Result<(), Error> {
log::debug!("Testing backend connection");
self.backend.test_connection().await
}
pub fn extract_key_from_url(&self, url: &str) -> Option<String> {
let base = self.backend.public_url("");
url.strip_prefix(base.trim_end_matches('/'))?
.strip_prefix('/')
.map(String::from)
}
}