feat: ship public ops features and cache docker builds
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Has been cancelled
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Has been cancelled
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Has been cancelled
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Has been cancelled
This commit is contained in:
@@ -22,7 +22,7 @@ use crate::{
|
||||
ai_chunks, comment_blacklist, comment_persona_analysis_logs, comments, friend_links, posts,
|
||||
reviews,
|
||||
},
|
||||
services::{admin_audit, ai, analytics, comment_guard, content, storage},
|
||||
services::{admin_audit, ai, analytics, comment_guard, content, media_assets, storage},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -170,6 +170,14 @@ pub struct AdminSiteSettingsResponse {
|
||||
pub music_playlist: Vec<site_settings::MusicTrackPayload>,
|
||||
pub ai_enabled: bool,
|
||||
pub paragraph_comments_enabled: bool,
|
||||
pub comment_turnstile_enabled: bool,
|
||||
pub subscription_turnstile_enabled: bool,
|
||||
pub web_push_enabled: bool,
|
||||
pub turnstile_site_key: Option<String>,
|
||||
pub turnstile_secret_key: Option<String>,
|
||||
pub web_push_vapid_public_key: Option<String>,
|
||||
pub web_push_vapid_private_key: Option<String>,
|
||||
pub web_push_vapid_subject: Option<String>,
|
||||
pub ai_provider: Option<String>,
|
||||
pub ai_api_base: Option<String>,
|
||||
pub ai_api_key: Option<String>,
|
||||
@@ -196,6 +204,7 @@ pub struct AdminSiteSettingsResponse {
|
||||
pub seo_default_og_image: Option<String>,
|
||||
pub seo_default_twitter_handle: Option<String>,
|
||||
pub notification_webhook_url: Option<String>,
|
||||
pub notification_channel_type: String,
|
||||
pub notification_comment_enabled: bool,
|
||||
pub notification_friend_link_enabled: bool,
|
||||
pub subscription_popup_enabled: bool,
|
||||
@@ -258,6 +267,11 @@ pub struct AdminMediaObjectResponse {
|
||||
pub url: String,
|
||||
pub size_bytes: i64,
|
||||
pub last_modified: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub alt_text: Option<String>,
|
||||
pub caption: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -304,6 +318,32 @@ pub struct AdminMediaReplaceResponse {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AdminMediaMetadataPayload {
|
||||
pub key: String,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub alt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub caption: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AdminMediaMetadataResponse {
|
||||
pub saved: bool,
|
||||
pub key: String,
|
||||
pub title: Option<String>,
|
||||
pub alt_text: Option<String>,
|
||||
pub caption: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AdminMediaListQuery {
|
||||
pub prefix: Option<String>,
|
||||
@@ -634,6 +674,25 @@ fn normalize_media_key(value: Option<String>) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_media_object_response(
|
||||
item: storage::StoredObjectSummary,
|
||||
metadata: Option<&crate::models::_entities::media_assets::Model>,
|
||||
) -> AdminMediaObjectResponse {
|
||||
AdminMediaObjectResponse {
|
||||
key: item.key,
|
||||
url: item.url,
|
||||
size_bytes: item.size_bytes,
|
||||
last_modified: item.last_modified,
|
||||
title: metadata.and_then(|entry| entry.title.clone()),
|
||||
alt_text: metadata.and_then(|entry| entry.alt_text.clone()),
|
||||
caption: metadata.and_then(|entry| entry.caption.clone()),
|
||||
tags: metadata
|
||||
.map(media_assets::tag_list)
|
||||
.unwrap_or_default(),
|
||||
notes: metadata.and_then(|entry| entry.notes.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tech_stack_values(value: &Option<serde_json::Value>) -> Vec<String> {
|
||||
value
|
||||
.as_ref()
|
||||
@@ -665,6 +724,11 @@ fn build_settings_response(
|
||||
) -> AdminSiteSettingsResponse {
|
||||
let ai_providers = site_settings::ai_provider_configs(&item);
|
||||
let ai_active_provider_id = site_settings::active_ai_provider_id(&item);
|
||||
let turnstile_site_key = crate::services::turnstile::site_key(&item);
|
||||
let turnstile_secret_key = crate::services::turnstile::secret_key(&item);
|
||||
let web_push_vapid_public_key = crate::services::web_push::public_key(&item);
|
||||
let web_push_vapid_private_key = crate::services::web_push::private_key(&item);
|
||||
let web_push_vapid_subject = crate::services::web_push::vapid_subject(&item);
|
||||
|
||||
AdminSiteSettingsResponse {
|
||||
id: item.id,
|
||||
@@ -687,6 +751,14 @@ fn build_settings_response(
|
||||
music_playlist: music_playlist_values(&item.music_playlist),
|
||||
ai_enabled: item.ai_enabled.unwrap_or(false),
|
||||
paragraph_comments_enabled: item.paragraph_comments_enabled.unwrap_or(true),
|
||||
comment_turnstile_enabled: item.comment_turnstile_enabled.unwrap_or(false),
|
||||
subscription_turnstile_enabled: item.subscription_turnstile_enabled.unwrap_or(false),
|
||||
web_push_enabled: item.web_push_enabled.unwrap_or(false),
|
||||
turnstile_site_key,
|
||||
turnstile_secret_key,
|
||||
web_push_vapid_public_key,
|
||||
web_push_vapid_private_key,
|
||||
web_push_vapid_subject,
|
||||
ai_provider: item.ai_provider,
|
||||
ai_api_base: item.ai_api_base,
|
||||
ai_api_key: item.ai_api_key,
|
||||
@@ -713,6 +785,9 @@ fn build_settings_response(
|
||||
seo_default_og_image: item.seo_default_og_image,
|
||||
seo_default_twitter_handle: item.seo_default_twitter_handle,
|
||||
notification_webhook_url: item.notification_webhook_url,
|
||||
notification_channel_type: item
|
||||
.notification_channel_type
|
||||
.unwrap_or_else(|| "webhook".to_string()),
|
||||
notification_comment_enabled: item.notification_comment_enabled.unwrap_or(false),
|
||||
notification_friend_link_enabled: item.notification_friend_link_enabled.unwrap_or(false),
|
||||
subscription_popup_enabled: item
|
||||
@@ -1115,14 +1190,18 @@ pub async fn list_media_objects(
|
||||
check_auth(&headers)?;
|
||||
|
||||
let settings = storage::require_r2_settings(&ctx).await?;
|
||||
let items = storage::list_objects(&ctx, query.prefix.as_deref(), query.limit.unwrap_or(200))
|
||||
.await?
|
||||
let objects = storage::list_objects(&ctx, query.prefix.as_deref(), query.limit.unwrap_or(200))
|
||||
.await?;
|
||||
let keys = objects
|
||||
.iter()
|
||||
.map(|item| item.key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let metadata_map = media_assets::list_by_keys(&ctx, &keys).await?;
|
||||
let items = objects
|
||||
.into_iter()
|
||||
.map(|item| AdminMediaObjectResponse {
|
||||
key: item.key,
|
||||
url: item.url,
|
||||
size_bytes: item.size_bytes,
|
||||
last_modified: item.last_modified,
|
||||
.map(|item| {
|
||||
let metadata = metadata_map.get(&item.key);
|
||||
build_media_object_response(item, metadata)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1148,6 +1227,9 @@ pub async fn delete_media_object(
|
||||
}
|
||||
|
||||
storage::delete_object(&ctx, key).await?;
|
||||
if let Err(error) = media_assets::delete_by_key(&ctx, key).await {
|
||||
tracing::warn!(?error, key, "failed to delete media metadata after object deletion");
|
||||
}
|
||||
|
||||
format::json(AdminMediaDeleteResponse {
|
||||
deleted: true,
|
||||
@@ -1241,7 +1323,12 @@ pub async fn batch_delete_media_objects(
|
||||
|
||||
for key in keys {
|
||||
match storage::delete_object(&ctx, &key).await {
|
||||
Ok(()) => deleted.push(key),
|
||||
Ok(()) => {
|
||||
if let Err(error) = media_assets::delete_by_key(&ctx, &key).await {
|
||||
tracing::warn!(?error, key, "failed to delete media metadata after batch removal");
|
||||
}
|
||||
deleted.push(key)
|
||||
}
|
||||
Err(_) => failed.push(key),
|
||||
}
|
||||
}
|
||||
@@ -1249,6 +1336,43 @@ pub async fn batch_delete_media_objects(
|
||||
format::json(AdminMediaBatchDeleteResponse { deleted, failed })
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn update_media_object_metadata(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<AdminMediaMetadataPayload>,
|
||||
) -> Result<Response> {
|
||||
check_auth(&headers)?;
|
||||
|
||||
let key = payload.key.trim();
|
||||
if key.is_empty() {
|
||||
return Err(Error::BadRequest("缺少对象 key".to_string()));
|
||||
}
|
||||
|
||||
let metadata = media_assets::upsert_by_key(
|
||||
&ctx,
|
||||
key,
|
||||
media_assets::MediaAssetMetadataInput {
|
||||
title: payload.title,
|
||||
alt_text: payload.alt_text,
|
||||
caption: payload.caption,
|
||||
tags: payload.tags,
|
||||
notes: payload.notes,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(AdminMediaMetadataResponse {
|
||||
saved: true,
|
||||
key: metadata.object_key.clone(),
|
||||
title: metadata.title.clone(),
|
||||
alt_text: metadata.alt_text.clone(),
|
||||
caption: metadata.caption.clone(),
|
||||
tags: media_assets::tag_list(&metadata),
|
||||
notes: metadata.notes.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn replace_media_object(
|
||||
headers: HeaderMap,
|
||||
@@ -1831,6 +1955,7 @@ pub fn routes() -> Routes {
|
||||
"/storage/media/batch-delete",
|
||||
post(batch_delete_media_objects),
|
||||
)
|
||||
.add("/storage/media/metadata", patch(update_media_object_metadata))
|
||||
.add("/storage/media/replace", post(replace_media_object))
|
||||
.add(
|
||||
"/comments/blacklist",
|
||||
|
||||
Reference in New Issue
Block a user