Skip to content
Clone
eba4fd1
213 lines 6.3 KB Rust
use export::redis;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use crate::errors::AdminError;
use crate::services::service::AdminService;
use crate::share::roles::{AdminActor, AdminRole};
const OVERVIEW_CACHE_KEY: &str = "admin:storage:overview";
const OVERVIEW_CACHE_TTL_SECS: u64 = 60;
const MAX_PRESIGN_SECS: u32 = 3600;
const CATEGORY_PREFIXES: [(&str, &str); 6] = [
("user_avatars", "avatars/"),
("space_avatars", "space-avatars/"),
("space_images", "space-images/"),
("chat", "chat/"),
("lfs", "lfs/"),
("git", "git/"),
];
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct StorageCategoryDto {
pub name: String,
pub prefix: String,
pub bytes: u64,
pub objects: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct StorageOverviewDto {
pub bucket: String,
pub total_bytes: u64,
pub total_objects: u64,
pub categories: Vec<StorageCategoryDto>,
pub generated_at: OffsetDateTime,
pub cached: bool,
}
#[derive(Debug, Clone, Deserialize, IntoParams)]
pub struct ListObjectsQuery {
pub prefix: Option<String>,
pub token: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct StorageObjectDto {
pub key: String,
pub size: u64,
pub last_modified: Option<serde_json::Value>,
pub etag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct StorageObjectPageDto {
pub prefix: String,
pub items: Vec<StorageObjectDto>,
pub next_token: Option<String>,
}
#[derive(Debug, Clone, Deserialize, IntoParams)]
pub struct ObjectKeyQuery {
pub key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DownloadUrlDto {
pub key: String,
pub url: String,
pub expires_in_secs: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeleteObjectResp {
pub key: String,
pub deleted: bool,
}
impl AdminService {
pub async fn storage_overview(&self, refresh: bool) -> Result<StorageOverviewDto, AdminError> {
if !refresh
&& let Ok(Some(raw)) = redis::get_string(self.redis(), OVERVIEW_CACHE_KEY).await
&& let Ok(mut cached) = serde_json::from_str::<StorageOverviewDto>(&raw)
{
cached.cached = true;
return Ok(cached);
}
let client = self.storage().client();
let objects = client.list("").await?;
let mut total_bytes: u64 = 0;
let mut total_objects: u64 = 0;
let mut categories: Vec<StorageCategoryDto> = CATEGORY_PREFIXES
.iter()
.map(|(name, prefix)| StorageCategoryDto {
name: name.to_string(),
prefix: prefix.to_string(),
bytes: 0,
objects: 0,
})
.collect();
let mut other = StorageCategoryDto {
name: "other".to_string(),
prefix: String::new(),
bytes: 0,
objects: 0,
};
for object in &objects {
total_bytes += object.size;
total_objects += 1;
match categories
.iter_mut()
.find(|category| object.key.starts_with(&category.prefix))
{
Some(category) => {
category.bytes += object.size;
category.objects += 1;
}
None => {
other.bytes += object.size;
other.objects += 1;
}
}
}
categories.push(other);
let overview = StorageOverviewDto {
bucket: client.bucket_name().to_string(),
total_bytes,
total_objects,
categories,
generated_at: OffsetDateTime::now_utc(),
cached: false,
};
if let Ok(serialized) = serde_json::to_string(&overview) {
let _ = redis::set_string(
self.redis(),
OVERVIEW_CACHE_KEY,
&serialized,
OVERVIEW_CACHE_TTL_SECS,
)
.await;
}
Ok(overview)
}
pub async fn list_storage_objects(
&self,
query: ListObjectsQuery,
) -> Result<StorageObjectPageDto, AdminError> {
let prefix = query.prefix.unwrap_or_default();
let limit = query.limit.unwrap_or(100).clamp(1, 1000);
let page = self
.storage()
.client()
.list_page(&prefix, query.token.as_deref(), Some(limit))
.await?;
let items = page
.objects
.into_iter()
.map(|object| StorageObjectDto {
key: object.key,
size: object.size,
last_modified: object
.last_modified
.as_ref()
.and_then(|value| serde_json::to_value(value).ok()),
etag: object.etag,
})
.collect();
Ok(StorageObjectPageDto {
prefix,
items,
next_token: page.next_continuation_token,
})
}
pub async fn storage_download_url(&self, key: String) -> Result<DownloadUrlDto, AdminError> {
if key.trim().is_empty() {
return Err(AdminError::BadRequest("key must not be empty".to_string()));
}
let exists = self.storage().client().exists(&key).await?;
if !exists {
return Err(AdminError::NotFound);
}
let url = self
.storage()
.client()
.presign_get(&key, MAX_PRESIGN_SECS)?;
Ok(DownloadUrlDto {
key,
url,
expires_in_secs: MAX_PRESIGN_SECS,
})
}
pub async fn delete_storage_object(
&self,
actor: &AdminActor,
key: String,
) -> Result<DeleteObjectResp, AdminError> {
actor.require(AdminRole::SuperAdmin)?;
if key.trim().is_empty() {
return Err(AdminError::BadRequest("key must not be empty".to_string()));
}
let exists = self.storage().client().exists(&key).await?;
if !exists {
return Err(AdminError::NotFound);
}
self.storage().client().delete(&key).await?;
Ok(DeleteObjectResp { key, deleted: true })
}
}