feat: add worker operations and fix gitea actions
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 29s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Successful in 33m13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 58s
ui-regression / playwright-regression (push) Failing after 13m24s
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 29s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Successful in 33m13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 58s
ui-regression / playwright-regression (push) Failing after 13m24s
This commit is contained in:
@@ -22,7 +22,10 @@ use crate::{
|
||||
ai_chunks, comment_blacklist, comment_persona_analysis_logs, comments, friend_links, posts,
|
||||
reviews,
|
||||
},
|
||||
services::{admin_audit, ai, analytics, comment_guard, content, media_assets, storage},
|
||||
services::{
|
||||
admin_audit, ai, analytics, comment_guard, content, media_assets, storage, worker_jobs,
|
||||
},
|
||||
workers::downloader::DownloadWorkerArgs,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -346,6 +349,30 @@ pub struct AdminMediaMetadataResponse {
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AdminMediaDownloadPayload {
|
||||
pub source_url: String,
|
||||
#[serde(default)]
|
||||
pub prefix: Option<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 AdminMediaDownloadResponse {
|
||||
pub queued: bool,
|
||||
pub job_id: i32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AdminMediaListQuery {
|
||||
pub prefix: Option<String>,
|
||||
@@ -1457,6 +1484,55 @@ pub async fn replace_media_object(
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn download_media_object(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<AdminMediaDownloadPayload>,
|
||||
) -> Result<Response> {
|
||||
let actor = check_auth(&headers)?;
|
||||
let worker_args = DownloadWorkerArgs {
|
||||
source_url: payload.source_url.clone(),
|
||||
prefix: payload.prefix.clone(),
|
||||
title: payload.title.clone(),
|
||||
alt_text: payload.alt_text.clone(),
|
||||
caption: payload.caption.clone(),
|
||||
tags: payload.tags.unwrap_or_default(),
|
||||
notes: payload.notes.clone(),
|
||||
job_id: None,
|
||||
};
|
||||
let job = worker_jobs::queue_download_job(
|
||||
&ctx,
|
||||
&worker_args,
|
||||
Some(actor.username.clone()),
|
||||
Some(actor.source.clone()),
|
||||
None,
|
||||
Some("manual".to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
"media.download",
|
||||
"media",
|
||||
Some(job.id.to_string()),
|
||||
Some(payload.source_url.clone()),
|
||||
Some(serde_json::json!({
|
||||
"job_id": job.id,
|
||||
"queued": true,
|
||||
"source_url": payload.source_url,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(AdminMediaDownloadResponse {
|
||||
queued: true,
|
||||
job_id: job.id,
|
||||
status: job.status,
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn list_comment_blacklist(
|
||||
headers: HeaderMap,
|
||||
@@ -1982,6 +2058,7 @@ pub fn routes() -> Routes {
|
||||
"/storage/media/metadata",
|
||||
patch(update_media_object_metadata),
|
||||
)
|
||||
.add("/storage/media/download", post(download_media_object))
|
||||
.add("/storage/media/replace", post(replace_media_object))
|
||||
.add(
|
||||
"/comments/blacklist",
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
},
|
||||
services::{
|
||||
admin_audit, backups, post_revisions as revision_service,
|
||||
subscriptions as subscription_service,
|
||||
subscriptions as subscription_service, worker_jobs,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,6 +35,15 @@ pub struct DeliveriesQuery {
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct WorkerJobsQuery {
|
||||
pub status: Option<String>,
|
||||
pub job_kind: Option<String>,
|
||||
pub worker_name: Option<String>,
|
||||
pub search: Option<String>,
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SubscriptionPayload {
|
||||
#[serde(alias = "channelType")]
|
||||
@@ -85,6 +94,11 @@ pub struct DigestDispatchRequest {
|
||||
pub period: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct RetryDeliveriesRequest {
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SiteBackupImportRequest {
|
||||
pub backup: backups::SiteBackupDocument,
|
||||
@@ -132,6 +146,12 @@ pub struct DeliveryListResponse {
|
||||
pub deliveries: Vec<notification_deliveries::Model>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct WorkerTaskActionResponse {
|
||||
pub queued: bool,
|
||||
pub job: worker_jobs::WorkerJobRecord,
|
||||
}
|
||||
|
||||
fn trim_to_option(value: Option<String>) -> Option<String> {
|
||||
value.and_then(|item| {
|
||||
let trimmed = item.trim().to_string();
|
||||
@@ -408,6 +428,13 @@ pub async fn test_subscription(
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
let delivery = subscription_service::send_test_notification(&ctx, &item).await?;
|
||||
let job = worker_jobs::find_latest_job_by_related_entity(
|
||||
&ctx,
|
||||
"notification_delivery",
|
||||
&delivery.id.to_string(),
|
||||
Some(worker_jobs::WORKER_NOTIFICATION_DELIVERY),
|
||||
)
|
||||
.await?;
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
@@ -419,7 +446,12 @@ pub async fn test_subscription(
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(serde_json::json!({ "queued": true, "id": item.id, "delivery_id": delivery.id }))
|
||||
format::json(serde_json::json!({
|
||||
"queued": true,
|
||||
"id": item.id,
|
||||
"delivery_id": delivery.id,
|
||||
"job_id": job.as_ref().map(|value| value.id),
|
||||
}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
@@ -450,6 +482,162 @@ pub async fn send_subscription_digest(
|
||||
format::json(summary)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn workers_overview(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
check_auth(&headers)?;
|
||||
format::json(worker_jobs::get_overview(&ctx).await?)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn list_worker_jobs(
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<WorkerJobsQuery>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
check_auth(&headers)?;
|
||||
format::json(
|
||||
worker_jobs::list_jobs(
|
||||
&ctx,
|
||||
worker_jobs::WorkerJobListQuery {
|
||||
status: query.status,
|
||||
job_kind: query.job_kind,
|
||||
worker_name: query.worker_name,
|
||||
search: query.search,
|
||||
limit: query.limit,
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_worker_job(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
check_auth(&headers)?;
|
||||
format::json(worker_jobs::get_job_record(&ctx, id).await?)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn cancel_worker_job(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let actor = check_auth(&headers)?;
|
||||
let updated = worker_jobs::request_cancel(&ctx, id).await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
"worker.cancel",
|
||||
"worker_job",
|
||||
Some(id.to_string()),
|
||||
Some(updated.worker_name.clone()),
|
||||
Some(serde_json::json!({ "status": updated.status })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(updated)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn retry_worker_job(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let actor = check_auth(&headers)?;
|
||||
let job = worker_jobs::retry_job(
|
||||
&ctx,
|
||||
id,
|
||||
Some(actor.username.clone()),
|
||||
Some(actor.source.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
"worker.retry",
|
||||
"worker_job",
|
||||
Some(job.id.to_string()),
|
||||
Some(job.worker_name.clone()),
|
||||
Some(serde_json::json!({ "source_job_id": id })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(WorkerTaskActionResponse { queued: true, job })
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn run_retry_deliveries_job(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<RetryDeliveriesRequest>,
|
||||
) -> Result<Response> {
|
||||
let actor = check_auth(&headers)?;
|
||||
let job = worker_jobs::spawn_retry_deliveries_task(
|
||||
&ctx,
|
||||
payload.limit,
|
||||
Some(actor.username.clone()),
|
||||
Some(actor.source.clone()),
|
||||
None,
|
||||
Some("manual".to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
"worker.task.retry_deliveries",
|
||||
"worker_job",
|
||||
Some(job.id.to_string()),
|
||||
Some(job.worker_name.clone()),
|
||||
Some(serde_json::json!({ "limit": payload.limit })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(WorkerTaskActionResponse { queued: true, job })
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn run_digest_worker_job(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(payload): Json<DigestDispatchRequest>,
|
||||
) -> Result<Response> {
|
||||
let actor = check_auth(&headers)?;
|
||||
let period = payload.period.unwrap_or_else(|| "weekly".to_string());
|
||||
let job = worker_jobs::spawn_digest_task(
|
||||
&ctx,
|
||||
&period,
|
||||
Some(actor.username.clone()),
|
||||
Some(actor.source.clone()),
|
||||
None,
|
||||
Some("manual".to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
admin_audit::log_event(
|
||||
&ctx,
|
||||
Some(&actor),
|
||||
"worker.task.digest",
|
||||
"worker_job",
|
||||
Some(job.id.to_string()),
|
||||
Some(job.worker_name.clone()),
|
||||
Some(serde_json::json!({ "period": period })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
format::json(WorkerTaskActionResponse { queued: true, job })
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn export_site_backup(
|
||||
headers: HeaderMap,
|
||||
@@ -481,6 +669,13 @@ pub fn routes() -> Routes {
|
||||
.add("/subscriptions/digest", post(send_subscription_digest))
|
||||
.add("/subscriptions/{id}", patch(update_subscription).delete(delete_subscription))
|
||||
.add("/subscriptions/{id}/test", post(test_subscription))
|
||||
.add("/workers/overview", get(workers_overview))
|
||||
.add("/workers/jobs", get(list_worker_jobs))
|
||||
.add("/workers/jobs/{id}", get(get_worker_job))
|
||||
.add("/workers/jobs/{id}/cancel", post(cancel_worker_job))
|
||||
.add("/workers/jobs/{id}/retry", post(retry_worker_job))
|
||||
.add("/workers/tasks/retry-deliveries", post(run_retry_deliveries_job))
|
||||
.add("/workers/tasks/digest", post(run_digest_worker_job))
|
||||
.add("/site-backup/export", get(export_site_backup))
|
||||
.add("/site-backup/import", post(import_site_backup))
|
||||
}
|
||||
|
||||
@@ -7,12 +7,19 @@ use sea_orm::{EntityTrait, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
controllers::admin::check_auth,
|
||||
controllers::admin::{check_auth, resolve_admin_identity},
|
||||
models::_entities::reviews::{self, Entity as ReviewEntity},
|
||||
services::{admin_audit, storage},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
fn is_public_review_status(status: Option<&str>) -> bool {
|
||||
matches!(
|
||||
status.unwrap_or_default().trim().to_ascii_lowercase().as_str(),
|
||||
"published" | "completed" | "done"
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CreateReviewRequest {
|
||||
pub title: String,
|
||||
pub review_type: String,
|
||||
@@ -25,7 +32,7 @@ pub struct CreateReviewRequest {
|
||||
pub link_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UpdateReviewRequest {
|
||||
pub title: Option<String>,
|
||||
pub review_type: Option<String>,
|
||||
@@ -38,23 +45,30 @@ pub struct UpdateReviewRequest {
|
||||
pub link_url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
|
||||
pub async fn list(headers: HeaderMap, State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
|
||||
let include_private = resolve_admin_identity(&headers).is_some();
|
||||
let reviews = ReviewEntity::find()
|
||||
.order_by_desc(reviews::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|review| include_private || is_public_review_status(review.status.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
format::json(reviews)
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let include_private = resolve_admin_identity(&headers).is_some();
|
||||
let review = ReviewEntity::find_by_id(id).one(&ctx.db).await?;
|
||||
|
||||
match review {
|
||||
Some(r) => format::json(r),
|
||||
Some(r) if include_private || is_public_review_status(r.status.as_deref()) => format::json(r),
|
||||
Some(_) => Err(Error::NotFound),
|
||||
None => Err(Error::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user