mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 11:49:30 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1de0715b72 |
Generated
-63
@@ -13507,69 +13507,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "w2l-download-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bucket_paths",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"filesys",
|
||||
"google_drive_downloader",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"r2d2_redis",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "w2l-inference-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bucket_paths",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"filesys",
|
||||
"google_drive_downloader",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"r2d2_redis",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.1"
|
||||
|
||||
@@ -139,8 +139,6 @@ members = [
|
||||
"crates/service/job/deprecated/inference_job",
|
||||
"crates/service/job/deprecated/tts_download_job",
|
||||
"crates/service/job/deprecated/tts_inference_job",
|
||||
"crates/service/job/deprecated/w2l_download_job",
|
||||
"crates/service/job/deprecated/w2l_inference_job",
|
||||
|
||||
# Service Plugins: complete subsystems that can be plugged into services (users, billing, etc.)
|
||||
"crates/service/plugins/billing",
|
||||
|
||||
@@ -43,7 +43,6 @@ pub mod vocoder;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod w2l;
|
||||
pub mod wallet_ledger_entries;
|
||||
pub mod wallets;
|
||||
pub mod web_referrals;
|
||||
@@ -1,6 +0,0 @@
|
||||
pub mod stats;
|
||||
pub mod w2l_download_jobs;
|
||||
pub mod w2l_inference_jobs;
|
||||
pub mod w2l_results;
|
||||
pub mod w2l_template_upload_jobs;
|
||||
pub mod w2l_templates;
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::MySql;
|
||||
use sqlx::pool::PoolConnection;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct W2lLeaderboardRecordForList {
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub gravatar_hash: String,
|
||||
pub creator_user_token: String,
|
||||
pub uploaded_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct W2lLeaderboardRecordForListRaw {
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub gravatar_hash: String,
|
||||
pub creator_user_token: String,
|
||||
pub uploaded_count: i64,
|
||||
}
|
||||
|
||||
// NB: This may need to become an offline query if it is expensive.
|
||||
pub async fn calculate_w2l_template_leaderboard(
|
||||
mysql_pool: &mut PoolConnection<MySql>
|
||||
) -> AnyhowResult<Vec<W2lLeaderboardRecordForList>> {
|
||||
|
||||
// NB: We're requiring "is_public_listing_approved IS TRUE" for W2L templates!
|
||||
// This is the opposite for TTS models (for now at least)
|
||||
let maybe_results = sqlx::query_as!(
|
||||
W2lLeaderboardRecordForListRaw,
|
||||
r#"
|
||||
SELECT
|
||||
users.username,
|
||||
users.display_name,
|
||||
users.email_gravatar_hash as gravatar_hash,
|
||||
creator_user_token,
|
||||
count(*) as uploaded_count
|
||||
FROM w2l_templates
|
||||
JOIN users
|
||||
ON
|
||||
users.token = creator_user_token
|
||||
WHERE
|
||||
is_public_listing_approved IS TRUE
|
||||
AND users.is_banned IS FALSE
|
||||
AND w2l_templates.user_deleted_at IS NULL
|
||||
AND w2l_templates.mod_deleted_at IS NULL
|
||||
GROUP BY creator_user_token
|
||||
ORDER BY uploaded_count desc
|
||||
LIMIT 25;
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut **mysql_pool)
|
||||
.await;
|
||||
|
||||
let results : Vec<W2lLeaderboardRecordForListRaw> = match maybe_results {
|
||||
Ok(results) => results,
|
||||
Err(err) => {
|
||||
warn!("Error: {:?}", err);
|
||||
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return Ok(Vec::new());
|
||||
},
|
||||
_ => {
|
||||
warn!("w2l leaderboard query error: {:?}", err);
|
||||
return Err(anyhow!("w2l leaderboard query error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(results.into_iter()
|
||||
.map(|result| {
|
||||
W2lLeaderboardRecordForList {
|
||||
username: result.username.to_string(),
|
||||
display_name: result.display_name.to_string(),
|
||||
gravatar_hash: result.gravatar_hash.to_string(),
|
||||
creator_user_token: result.creator_user_token.to_string(),
|
||||
uploaded_count: result.uploaded_count,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<W2lLeaderboardRecordForList>>())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod calculate_w2l_template_leaderboard;
|
||||
@@ -1,2 +0,0 @@
|
||||
// TODO: The "job queries" were moved here; now break them down into several files.
|
||||
pub mod w2l_download_job_queries;
|
||||
-307
@@ -1,307 +0,0 @@
|
||||
//! NB: This seems required for sqlx to generate the cached queries.
|
||||
//! Sqlx's prepare needs a *single* binary to work against, so we need to
|
||||
//! include these in the main binary to generate all the queries.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::w2l_templates::W2lTemplateToken;
|
||||
|
||||
// TODO(2022-08-04): These were moved into the 'mysql_queries' crate, but they need
|
||||
// to be split up into several modules for better maintainability. cf the already moved
|
||||
// `tts_inference_job` queries.
|
||||
|
||||
/// table: w2l_template_upload_jobs
|
||||
#[derive(Debug)]
|
||||
pub struct W2lTemplateUploadJobRecord {
|
||||
pub id: i64,
|
||||
pub token: String,
|
||||
pub uuid_idempotency_token: String,
|
||||
pub on_success_result_token: Option<String>,
|
||||
pub creator_user_token: String,
|
||||
pub creator_ip_address: String,
|
||||
pub creator_set_visibility: String, // TODO
|
||||
pub title: String,
|
||||
pub template_type: String, // TODO
|
||||
pub download_url: Option<String>,
|
||||
pub status: String, // TODO
|
||||
pub attempt_count: i32,
|
||||
pub failure_reason: Option<String>,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
pub retry_at: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn query_w2l_template_upload_job_records(pool: &MySqlPool, num_records: u32)
|
||||
-> AnyhowResult<Vec<W2lTemplateUploadJobRecord>>
|
||||
{
|
||||
let job_records = sqlx::query_as!(
|
||||
W2lTemplateUploadJobRecord,
|
||||
r#"
|
||||
SELECT *
|
||||
FROM w2l_template_upload_jobs
|
||||
WHERE
|
||||
(
|
||||
status IN ("pending", "attempt_failed")
|
||||
)
|
||||
AND
|
||||
(
|
||||
retry_at IS NULL
|
||||
OR
|
||||
retry_at < CURRENT_TIMESTAMP
|
||||
)
|
||||
LIMIT ?
|
||||
"#,
|
||||
num_records,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(job_records)
|
||||
}
|
||||
|
||||
pub struct W2lDownloadLockRecord {
|
||||
id: i64,
|
||||
status: String,
|
||||
}
|
||||
|
||||
pub async fn grab_job_lock_and_mark_pending(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lTemplateUploadJobRecord
|
||||
) -> AnyhowResult<bool> {
|
||||
|
||||
// NB: We use transactions and "SELECT ... FOR UPDATE" to simulate mutexes.
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let maybe_record = sqlx::query_as!(
|
||||
W2lDownloadLockRecord,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
status
|
||||
FROM w2l_template_upload_jobs
|
||||
WHERE id = ?
|
||||
FOR UPDATE
|
||||
"#,
|
||||
job.id,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await;
|
||||
|
||||
let record : W2lDownloadLockRecord = match maybe_record {
|
||||
Ok(record) => record,
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return Err(anyhow!("could not job"));
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("query error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let can_transact = match record.status.as_ref() {
|
||||
"pending" => true, // It's okay for us to take the lock.
|
||||
"attempt_failed" => true, // We can retry.
|
||||
"started" => false, // Job in progress (another job beat us, and we can't take the lock)
|
||||
"complete_success" => false, // Job already complete
|
||||
"complete_failure" => false, // Job already complete (permanently dead; no need to retry)
|
||||
"dead" => false, // Job already complete (permanently dead; retries exhausted)
|
||||
_ => false, // Future-proof
|
||||
};
|
||||
|
||||
if !can_transact {
|
||||
transaction.rollback().await?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let _acquire_lock = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_template_upload_jobs
|
||||
SET
|
||||
status = 'started',
|
||||
attempt_count = attempt_count + 1,
|
||||
retry_at = NOW() + interval 2 minute
|
||||
WHERE id = ?
|
||||
"#,
|
||||
job.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn mark_w2l_template_upload_job_failure(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lTemplateUploadJobRecord,
|
||||
failure_reason: &str,
|
||||
max_attempts: i32
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
// statuses: "attempt_failed", "complete_failure", "dead"
|
||||
let mut next_status = "attempt_failed";
|
||||
|
||||
if job.attempt_count >= max_attempts {
|
||||
// NB: Job attempt count is incremented at start
|
||||
next_status = "dead";
|
||||
}
|
||||
|
||||
let _query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_template_upload_jobs
|
||||
SET
|
||||
status = ?,
|
||||
failure_reason = ?,
|
||||
retry_at = NOW() + interval 2 minute
|
||||
WHERE id = ?
|
||||
"#,
|
||||
next_status,
|
||||
failure_reason.to_string(),
|
||||
job.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_w2l_template_upload_job_permanently_dead(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lTemplateUploadJobRecord,
|
||||
failure_reason: &str,
|
||||
) -> AnyhowResult<()> {
|
||||
let _query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_template_upload_jobs
|
||||
SET
|
||||
status = "dead",
|
||||
failure_reason = ?,
|
||||
retry_at = NULL
|
||||
WHERE id = ?
|
||||
"#,
|
||||
failure_reason.to_string(),
|
||||
job.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_w2l_template_upload_job_done(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lTemplateUploadJobRecord,
|
||||
success: bool,
|
||||
maybe_model_token: Option<&str>
|
||||
) -> AnyhowResult<()>
|
||||
{
|
||||
let status = if success { "complete_success" } else { "complete_failure" };
|
||||
|
||||
let _query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_template_upload_jobs
|
||||
SET
|
||||
status = ?,
|
||||
on_success_result_token = ?,
|
||||
failure_reason = NULL,
|
||||
retry_at = NULL
|
||||
WHERE id = ?
|
||||
"#,
|
||||
status,
|
||||
maybe_model_token,
|
||||
job.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_w2l_template(
|
||||
pool: &MySqlPool,
|
||||
template_type: &str, // TODO: ENUM!
|
||||
job: &W2lTemplateUploadJobRecord,
|
||||
private_bucket_hash: &str,
|
||||
private_bucket_object_name: &str,
|
||||
private_bucket_cached_faces_object_name: &str,
|
||||
maybe_image_preview_object_name: Option<&str>,
|
||||
maybe_video_preview_object_name: Option<&str>,
|
||||
file_size_bytes: u64,
|
||||
maybe_mime_type: Option<&str>,
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
frame_count: u64,
|
||||
fps: f32,
|
||||
duration_millis: u64
|
||||
) -> AnyhowResult<(u64, String)> {
|
||||
|
||||
let model_token = W2lTemplateToken::generate().to_string();
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_templates
|
||||
SET
|
||||
token = ?,
|
||||
template_type = ?,
|
||||
title = ?,
|
||||
description_markdown = '',
|
||||
description_rendered_html = '',
|
||||
creator_user_token = ?,
|
||||
creator_ip_address_creation = ?,
|
||||
creator_ip_address_last_update = ?,
|
||||
original_download_url = ?,
|
||||
private_bucket_hash = ?,
|
||||
private_bucket_object_name = ?,
|
||||
private_bucket_cached_faces_object_name = ?,
|
||||
maybe_public_bucket_preview_image_object_name = ?,
|
||||
maybe_public_bucket_preview_video_object_name = ?,
|
||||
file_size_bytes = ?,
|
||||
mime_type = ?,
|
||||
frame_width = ?,
|
||||
frame_height = ?,
|
||||
frame_count = ?,
|
||||
fps = ?,
|
||||
duration_millis = ?
|
||||
"#,
|
||||
model_token,
|
||||
template_type,
|
||||
job.title.to_string(),
|
||||
job.creator_user_token.clone(),
|
||||
job.creator_ip_address.clone(),
|
||||
job.creator_ip_address.clone(),
|
||||
job.download_url.clone(),
|
||||
private_bucket_hash.to_string(),
|
||||
private_bucket_object_name.to_string(),
|
||||
private_bucket_cached_faces_object_name.to_string(),
|
||||
maybe_image_preview_object_name,
|
||||
maybe_video_preview_object_name,
|
||||
file_size_bytes,
|
||||
maybe_mime_type.unwrap_or(""),
|
||||
frame_width,
|
||||
frame_height,
|
||||
frame_count,
|
||||
fps,
|
||||
duration_millis
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
Ok(res) => {
|
||||
res.last_insert_id()
|
||||
},
|
||||
Err(err) => {
|
||||
// TODO: handle better
|
||||
return Err(anyhow!("Mysql error: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok((record_id, model_token.clone()))
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PendingCountResult {
|
||||
pub pending_count: Option<i64>,
|
||||
pub seconds_since_first: i64,
|
||||
}
|
||||
|
||||
pub async fn get_pending_w2l_inference_job_detailed_stats(mysql_pool: &MySqlPool) -> AnyhowResult<Option<PendingCountResult>> {
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
let maybe_result = sqlx::query_as!(
|
||||
PendingCountResult,
|
||||
r#"
|
||||
SELECT
|
||||
NOW() - t2.created_at AS seconds_since_first,
|
||||
(
|
||||
SELECT
|
||||
count(t1.id) as pending_count
|
||||
FROM w2l_inference_jobs AS t1
|
||||
WHERE t1.status = "pending"
|
||||
) as pending_count
|
||||
FROM w2l_inference_jobs AS t2
|
||||
WHERE t2.status = "pending"
|
||||
ORDER BY t2.id ASC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await;
|
||||
|
||||
match maybe_result {
|
||||
Ok(result) => Ok(Some(result)),
|
||||
Err(ref err) => match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
// NB: Not Found for null results means nothing is pending in the queue (not an error!)
|
||||
Ok(None)
|
||||
},
|
||||
_ => {
|
||||
Err(anyhow!("get w2l pending count error: {:?}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct W2lInferenceJobStatusRecord {
|
||||
pub job_token: String,
|
||||
|
||||
pub status: String,
|
||||
pub attempt_count: i32,
|
||||
pub maybe_result_token: Option<String>,
|
||||
pub maybe_public_bucket_video_path: Option<String>,
|
||||
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub w2l_template_type: String,
|
||||
|
||||
pub title: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_w2l_inference_job_status(
|
||||
job_token: &str,
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<W2lInferenceJobStatusRecord>> {
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_status = sqlx::query_as!(
|
||||
W2lInferenceJobStatusRecord,
|
||||
r#"
|
||||
SELECT
|
||||
jobs.token as job_token,
|
||||
|
||||
jobs.status,
|
||||
jobs.attempt_count,
|
||||
jobs.on_success_result_token as maybe_result_token,
|
||||
results.public_bucket_video_path as maybe_public_bucket_video_path,
|
||||
|
||||
jobs.maybe_w2l_template_token,
|
||||
w2l.template_type as w2l_template_type,
|
||||
w2l.title,
|
||||
|
||||
jobs.created_at,
|
||||
jobs.updated_at
|
||||
|
||||
FROM w2l_inference_jobs as jobs
|
||||
JOIN w2l_templates as w2l
|
||||
ON w2l.token = jobs.maybe_w2l_template_token
|
||||
LEFT OUTER JOIN w2l_results as results
|
||||
ON jobs.on_success_result_token = results.token
|
||||
|
||||
WHERE jobs.token = ?
|
||||
"#,
|
||||
job_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await;
|
||||
|
||||
match maybe_status {
|
||||
Ok(record) => Ok(Some(record)),
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => Ok(None),
|
||||
_ => Err(anyhow!("w2l template query error: {:?}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use log::error;
|
||||
use sqlx::error::Error::Database;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
// FIXME: NB: This is an old query that was somewhat modernized when moved.
|
||||
// All the same, do not copy this example!
|
||||
|
||||
pub struct InsertW2lInferenceJobArgs<'a> {
|
||||
pub w2l_template_token: &'a str,
|
||||
pub tts_inference_result_token: &'a str,
|
||||
pub maybe_user_token: Option<&'a str>,
|
||||
pub ip_address: &'a str,
|
||||
//pub creator_set_visibility: Visibility,
|
||||
pub creator_set_visibility: &'a str,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub async fn insert_w2l_inference_job(args: InsertW2lInferenceJobArgs<'_>) -> AnyhowResult<()> {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_inference_jobs
|
||||
SET
|
||||
maybe_w2l_template_token = ?,
|
||||
maybe_tts_inference_result_token = ?,
|
||||
maybe_public_audio_bucket_location = NULL,
|
||||
maybe_public_image_bucket_location = NULL,
|
||||
maybe_creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
args.w2l_template_token,
|
||||
args.tts_inference_result_token,
|
||||
args.maybe_user_token,
|
||||
args.ip_address,
|
||||
args.creator_set_visibility
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => {
|
||||
res.last_insert_id()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("New w2l inference job creation DB error: {:?}", err);
|
||||
|
||||
// NB: SQLSTATE[23000]: Integrity constraint violation
|
||||
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
|
||||
match err {
|
||||
Database(ref err) => {
|
||||
let _maybe_code = err.code().map(|c| c.into_owned());
|
||||
/*match maybe_code.as_deref() {
|
||||
Some("23000") => {
|
||||
if err.message().contains("username") {
|
||||
return Err(UsernameTaken);
|
||||
} else if err.message().contains("email_address") {
|
||||
return Err(EmailTaken);
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
}*/
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
return Err(anyhow!("error with query: {:?}", &err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::w2l_inference_jobs::W2lInferenceJobToken;
|
||||
|
||||
pub struct InsertW2lInferenceJobExtendedArgs<'a> {
|
||||
pub uuid_idempotency_token: &'a str,
|
||||
pub maybe_template_token: Option<&'a str>,
|
||||
pub audio_upload_bucket_hash: Option<&'a str>,
|
||||
pub audio_upload_bucket_path: Option<&'a str>,
|
||||
pub maybe_audio_file_name: Option<&'a str>,
|
||||
pub audio_type: Option<&'a str>,
|
||||
pub maybe_user_token: Option<&'a str>,
|
||||
pub ip_address: &'a str,
|
||||
pub set_visibility: Visibility,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
/// Returns the new job's token on success
|
||||
pub async fn insert_w2l_inference_job_extended(args: InsertW2lInferenceJobExtendedArgs<'_>) -> AnyhowResult<String> {
|
||||
// This token is returned to the client.
|
||||
let job_token = W2lInferenceJobToken::generate().to_string();
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_inference_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
|
||||
maybe_w2l_template_token = ?,
|
||||
maybe_public_audio_bucket_hash = ?,
|
||||
maybe_public_audio_bucket_location = ?,
|
||||
|
||||
maybe_original_audio_filename = ?,
|
||||
maybe_audio_mime_type = ?,
|
||||
|
||||
maybe_creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
disable_end_bump = false,
|
||||
creator_set_visibility = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
&job_token,
|
||||
args.uuid_idempotency_token.to_string(),
|
||||
args.maybe_template_token.clone(),
|
||||
args.audio_upload_bucket_hash.clone(),
|
||||
args.audio_upload_bucket_path.clone(),
|
||||
args.maybe_audio_file_name.clone(),
|
||||
args.audio_type.clone(),
|
||||
args.maybe_user_token.clone(),
|
||||
args.ip_address.to_string(),
|
||||
args.set_visibility.to_str(),
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => {
|
||||
return Err(anyhow!("New w2l template upload creation DB error: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(job_token)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod get_pending_w2l_inference_job_detailed_stats;
|
||||
pub mod get_w2l_inference_job_status;
|
||||
pub mod insert_w2l_inference_job;
|
||||
pub mod insert_w2l_inference_job_extended;
|
||||
|
||||
// TODO: The grab bag of "job queries" were moved here; now break them down into several files.
|
||||
pub mod w2l_inference_job_queries;
|
||||
-458
@@ -1,458 +0,0 @@
|
||||
//! NB: This seems required for sqlx to generate the cached queries.
|
||||
//! Sqlx's prepare needs a *single* binary to work against, so we need to
|
||||
//! include these in the main binary to generate all the queries.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::w2l_results::W2lResultToken;
|
||||
|
||||
// TODO(2022-08-04): These were moved into the 'mysql_queries' crate, but they need
|
||||
// to be split up into several modules for better maintainability. cf the already moved
|
||||
// `tts_inference_job` queries.
|
||||
|
||||
/// table: w2l_inference_jobs
|
||||
#[derive(Debug)]
|
||||
pub struct W2lInferenceJobRecord {
|
||||
pub id: i64,
|
||||
pub inference_job_token: String,
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
// ===== FACE TEMPLATE OPTIONS =====
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub maybe_public_image_bucket_location: Option<String>,
|
||||
|
||||
// ===== AUDIO SOURCE OPTIONS =====
|
||||
pub maybe_tts_inference_result_token: Option<String>,
|
||||
pub maybe_public_audio_bucket_hash: Option<String>,
|
||||
pub maybe_public_audio_bucket_location: Option<String>,
|
||||
|
||||
pub maybe_original_audio_filename: Option<String>,
|
||||
pub maybe_original_audio_download_url: Option<String>,
|
||||
pub maybe_audio_mime_type: Option<String>,
|
||||
|
||||
pub creator_ip_address: String,
|
||||
pub maybe_creator_user_token: Option<String>,
|
||||
|
||||
pub creator_set_visibility: Visibility,
|
||||
pub disable_end_bump: i8, // bool
|
||||
pub disable_watermark: i8, // bool
|
||||
|
||||
//pub maybe_subject_token: Option<String>,
|
||||
//pub maybe_actor_subject_token: Option<String>,
|
||||
pub status: String, // TODO
|
||||
pub attempt_count: i32,
|
||||
pub failure_reason: Option<String>,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
pub retry_at: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn query_w2l_inference_job_records(pool: &MySqlPool, num_records: u32)
|
||||
-> AnyhowResult<Vec<W2lInferenceJobRecord>>
|
||||
{
|
||||
let job_records = sqlx::query_as!(
|
||||
W2lInferenceJobRecord,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
token AS inference_job_token,
|
||||
uuid_idempotency_token,
|
||||
|
||||
maybe_w2l_template_token,
|
||||
maybe_public_image_bucket_location,
|
||||
maybe_tts_inference_result_token,
|
||||
maybe_public_audio_bucket_hash,
|
||||
maybe_public_audio_bucket_location,
|
||||
|
||||
maybe_original_audio_filename,
|
||||
maybe_original_audio_download_url,
|
||||
maybe_audio_mime_type,
|
||||
|
||||
creator_ip_address,
|
||||
maybe_creator_user_token,
|
||||
|
||||
creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
|
||||
disable_end_bump,
|
||||
disable_watermark,
|
||||
|
||||
status,
|
||||
attempt_count,
|
||||
failure_reason,
|
||||
created_at,
|
||||
updated_at,
|
||||
retry_at
|
||||
FROM w2l_inference_jobs
|
||||
WHERE
|
||||
(
|
||||
status IN ("pending", "attempt_failed")
|
||||
)
|
||||
AND
|
||||
(
|
||||
retry_at IS NULL
|
||||
OR
|
||||
retry_at < CURRENT_TIMESTAMP
|
||||
)
|
||||
LIMIT ?
|
||||
"#,
|
||||
num_records,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(job_records)
|
||||
}
|
||||
|
||||
pub struct W2lInferenceLockRecord {
|
||||
id: i64,
|
||||
status: String,
|
||||
}
|
||||
|
||||
pub async fn grab_job_lock_and_mark_pending(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lInferenceJobRecord
|
||||
) -> AnyhowResult<bool> {
|
||||
|
||||
// NB: We use transactions and "SELECT ... FOR UPDATE" to simulate mutexes.
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let maybe_record = sqlx::query_as!(
|
||||
W2lInferenceLockRecord,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
status
|
||||
FROM w2l_inference_jobs
|
||||
WHERE id = ?
|
||||
FOR UPDATE
|
||||
"#,
|
||||
job.id,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await;
|
||||
|
||||
let record : W2lInferenceLockRecord = match maybe_record {
|
||||
Ok(record) => record,
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return Err(anyhow!("could not job"));
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("query error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let can_transact = match record.status.as_ref() {
|
||||
"pending" => true, // It's okay for us to take the lock.
|
||||
"attempt_failed" => true, // We can retry.
|
||||
"started" => false, // Job in progress (another job beat us, and we can't take the lock)
|
||||
"complete_success" => false, // Job already complete
|
||||
"complete_failure" => false, // Job already complete (permanently dead; no need to retry)
|
||||
"dead" => false, // Job already complete (permanently dead; retries exhausted)
|
||||
_ => false, // Future-proof
|
||||
};
|
||||
|
||||
if !can_transact {
|
||||
transaction.rollback().await?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let _acquire_lock = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_inference_jobs
|
||||
SET
|
||||
status = 'started',
|
||||
attempt_count = attempt_count + 1,
|
||||
retry_at = NOW() + interval 2 minute
|
||||
WHERE id = ?
|
||||
"#,
|
||||
job.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
pub async fn mark_w2l_inference_job_failure(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lInferenceJobRecord,
|
||||
failure_reason: &str,
|
||||
max_attempts: i32
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
// statuses: "attempt_failed", "complete_failure", "dead"
|
||||
let mut next_status = "attempt_failed";
|
||||
|
||||
if job.attempt_count >= max_attempts {
|
||||
// NB: Job attempt count is incremented at start
|
||||
next_status = "dead";
|
||||
}
|
||||
|
||||
let _query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_inference_jobs
|
||||
SET
|
||||
status = ?,
|
||||
failure_reason = ?,
|
||||
retry_at = NOW() + interval 2 minute
|
||||
WHERE id = ?
|
||||
"#,
|
||||
next_status,
|
||||
failure_reason.to_string(),
|
||||
job.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_w2l_inference_job_done(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lInferenceJobRecord,
|
||||
success: bool,
|
||||
maybe_result_token: Option<&str>
|
||||
) -> AnyhowResult<()> {
|
||||
let status = if success { "complete_success" } else { "complete_failure" };
|
||||
|
||||
let _query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_inference_jobs
|
||||
SET
|
||||
status = ?,
|
||||
on_success_result_token = ?,
|
||||
failure_reason = NULL,
|
||||
retry_at = NULL
|
||||
WHERE id = ?
|
||||
"#,
|
||||
status,
|
||||
maybe_result_token,
|
||||
job.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct SyntheticIdRecord {
|
||||
pub next_id: i64,
|
||||
}
|
||||
|
||||
pub async fn insert_w2l_result<P: AsRef<Path>>(
|
||||
pool: &MySqlPool,
|
||||
job: &W2lInferenceJobRecord,
|
||||
bucket_video_results_path: P,
|
||||
file_size_bytes: u64,
|
||||
maybe_mime_type: Option<&str>,
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
duration_millis: u64
|
||||
) -> AnyhowResult<(u64, String)>
|
||||
{
|
||||
let inference_result_token = W2lResultToken::generate().to_string();
|
||||
|
||||
let bucket_video_result_path = &bucket_video_results_path
|
||||
.as_ref()
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
let maybe_creator_user_token = job.maybe_creator_user_token.clone();
|
||||
let mut maybe_creator_synthetic_id : Option<u64> = None;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if let Some(creator_user_token) = maybe_creator_user_token.as_deref() {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_result_synthetic_ids
|
||||
SET
|
||||
user_token = ?,
|
||||
next_id = 1
|
||||
ON DUPLICATE KEY UPDATE
|
||||
user_token = ?,
|
||||
next_id = next_id + 1
|
||||
"#,
|
||||
creator_user_token,
|
||||
creator_user_token
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
//transaction.rollback().await?;
|
||||
warn!("Transaction failure: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let query_result = sqlx::query_as!(
|
||||
SyntheticIdRecord,
|
||||
r#"
|
||||
SELECT
|
||||
next_id
|
||||
FROM
|
||||
w2l_result_synthetic_ids
|
||||
WHERE
|
||||
user_token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
creator_user_token,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await;
|
||||
|
||||
let record : SyntheticIdRecord = match query_result {
|
||||
Ok(record) => record,
|
||||
Err(err) => {
|
||||
warn!("Transaction failure: {:?}", err);
|
||||
//transaction.rollback().await?;
|
||||
return Err(anyhow!("Transaction failure: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
let next_id = record.next_id as u64;
|
||||
maybe_creator_synthetic_id = Some(next_id);
|
||||
}
|
||||
|
||||
let record_id = {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_results
|
||||
SET
|
||||
token = ?,
|
||||
maybe_w2l_template_token = ?,
|
||||
maybe_creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
|
||||
maybe_creator_synthetic_id = ?,
|
||||
|
||||
public_bucket_video_path = ?,
|
||||
|
||||
file_size_bytes = ?,
|
||||
mime_type = ?,
|
||||
frame_width = ?,
|
||||
frame_height = ?,
|
||||
duration_millis = ?
|
||||
"#,
|
||||
inference_result_token,
|
||||
job.maybe_w2l_template_token.clone(),
|
||||
job.maybe_creator_user_token.clone(),
|
||||
job.creator_ip_address.clone(),
|
||||
job.creator_set_visibility.to_str(),
|
||||
|
||||
maybe_creator_synthetic_id,
|
||||
|
||||
bucket_video_result_path,
|
||||
|
||||
file_size_bytes,
|
||||
maybe_mime_type.unwrap_or(""),
|
||||
frame_width,
|
||||
frame_height,
|
||||
duration_millis
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
Ok(res) => {
|
||||
res.last_insert_id()
|
||||
},
|
||||
Err(err) => {
|
||||
// TODO: handle better
|
||||
transaction.rollback().await?;
|
||||
return Err(anyhow!("Mysql error: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
record_id
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok((record_id, inference_result_token.clone()))
|
||||
}
|
||||
|
||||
pub struct W2lTemplateRecord2 {
|
||||
pub template_token: String,
|
||||
pub template_type: String,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub private_bucket_hash: String,
|
||||
pub creator_display_name: String,
|
||||
pub title: String,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub duration_millis: i32,
|
||||
pub maybe_public_bucket_preview_image_object_name: Option<String>,
|
||||
pub maybe_public_bucket_preview_video_object_name: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_w2l_template_by_token(pool: &MySqlPool, template_token: &str)
|
||||
-> AnyhowResult<Option<W2lTemplateRecord2>>
|
||||
{
|
||||
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_template = sqlx::query_as!(
|
||||
W2lTemplateRecord2,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
w2l.title,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.private_bucket_hash,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.created_at,
|
||||
w2l.updated_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON users.token = w2l.creator_user_token
|
||||
WHERE w2l.token = ?
|
||||
AND w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#,
|
||||
&template_token
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await; // TODO: This will return error if it doesn't exist
|
||||
|
||||
match maybe_template {
|
||||
Ok(template) => Ok(Some(template)),
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
Ok(None)
|
||||
},
|
||||
_ => {
|
||||
warn!("w2l template query error: {:?}", err);
|
||||
Err(anyhow!("Mysql error: {:?}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn delete_w2l_inference_result_as_user(
|
||||
inference_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
user_deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_w2l_inference_result_as_mod(
|
||||
inference_result_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
mod_deleted_at = CURRENT_TIMESTAMP,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_w2l_inference_result_as_user(
|
||||
inference_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
user_deleted_at = NULL
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_w2l_inference_result_as_mod(
|
||||
inference_result_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
mod_deleted_at = NULL,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct EditW2lResultArgs<'a> {
|
||||
pub w2l_result_token: &'a str,
|
||||
pub creator_set_visibility: Visibility,
|
||||
pub role_dependent_fields: CreatorOrModFields<'a>,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub enum CreatorOrModFields<'a> {
|
||||
CreatorFields {
|
||||
creator_ip_address: &'a str,
|
||||
},
|
||||
ModFields {
|
||||
mod_user_token: &'a str,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_w2l_result(args: EditW2lResultArgs<'_>) -> AnyhowResult<()> {
|
||||
let query_result = match args.role_dependent_fields {
|
||||
//
|
||||
// Creator
|
||||
CreatorOrModFields::CreatorFields { creator_ip_address } => {
|
||||
|
||||
// TODO: Don't update the original IP address. Create a new field.
|
||||
// We need to store the IP address details.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
creator_set_visibility = ?,
|
||||
creator_ip_address = ?
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.creator_set_visibility.to_str(),
|
||||
creator_ip_address,
|
||||
args.w2l_result_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await
|
||||
},
|
||||
//
|
||||
// Moderator
|
||||
CreatorOrModFields::ModFields { mod_user_token } => {
|
||||
|
||||
// We need to store the moderator details.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_results
|
||||
SET
|
||||
creator_set_visibility = ?,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.creator_set_visibility.to_str(),
|
||||
mod_user_token,
|
||||
args.w2l_result_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow!("Update W2L result DB error: {:?}", err)),
|
||||
}
|
||||
}
|
||||
-665
@@ -1,665 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lInferenceListPage {
|
||||
pub inference_records: Vec<W2lInferenceRecordForList>,
|
||||
pub sort_ascending: bool,
|
||||
|
||||
/// ID of the first record in `inference_records`.
|
||||
pub first_id: Option<i64>,
|
||||
|
||||
/// ID of the last record in `inference_records`.
|
||||
pub last_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lInferenceRecordForList {
|
||||
pub w2l_result_token: String,
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub maybe_tts_inference_result_token: Option<String>,
|
||||
|
||||
pub template_type: Option<String>,
|
||||
pub template_title: Option<String>,
|
||||
|
||||
pub maybe_creator_user_token: Option<String>,
|
||||
pub maybe_creator_username: Option<String>,
|
||||
pub maybe_creator_display_name: Option<String>,
|
||||
|
||||
pub maybe_creator_result_id: Option<u64>,
|
||||
|
||||
pub file_size_bytes: u32,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
pub duration_millis: u32,
|
||||
|
||||
pub visibility: Visibility,
|
||||
|
||||
//pub template_is_mod_approved: bool, // converted
|
||||
//pub maybe_mod_user_token: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Query Builder for listing W2L results.
|
||||
/// These are very difficult queries, so the builder helps for
|
||||
/// testability, construction, and correctness.
|
||||
pub struct ListW2lResultsQueryBuilder {
|
||||
scope_creator_username: Option<String>,
|
||||
include_user_hidden: bool,
|
||||
include_mod_deleted_results: bool,
|
||||
include_user_deleted_results: bool,
|
||||
include_templates_not_approved_for_public_listing: bool,
|
||||
sort_ascending: bool,
|
||||
offset: Option<u64>,
|
||||
limit: u16,
|
||||
cursor_is_reversed: bool,
|
||||
}
|
||||
|
||||
impl ListW2lResultsQueryBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scope_creator_username: None,
|
||||
include_user_hidden: false,
|
||||
include_mod_deleted_results: false,
|
||||
include_user_deleted_results: false,
|
||||
include_templates_not_approved_for_public_listing: false,
|
||||
sort_ascending: false,
|
||||
offset: None,
|
||||
limit: DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE,
|
||||
cursor_is_reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scope_creator_username(mut self, scope_creator_username: Option<&str>) -> Self {
|
||||
self.scope_creator_username = scope_creator_username.map(|u| u.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn include_user_hidden(mut self, include_user_hidden: bool) -> Self {
|
||||
self.include_user_hidden = include_user_hidden;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn include_mod_deleted_results(mut self, include_mod_deleted_results: bool) -> Self {
|
||||
self.include_mod_deleted_results = include_mod_deleted_results;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn include_user_deleted_results(mut self, include_user_deleted_results: bool) -> Self {
|
||||
self.include_user_deleted_results = include_user_deleted_results;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn include_templates_not_approved_for_public_listing(mut self, include_templates_not_approved_for_public_listing: bool) -> Self {
|
||||
self.include_templates_not_approved_for_public_listing = include_templates_not_approved_for_public_listing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sort_ascending(mut self, sort_ascending: bool) -> Self {
|
||||
self.sort_ascending = sort_ascending;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: Option<u64>) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn limit(mut self, limit: u16) -> Self {
|
||||
self.limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor_is_reversed(mut self, cursor_is_reversed: bool) -> Self {
|
||||
self.cursor_is_reversed = cursor_is_reversed;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the query based on the set predicates.
|
||||
/// Wrap the results in a "Page" we can return to the frontend.
|
||||
/// This will remove any IDs from the records, but the ids themselves also need scrubbing.
|
||||
pub async fn perform_query_for_page(
|
||||
&self,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<W2lInferenceListPage> {
|
||||
|
||||
let internal_results = self.perform_internal_query(mysql_pool).await?;
|
||||
|
||||
let first_id = internal_results.first()
|
||||
.map(|raw_result| raw_result.w2l_result_id);
|
||||
|
||||
let last_id = internal_results.last()
|
||||
.map(|raw_result| raw_result.w2l_result_id);
|
||||
|
||||
let inference_results = internal_results
|
||||
.iter()
|
||||
.map(|r| {
|
||||
W2lInferenceRecordForList {
|
||||
w2l_result_token: r.w2l_result_token.clone(),
|
||||
maybe_w2l_template_token: r.maybe_w2l_template_token.clone(),
|
||||
maybe_tts_inference_result_token: r.maybe_tts_inference_result_token.clone(),
|
||||
template_type: r.template_type.clone(),
|
||||
template_title: r.template_title.clone(),
|
||||
maybe_creator_user_token: r.maybe_creator_user_token.clone(),
|
||||
maybe_creator_username: r.maybe_creator_username.clone(),
|
||||
maybe_creator_display_name: r.maybe_creator_display_name.clone(),
|
||||
maybe_creator_result_id: r.maybe_creator_result_id.map(|v| v as u64),
|
||||
file_size_bytes: if r.file_size_bytes > 0 { r.file_size_bytes as u32 } else { 0 },
|
||||
frame_width: if r.frame_width > 0 { r.frame_width as u32 } else { 0 },
|
||||
frame_height: if r.frame_height > 0 { r.frame_height as u32 } else { 0 },
|
||||
duration_millis: if r.duration_millis > 0 { r.duration_millis as u32 } else { 0 },
|
||||
visibility: Visibility::from_str(&r.creator_set_visibility).unwrap_or(Visibility::Public),
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<W2lInferenceRecordForList>>();
|
||||
|
||||
Ok(W2lInferenceListPage {
|
||||
inference_records: inference_results,
|
||||
sort_ascending: self.sort_ascending,
|
||||
first_id,
|
||||
last_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform the query based on the set predicates.
|
||||
/// The records contain IDs, which we should not expose to the frontend.
|
||||
pub async fn perform_internal_query(
|
||||
&self,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Vec<RawInternalTtsRecord>> {
|
||||
|
||||
let query = self.build_query_string();
|
||||
let mut query = sqlx::query_as::<_, RawInternalTtsRecord>(&query);
|
||||
|
||||
// NB: The following bindings must match the order of the query builder !!
|
||||
|
||||
if let Some(offset) = self.offset {
|
||||
query = query.bind(offset);
|
||||
}
|
||||
|
||||
if let Some(username) = self.scope_creator_username.as_deref() {
|
||||
query = query.bind(username);
|
||||
}
|
||||
|
||||
query = query.bind(self.limit);
|
||||
|
||||
let mut results = query.fetch_all(mysql_pool)
|
||||
.await?;
|
||||
|
||||
if self.cursor_is_reversed {
|
||||
results.reverse()
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn build_query_string(&self) -> String {
|
||||
// TODO: I haven't figured out how to get field name disambiguation and type coercion working here.
|
||||
// (1) w2l_results.creator_set_visibility `creator_set_visibility: crate::database::enums::record_visibility::RecordVisibility`,
|
||||
// Query error: no column found for name: creator_set_visibility
|
||||
// (2) creator_set_visibility `creator_set_visibility: crate::database::enums::record_visibility::RecordVisibility`,
|
||||
// Column 'creator_set_visibility' in field list is ambiguous
|
||||
|
||||
// TODO/NB: Unfortunately SQLx can't statically typecheck this query
|
||||
let mut query = r#"
|
||||
SELECT
|
||||
w2l_results.id as w2l_result_id,
|
||||
w2l_results.token as w2l_result_token,
|
||||
w2l_results.maybe_tts_inference_result_token,
|
||||
|
||||
w2l_templates.token as maybe_w2l_template_token,
|
||||
w2l_templates.template_type,
|
||||
w2l_templates.title as template_title,
|
||||
|
||||
users.token as maybe_creator_user_token,
|
||||
users.username as maybe_creator_username,
|
||||
users.display_name as maybe_creator_display_name,
|
||||
|
||||
w2l_results.maybe_creator_synthetic_id as maybe_creator_result_id,
|
||||
|
||||
w2l_results.file_size_bytes,
|
||||
w2l_results.frame_width,
|
||||
w2l_results.frame_height,
|
||||
w2l_results.duration_millis,
|
||||
|
||||
w2l_results.creator_set_visibility,
|
||||
|
||||
w2l_results.created_at,
|
||||
w2l_results.updated_at
|
||||
|
||||
FROM w2l_results
|
||||
LEFT OUTER JOIN w2l_templates
|
||||
ON w2l_results.maybe_w2l_template_token = w2l_templates.token
|
||||
LEFT OUTER JOIN users
|
||||
ON w2l_results.maybe_creator_user_token = users.token
|
||||
"#.to_string();
|
||||
|
||||
query.push_str(&self.build_predicates());
|
||||
query
|
||||
}
|
||||
|
||||
pub fn build_predicates(&self) -> String {
|
||||
// NB: Reverse cursors require us to invert the sort direction.
|
||||
let mut sort_ascending = self.sort_ascending;
|
||||
|
||||
let mut first_predicate_added = false;
|
||||
|
||||
let mut query = "".to_string();
|
||||
|
||||
if let Some(_offset) = self.offset {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND");
|
||||
}
|
||||
|
||||
if sort_ascending {
|
||||
if self.cursor_is_reversed {
|
||||
// NB: We're searching backwards.
|
||||
query.push_str(" w2l_results.id < ?");
|
||||
sort_ascending = !sort_ascending;
|
||||
} else {
|
||||
query.push_str(" w2l_results.id > ?");
|
||||
}
|
||||
} else {
|
||||
if self.cursor_is_reversed {
|
||||
// NB: We're searching backwards.
|
||||
query.push_str(" w2l_results.id > ?");
|
||||
sort_ascending = !sort_ascending;
|
||||
} else {
|
||||
query.push_str(" w2l_results.id < ?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_username) = self.scope_creator_username.as_deref() {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE users.username = ?");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND users.username = ?");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.include_templates_not_approved_for_public_listing {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE w2l_templates.is_public_listing_approved IS TRUE");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND w2l_templates.is_public_listing_approved IS TRUE");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.include_user_hidden {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE w2l_results.creator_set_visibility = 'public'");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND w2l_results.creator_set_visibility = 'public'");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.include_mod_deleted_results {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE w2l_results.mod_deleted_at IS NULL");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND w2l_results.mod_deleted_at IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.include_user_deleted_results {
|
||||
if !first_predicate_added {
|
||||
query.push_str(" WHERE w2l_results.user_deleted_at IS NULL");
|
||||
first_predicate_added = true;
|
||||
} else {
|
||||
query.push_str(" AND w2l_results.user_deleted_at IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
if sort_ascending {
|
||||
query.push_str(" ORDER BY w2l_results.id ASC");
|
||||
} else {
|
||||
query.push_str(" ORDER BY w2l_results.id DESC");
|
||||
}
|
||||
|
||||
query.push_str(" LIMIT ?");
|
||||
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct RawInternalTtsRecord {
|
||||
pub w2l_result_id: i64,
|
||||
pub w2l_result_token: String,
|
||||
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub maybe_tts_inference_result_token: Option<String>,
|
||||
|
||||
pub template_type: Option<String>,
|
||||
pub template_title: Option<String>, // from field `w2l_templates.title`
|
||||
|
||||
pub maybe_creator_user_token: Option<String>,
|
||||
pub maybe_creator_username: Option<String>,
|
||||
pub maybe_creator_display_name: Option<String>,
|
||||
|
||||
pub maybe_creator_result_id: Option<i64>,
|
||||
|
||||
pub file_size_bytes: i32,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub duration_millis: i32,
|
||||
|
||||
pub creator_set_visibility: String,
|
||||
|
||||
//pub template_is_mod_approved: i8, // needs convert
|
||||
//pub maybe_mod_user_token: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::queries::w2l::w2l_results::list_w2l_inference_results_query_builder::ListW2lResultsQueryBuilder;
|
||||
|
||||
#[test]
|
||||
fn predicates_without_scoping() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new();
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_scoped_to_user() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.scope_creator_username(Some("echelon"));
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE users.username = ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_user_hidden() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_user_hidden(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_mod_deleted() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_mod_deleted_results(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_user_deleted() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_user_deleted_results(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_mod_deleted_and_user_deleted() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_mod_deleted_results(true)
|
||||
.include_user_deleted_results(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_unapproved() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_templates_not_approved_for_public_listing(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_unapproved_and_mod_deleted() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_templates_not_approved_for_public_listing(true)
|
||||
.include_mod_deleted_results(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_unapproved_and_user_deleted() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_templates_not_approved_for_public_listing(true)
|
||||
.include_user_deleted_results(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_including_mod_deleted_user_deleted_and_unapproved() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.include_mod_deleted_results(true)
|
||||
.include_user_deleted_results(true)
|
||||
.include_templates_not_approved_for_public_listing(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.creator_set_visibility = 'public' \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_sort_ascending() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.sort_ascending(true);
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id ASC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_offset() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.offset(Some(100));
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id < ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_offset_and_sort_ascending() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.sort_ascending(true)
|
||||
.offset(Some(100));
|
||||
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id > ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id ASC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_limit() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.limit(15);
|
||||
|
||||
// NB: Does not change the query itself! Just the downstream binding.
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_cursor_is_reversed_without_cursor() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.cursor_is_reversed(true);
|
||||
|
||||
// NB: Without a cursor, nothing happens.
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_offset_cursor_is_reversed() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.offset(Some(100))
|
||||
.cursor_is_reversed(true);
|
||||
|
||||
// NB: This will change the sort order and greater/less than direction!
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id > ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id ASC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_offset_cursor_is_reversed_sort_ascending() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.offset(Some(100))
|
||||
.cursor_is_reversed(true)
|
||||
.sort_ascending(true);
|
||||
|
||||
// NB: This will change the sort order and greater/less than direction!
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id < ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_limit_scope_user_offset_cursor_is_reversed_sort_ascending() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.limit(1000)
|
||||
.scope_creator_username(Some("pikachu"))
|
||||
.offset(Some(100))
|
||||
.cursor_is_reversed(true)
|
||||
.sort_ascending(true);
|
||||
|
||||
// NB: This will change the sort order and greater/less than direction!
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id < ? \
|
||||
AND users.username = ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.creator_set_visibility = 'public' \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predicates_limit_scope_user_show_hidden_offset_cursor_is_reversed_sort_ascending() {
|
||||
let query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.limit(1000)
|
||||
.include_user_hidden(true)
|
||||
.scope_creator_username(Some("pikachu"))
|
||||
.offset(Some(100))
|
||||
.cursor_is_reversed(true)
|
||||
.sort_ascending(true);
|
||||
|
||||
// NB: This will change the sort order and greater/less than direction!
|
||||
assert_eq!(&query_builder.build_predicates(),
|
||||
" WHERE w2l_results.id < ? \
|
||||
AND users.username = ? \
|
||||
AND w2l_templates.is_public_listing_approved IS TRUE \
|
||||
AND w2l_results.mod_deleted_at IS NULL \
|
||||
AND w2l_results.user_deleted_at IS NULL \
|
||||
ORDER BY w2l_results.id DESC \
|
||||
LIMIT ?");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod delete_w2l_result_various_scopes;
|
||||
pub mod edit_w2l_result;
|
||||
pub mod list_w2l_inference_results_query_builder;
|
||||
pub mod query_w2l_result;
|
||||
@@ -1,298 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::boolean_converters::nullable_i8_to_bool;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lResultRecordForResponse {
|
||||
pub w2l_result_token: String,
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub maybe_tts_inference_result_token: Option<String>,
|
||||
|
||||
pub public_bucket_video_path: String,
|
||||
|
||||
pub template_type: Option<String>,
|
||||
pub template_title: Option<String>,
|
||||
|
||||
pub maybe_creator_user_token: Option<String>,
|
||||
pub maybe_creator_username: Option<String>,
|
||||
pub maybe_creator_display_name: Option<String>,
|
||||
pub maybe_creator_gravatar_hash: Option<String>,
|
||||
|
||||
pub maybe_template_creator_user_token: Option<String>,
|
||||
pub maybe_template_creator_username: Option<String>,
|
||||
pub maybe_template_creator_display_name: Option<String>,
|
||||
pub maybe_template_creator_gravatar_hash: Option<String>,
|
||||
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub file_size_bytes: u32,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
pub duration_millis: u32,
|
||||
|
||||
//pub template_is_mod_approved: bool, // converted
|
||||
//pub maybe_mod_user_token: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
pub maybe_moderator_fields: Option<W2lResultModeratorFields>,
|
||||
}
|
||||
|
||||
/// "Moderator-only fields" that we wouldn't want to expose to ordinary users.
|
||||
/// It's the web endpoint controller's responsibility to clear these for non-mods.
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lResultModeratorFields {
|
||||
pub template_creator_is_banned: bool,
|
||||
pub result_creator_is_banned_if_user: bool,
|
||||
pub result_creator_ip_address: String,
|
||||
pub result_creator_deleted_at: Option<DateTime<Utc>>,
|
||||
pub mod_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lResultRecordRaw {
|
||||
pub w2l_result_token: String, // from field `w2l_results.token`
|
||||
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub maybe_tts_inference_result_token: Option<String>,
|
||||
|
||||
pub public_bucket_video_path: String,
|
||||
|
||||
pub template_type: Option<String>,
|
||||
pub template_title: Option<String>, // from field `w2l_templates.title`
|
||||
|
||||
pub maybe_creator_is_banned: Option<i8>,
|
||||
pub maybe_creator_user_token: Option<String>,
|
||||
pub maybe_creator_username: Option<String>,
|
||||
pub maybe_creator_display_name: Option<String>,
|
||||
pub maybe_creator_gravatar_hash: Option<String>,
|
||||
|
||||
pub maybe_template_creator_is_banned: Option<i8>,
|
||||
pub maybe_template_creator_user_token: Option<String>,
|
||||
pub maybe_template_creator_username: Option<String>,
|
||||
pub maybe_template_creator_display_name: Option<String>,
|
||||
pub maybe_template_creator_gravatar_hash: Option<String>,
|
||||
|
||||
pub creator_set_visibility: String,
|
||||
|
||||
pub file_size_bytes: i32,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub duration_millis: i32,
|
||||
|
||||
//pub template_is_mod_approved: i8, // needs convert
|
||||
//pub maybe_mod_user_token: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
pub creator_ip_address: String,
|
||||
pub user_deleted_at: Option<DateTime<Utc>>,
|
||||
pub mod_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn select_w2l_result_by_token(
|
||||
w2l_result_token: &str,
|
||||
can_see_deleted: bool,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Option<W2lResultRecordForResponse>> {
|
||||
|
||||
let maybe_record = if can_see_deleted {
|
||||
select_including_deleted(w2l_result_token, mysql_pool).await
|
||||
} else {
|
||||
select_without_deleted(w2l_result_token, mysql_pool).await
|
||||
};
|
||||
|
||||
let ir : W2lResultRecordRaw = match maybe_record {
|
||||
Ok(inference_result) => inference_result,
|
||||
Err(ref err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
warn!("w2l result not found: {:?}", &err);
|
||||
return Ok(None);
|
||||
},
|
||||
_ => {
|
||||
warn!("w2l result query error: {:?}", &err);
|
||||
return Err(anyhow!("database error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ir_for_response = W2lResultRecordForResponse {
|
||||
w2l_result_token: ir.w2l_result_token,
|
||||
maybe_w2l_template_token: ir.maybe_w2l_template_token,
|
||||
maybe_tts_inference_result_token: ir.maybe_tts_inference_result_token,
|
||||
|
||||
public_bucket_video_path: ir.public_bucket_video_path,
|
||||
|
||||
template_type: ir.template_type,
|
||||
template_title: ir.template_title,
|
||||
|
||||
maybe_creator_user_token: ir.maybe_creator_user_token,
|
||||
maybe_creator_username: ir.maybe_creator_username,
|
||||
maybe_creator_display_name: ir.maybe_creator_display_name,
|
||||
maybe_creator_gravatar_hash: ir.maybe_creator_gravatar_hash,
|
||||
|
||||
maybe_template_creator_user_token: ir.maybe_template_creator_user_token,
|
||||
maybe_template_creator_username: ir.maybe_template_creator_username,
|
||||
maybe_template_creator_display_name: ir.maybe_template_creator_display_name,
|
||||
maybe_template_creator_gravatar_hash: ir.maybe_template_creator_gravatar_hash,
|
||||
|
||||
// NB: Fail open/public since we're already looking at it
|
||||
creator_set_visibility: Visibility::from_str(&ir.creator_set_visibility)
|
||||
.unwrap_or(Visibility::Public),
|
||||
|
||||
//template_is_mod_approved: if ir.template_is_mod_approved == 0 { false } else { true },
|
||||
|
||||
file_size_bytes: if ir.file_size_bytes > 0 { ir.file_size_bytes as u32 } else { 0 },
|
||||
frame_width: if ir.frame_width > 0 { ir.frame_width as u32 } else { 0 },
|
||||
frame_height: if ir.frame_height > 0 { ir.frame_height as u32 } else { 0 },
|
||||
duration_millis: if ir.duration_millis > 0 { ir.duration_millis as u32 } else { 0 },
|
||||
|
||||
created_at: ir.created_at,
|
||||
updated_at: ir.updated_at,
|
||||
|
||||
maybe_moderator_fields: Some(W2lResultModeratorFields {
|
||||
template_creator_is_banned:
|
||||
nullable_i8_to_bool(ir.maybe_template_creator_is_banned, false),
|
||||
result_creator_is_banned_if_user:
|
||||
nullable_i8_to_bool(ir.maybe_creator_is_banned, false),
|
||||
result_creator_ip_address: ir.creator_ip_address,
|
||||
result_creator_deleted_at: ir.user_deleted_at,
|
||||
mod_deleted_at: ir.mod_deleted_at,
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Some(ir_for_response))
|
||||
}
|
||||
|
||||
async fn select_including_deleted(
|
||||
w2l_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> Result<W2lResultRecordRaw, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
W2lResultRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
w2l_results.token as w2l_result_token,
|
||||
w2l_results.maybe_tts_inference_result_token,
|
||||
|
||||
w2l_results.public_bucket_video_path,
|
||||
|
||||
w2l_templates.token as maybe_w2l_template_token,
|
||||
w2l_templates.template_type,
|
||||
w2l_templates.title as template_title,
|
||||
|
||||
users.token as maybe_creator_user_token,
|
||||
users.username as maybe_creator_username,
|
||||
users.display_name as maybe_creator_display_name,
|
||||
users.email_gravatar_hash as maybe_creator_gravatar_hash,
|
||||
users.is_banned as maybe_creator_is_banned,
|
||||
|
||||
template_users.token as maybe_template_creator_user_token,
|
||||
template_users.username as maybe_template_creator_username,
|
||||
template_users.display_name as maybe_template_creator_display_name,
|
||||
template_users.email_gravatar_hash as maybe_template_creator_gravatar_hash,
|
||||
template_users.is_banned as maybe_template_creator_is_banned,
|
||||
|
||||
w2l_results.creator_set_visibility,
|
||||
|
||||
w2l_results.file_size_bytes,
|
||||
w2l_results.frame_width,
|
||||
w2l_results.frame_height,
|
||||
w2l_results.duration_millis,
|
||||
w2l_results.created_at,
|
||||
w2l_results.updated_at,
|
||||
|
||||
w2l_results.creator_ip_address,
|
||||
w2l_results.user_deleted_at,
|
||||
w2l_results.mod_deleted_at
|
||||
|
||||
FROM w2l_results
|
||||
LEFT OUTER JOIN w2l_templates
|
||||
ON w2l_results.maybe_w2l_template_token = w2l_templates.token
|
||||
LEFT OUTER JOIN users
|
||||
ON w2l_results.maybe_creator_user_token = users.token
|
||||
LEFT OUTER JOIN users as template_users
|
||||
ON w2l_templates.creator_user_token = template_users.token
|
||||
WHERE
|
||||
w2l_results.token = ?
|
||||
"#,
|
||||
w2l_result_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await // TODO: This will return error if it doesn't exist
|
||||
}
|
||||
|
||||
async fn select_without_deleted(
|
||||
w2l_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> Result<W2lResultRecordRaw, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
W2lResultRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
w2l_results.token as w2l_result_token,
|
||||
w2l_results.maybe_tts_inference_result_token,
|
||||
|
||||
w2l_results.public_bucket_video_path,
|
||||
|
||||
w2l_templates.token as maybe_w2l_template_token,
|
||||
w2l_templates.template_type,
|
||||
w2l_templates.title as template_title,
|
||||
|
||||
users.token as maybe_creator_user_token,
|
||||
users.username as maybe_creator_username,
|
||||
users.display_name as maybe_creator_display_name,
|
||||
users.email_gravatar_hash as maybe_creator_gravatar_hash,
|
||||
users.is_banned as maybe_creator_is_banned,
|
||||
|
||||
template_users.token as maybe_template_creator_user_token,
|
||||
template_users.username as maybe_template_creator_username,
|
||||
template_users.display_name as maybe_template_creator_display_name,
|
||||
template_users.email_gravatar_hash as maybe_template_creator_gravatar_hash,
|
||||
template_users.is_banned as maybe_template_creator_is_banned,
|
||||
|
||||
w2l_results.creator_set_visibility,
|
||||
|
||||
w2l_results.file_size_bytes,
|
||||
w2l_results.frame_width,
|
||||
w2l_results.frame_height,
|
||||
w2l_results.duration_millis,
|
||||
w2l_results.created_at,
|
||||
w2l_results.updated_at,
|
||||
|
||||
w2l_results.creator_ip_address,
|
||||
w2l_results.user_deleted_at,
|
||||
w2l_results.mod_deleted_at
|
||||
|
||||
FROM w2l_results
|
||||
LEFT OUTER JOIN w2l_templates
|
||||
ON w2l_results.maybe_w2l_template_token = w2l_templates.token
|
||||
LEFT OUTER JOIN users
|
||||
ON w2l_results.maybe_creator_user_token = users.token
|
||||
LEFT OUTER JOIN users as template_users
|
||||
ON w2l_templates.creator_user_token = template_users.token
|
||||
WHERE
|
||||
w2l_results.token = ?
|
||||
AND w2l_results.user_deleted_at IS NULL
|
||||
AND w2l_results.mod_deleted_at IS NULL
|
||||
"#,
|
||||
w2l_result_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await // TODO: This will return error if it doesn't exist
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct W2lUploadTemplateJobStatusRecord {
|
||||
pub job_token: String,
|
||||
|
||||
pub status: String,
|
||||
pub attempt_count: i32,
|
||||
pub maybe_template_token: Option<String>,
|
||||
|
||||
pub maybe_failure_reason: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_w2l_template_upload_job_status(
|
||||
job_token: &str,
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<W2lUploadTemplateJobStatusRecord>> {
|
||||
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_status = sqlx::query_as!(
|
||||
W2lUploadTemplateJobStatusRecord,
|
||||
r#"
|
||||
SELECT
|
||||
jobs.token as job_token,
|
||||
|
||||
jobs.status,
|
||||
jobs.attempt_count,
|
||||
jobs.on_success_result_token as maybe_template_token,
|
||||
|
||||
jobs.failure_reason as maybe_failure_reason,
|
||||
|
||||
jobs.created_at,
|
||||
jobs.updated_at
|
||||
|
||||
FROM w2l_template_upload_jobs as jobs
|
||||
|
||||
WHERE jobs.token = ?
|
||||
"#,
|
||||
job_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await; // TODO: This will return error if it doesn't exist
|
||||
|
||||
match maybe_status {
|
||||
Ok(record) => Ok(Some(record)),
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => Ok(None),
|
||||
_ => Err(anyhow!("w2l template query error: {:?}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::w2l_template_upload_jobs::W2lTemplateUploadJobToken;
|
||||
|
||||
pub struct InsertW2lTemplateUploadJobArgs<'a> {
|
||||
pub uuid: &'a str,
|
||||
pub creator_user_token: &'a str,
|
||||
pub creator_ip_address: &'a str,
|
||||
pub creator_set_visibility: &'a str,
|
||||
pub title: &'a str,
|
||||
pub template_type: &'a str,
|
||||
pub download_url: &'a str,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
// NB: Returns the new token
|
||||
pub async fn insert_w2l_template_upload_job(args: InsertW2lTemplateUploadJobArgs<'_>) -> AnyhowResult<String> {
|
||||
// This token is returned to the client.
|
||||
let job_token = W2lTemplateUploadJobToken::generate().to_string();
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO w2l_template_upload_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
title = ?,
|
||||
template_type = ?,
|
||||
download_url = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
&job_token,
|
||||
args.uuid.to_string(),
|
||||
args.creator_user_token.to_string(),
|
||||
args.creator_ip_address.to_string(),
|
||||
args.creator_set_visibility.to_string(),
|
||||
args.title.to_string(),
|
||||
args.template_type,
|
||||
args.download_url,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => {
|
||||
return Err(anyhow!("failure to create w2l template upload job: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(job_token)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod get_w2l_template_upload_job_status;
|
||||
pub mod insert_w2l_template_upload_job;
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Just to query for existence
|
||||
pub (crate) struct W2lTemplateExistenceRecord {
|
||||
pub template_token: String,
|
||||
}
|
||||
|
||||
pub async fn check_w2l_template_exists(template_token: &str, mysql_pool: &MySqlPool) -> AnyhowResult<bool>
|
||||
{
|
||||
let maybe_template = sqlx::query_as!(
|
||||
W2lTemplateExistenceRecord,
|
||||
r#"
|
||||
SELECT
|
||||
token as template_token
|
||||
FROM w2l_templates
|
||||
WHERE
|
||||
token = ?
|
||||
AND user_deleted_at IS NULL
|
||||
AND mod_deleted_at IS NULL
|
||||
"#,
|
||||
&template_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await;
|
||||
|
||||
let record_exists = match maybe_template {
|
||||
Ok(_record) => {
|
||||
true
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
false
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("query w2l template existence query error: {:?}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(record_exists)
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn delete_w2l_template_as_user(
|
||||
template_token: &str,
|
||||
creator_ip_address: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
creator_ip_address_last_update = ?,
|
||||
user_deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
creator_ip_address,
|
||||
template_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_w2l_template_as_mod(
|
||||
template_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
maybe_mod_user_token = ?,
|
||||
mod_deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
template_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_w2l_template_as_user(
|
||||
template_token: &str,
|
||||
creator_ip_address: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
creator_ip_address_last_update = ?,
|
||||
user_deleted_at = NULL
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
creator_ip_address,
|
||||
template_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_w2l_template_as_mod(
|
||||
template_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
maybe_mod_user_token = ?,
|
||||
mod_deleted_at = NULL
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
template_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct EditW2lTemplateArgs<'a> {
|
||||
pub w2l_template_token: &'a str,
|
||||
|
||||
pub title: Option<&'a str>,
|
||||
pub description_markdown: Option<&'a str>,
|
||||
pub description_rendered_html: Option<&'a str>,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub role_dependent_fields: CreatorOrModFields<'a>,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub enum CreatorOrModFields<'a> {
|
||||
CreatorFields {
|
||||
creator_ip_address: &'a str,
|
||||
},
|
||||
ModFields(ModFields<'a>)
|
||||
}
|
||||
|
||||
pub struct ModFields<'a> {
|
||||
pub mod_user_token: &'a str,
|
||||
pub is_public_listing_approved: bool,
|
||||
pub is_locked_from_user_modification: bool,
|
||||
pub is_locked_from_use: bool,
|
||||
pub maybe_mod_comments: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub async fn edit_w2l_template(args: EditW2lTemplateArgs<'_>) -> AnyhowResult<()> {
|
||||
|
||||
let query_results = match args.role_dependent_fields {
|
||||
CreatorOrModFields::CreatorFields { creator_ip_address } => {
|
||||
// We need to store the IP address details.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
title = ?,
|
||||
description_markdown = ?,
|
||||
description_rendered_html = ?,
|
||||
creator_set_visibility = ?,
|
||||
creator_ip_address_last_update = ?,
|
||||
version = version + 1
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.title,
|
||||
args.description_markdown,
|
||||
args.description_rendered_html,
|
||||
args.creator_set_visibility.to_str(),
|
||||
creator_ip_address,
|
||||
args.w2l_template_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await
|
||||
},
|
||||
CreatorOrModFields::ModFields(ref mod_fields) => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
title = ?,
|
||||
description_markdown = ?,
|
||||
description_rendered_html = ?,
|
||||
creator_set_visibility = ?,
|
||||
maybe_mod_user_token = ?,
|
||||
version = version + 1
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.title,
|
||||
args.description_markdown,
|
||||
args.description_rendered_html,
|
||||
args.creator_set_visibility.to_str(),
|
||||
mod_fields.mod_user_token,
|
||||
args.w2l_template_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
anyhow!("update W2L template mod details (first query) error: {:?}", err)
|
||||
})?;
|
||||
|
||||
// TODO: This is lazy and suboptimal af to UPDATE again.
|
||||
// The reason we're doing this is because `sqlx` only does static type checking of queries
|
||||
// with string literals. It does not support dynamic query building, thus the PREDICATES
|
||||
// MUST BE HELD CONSTANT (at least in type signature). :(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
is_public_listing_approved = ?,
|
||||
is_locked_from_user_modification = ?,
|
||||
is_locked_from_use = ?,
|
||||
maybe_mod_comments = ?,
|
||||
maybe_mod_user_token = ?,
|
||||
version = version + 1
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_fields.is_public_listing_approved,
|
||||
mod_fields.is_locked_from_user_modification,
|
||||
mod_fields.is_locked_from_use,
|
||||
mod_fields.maybe_mod_comments,
|
||||
mod_fields.mod_user_token,
|
||||
args.w2l_template_token
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await
|
||||
},
|
||||
};
|
||||
|
||||
match query_results {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow!("Update W2L template DB error: {:?}", err))
|
||||
}
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_optional_bool};
|
||||
|
||||
// FIXME: This is the old style of query scoping and shouldn't be copied.
|
||||
// The moderator-only fields are good practice, though.
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lTemplateRecordForResponse {
|
||||
pub template_token: String,
|
||||
pub template_type: String,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub creator_gravatar_hash: String,
|
||||
pub title: String,
|
||||
pub description_markdown: String,
|
||||
pub description_rendered_html: String,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
pub duration_millis: u32,
|
||||
pub maybe_image_object_name: Option<String>,
|
||||
pub maybe_video_object_name: Option<String>,
|
||||
pub creator_set_visibility: Visibility,
|
||||
pub is_public_listing_approved: Option<bool>,
|
||||
pub is_locked_from_use: bool,
|
||||
pub is_locked_from_user_modification: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub maybe_moderator_fields: Option<W2lTemplateModeratorFields>,
|
||||
}
|
||||
|
||||
/// "Moderator-only fields" that we wouldn't want to expose to ordinary users.
|
||||
/// It's the web endpoint controller's responsibility to clear these for non-mods.
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lTemplateModeratorFields {
|
||||
pub creator_is_banned: bool,
|
||||
pub creator_ip_address_creation: String,
|
||||
pub creator_ip_address_last_update: String,
|
||||
pub user_deleted_at: Option<DateTime<Utc>>,
|
||||
pub mod_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lTemplateRecordRaw {
|
||||
pub template_token: String,
|
||||
pub template_type: String,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub creator_gravatar_hash: String,
|
||||
pub creator_is_banned: i8,
|
||||
pub title: String,
|
||||
pub description_markdown: String,
|
||||
pub description_rendered_html: String,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub duration_millis: i32,
|
||||
pub maybe_public_bucket_preview_image_object_name: Option<String>,
|
||||
pub maybe_public_bucket_preview_video_object_name: Option<String>,
|
||||
pub creator_set_visibility: String,
|
||||
pub is_public_listing_approved: Option<i8>,
|
||||
pub is_locked_from_use: i8,
|
||||
pub is_locked_from_user_modification: i8,
|
||||
pub creator_ip_address_creation: String,
|
||||
pub creator_ip_address_last_update: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub user_deleted_at: Option<DateTime<Utc>>,
|
||||
pub mod_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn select_w2l_template_by_token(
|
||||
w2l_template_token: &str,
|
||||
can_see_deleted: bool,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Option<W2lTemplateRecordForResponse>> {
|
||||
|
||||
let maybe_record = if can_see_deleted {
|
||||
select_including_deleted(w2l_template_token, mysql_pool).await
|
||||
} else {
|
||||
select_without_deleted(w2l_template_token, mysql_pool).await
|
||||
};
|
||||
|
||||
let template : W2lTemplateRecordRaw = match maybe_record {
|
||||
Ok(template) => template,
|
||||
Err(ref err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
warn!("w2l template not found: {:?}", &err);
|
||||
return Ok(None);
|
||||
},
|
||||
_ => {
|
||||
warn!("w2l template query error: {:?}", &err);
|
||||
return Err(anyhow!("database error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let template_for_response = W2lTemplateRecordForResponse {
|
||||
template_token: template.template_token,
|
||||
template_type: template.template_type,
|
||||
creator_user_token: template.creator_user_token,
|
||||
creator_username: template.creator_username,
|
||||
creator_display_name: template.creator_display_name,
|
||||
creator_gravatar_hash: template.creator_gravatar_hash,
|
||||
title: template.title,
|
||||
description_markdown: template.description_markdown,
|
||||
description_rendered_html: template.description_rendered_html,
|
||||
frame_width: if template.frame_width > 0 { template.frame_width as u32 } else { 0 },
|
||||
frame_height: if template.frame_height > 0 { template.frame_height as u32 } else { 0 },
|
||||
duration_millis: if template.duration_millis > 0 { template.duration_millis as u32 } else { 0 },
|
||||
maybe_image_object_name: template.maybe_public_bucket_preview_image_object_name,
|
||||
maybe_video_object_name: template.maybe_public_bucket_preview_video_object_name,
|
||||
// NB: Fail open/public with creator_set_visibility since we're already looking at it
|
||||
creator_set_visibility: Visibility::from_str(&template.creator_set_visibility)
|
||||
.unwrap_or(Visibility::Public),
|
||||
is_public_listing_approved: nullable_i8_to_optional_bool(template.is_public_listing_approved),
|
||||
is_locked_from_use: i8_to_bool(template.is_locked_from_use),
|
||||
is_locked_from_user_modification: i8_to_bool(template.is_locked_from_user_modification),
|
||||
created_at: template.created_at,
|
||||
updated_at: template.updated_at,
|
||||
maybe_moderator_fields: Some(W2lTemplateModeratorFields {
|
||||
creator_is_banned: i8_to_bool(template.creator_is_banned),
|
||||
creator_ip_address_creation: template.creator_ip_address_creation,
|
||||
creator_ip_address_last_update: template.creator_ip_address_last_update,
|
||||
user_deleted_at: template.user_deleted_at,
|
||||
mod_deleted_at: template.mod_deleted_at,
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Some(template_for_response))
|
||||
}
|
||||
|
||||
async fn select_including_deleted(
|
||||
w2l_template_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> Result<W2lTemplateRecordRaw, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
W2lTemplateRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.is_banned as creator_is_banned,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
users.email_gravatar_hash as creator_gravatar_hash,
|
||||
w2l.title,
|
||||
w2l.description_markdown,
|
||||
w2l.description_rendered_html,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.creator_set_visibility,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.is_locked_from_use,
|
||||
w2l.is_locked_from_user_modification,
|
||||
w2l.creator_ip_address_creation,
|
||||
w2l.creator_ip_address_last_update,
|
||||
w2l.created_at,
|
||||
w2l.updated_at,
|
||||
w2l.user_deleted_at,
|
||||
w2l.mod_deleted_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON users.token = w2l.creator_user_token
|
||||
WHERE w2l.token = ?
|
||||
"#,
|
||||
w2l_template_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await // TODO: This will return error if it doesn't exist
|
||||
}
|
||||
|
||||
async fn select_without_deleted(
|
||||
w2l_template_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> Result<W2lTemplateRecordRaw, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
W2lTemplateRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.is_banned as creator_is_banned,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
users.email_gravatar_hash as creator_gravatar_hash,
|
||||
w2l.title,
|
||||
w2l.description_markdown,
|
||||
w2l.description_rendered_html,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.creator_set_visibility,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.is_locked_from_use,
|
||||
w2l.is_locked_from_user_modification,
|
||||
w2l.creator_ip_address_creation,
|
||||
w2l.creator_ip_address_last_update,
|
||||
w2l.created_at,
|
||||
w2l.updated_at,
|
||||
w2l.user_deleted_at,
|
||||
w2l.mod_deleted_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON users.token = w2l.creator_user_token
|
||||
WHERE
|
||||
w2l.token = ?
|
||||
AND w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#,
|
||||
w2l_template_token
|
||||
)
|
||||
.fetch_one(mysql_pool)
|
||||
.await // TODO: This will return error if it doesn't exist
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct PendingW2lTemplateRow {
|
||||
pub template_token: String,
|
||||
pub title: String,
|
||||
pub template_type: String,
|
||||
pub duration_millis: i32,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub creator_gravatar_hash: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn list_pending_w2l_templates(
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Vec<PendingW2lTemplateRow>> {
|
||||
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
let maybe_results = sqlx::query_as!(
|
||||
PendingW2lTemplatesRaw,
|
||||
r#"
|
||||
SELECT
|
||||
w2l_templates.token as template_token,
|
||||
w2l_templates.title,
|
||||
w2l_templates.template_type,
|
||||
w2l_templates.duration_millis,
|
||||
w2l_templates.frame_width,
|
||||
w2l_templates.frame_height,
|
||||
w2l_templates.creator_user_token,
|
||||
users.username AS creator_username,
|
||||
users.display_name AS creator_display_name,
|
||||
users.email_gravatar_hash AS creator_gravatar_hash,
|
||||
w2l_templates.created_at
|
||||
FROM
|
||||
w2l_templates
|
||||
JOIN
|
||||
users
|
||||
ON
|
||||
users.token = w2l_templates.creator_user_token
|
||||
WHERE
|
||||
w2l_templates.is_public_listing_approved IS NULL
|
||||
AND w2l_templates.user_deleted_at IS NULL
|
||||
AND w2l_templates.mod_deleted_at IS NULL
|
||||
AND w2l_templates.is_locked_from_use IS FALSE
|
||||
"#,
|
||||
)
|
||||
.fetch_all(mysql_pool)
|
||||
.await;
|
||||
|
||||
let results : Vec<PendingW2lTemplatesRaw> = match maybe_results {
|
||||
Ok(results) => results,
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
Vec::new()
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("database err listing pending w2l templates: {:?}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let results = results.into_iter().map(|r| {
|
||||
PendingW2lTemplateRow {
|
||||
template_token: r.template_token,
|
||||
title: r.title,
|
||||
template_type: r.template_type,
|
||||
duration_millis: r.duration_millis,
|
||||
frame_width: r.frame_width,
|
||||
frame_height: r.frame_height,
|
||||
creator_user_token: r.creator_user_token,
|
||||
creator_username: r.creator_username,
|
||||
creator_display_name: r.creator_display_name,
|
||||
creator_gravatar_hash: r.creator_gravatar_hash,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}).collect::<Vec<PendingW2lTemplateRow>>();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub (crate) struct PendingW2lTemplatesRaw {
|
||||
pub template_token: String,
|
||||
pub title: String,
|
||||
pub template_type: String,
|
||||
pub duration_millis: i32,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub creator_gravatar_hash: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{info, warn};
|
||||
use sqlx::{Error, MySqlPool};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::boolean_converters::nullable_i8_to_optional_bool;
|
||||
|
||||
// FIXME: This is the old style of query scoping and shouldn't be copied.
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct W2lTemplateRecordForList {
|
||||
pub template_token: String,
|
||||
pub template_type: String,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub title: String,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
pub duration_millis: u32,
|
||||
pub maybe_image_object_name: Option<String>,
|
||||
pub maybe_video_object_name: Option<String>,
|
||||
pub is_public_listing_approved: Option<bool>, // converted
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
struct RawW2lTemplateRecordForList {
|
||||
pub template_token: String,
|
||||
pub template_type: String,
|
||||
pub creator_user_token: String,
|
||||
pub creator_username: String,
|
||||
pub creator_display_name: String,
|
||||
pub title: String,
|
||||
pub frame_width: i32,
|
||||
pub frame_height: i32,
|
||||
pub duration_millis: i32,
|
||||
pub maybe_public_bucket_preview_image_object_name: Option<String>,
|
||||
pub maybe_public_bucket_preview_video_object_name: Option<String>,
|
||||
pub is_public_listing_approved: Option<i8>, // NB: needs conversion
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn list_w2l_templates(
|
||||
mysql_pool: &MySqlPool,
|
||||
scope_creator_username: Option<&str>,
|
||||
require_mod_approved: bool
|
||||
) -> AnyhowResult<Vec<W2lTemplateRecordForList>> {
|
||||
|
||||
let maybe_templates = match scope_creator_username {
|
||||
Some(username) => {
|
||||
list_w2l_templates_creator_scoped(mysql_pool, username, require_mod_approved)
|
||||
.await
|
||||
},
|
||||
None => {
|
||||
list_w2l_templates_for_all_creators(mysql_pool, require_mod_approved)
|
||||
.await
|
||||
},
|
||||
};
|
||||
|
||||
let templates : Vec<RawW2lTemplateRecordForList> = match maybe_templates {
|
||||
Ok(templates) => {
|
||||
info!("Template length: {}", templates.len());
|
||||
templates
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error: {:?}", err);
|
||||
|
||||
return match err {
|
||||
Error::RowNotFound => {
|
||||
Ok(Vec::new())
|
||||
},
|
||||
_ => {
|
||||
warn!("w2l template list query error: {:?}", err);
|
||||
Err(anyhow!("w2l template list query error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(templates.into_iter()
|
||||
.map(|template| {
|
||||
W2lTemplateRecordForList {
|
||||
template_token: template.template_token,
|
||||
template_type: template.template_type,
|
||||
creator_user_token: template.creator_user_token,
|
||||
creator_username: template.creator_username,
|
||||
creator_display_name: template.creator_display_name,
|
||||
title: template.title,
|
||||
frame_width: if template.frame_width > 0 { template.frame_width as u32 } else { 0 },
|
||||
frame_height: if template.frame_height > 0 { template.frame_height as u32 } else { 0 },
|
||||
duration_millis: if template.duration_millis > 0 { template.duration_millis as u32 } else { 0 },
|
||||
maybe_image_object_name: template.maybe_public_bucket_preview_image_object_name,
|
||||
maybe_video_object_name: template.maybe_public_bucket_preview_video_object_name,
|
||||
is_public_listing_approved: nullable_i8_to_optional_bool(template.is_public_listing_approved),
|
||||
created_at: template.created_at,
|
||||
updated_at: template.updated_at,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<W2lTemplateRecordForList>>())
|
||||
}
|
||||
|
||||
async fn list_w2l_templates_for_all_creators(
|
||||
mysql_pool: &MySqlPool,
|
||||
require_mod_approved: bool
|
||||
) -> Result<Vec<RawW2lTemplateRecordForList>, Error> {
|
||||
// TODO: There has to be a better way.
|
||||
// Sqlx doesn't like anything except string literals.
|
||||
let maybe_templates = if require_mod_approved {
|
||||
info!("listing w2l templates for everyone; mod-approved only");
|
||||
sqlx::query_as!(
|
||||
RawW2lTemplateRecordForList,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
w2l.title,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.created_at,
|
||||
w2l.updated_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON users.token = w2l.creator_user_token
|
||||
WHERE
|
||||
w2l.is_public_listing_approved IS TRUE
|
||||
AND w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#)
|
||||
.fetch_all(mysql_pool)
|
||||
.await?
|
||||
} else {
|
||||
info!("listing w2l templates for everyone; all");
|
||||
sqlx::query_as!(
|
||||
RawW2lTemplateRecordForList,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
w2l.title,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.created_at,
|
||||
w2l.updated_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON users.token = w2l.creator_user_token
|
||||
WHERE
|
||||
w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#)
|
||||
.fetch_all(mysql_pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(maybe_templates)
|
||||
}
|
||||
|
||||
async fn list_w2l_templates_creator_scoped(
|
||||
mysql_pool: &MySqlPool,
|
||||
scope_creator_username: &str,
|
||||
require_mod_approved: bool
|
||||
) -> Result<Vec<RawW2lTemplateRecordForList>, Error> {
|
||||
// TODO: There has to be a better way.
|
||||
// Sqlx doesn't like anything except string literals.
|
||||
let maybe_templates = if require_mod_approved {
|
||||
info!("listing w2l templates for user; mod-approved only");
|
||||
sqlx::query_as!(
|
||||
RawW2lTemplateRecordForList,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
w2l.title,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.created_at,
|
||||
w2l.updated_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON
|
||||
users.token = w2l.creator_user_token
|
||||
WHERE
|
||||
users.username = ?
|
||||
AND w2l.is_public_listing_approved IS TRUE
|
||||
AND w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#,
|
||||
scope_creator_username)
|
||||
.fetch_all(mysql_pool)
|
||||
.await?
|
||||
} else {
|
||||
info!("listing w2l templates for user; all");
|
||||
sqlx::query_as!(
|
||||
RawW2lTemplateRecordForList,
|
||||
r#"
|
||||
SELECT
|
||||
w2l.token as template_token,
|
||||
w2l.template_type,
|
||||
w2l.creator_user_token,
|
||||
users.username as creator_username,
|
||||
users.display_name as creator_display_name,
|
||||
w2l.title,
|
||||
w2l.frame_width,
|
||||
w2l.frame_height,
|
||||
w2l.duration_millis,
|
||||
w2l.maybe_public_bucket_preview_image_object_name,
|
||||
w2l.maybe_public_bucket_preview_video_object_name,
|
||||
w2l.is_public_listing_approved,
|
||||
w2l.created_at,
|
||||
w2l.updated_at
|
||||
FROM w2l_templates as w2l
|
||||
JOIN users
|
||||
ON
|
||||
users.token = w2l.creator_user_token
|
||||
WHERE
|
||||
users.username = ?
|
||||
AND w2l.user_deleted_at IS NULL
|
||||
AND w2l.mod_deleted_at IS NULL
|
||||
"#,
|
||||
scope_creator_username)
|
||||
.fetch_all(mysql_pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(maybe_templates)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod check_w2l_template_exists;
|
||||
pub mod delete_w2l_template_various_scopes;
|
||||
pub mod edit_w2l_template;
|
||||
pub mod get_w2l_template;
|
||||
pub mod list_pending_w2l_templates;
|
||||
pub mod list_w2l_templates;
|
||||
pub mod set_w2l_template_mod_approval;
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn set_w2l_template_mod_approval(
|
||||
w2l_template_token: &str,
|
||||
mod_user_token: &str,
|
||||
is_approved: bool,
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE w2l_templates
|
||||
SET
|
||||
is_public_listing_approved = ?,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
|
||||
is_approved,
|
||||
mod_user_token,
|
||||
w2l_template_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow!("error setting w2l mod approval status: {:?}", err)),
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
[package]
|
||||
name = "w2l-download-job"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "w2l-download-job"
|
||||
path = "src/main.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
bucket_paths.workspace = true
|
||||
cloud_storage = { path = "../../../../api_clients/cloud_storage" }
|
||||
config = { path = "../../../../lib/deprecated/config" }
|
||||
easyenv = { path = "../../../../lib/easyenv" }
|
||||
errors.workspace = true
|
||||
filesys.workspace = true
|
||||
google_drive_downloader = { path = "../../../../api_clients/google_drive_downloader" }
|
||||
hashing = { path = "../../../../lib/files/hashing" }
|
||||
jobs_common = { path = "../../../../lib/jobs_common" }
|
||||
mysql_queries = { path = "../../../../schema/database/mysql_queries" }
|
||||
shared_env_var_config.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
dotenv.workspace = true
|
||||
hostname.workspace = true
|
||||
log.workspace = true
|
||||
subprocess = "0.2.9"
|
||||
tempdir.workspace = true
|
||||
|
||||
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] }
|
||||
|
||||
# Redis
|
||||
r2d2_redis.workspace = true
|
||||
|
||||
# Runtime
|
||||
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
|
||||
tokio.workspace = true
|
||||
|
||||
# NB: Hyper is just for HTTP client use in jobs. I'd use Actix client, but I need tokio runtime
|
||||
# NB: Furthermore, the Tokio runtime is old, so I have to use an older Hyper.
|
||||
hyper = { version = "1.3.1" }
|
||||
|
||||
# Serialization
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -1,676 +0,0 @@
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)]
|
||||
//#![forbid(warnings)]
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unreachable_patterns)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
use std::fs::{File, metadata};
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use chrono::Utc;
|
||||
use log::{info, warn};
|
||||
use r2d2_redis::r2d2;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::MySqlPool;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use config::common_env::CommonEnv;
|
||||
use config::is_bad_video_download_url::is_bad_video_download_url;
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::check_directory_exists::check_directory_exists;
|
||||
use filesys::check_file_exists::check_file_exists;
|
||||
use filesys::file_deletion::safe_delete_directory::safe_delete_directory;
|
||||
use filesys::file_deletion::safe_delete_file::safe_delete_file;
|
||||
use google_drive_downloader::google_drive_download_command::GoogleDriveDownloadCommand;
|
||||
use hashing::sha256::sha256_hash_file::sha256_hash_file;
|
||||
use jobs_common::noop_logger::NoOpLogger;
|
||||
use jobs_common::redis_job_status_logger::RedisJobStatusLogger;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::grab_job_lock_and_mark_pending;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::insert_w2l_template;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::mark_w2l_template_upload_job_done;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::mark_w2l_template_upload_job_failure;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::mark_w2l_template_upload_job_permanently_dead;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::query_w2l_template_upload_job_records;
|
||||
use mysql_queries::queries::w2l::w2l_download_jobs::w2l_download_job_queries::W2lTemplateUploadJobRecord;
|
||||
|
||||
use crate::script_execution::ffmpeg_generate_preview_image_command::FfmpegGeneratePreviewImageCommand;
|
||||
use crate::script_execution::ffmpeg_generate_preview_video_command::FfmpegGeneratePreviewVideoCommand;
|
||||
use crate::script_execution::imagemagick_generate_preview_image_command::ImagemagickGeneratePreviewImageCommand;
|
||||
use crate::script_execution::wav2lip_process_upload_command::{Wav2LipPreprocessClient, Wav2LipPreprocessError};
|
||||
|
||||
pub mod script_execution;
|
||||
|
||||
// Buckets (shared config)
|
||||
const ENV_ACCESS_KEY : &str = "ACCESS_KEY";
|
||||
const ENV_SECRET_KEY : &str = "SECRET_KEY";
|
||||
const ENV_REGION_NAME : &str = "REGION_NAME";
|
||||
|
||||
// Buckets (private data)
|
||||
const ENV_PRIVATE_BUCKET_NAME : &str = "W2L_PRIVATE_DOWNLOAD_BUCKET_NAME";
|
||||
// Buckets (public data)
|
||||
const ENV_PUBLIC_BUCKET_NAME : &str = "W2L_PUBLIC_DOWNLOAD_BUCKET_NAME";
|
||||
|
||||
// Various bucket roots
|
||||
const ENV_DOWNLOAD_BUCKET_ROOT : &str = "W2L_DOWNLOAD_BUCKET_ROOT";
|
||||
|
||||
// Python code
|
||||
const ENV_CODE_DIRECTORY : &str = "W2L_CODE_DIRECTORY";
|
||||
const ENV_MODEL_CHECKPOINT : &str = "W2L_MODEL_CHECKPOINT";
|
||||
const ENV_DOWNLOAD_SCRIPT_NAME : &str = "W2L_DOWNLOAD_SCRIPT_NAME";
|
||||
|
||||
const DEFAULT_TEMP_DIR: &str = "/tmp";
|
||||
|
||||
struct Downloader {
|
||||
pub download_temp_directory: PathBuf,
|
||||
pub mysql_pool: MySqlPool,
|
||||
|
||||
pub redis_pool: r2d2::Pool<RedisConnectionManager>,
|
||||
|
||||
pub private_bucket_client: BucketClient,
|
||||
pub public_bucket_client: BucketClient,
|
||||
|
||||
pub firehose_publisher: FirehosePublisher,
|
||||
pub badge_granter: BadgeGranter,
|
||||
|
||||
pub google_drive_downloader: GoogleDriveDownloadCommand,
|
||||
pub w2l_processor: Wav2LipPreprocessClient,
|
||||
pub ffmpeg_image_preview_generator: FfmpegGeneratePreviewImageCommand,
|
||||
pub ffmpeg_video_preview_generator: FfmpegGeneratePreviewVideoCommand,
|
||||
pub imagemagick_image_preview_generator: ImagemagickGeneratePreviewImageCommand,
|
||||
|
||||
// Command to run
|
||||
pub download_script: String,
|
||||
|
||||
// Root to store W2L templates (public and private)
|
||||
pub bucket_root_w2l_template_uploads: String,
|
||||
|
||||
// Temporary for debugging
|
||||
// Arbitrary timeouts can be inserted so we can exec in and poke around.
|
||||
pub debug_download_sleep_millis: u64,
|
||||
pub debug_face_detect_sleep_millis: u64,
|
||||
pub debug_job_end_sleep_millis: u64,
|
||||
|
||||
// Sleep between batches
|
||||
pub job_batch_wait_millis: u64,
|
||||
|
||||
// How long to wait between log lines
|
||||
pub no_op_logger_millis: u64,
|
||||
|
||||
// Max job attempts before failure.
|
||||
// NB: This is an i32 so we don't need to convert to db column type.
|
||||
pub job_max_attempts: i32,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnyhowResult<()> {
|
||||
easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG));
|
||||
|
||||
// NB: Do not check this secrets-containing dotenv file into VCS.
|
||||
// This file should only contain *development* secrets, never production.
|
||||
let _ = dotenv::from_filename(".env-secrets").ok();
|
||||
|
||||
info!("Obtaining hostname...");
|
||||
|
||||
let server_hostname = hostname::get()
|
||||
.ok()
|
||||
.and_then(|h| h.into_string().ok())
|
||||
.unwrap_or("w2l-download-job".to_string());
|
||||
|
||||
info!("Hostname: {}", &server_hostname);
|
||||
|
||||
// Bucket stuff (shared)
|
||||
let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?;
|
||||
let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?;
|
||||
let region_name = easyenv::get_env_string_required(ENV_REGION_NAME)?;
|
||||
|
||||
// Private and Public Buckets
|
||||
let private_bucket_name = easyenv::get_env_string_required(ENV_PRIVATE_BUCKET_NAME)?;
|
||||
let public_bucket_name = easyenv::get_env_string_required(ENV_PUBLIC_BUCKET_NAME)?;
|
||||
|
||||
// Bucket roots
|
||||
let bucket_root = easyenv::get_env_string_required(ENV_DOWNLOAD_BUCKET_ROOT)?;
|
||||
|
||||
let s3_compatible_endpoint_url = easyenv::get_env_string_or_default(
|
||||
"S3_COMPATIBLE_ENDPOINT_URL",
|
||||
"https://storage.googleapis.com");
|
||||
let bucket_timeout = easyenv::get_env_duration_seconds_or_default("BUCKET_TIMEOUT_SECONDS",
|
||||
Duration::from_secs(60 * 5));
|
||||
|
||||
let private_bucket_client = BucketClient::create(
|
||||
&access_key,
|
||||
&secret_key,
|
||||
®ion_name,
|
||||
&private_bucket_name,
|
||||
&s3_compatible_endpoint_url,
|
||||
None,
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
let public_bucket_client = BucketClient::create(
|
||||
&access_key,
|
||||
&secret_key,
|
||||
®ion_name,
|
||||
&public_bucket_name,
|
||||
&s3_compatible_endpoint_url,
|
||||
None,
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
let py_code_directory = easyenv::get_env_string_required(ENV_CODE_DIRECTORY)?;
|
||||
let py_script_name = easyenv::get_env_string_required(ENV_DOWNLOAD_SCRIPT_NAME)?;
|
||||
let py_model_checkpoint = easyenv::get_env_string_required(ENV_MODEL_CHECKPOINT)?;
|
||||
|
||||
let w2l_preprecess_command = Wav2LipPreprocessClient::new(
|
||||
&py_code_directory,
|
||||
&py_script_name,
|
||||
&py_model_checkpoint,
|
||||
);
|
||||
|
||||
let temp_directory = easyenv::get_env_string_or_default(
|
||||
"DOWNLOAD_TEMP_DIR",
|
||||
DEFAULT_TEMP_DIR);
|
||||
|
||||
let download_script = easyenv::get_env_string_or_default(
|
||||
"DOWNLOAD_SCRIPT",
|
||||
"./scripts/download_internet_file.py");
|
||||
|
||||
let google_drive_downloader =
|
||||
GoogleDriveDownloadCommand::new_production(&download_script);
|
||||
|
||||
let temp_directory = PathBuf::from(temp_directory);
|
||||
|
||||
check_directory_exists(&temp_directory)?;
|
||||
|
||||
let db_connection_string = env_get_mysql_connection_string_or_default();
|
||||
|
||||
info!("Connecting to database...");
|
||||
|
||||
let mysql_pool = MySqlPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_connection_string)
|
||||
.await?;
|
||||
|
||||
let common_env = CommonEnv::read_from_env()?;
|
||||
|
||||
info!("Connecting to redis...");
|
||||
|
||||
let redis_connection_string = env_get_redis_0_connection_string_or_default();
|
||||
let redis_manager = RedisConnectionManager::new(redis_connection_string.as_str())?;
|
||||
|
||||
let redis_pool = r2d2::Pool::builder()
|
||||
.build(redis_manager)?;
|
||||
|
||||
let firehose_publisher = FirehosePublisher {
|
||||
mysql_pool: mysql_pool.clone(), // NB: Pool is sync/send/clone-safe
|
||||
};
|
||||
|
||||
let badge_granter = BadgeGranter {
|
||||
mysql_pool: mysql_pool.clone(), // NB: Pool is sync/send/clone-safe
|
||||
firehose_publisher: firehose_publisher.clone(), // NB: Also safe
|
||||
};
|
||||
|
||||
let downloader = Downloader {
|
||||
download_temp_directory: temp_directory,
|
||||
mysql_pool,
|
||||
redis_pool,
|
||||
public_bucket_client,
|
||||
private_bucket_client,
|
||||
download_script,
|
||||
google_drive_downloader,
|
||||
firehose_publisher,
|
||||
badge_granter,
|
||||
ffmpeg_image_preview_generator: FfmpegGeneratePreviewImageCommand {},
|
||||
ffmpeg_video_preview_generator: FfmpegGeneratePreviewVideoCommand {},
|
||||
imagemagick_image_preview_generator: ImagemagickGeneratePreviewImageCommand {},
|
||||
w2l_processor: w2l_preprecess_command,
|
||||
bucket_root_w2l_template_uploads: bucket_root.to_string(),
|
||||
debug_download_sleep_millis: easyenv::get_env_num("DEBUG_DOWNLOAD_SLEEP_MILLIS", 0)?,
|
||||
debug_face_detect_sleep_millis: easyenv::get_env_num("DEBUG_FACE_DETECT_SLEEP_MILLIS", 0)?,
|
||||
job_batch_wait_millis: common_env.job_batch_wait_millis,
|
||||
job_max_attempts: common_env.job_max_attempts as i32,
|
||||
no_op_logger_millis: common_env.no_op_logger_millis,
|
||||
debug_job_end_sleep_millis: common_env.debug_job_end_sleep_millis,
|
||||
};
|
||||
|
||||
main_loop(downloader).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const START_TIMEOUT_MILLIS : u64 = 500;
|
||||
const INCREASE_TIMEOUT_MILLIS : u64 = 1000;
|
||||
|
||||
async fn main_loop(downloader: Downloader) {
|
||||
let mut error_timeout_millis = START_TIMEOUT_MILLIS;
|
||||
|
||||
let mut noop_logger = NoOpLogger::new(downloader.no_op_logger_millis as i64);
|
||||
|
||||
loop {
|
||||
let num_records = 1;
|
||||
|
||||
let query_result = query_w2l_template_upload_job_records(
|
||||
&downloader.mysql_pool,
|
||||
num_records)
|
||||
.await;
|
||||
|
||||
let jobs = match query_result {
|
||||
Ok(jobs) => jobs,
|
||||
Err(e) => {
|
||||
warn!("Error querying jobs: {:?}", e);
|
||||
std::thread::sleep(Duration::from_millis(error_timeout_millis));
|
||||
error_timeout_millis += INCREASE_TIMEOUT_MILLIS;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if jobs.is_empty() {
|
||||
noop_logger.log_after_awhile();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(downloader.job_batch_wait_millis));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = process_jobs(&downloader, jobs).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
warn!("Error querying jobs: {:?}", e);
|
||||
std::thread::sleep(Duration::from_millis(error_timeout_millis));
|
||||
error_timeout_millis += INCREASE_TIMEOUT_MILLIS;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
error_timeout_millis = START_TIMEOUT_MILLIS; // reset
|
||||
|
||||
std::thread::sleep(Duration::from_millis(downloader.job_batch_wait_millis));
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_jobs(downloader: &Downloader, jobs: Vec<W2lTemplateUploadJobRecord>) -> AnyhowResult<()> {
|
||||
for job in jobs.into_iter() {
|
||||
let result = process_job(downloader, &job).await;
|
||||
match result {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
warn!("Failure to process job: {:?}", e);
|
||||
let failure_reason = "";
|
||||
let _r = mark_w2l_template_upload_job_failure(
|
||||
&downloader.mysql_pool,
|
||||
&job,
|
||||
failure_reason,
|
||||
downloader.job_max_attempts
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FileMetadata {
|
||||
pub is_video: bool,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub num_frames: u64,
|
||||
pub fps: Option<f32>,
|
||||
pub duration_millis: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
pub file_size_bytes: u64,
|
||||
}
|
||||
|
||||
fn read_metadata_file(filename: &str) -> AnyhowResult<FileMetadata> {
|
||||
let mut file = File::open(filename)?;
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer)?;
|
||||
Ok(serde_json::from_str(&buffer)?)
|
||||
}
|
||||
|
||||
async fn process_job(downloader: &Downloader, job: &W2lTemplateUploadJobRecord) -> AnyhowResult<()> {
|
||||
// TODO: 1. Mark processing.
|
||||
// TODO: 2. Download. (DONE)
|
||||
// TODO: 3. Process template with face detection (DONE)
|
||||
// TODO: 4. Determine if picture or video
|
||||
// TODO: 5. Take a screenshot/gif
|
||||
// TODO: 6. Upload all (partially done).
|
||||
// TODO: 7. Save record. (DONE)
|
||||
// TODO: 8. Mark job done. (DONE)
|
||||
|
||||
let mut redis = downloader.redis_pool.get()?;
|
||||
let mut redis_logger = RedisJobStatusLogger::new_w2l_download(
|
||||
&mut redis,
|
||||
&job.token);
|
||||
|
||||
// ==================== ATTEMPT TO GRAB JOB LOCK ==================== //
|
||||
|
||||
let lock_acquired = grab_job_lock_and_mark_pending(&downloader.mysql_pool, job).await?;
|
||||
|
||||
if !lock_acquired {
|
||||
warn!("Could not acquire job lock for: {}", &job.id);
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// ==================== SETUP TEMP DIRS ==================== //
|
||||
|
||||
let temp_dir = format!("temp_{}", job.id);
|
||||
let temp_dir = TempDir::new(&temp_dir)?;
|
||||
|
||||
// ==================== DOWNLOAD FILE ==================== //
|
||||
|
||||
redis_logger.log_status("downloading media")?;
|
||||
|
||||
let download_url = job.download_url.as_ref()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
|
||||
if is_bad_video_download_url(&download_url).unwrap_or(true) {
|
||||
warn!("Download URL is invalid: {}", download_url);
|
||||
|
||||
mark_w2l_template_upload_job_permanently_dead(
|
||||
&downloader.mysql_pool,
|
||||
job,
|
||||
"bad download URL"
|
||||
).await?;
|
||||
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
info!("Calling downloader...");
|
||||
let download_filename = match downloader.google_drive_downloader.download_file(&download_url, &temp_dir).await {
|
||||
Ok(filename) => filename,
|
||||
Err(e) => {
|
||||
safe_delete_directory(&temp_dir);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if downloader.debug_download_sleep_millis != 0 {
|
||||
warn!("Debug sleep after download: {} ms", downloader.debug_download_sleep_millis);
|
||||
thread::sleep(Duration::from_millis(downloader.debug_download_sleep_millis));
|
||||
}
|
||||
|
||||
// ==================== PROCESS FACES ==================== //
|
||||
|
||||
redis_logger.log_status("detecting faces")?;
|
||||
|
||||
// This is the Python Pickle file with all the face frames.
|
||||
// We'll retain it forever since it's expensive to compute.
|
||||
let cached_faces_filename = format!("{}_detected_faces.pickle", &download_filename);
|
||||
|
||||
// This is a file that we'll use to determine if the file is an image or video.
|
||||
let output_metadata_filename = format!("{}_metadata.json", &download_filename);
|
||||
|
||||
let is_image = false; // TODO: Don't always treat as video.
|
||||
let spawn_process = false;
|
||||
|
||||
let face_detect_result = downloader.w2l_processor.execute(
|
||||
&download_filename,
|
||||
&cached_faces_filename,
|
||||
&output_metadata_filename,
|
||||
is_image,
|
||||
spawn_process);
|
||||
|
||||
match face_detect_result {
|
||||
Ok(_) => {}, // Ok to proceed.
|
||||
Err(Wav2LipPreprocessError::UnknownError) => {} // Ok to proceed.
|
||||
Err(Wav2LipPreprocessError::NoFacesDetected) => {
|
||||
// Permanently fail.
|
||||
warn!("Permanently failed due to no face detection!");
|
||||
|
||||
safe_delete_directory(&temp_dir);
|
||||
|
||||
mark_w2l_template_upload_job_permanently_dead(
|
||||
&downloader.mysql_pool,
|
||||
job,
|
||||
"faces must be present in every frame"
|
||||
).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if downloader.debug_face_detect_sleep_millis != 0 {
|
||||
warn!("Debug sleep after face detect: {} ms", downloader.debug_face_detect_sleep_millis);
|
||||
thread::sleep(Duration::from_millis(downloader.debug_face_detect_sleep_millis));
|
||||
}
|
||||
|
||||
// ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== //
|
||||
|
||||
let video_or_image_path = PathBuf::from(&download_filename);
|
||||
let cached_faces_path = PathBuf::from(&cached_faces_filename);
|
||||
let output_metadata_path = PathBuf::from(&output_metadata_filename);
|
||||
|
||||
info!("Checking that both files exist (original source + cached faces) ...");
|
||||
|
||||
check_file_exists(&video_or_image_path)?;
|
||||
check_file_exists(&cached_faces_path)?;
|
||||
check_file_exists(&output_metadata_path)?;
|
||||
|
||||
let file_metadata = read_metadata_file(&output_metadata_filename)?;
|
||||
|
||||
// ==================== BASE OBJECT NAMES BASED ON HASH ==================== //
|
||||
|
||||
let private_bucket_hash = sha256_hash_file(&download_filename)?;
|
||||
|
||||
info!("File hash: {}", private_bucket_hash);
|
||||
|
||||
// Full path to video/image
|
||||
//let full_object_path = hash_to_bucket_path_string(
|
||||
// &private_bucket_hash,
|
||||
// Some(&downloader.bucket_root_w2l_template_uploads))?;
|
||||
|
||||
let full_object_path = "THIS_IS_BROKEN_BECAUSE_W2L_IS_DEAD";
|
||||
|
||||
// ==================== GENERATE VIDEO PREVIEWS ==================== //
|
||||
|
||||
let mut maybe_image_preview_filename : Option<PathBuf> = None;
|
||||
let mut maybe_image_preview_object_name : Option<String> = None;
|
||||
|
||||
let mut maybe_video_preview_filename : Option<PathBuf> = None;
|
||||
let mut maybe_video_preview_object_name : Option<String> = None;
|
||||
|
||||
redis_logger.log_status("generating preview")?;
|
||||
|
||||
if file_metadata.is_video {
|
||||
let preview_filename = format!("{}_preview.webp", &download_filename);
|
||||
|
||||
let preview_result = downloader.ffmpeg_video_preview_generator.execute(
|
||||
&download_filename,
|
||||
&preview_filename,
|
||||
500,
|
||||
500,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
if let Err(e) = preview_result {
|
||||
safe_delete_file(&download_filename);
|
||||
safe_delete_file(&preview_filename);
|
||||
safe_delete_directory(&temp_dir);
|
||||
}
|
||||
|
||||
let video_preview_path = PathBuf::from(&preview_filename);
|
||||
check_file_exists(&video_preview_path)?;
|
||||
|
||||
let preview_object_path = format!("{}_preview.webp", full_object_path);
|
||||
maybe_video_preview_object_name = Some(preview_object_path);
|
||||
maybe_video_preview_filename = Some(video_preview_path);
|
||||
|
||||
} else {
|
||||
let preview_filename = format!("{}_preview.webp", &download_filename);
|
||||
|
||||
downloader.imagemagick_image_preview_generator.execute(
|
||||
&download_filename,
|
||||
&preview_filename,
|
||||
500,
|
||||
500,
|
||||
false
|
||||
)?;
|
||||
|
||||
let image_preview_path = PathBuf::from(&preview_filename);
|
||||
check_file_exists(&image_preview_path)?;
|
||||
|
||||
let preview_object_path = format!("{}_preview.webp", full_object_path);
|
||||
maybe_image_preview_object_name = Some(preview_object_path);
|
||||
maybe_image_preview_filename = Some(image_preview_path);
|
||||
}
|
||||
|
||||
// ==================== UPLOAD TO BUCKETS ==================== //
|
||||
|
||||
redis_logger.log_status("uploading results")?;
|
||||
|
||||
info!("Image/video destination bucket path: {}", full_object_path);
|
||||
|
||||
// Full path to cached faces
|
||||
let full_object_path_cached_faces = format!("{}_detected_faces.pickle", full_object_path);
|
||||
|
||||
info!("Cached faces destination bucket path: {}", full_object_path_cached_faces);
|
||||
|
||||
info!("Uploading image/video...");
|
||||
|
||||
let original_mime_type = file_metadata.mimetype
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
|
||||
downloader.private_bucket_client.upload_filename_with_content_type(
|
||||
&full_object_path,
|
||||
&video_or_image_path,
|
||||
original_mime_type).await?;
|
||||
|
||||
downloader.public_bucket_client.upload_filename_with_content_type(
|
||||
&full_object_path,
|
||||
&video_or_image_path,
|
||||
original_mime_type).await?;
|
||||
|
||||
safe_delete_file(&video_or_image_path);
|
||||
|
||||
info!("Uploading cached faces...");
|
||||
let path_copy: PathBuf = cached_faces_path.clone();
|
||||
downloader.private_bucket_client.upload_filename(
|
||||
&full_object_path_cached_faces,
|
||||
&path_copy).await?;
|
||||
|
||||
safe_delete_file(&path_copy);
|
||||
|
||||
// TODO: Fix this ugh.
|
||||
if let Some(image_preview_filename) = maybe_image_preview_filename.as_deref() {
|
||||
if let Some(image_preview_object_name) = maybe_image_preview_object_name.as_deref() {
|
||||
info!("Uploading image preview...");
|
||||
downloader.private_bucket_client.upload_filename_with_content_type(
|
||||
&image_preview_object_name,
|
||||
image_preview_filename,
|
||||
"image/webp").await?;
|
||||
|
||||
info!("Uploading image preview...");
|
||||
downloader.public_bucket_client.upload_filename_with_content_type(
|
||||
&image_preview_object_name,
|
||||
image_preview_filename,
|
||||
"image/webp").await?;
|
||||
}
|
||||
|
||||
safe_delete_file(&image_preview_filename);
|
||||
}
|
||||
|
||||
// TODO: Fix this ugh.
|
||||
if let Some(video_preview_filename) = maybe_video_preview_filename.as_deref() {
|
||||
if let Some(video_preview_object_name) = maybe_video_preview_object_name.as_deref() {
|
||||
info!("Uploading video preview...");
|
||||
downloader.private_bucket_client.upload_filename_with_content_type(
|
||||
&video_preview_object_name,
|
||||
video_preview_filename,
|
||||
"image/webp").await?;
|
||||
|
||||
info!("Uploading video preview...");
|
||||
downloader.public_bucket_client.upload_filename_with_content_type(
|
||||
&video_preview_object_name,
|
||||
video_preview_filename,
|
||||
"image/webp").await?;
|
||||
}
|
||||
|
||||
safe_delete_file(&video_preview_filename);
|
||||
}
|
||||
|
||||
// ==================== DELETE DOWNLOADED FILE ==================== //
|
||||
|
||||
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
|
||||
safe_delete_directory(&temp_dir);
|
||||
|
||||
// ==================== SAVE RECORDS ==================== //
|
||||
|
||||
let template_type = if file_metadata.is_video { "video" } else { "image" };
|
||||
|
||||
info!("Saving model record...");
|
||||
let (id, model_token) = insert_w2l_template(
|
||||
&downloader.mysql_pool,
|
||||
template_type,
|
||||
job,
|
||||
&private_bucket_hash,
|
||||
&full_object_path,
|
||||
&full_object_path_cached_faces,
|
||||
maybe_image_preview_object_name.as_deref(),
|
||||
maybe_video_preview_object_name.as_deref(),
|
||||
file_metadata.file_size_bytes,
|
||||
file_metadata.mimetype.as_deref(),
|
||||
file_metadata.width,
|
||||
file_metadata.height,
|
||||
file_metadata.num_frames,
|
||||
file_metadata.fps.unwrap_or(0.0f32),
|
||||
file_metadata.duration_millis.unwrap_or(0))
|
||||
.await?;
|
||||
|
||||
info!("Marking job complete...");
|
||||
|
||||
mark_w2l_template_upload_job_done(
|
||||
&downloader.mysql_pool,
|
||||
job,
|
||||
true,
|
||||
Some(&model_token)
|
||||
).await?;
|
||||
|
||||
downloader.firehose_publisher.publish_w2l_template_upload_finished(&job.creator_user_token, &model_token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error publishing event: {:?}", e);
|
||||
anyhow!("error publishing event")
|
||||
})?;
|
||||
|
||||
downloader.badge_granter.maybe_grant_w2l_template_uploads_badge(&job.creator_user_token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error maybe awarding badge: {:?}", e);
|
||||
anyhow!("error maybe awarding badge")
|
||||
})?;
|
||||
|
||||
if downloader.debug_job_end_sleep_millis != 0 {
|
||||
warn!("Debug sleep after job end: {} ms", downloader.debug_job_end_sleep_millis);
|
||||
thread::sleep(Duration::from_millis(downloader.debug_job_end_sleep_millis));
|
||||
}
|
||||
|
||||
redis_logger.log_status("done")?;
|
||||
|
||||
info!("Job {} complete success! Downloaded, processed, and uploaded. Saved model record: {}",
|
||||
job.id, id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use log::info;
|
||||
use subprocess::{Popen, PopenConfig, Redirection};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// This command is used to generate ffmpeg previews for videos.
|
||||
#[derive(Clone)]
|
||||
pub struct FfmpegGeneratePreviewImageCommand;
|
||||
|
||||
impl FfmpegGeneratePreviewImageCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self,
|
||||
input_image_filename: &str,
|
||||
output_image_filename: &str,
|
||||
output_width: u32,
|
||||
output_height: u32,
|
||||
spawn_process: bool) -> AnyhowResult<()>
|
||||
{
|
||||
let mut command = String::new();
|
||||
|
||||
let scale = format!("scale={}:{}", output_width, output_height);
|
||||
|
||||
command.push_str("echo 'test'");
|
||||
command.push_str(" && ");
|
||||
command.push_str("ffmpeg");
|
||||
command.push_str(" -i ");
|
||||
command.push_str(&input_image_filename);
|
||||
command.push_str(" -vf ");
|
||||
command.push_str(&scale);
|
||||
|
||||
command.push_str(" "); // NB: no arg flag for output filename
|
||||
command.push_str(&output_image_filename);
|
||||
|
||||
info!("Command: {:?}", command);
|
||||
|
||||
let command_parts = [
|
||||
"bash",
|
||||
"-c",
|
||||
&command
|
||||
];
|
||||
|
||||
if spawn_process {
|
||||
// NB: This forks and returns immediately.
|
||||
//let _child_pid = command_builder.spawn()?;
|
||||
|
||||
let stdout_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stdout.txt")?;
|
||||
|
||||
let stderr_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stderr.txt")?;
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
stdout: Redirection::File(stdout_file),
|
||||
stderr: Redirection::File(stderr_file),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
p.detach();
|
||||
|
||||
} else {
|
||||
// NB: This is a blocking call.
|
||||
/*let output = command_builder.output()?;
|
||||
|
||||
info!("Output status: {}", output.status);
|
||||
info!("Stdout: {:?}", String::from_utf8(output.stdout));
|
||||
error!("Stderr: {:?}", String::from_utf8(output.stderr));
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("Bad error code: {:?}", output.status);
|
||||
}*/
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
let exit_status = p.wait()?;
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use log::info;
|
||||
use subprocess::{Popen, PopenConfig, Redirection};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// This command is used to generate ffmpeg previews for videos.
|
||||
#[derive(Clone)]
|
||||
pub struct FfmpegGeneratePreviewVideoCommand;
|
||||
|
||||
impl FfmpegGeneratePreviewVideoCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self,
|
||||
input_video_filename: &str,
|
||||
output_video_filename: &str,
|
||||
output_width: u32,
|
||||
output_height: u32,
|
||||
looping: bool,
|
||||
spawn_process: bool) -> AnyhowResult<()>
|
||||
{
|
||||
let mut command = String::new();
|
||||
|
||||
let dimensions = format!("{}x{}", output_width, output_height);
|
||||
|
||||
command.push_str("echo 'test'");
|
||||
command.push_str(" && ");
|
||||
command.push_str("ffmpeg");
|
||||
command.push_str(" -i ");
|
||||
command.push_str(&input_video_filename);
|
||||
command.push_str(" -lossless 0 ");
|
||||
command.push_str(" -ss 00:00:00 ");
|
||||
command.push_str(" -t 00:00:03 ");
|
||||
command.push_str(" -s ");
|
||||
command.push_str(&dimensions);
|
||||
|
||||
if looping {
|
||||
command.push_str(" -loop 0 "); // NB: -loop 1 disables looping
|
||||
}
|
||||
|
||||
command.push_str(" "); // NB: no arg flag for output filename
|
||||
command.push_str(&output_video_filename);
|
||||
|
||||
info!("Command: {:?}", command);
|
||||
|
||||
let command_parts = [
|
||||
"bash",
|
||||
"-c",
|
||||
&command
|
||||
];
|
||||
|
||||
if spawn_process {
|
||||
// NB: This forks and returns immediately.
|
||||
//let _child_pid = command_builder.spawn()?;
|
||||
|
||||
let stdout_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stdout.txt")?;
|
||||
|
||||
let stderr_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stderr.txt")?;
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
stdout: Redirection::File(stdout_file),
|
||||
stderr: Redirection::File(stderr_file),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
p.detach();
|
||||
|
||||
} else {
|
||||
// NB: This is a blocking call.
|
||||
/*let output = command_builder.output()?;
|
||||
|
||||
info!("Output status: {}", output.status);
|
||||
info!("Stdout: {:?}", String::from_utf8(output.stdout));
|
||||
error!("Stderr: {:?}", String::from_utf8(output.stderr));
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("Bad error code: {:?}", output.status);
|
||||
}*/
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
let exit_status = p.wait()?;
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use log::info;
|
||||
use subprocess::{Popen, PopenConfig, Redirection};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// This command is used to generate ffmpeg previews for videos.
|
||||
#[derive(Clone)]
|
||||
pub struct ImagemagickGeneratePreviewImageCommand;
|
||||
|
||||
impl ImagemagickGeneratePreviewImageCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self,
|
||||
input_image_filename: &str,
|
||||
output_image_filename: &str,
|
||||
output_width: u32,
|
||||
output_height: u32,
|
||||
spawn_process: bool) -> AnyhowResult<()>
|
||||
{
|
||||
let mut command = String::new();
|
||||
|
||||
// NB: `\!` ignores aspect ratio preservation
|
||||
let dimensions = format!("{}x{}\\!", output_width, output_height);
|
||||
|
||||
// magick wizard.png -quality 50 -define webp:lossless=true wizard.webp
|
||||
command.push_str("echo 'test'");
|
||||
command.push_str(" && ");
|
||||
//command.push_str("magick");
|
||||
command.push_str("convert ");
|
||||
command.push_str(&input_image_filename);
|
||||
command.push_str(" -resize ");
|
||||
command.push_str(&dimensions);
|
||||
command.push_str(" -quality 50 ");
|
||||
command.push_str(" -define webp:loseless=false ");
|
||||
|
||||
command.push_str(" "); // NB: no arg flag for output filename
|
||||
command.push_str(&output_image_filename);
|
||||
|
||||
info!("Command: {:?}", command);
|
||||
|
||||
let command_parts = [
|
||||
"bash",
|
||||
"-c",
|
||||
&command
|
||||
];
|
||||
|
||||
if spawn_process {
|
||||
// NB: This forks and returns immediately.
|
||||
//let _child_pid = command_builder.spawn()?;
|
||||
|
||||
let stdout_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stdout.txt")?;
|
||||
|
||||
let stderr_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/ffmpeg_stderr.txt")?;
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
stdout: Redirection::File(stdout_file),
|
||||
stderr: Redirection::File(stderr_file),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
p.detach();
|
||||
|
||||
} else {
|
||||
// NB: This is a blocking call.
|
||||
/*let output = command_builder.output()?;
|
||||
|
||||
info!("Output status: {}", output.status);
|
||||
info!("Stdout: {:?}", String::from_utf8(output.stdout));
|
||||
error!("Stderr: {:?}", String::from_utf8(output.stderr));
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("Bad error code: {:?}", output.status);
|
||||
}*/
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
let exit_status = p.wait()?;
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod ffmpeg_generate_preview_image_command;
|
||||
pub mod ffmpeg_generate_preview_video_command;
|
||||
pub mod imagemagick_generate_preview_image_command;
|
||||
pub mod wav2lip_process_upload_command;
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use log::{info, warn};
|
||||
use subprocess::{ExitStatus, Popen, PopenConfig};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// The python script uses this exit code when face detection fails.
|
||||
const FACE_DETECT_FAILURE_CODE : u32 = 5;
|
||||
|
||||
/// This command is used to preprocess the face detection frames from user-submitted video.
|
||||
/// This should only ever need to run once. The frames can then be uploaded to Buckets and saved.
|
||||
#[derive(Clone)]
|
||||
pub struct Wav2LipPreprocessClient {
|
||||
w2l_directory: String,
|
||||
script_name: String,
|
||||
checkpoint_path: String,
|
||||
}
|
||||
|
||||
/// Command failure
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Wav2LipPreprocessError {
|
||||
/// Permanent failure
|
||||
NoFacesDetected,
|
||||
/// Unknown. Retry allowed.
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
impl Error for Wav2LipPreprocessError {}
|
||||
|
||||
impl fmt::Display for Wav2LipPreprocessError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Wav2LipPreprocessError::NoFacesDetected => write!(f, "NoFacesDetected"),
|
||||
Wav2LipPreprocessError::UnknownError => write!(f, "UnknownError"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Wav2LipPreprocessClient {
|
||||
pub fn new(
|
||||
w2l_directory: &str,
|
||||
script_name: &str,
|
||||
checkpoint_path: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
w2l_directory: w2l_directory.to_string(),
|
||||
script_name: script_name.to_string(),
|
||||
checkpoint_path: checkpoint_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self,
|
||||
image_or_video_filename: &str,
|
||||
output_cached_faces_filename: &str,
|
||||
output_metadata_filename: &str,
|
||||
is_image: bool,
|
||||
spawn_process: bool) -> Result<(), Wav2LipPreprocessError>
|
||||
{
|
||||
let mut command = String::new();
|
||||
|
||||
command.push_str("echo 'test'");
|
||||
command.push_str(" && ");
|
||||
command.push_str(&format!("cd {}", self.w2l_directory));
|
||||
command.push_str(" && ");
|
||||
command.push_str("source python/bin/activate");
|
||||
command.push_str(" && ");
|
||||
command.push_str("python ");
|
||||
command.push_str(&self.script_name);
|
||||
command.push_str(" --checkpoint_path ");
|
||||
command.push_str(&self.checkpoint_path);
|
||||
command.push_str(" --image_or_video_filename ");
|
||||
command.push_str(image_or_video_filename);
|
||||
command.push_str(" --output_cached_faces_filename ");
|
||||
command.push_str(output_cached_faces_filename);
|
||||
command.push_str(" --output_metadata_filename ");
|
||||
command.push_str(output_metadata_filename);
|
||||
|
||||
if is_image {
|
||||
command.push_str(" --is_image ");
|
||||
}
|
||||
|
||||
info!("Command: {:?}", command);
|
||||
|
||||
// NB: Got rid of the previous (unused) child process spawning
|
||||
|
||||
let exit_status = match self.do_execute(&command) {
|
||||
Err(e) => {
|
||||
warn!("Unknown execution error: {:?}", e);
|
||||
return Err(Wav2LipPreprocessError::UnknownError);
|
||||
},
|
||||
Ok(exit_status) => exit_status,
|
||||
};
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
|
||||
if !exit_status.success() {
|
||||
if let ExitStatus::Exited(code) = exit_status {
|
||||
if code == FACE_DETECT_FAILURE_CODE {
|
||||
// We want to permanently fail retries.
|
||||
warn!("Failure to detect faces error code returned. This is a permanent failure.");
|
||||
return Err(Wav2LipPreprocessError::NoFacesDetected);
|
||||
}
|
||||
}
|
||||
warn!("Unknown failure reason.");
|
||||
return Err(Wav2LipPreprocessError::UnknownError);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_execute(&self, command: &str) -> AnyhowResult<ExitStatus> {
|
||||
let command_parts = [
|
||||
"bash",
|
||||
"-c",
|
||||
command
|
||||
];
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
let exit_status = p.wait()?;
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
[package]
|
||||
name = "w2l-inference-job"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "w2l-inference-job"
|
||||
path = "src/main.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
bucket_paths.workspace = true
|
||||
cloud_storage.workspace = true
|
||||
config = { path = "../../../../lib/deprecated/config" }
|
||||
easyenv = { path = "../../../../lib/easyenv" }
|
||||
errors.workspace = true
|
||||
filesys.workspace = true
|
||||
google_drive_downloader = { path = "../../../../api_clients/google_drive_downloader" }
|
||||
jobs_common = { path = "../../../../lib/jobs_common" }
|
||||
mysql_queries = { path = "../../../../schema/database/mysql_queries" }
|
||||
shared_env_var_config.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
dotenv.workspace = true
|
||||
hostname.workspace = true
|
||||
log.workspace = true
|
||||
subprocess = "0.2.9"
|
||||
tempdir.workspace = true
|
||||
|
||||
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] }
|
||||
|
||||
# Redis
|
||||
r2d2_redis.workspace = true
|
||||
|
||||
# Runtime
|
||||
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
|
||||
tokio.workspace = true
|
||||
|
||||
# NB: Hyper is just for HTTP client use in jobs. I'd use Actix client, but I need tokio runtime
|
||||
# NB: Furthermore, the Tokio runtime is old, so I have to use an older Hyper.
|
||||
hyper = { version = "1.3.1" }
|
||||
|
||||
# Serialization
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -1,662 +0,0 @@
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)]
|
||||
//#![forbid(warnings)]
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unreachable_patterns)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
use std::fs::{File, metadata};
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use log::{info, warn};
|
||||
use r2d2_redis::r2d2;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::MySqlPool;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use bucket_paths::legacy::old_bespoke_paths::bucket_path_unifier::BucketPathUnifier;
|
||||
use config::common_env::CommonEnv;
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::check_directory_exists::check_directory_exists;
|
||||
use filesys::check_file_exists::check_file_exists;
|
||||
use filesys::file_deletion::safe_delete_directory::safe_delete_directory;
|
||||
use filesys::file_deletion::safe_delete_file::safe_delete_file;
|
||||
use google_drive_downloader::google_drive_download_command::GoogleDriveDownloadCommand;
|
||||
use jobs_common::noop_logger::NoOpLogger;
|
||||
use jobs_common::redis_job_status_logger::RedisJobStatusLogger;
|
||||
use jobs_common::semi_persistent_cache_dir::SemiPersistentCacheDir;
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::get_w2l_template_by_token;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::grab_job_lock_and_mark_pending;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::insert_w2l_result;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::mark_w2l_inference_job_done;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::mark_w2l_inference_job_failure;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::query_w2l_inference_job_records;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::w2l_inference_job_queries::W2lInferenceJobRecord;
|
||||
|
||||
use crate::script_execution::wav2lip_inference_command::Wav2LipInferenceCommand;
|
||||
|
||||
mod script_execution;
|
||||
|
||||
// Buckets (shared config)
|
||||
const ENV_ACCESS_KEY : &str = "ACCESS_KEY";
|
||||
const ENV_SECRET_KEY : &str = "SECRET_KEY";
|
||||
const ENV_REGION_NAME : &str = "REGION_NAME";
|
||||
|
||||
// Buckets (private data)
|
||||
const ENV_PRIVATE_BUCKET_NAME : &str = "W2L_PRIVATE_DOWNLOAD_BUCKET_NAME";
|
||||
// Buckets (public data)
|
||||
const ENV_PUBLIC_BUCKET_NAME : &str = "W2L_PUBLIC_DOWNLOAD_BUCKET_NAME";
|
||||
|
||||
// Where models and other assets get downloaded to.
|
||||
const ENV_SEMIPERSISTENT_CACHE_DIR : &str = "SEMIPERSISTENT_CACHE_DIR";
|
||||
|
||||
// Python code
|
||||
const ENV_CODE_DIRECTORY : &str = "W2L_CODE_DIRECTORY";
|
||||
const ENV_MODEL_CHECKPOINT : &str = "W2L_MODEL_CHECKPOINT";
|
||||
const ENV_INFERENCE_SCRIPT_NAME : &str = "W2L_INFERENCE_SCRIPT_NAME";
|
||||
|
||||
const DEFAULT_TEMP_DIR: &str = "/tmp";
|
||||
|
||||
struct Inferencer {
|
||||
pub download_temp_directory: PathBuf,
|
||||
pub mysql_pool: MySqlPool,
|
||||
|
||||
pub redis_pool: r2d2::Pool<RedisConnectionManager>,
|
||||
|
||||
pub private_bucket_client: BucketClient,
|
||||
pub public_bucket_client: BucketClient,
|
||||
|
||||
pub firehose_publisher: FirehosePublisher,
|
||||
|
||||
pub bucket_path_unifier: BucketPathUnifier,
|
||||
pub semi_persistent_cache: SemiPersistentCacheDir,
|
||||
|
||||
pub google_drive_downloader: GoogleDriveDownloadCommand,
|
||||
pub w2l_inference: Wav2LipInferenceCommand,
|
||||
//pub ffmpeg_image_preview_generator: FfmpegGeneratePreviewImageCommand,
|
||||
//pub ffmpeg_video_preview_generator: FfmpegGeneratePreviewVideoCommand,
|
||||
//pub imagemagick_image_preview_generator: ImagemagickGeneratePreviewImageCommand,
|
||||
|
||||
// Command to run
|
||||
pub inference_script: String,
|
||||
pub w2l_model_filename: String,
|
||||
pub w2l_end_bump_filename: String,
|
||||
|
||||
// Sleep between batches
|
||||
pub job_batch_wait_millis: u64,
|
||||
|
||||
// How long to wait between log lines
|
||||
pub no_op_logger_millis: u64,
|
||||
|
||||
// Max job attempts before failure.
|
||||
// NB: This is an i32 so we don't need to convert to db column type.
|
||||
pub job_max_attempts: i32,
|
||||
|
||||
// Temporary for debugging
|
||||
// Arbitrary timeouts can be inserted so we can exec in and poke around.
|
||||
pub debug_job_end_sleep_millis: u64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnyhowResult<()> {
|
||||
easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG));
|
||||
|
||||
// NB: Do not check this secrets-containing dotenv file into VCS.
|
||||
// This file should only contain *development* secrets, never production.
|
||||
let _ = dotenv::from_filename(".env-secrets").ok();
|
||||
|
||||
info!("Obtaining hostname...");
|
||||
|
||||
let server_hostname = hostname::get()
|
||||
.ok()
|
||||
.and_then(|h| h.into_string().ok())
|
||||
.unwrap_or("w2l-inference-job".to_string());
|
||||
|
||||
info!("Hostname: {}", &server_hostname);
|
||||
|
||||
// Bucket stuff (shared)
|
||||
let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?;
|
||||
let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?;
|
||||
let region_name = easyenv::get_env_string_required(ENV_REGION_NAME)?;
|
||||
|
||||
// Private and Public Buckets
|
||||
let private_bucket_name = easyenv::get_env_string_required(ENV_PRIVATE_BUCKET_NAME)?;
|
||||
let public_bucket_name = easyenv::get_env_string_required(ENV_PUBLIC_BUCKET_NAME)?;
|
||||
|
||||
let s3_compatible_endpoint_url = easyenv::get_env_string_or_default(
|
||||
"S3_COMPATIBLE_ENDPOINT_URL",
|
||||
"https://storage.googleapis.com");
|
||||
let bucket_timeout = easyenv::get_env_duration_seconds_or_default("BUCKET_TIMEOUT_SECONDS",
|
||||
Duration::from_secs(60 * 5));
|
||||
|
||||
let private_bucket_client = BucketClient::create(
|
||||
&access_key,
|
||||
&secret_key,
|
||||
®ion_name,
|
||||
&private_bucket_name,
|
||||
&s3_compatible_endpoint_url,
|
||||
None,
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
let public_bucket_client = BucketClient::create(
|
||||
&access_key,
|
||||
&secret_key,
|
||||
®ion_name,
|
||||
&public_bucket_name,
|
||||
&s3_compatible_endpoint_url,
|
||||
None,
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
let py_code_directory = easyenv::get_env_string_required(ENV_CODE_DIRECTORY)?;
|
||||
let py_script_name = easyenv::get_env_string_required(ENV_INFERENCE_SCRIPT_NAME)?;
|
||||
let py_model_checkpoint = easyenv::get_env_string_required(ENV_MODEL_CHECKPOINT)?;
|
||||
|
||||
let w2l_inference_command = Wav2LipInferenceCommand::new(
|
||||
&py_code_directory,
|
||||
&py_script_name,
|
||||
&py_model_checkpoint,
|
||||
);
|
||||
|
||||
let temp_directory = easyenv::get_env_string_or_default(
|
||||
"DOWNLOAD_TEMP_DIR",
|
||||
DEFAULT_TEMP_DIR);
|
||||
|
||||
// TODO: In the future, we may want to enable downloading images or audio files.
|
||||
let download_script = easyenv::get_env_string_or_default(
|
||||
"DOWNLOAD_SCRIPT",
|
||||
"./scripts/download_internet_file.py");
|
||||
let google_drive_downloader =
|
||||
GoogleDriveDownloadCommand::new_production(&download_script);
|
||||
|
||||
let temp_directory = PathBuf::from(temp_directory);
|
||||
|
||||
check_directory_exists(&temp_directory)?;
|
||||
|
||||
let db_connection_string = env_get_mysql_connection_string_or_default();
|
||||
|
||||
info!("Connecting to database...");
|
||||
|
||||
let mysql_pool = MySqlPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_connection_string)
|
||||
.await?;
|
||||
|
||||
let common_env = CommonEnv::read_from_env()?;
|
||||
|
||||
info!("Connecting to redis...");
|
||||
|
||||
let redis_connection_string = env_get_redis_0_connection_string_or_default();
|
||||
let redis_manager = RedisConnectionManager::new(redis_connection_string.as_str())?;
|
||||
|
||||
let redis_pool = r2d2::Pool::builder()
|
||||
.build(redis_manager)?;
|
||||
|
||||
let inference_script = "TODO".to_string();
|
||||
|
||||
let persistent_cache_path = easyenv::get_env_string_or_default(
|
||||
ENV_SEMIPERSISTENT_CACHE_DIR,
|
||||
"/tmp");
|
||||
|
||||
let semi_persistent_cache = SemiPersistentCacheDir::configured_root(&persistent_cache_path);
|
||||
|
||||
info!("Creating pod semi-persistent cache dirs...");
|
||||
semi_persistent_cache.create_w2l_model_path()?;
|
||||
semi_persistent_cache.create_w2l_end_bump_path()?;
|
||||
semi_persistent_cache.create_w2l_face_template_path()?;
|
||||
semi_persistent_cache.create_w2l_template_media_path()?;
|
||||
|
||||
let w2l_model_filename = easyenv::get_env_string_or_default(
|
||||
"W2L_MODEL_FILENAME", "wav2lip_gan.pth");
|
||||
|
||||
let w2l_end_bump_filename = easyenv::get_env_string_or_default(
|
||||
"W2L_END_BUMP_FILENAME", "vocodes-short-end-bump.mp4");
|
||||
|
||||
let firehose_publisher = FirehosePublisher {
|
||||
mysql_pool: mysql_pool.clone(), // NB: MySqlPool is clone/send/sync safe
|
||||
};
|
||||
|
||||
let common_env = CommonEnv::read_from_env()?;
|
||||
|
||||
let inferencer = Inferencer {
|
||||
download_temp_directory: temp_directory,
|
||||
mysql_pool,
|
||||
redis_pool,
|
||||
public_bucket_client,
|
||||
private_bucket_client,
|
||||
inference_script,
|
||||
google_drive_downloader,
|
||||
w2l_inference: w2l_inference_command,
|
||||
bucket_path_unifier: BucketPathUnifier::default_paths(),
|
||||
semi_persistent_cache,
|
||||
firehose_publisher,
|
||||
w2l_model_filename,
|
||||
w2l_end_bump_filename,
|
||||
job_batch_wait_millis: common_env.job_batch_wait_millis,
|
||||
job_max_attempts: common_env.job_max_attempts as i32,
|
||||
no_op_logger_millis: common_env.no_op_logger_millis,
|
||||
debug_job_end_sleep_millis: common_env.debug_job_end_sleep_millis,
|
||||
};
|
||||
|
||||
main_loop(inferencer).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const START_TIMEOUT_MILLIS : u64 = 500;
|
||||
const INCREASE_TIMEOUT_MILLIS : u64 = 1000;
|
||||
|
||||
async fn main_loop(inferencer: Inferencer) {
|
||||
let mut error_timeout_millis = START_TIMEOUT_MILLIS;
|
||||
|
||||
let mut noop_logger = NoOpLogger::new(inferencer.no_op_logger_millis as i64);
|
||||
|
||||
loop {
|
||||
let num_records = 1;
|
||||
|
||||
let query_result = query_w2l_inference_job_records(
|
||||
&inferencer.mysql_pool,
|
||||
num_records)
|
||||
.await;
|
||||
|
||||
let jobs = match query_result {
|
||||
Ok(jobs) => jobs,
|
||||
Err(e) => {
|
||||
warn!("Error querying jobs: {:?}", e);
|
||||
std::thread::sleep(Duration::from_millis(error_timeout_millis));
|
||||
error_timeout_millis += INCREASE_TIMEOUT_MILLIS;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if jobs.is_empty() {
|
||||
noop_logger.log_after_awhile();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(inferencer.job_batch_wait_millis));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = process_jobs(&inferencer, jobs).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
warn!("Error querying jobs: {:?}", e);
|
||||
std::thread::sleep(Duration::from_millis(error_timeout_millis));
|
||||
error_timeout_millis += INCREASE_TIMEOUT_MILLIS;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
error_timeout_millis = START_TIMEOUT_MILLIS; // reset
|
||||
|
||||
std::thread::sleep(Duration::from_millis(inferencer.job_batch_wait_millis));
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_jobs(inferencer: &Inferencer, jobs: Vec<W2lInferenceJobRecord>) -> AnyhowResult<()> {
|
||||
for job in jobs.into_iter() {
|
||||
let result = process_job(inferencer, &job).await;
|
||||
match result {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
warn!("Failure to process job: {:?}", e);
|
||||
let failure_reason = "";
|
||||
let _r = mark_w2l_inference_job_failure(
|
||||
&inferencer.mysql_pool,
|
||||
&job,
|
||||
failure_reason,
|
||||
inferencer.job_max_attempts
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FileMetadata {
|
||||
pub is_video: bool,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
//pub num_frames: u64,
|
||||
//pub fps: Option<f32>,
|
||||
pub duration_millis: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
pub file_size_bytes: u64,
|
||||
}
|
||||
|
||||
fn read_metadata_file(filename: &PathBuf) -> AnyhowResult<FileMetadata> {
|
||||
let mut file = File::open(filename)?;
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer)?;
|
||||
Ok(serde_json::from_str(&buffer)?)
|
||||
}
|
||||
|
||||
async fn process_job(inferencer: &Inferencer, job: &W2lInferenceJobRecord) -> AnyhowResult<()> {
|
||||
|
||||
// TODO 1. Mark Processing
|
||||
//
|
||||
// TODO: 2. Check if w2l model is downloaded / download it to a stable cache location (DONE)
|
||||
// TODO: 3. Check if w2l template faces are downloaded and download it (done)
|
||||
// TODO: 4. Download user audio (done)
|
||||
|
||||
// TODO: 5. Process Inference
|
||||
|
||||
// TODO 6. Upload result
|
||||
// TODO 7. Save record
|
||||
// TODO 8. Mark job done
|
||||
|
||||
let mut redis = inferencer.redis_pool.get()?;
|
||||
let mut redis_logger = RedisJobStatusLogger::new_w2l_inference(
|
||||
&mut redis,
|
||||
&job.inference_job_token);
|
||||
|
||||
// ==================== ATTEMPT TO GRAB JOB LOCK ==================== //
|
||||
|
||||
let lock_acquired = grab_job_lock_and_mark_pending(&inferencer.mysql_pool, job).await?;
|
||||
|
||||
if !lock_acquired {
|
||||
warn!("Could not acquire job lock for: {}", &job.id);
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// ==================== CONFIRM OR DOWNLOAD W2L MODEL ==================== //
|
||||
|
||||
let model_filename = inferencer.w2l_model_filename.clone();
|
||||
let model_fs_path = inferencer.semi_persistent_cache.w2l_model_path(&model_filename);
|
||||
|
||||
if !model_fs_path.exists() {
|
||||
warn!("Model file does not exist: {:?}", &model_fs_path);
|
||||
|
||||
redis_logger.log_status("downloading model")?;
|
||||
|
||||
let model_object_path = inferencer.bucket_path_unifier
|
||||
.w2l_pretrained_models_path(&model_filename);
|
||||
|
||||
info!("Download from bucket path: {:?}", &model_object_path);
|
||||
|
||||
inferencer.private_bucket_client.download_file_to_disk(
|
||||
&model_object_path,
|
||||
&model_fs_path
|
||||
).await?;
|
||||
|
||||
info!("Downloaded model from bucket!");
|
||||
}
|
||||
|
||||
// ==================== CONFIRM OR DOWNLOAD W2L END BUMP ==================== //
|
||||
|
||||
let end_bump_filename = inferencer.w2l_end_bump_filename.clone();
|
||||
let end_bump_fs_path = inferencer.semi_persistent_cache.w2l_end_bump_path(&end_bump_filename);
|
||||
|
||||
if !end_bump_fs_path.exists() {
|
||||
warn!("End bump file does not exist: {:?}", &end_bump_fs_path);
|
||||
|
||||
redis_logger.log_status("downloading assets")?;
|
||||
|
||||
let end_bump_object_path = inferencer.bucket_path_unifier
|
||||
.end_bump_video_for_w2l_path(&end_bump_filename);
|
||||
|
||||
info!("Download from bucket path: {:?}", &end_bump_object_path);
|
||||
|
||||
inferencer.private_bucket_client.download_file_to_disk(
|
||||
&end_bump_object_path,
|
||||
&end_bump_fs_path
|
||||
).await?;
|
||||
|
||||
info!("Downloaded end bump from bucket!");
|
||||
}
|
||||
|
||||
// ==================== LOOK UP TEMPLATE RECORD ==================== //
|
||||
|
||||
let template_token = match &job.maybe_w2l_template_token {
|
||||
Some(token) => token.to_string(),
|
||||
None => {
|
||||
warn!("non-template token based inference not yet supported");
|
||||
return Err(anyhow!("non-template token based inference not yet supported"))
|
||||
},
|
||||
};
|
||||
|
||||
info!("Looking up w2l template by token: {}", &template_token);
|
||||
|
||||
let query_result = get_w2l_template_by_token(&inferencer.mysql_pool, &template_token).await?;
|
||||
|
||||
let w2l_template = match query_result {
|
||||
Some(template) => template,
|
||||
None => {
|
||||
warn!("W2L Template not found: {}", &template_token);
|
||||
return Err(anyhow!("Template not found!"))
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== CONFIRM OR DOWNLOAD W2L TEMPLATE AUDIO OR VIDEO ==================== //
|
||||
|
||||
// Template is based on the `private_bucket_hash`:
|
||||
// - private_bucket_hash: 1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2
|
||||
// - private_bucket_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2
|
||||
// - private_bucket_cached_faces_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2_detected_faces.pickle
|
||||
// - maybe_public_bucket_preview_image_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2_preview.webp
|
||||
|
||||
let template_media_fs_path = inferencer.semi_persistent_cache.w2l_template_media_path(
|
||||
&w2l_template.private_bucket_hash);
|
||||
|
||||
if !template_media_fs_path.exists() {
|
||||
info!("W2L template media file does not exist: {:?}", &template_media_fs_path);
|
||||
|
||||
redis_logger.log_status("downloading template")?;
|
||||
|
||||
let template_media_object_path = inferencer.bucket_path_unifier
|
||||
.media_templates_for_w2l_path(&w2l_template.private_bucket_hash);
|
||||
|
||||
info!("Download from template media path: {:?}", &template_media_object_path);
|
||||
|
||||
inferencer.private_bucket_client.download_file_to_disk(
|
||||
&template_media_object_path,
|
||||
&template_media_fs_path
|
||||
).await?;
|
||||
|
||||
info!("Downloaded template media from bucket!");
|
||||
}
|
||||
|
||||
// ==================== CONFIRM OR DOWNLOAD W2L TEMPLATE FACE ==================== //
|
||||
|
||||
// Template is based on the `private_bucket_hash`:
|
||||
// - private_bucket_hash: 1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2
|
||||
// - private_bucket_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2
|
||||
// - private_bucket_cached_faces_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2_detected_faces.pickle
|
||||
// - maybe_public_bucket_preview_image_object_name: /user_uploaded_w2l_templates/1/5/1/1519edf86e6975fdcd0a56a5953d84948db79f2b9ce588818d7fa544d5cb38b2_preview.webp
|
||||
|
||||
let face_template_fs_path = inferencer.semi_persistent_cache.w2l_face_template_path(
|
||||
&w2l_template.private_bucket_hash);
|
||||
|
||||
if !face_template_fs_path.exists() {
|
||||
info!("W2L face template file does not exist: {:?}", &face_template_fs_path);
|
||||
|
||||
redis_logger.log_status("downloading template metadata")?;
|
||||
|
||||
let face_template_object_path = inferencer.bucket_path_unifier
|
||||
.precomputed_faces_for_w2l_path(&w2l_template.private_bucket_hash);
|
||||
|
||||
info!("Download from face template path: {:?}", &face_template_object_path);
|
||||
|
||||
inferencer.private_bucket_client.download_file_to_disk(
|
||||
&face_template_object_path,
|
||||
&face_template_fs_path
|
||||
).await?;
|
||||
|
||||
info!("Downloaded face template from bucket!");
|
||||
}
|
||||
|
||||
// ==================== DOWNLOAD USER AUDIO ==================== //
|
||||
|
||||
redis_logger.log_status("downloading audio")?;
|
||||
|
||||
let temp_dir = format!("temp_{}", job.id);
|
||||
let temp_dir = TempDir::new(&temp_dir)?; // NB: Exists until it goes out of scope.
|
||||
|
||||
let audio_bucket_hash = match &job.maybe_public_audio_bucket_hash {
|
||||
Some(l) => l.clone(),
|
||||
None => {
|
||||
warn!("Only W2L jobs with user-uploaded audio are supported right now");
|
||||
return Err(anyhow!("Only W2L jobs with user-uploaded audio are supported right now"))
|
||||
},
|
||||
};
|
||||
|
||||
let audio_fs_path = temp_dir.path().join(&audio_bucket_hash);
|
||||
|
||||
let audio_object_path = inferencer.bucket_path_unifier
|
||||
.user_audio_for_w2l_inference_path(&audio_bucket_hash);
|
||||
|
||||
inferencer.private_bucket_client.download_file_to_disk(
|
||||
&audio_object_path,
|
||||
&audio_fs_path
|
||||
).await?;
|
||||
|
||||
|
||||
// ==================== RUN INFERENCE ==================== //
|
||||
|
||||
redis_logger.log_status("executing")?;
|
||||
|
||||
let output_video_fs_path = temp_dir.path().join("output.mp4");
|
||||
let output_metadata_fs_path = temp_dir.path().join("metadata.json");
|
||||
|
||||
let is_image = w2l_template.template_type.contains("image");
|
||||
|
||||
info!("Is image? {}", is_image);
|
||||
info!("Running W2L inference...");
|
||||
|
||||
let inference_result = inferencer.w2l_inference.execute(
|
||||
&model_fs_path,
|
||||
&audio_fs_path,
|
||||
&end_bump_fs_path,
|
||||
&template_media_fs_path,
|
||||
&face_template_fs_path,
|
||||
&output_metadata_fs_path,
|
||||
&output_video_fs_path,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if let Err(e) = inference_result {
|
||||
safe_delete_file(&audio_fs_path);
|
||||
safe_delete_file(&output_video_fs_path);
|
||||
safe_delete_directory(&temp_dir);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
info!("Output filename: {:?}", &output_video_fs_path);
|
||||
|
||||
// ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== //
|
||||
|
||||
info!("Checking that output files exist...");
|
||||
|
||||
check_file_exists(&output_video_fs_path)?;
|
||||
check_file_exists(&output_metadata_fs_path)?;
|
||||
|
||||
let file_metadata = read_metadata_file(&output_metadata_fs_path)?;
|
||||
|
||||
safe_delete_file(&output_metadata_fs_path);
|
||||
|
||||
// ==================== UPLOAD TO BUCKETS ==================== //
|
||||
|
||||
redis_logger.log_status("uploading result")?;
|
||||
|
||||
let result_object_path = inferencer.bucket_path_unifier.w2l_inference_video_output_path(
|
||||
&job.inference_job_token);
|
||||
|
||||
info!("Image/video destination bucket path: {:?}", &result_object_path);
|
||||
|
||||
info!("Uploading image/video...");
|
||||
|
||||
let original_mime_type = file_metadata.mimetype
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
|
||||
let upload_result = inferencer.public_bucket_client.upload_filename_with_content_type(
|
||||
&result_object_path,
|
||||
&output_video_fs_path,
|
||||
original_mime_type)
|
||||
.await;
|
||||
|
||||
if let Err(e) = upload_result {
|
||||
safe_delete_file(&audio_fs_path);
|
||||
safe_delete_file(&output_video_fs_path);
|
||||
safe_delete_directory(&temp_dir);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
safe_delete_file(&output_video_fs_path);
|
||||
|
||||
// ==================== DELETE DOWNLOADED FILE ==================== //
|
||||
|
||||
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
|
||||
safe_delete_directory(&temp_dir);
|
||||
|
||||
// ==================== SAVE RECORDS ==================== //
|
||||
|
||||
info!("Saving w2l inference record...");
|
||||
let (id, inference_result_token) = insert_w2l_result(
|
||||
&inferencer.mysql_pool,
|
||||
job,
|
||||
&result_object_path,
|
||||
file_metadata.file_size_bytes,
|
||||
file_metadata.mimetype.as_deref(),
|
||||
file_metadata.width,
|
||||
file_metadata.height,
|
||||
file_metadata.duration_millis.unwrap_or(0))
|
||||
.await?;
|
||||
|
||||
info!("Marking job complete...");
|
||||
|
||||
mark_w2l_inference_job_done(
|
||||
&inferencer.mysql_pool,
|
||||
job,
|
||||
true,
|
||||
Some(&inference_result_token)
|
||||
).await?;
|
||||
|
||||
inferencer.firehose_publisher.w2l_inference_finished(
|
||||
job.maybe_creator_user_token.as_deref(),
|
||||
&job.inference_job_token,
|
||||
&inference_result_token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error publishing event: {:?}", e);
|
||||
anyhow!("error publishing event")
|
||||
})?;
|
||||
|
||||
redis_logger.log_status("done")?;
|
||||
|
||||
if inferencer.debug_job_end_sleep_millis != 0 {
|
||||
warn!("Debug sleep after job end: {} ms", inferencer.debug_job_end_sleep_millis);
|
||||
thread::sleep(Duration::from_millis(inferencer.debug_job_end_sleep_millis));
|
||||
}
|
||||
|
||||
info!("Job {} complete success! Downloaded, ran inference, and uploaded. Saved model record: {}",
|
||||
job.id, id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod wav2lip_inference_command;
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
use log::info;
|
||||
use subprocess::{Popen, PopenConfig, Redirection};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// This command is used to run inference.
|
||||
/// It uses preprocessed face files so that it's much faster.
|
||||
#[derive(Clone)]
|
||||
pub struct Wav2LipInferenceCommand {
|
||||
w2l_directory: String,
|
||||
script_name: String,
|
||||
checkpoint_path: String,
|
||||
}
|
||||
|
||||
impl Wav2LipInferenceCommand {
|
||||
pub fn new(
|
||||
w2l_directory: &str,
|
||||
script_name: &str,
|
||||
checkpoint_path: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
w2l_directory: w2l_directory.to_string(),
|
||||
script_name: script_name.to_string(),
|
||||
checkpoint_path: checkpoint_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute<P: AsRef<Path>>(
|
||||
&self,
|
||||
checkpoint_path: P,
|
||||
audio_filename: P,
|
||||
end_bump_video_filename: P,
|
||||
media_template_filename: P,
|
||||
cached_faces_filename: P,
|
||||
output_metadata_filename: P,
|
||||
output_video_filename: P,
|
||||
is_image: bool,
|
||||
spawn_process: bool
|
||||
) -> AnyhowResult<()> {
|
||||
let mut command = String::new();
|
||||
|
||||
command.push_str("echo 'test'");
|
||||
command.push_str(" && ");
|
||||
command.push_str(&format!("cd {}", self.w2l_directory));
|
||||
command.push_str(" && ");
|
||||
command.push_str("source python/bin/activate");
|
||||
command.push_str(" && ");
|
||||
command.push_str("python ");
|
||||
command.push_str(&self.script_name);
|
||||
command.push_str(" --checkpoint_path ");
|
||||
command.push_str(&checkpoint_path.as_ref().display().to_string());
|
||||
command.push_str(" --image_or_video_filename ");
|
||||
command.push_str(&media_template_filename.as_ref().display().to_string());
|
||||
command.push_str(" --audio_filename ");
|
||||
command.push_str(&audio_filename.as_ref().display().to_string());
|
||||
command.push_str(" --end_bump_file ");
|
||||
command.push_str(&end_bump_video_filename.as_ref().display().to_string());
|
||||
command.push_str(" --cached_faces_filename ");
|
||||
command.push_str(&cached_faces_filename.as_ref().display().to_string());
|
||||
command.push_str(" --output_video_filename ");
|
||||
command.push_str(&output_video_filename.as_ref().display().to_string());
|
||||
command.push_str(" --output_metadata_filename ");
|
||||
command.push_str(&output_metadata_filename.as_ref().display().to_string());
|
||||
|
||||
if is_image {
|
||||
command.push_str(" --is_image ");
|
||||
}
|
||||
|
||||
info!("Command: {:?}", command);
|
||||
|
||||
let command_parts = [
|
||||
"bash",
|
||||
"-c",
|
||||
&command
|
||||
];
|
||||
|
||||
if spawn_process {
|
||||
// NB: This forks and returns immediately.
|
||||
//let _child_pid = command_builder.spawn()?;
|
||||
|
||||
let stdout_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/wav2lip_upload_stdout.txt")?;
|
||||
|
||||
let stderr_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("/tmp/wav2lip_upload_stderr.txt")?;
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
stdout: Redirection::File(stdout_file),
|
||||
stderr: Redirection::File(stderr_file),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
p.detach();
|
||||
|
||||
} else {
|
||||
// NB: This is a blocking call.
|
||||
/*let output = command_builder.output()?;
|
||||
|
||||
info!("Output status: {}", output.status);
|
||||
info!("Stdout: {:?}", String::from_utf8(output.stdout));
|
||||
error!("Stderr: {:?}", String::from_utf8(output.stderr));
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("Bad error code: {:?}", output.status);
|
||||
}*/
|
||||
|
||||
let mut p = Popen::create(&command_parts, PopenConfig {
|
||||
//stdout: Redirection::Pipe,
|
||||
//stderr: Redirection::Pipe,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
info!("Pid : {:?}", p.pid());
|
||||
|
||||
let exit_status = p.wait()?;
|
||||
|
||||
info!("Exit status: {:?}", exit_status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+7
-43
@@ -17,7 +17,6 @@ use crate::http_server::common_responses::user_avatars::default_avatar_from_user
|
||||
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
|
||||
use errors::AnyhowResult;
|
||||
use mysql_queries::queries::tts::stats::calculate_tts_model_leaderboard::calculate_tts_model_leaderboard;
|
||||
use mysql_queries::queries::w2l::stats::calculate_w2l_template_leaderboard::calculate_w2l_template_leaderboard;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
|
||||
@@ -154,7 +153,7 @@ pub async fn leaderboard_handler(
|
||||
let response = LeaderboardResponse {
|
||||
success: true,
|
||||
tts_leaderboard: leaderboard_info.tts_leaderboard,
|
||||
w2l_leaderboard: leaderboard_info.w2l_leaderboard,
|
||||
w2l_leaderboard: vec![],
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
@@ -168,58 +167,24 @@ pub async fn leaderboard_handler(
|
||||
#[derive(Clone)]
|
||||
pub struct LeaderboardInfo {
|
||||
tts_leaderboard: Vec<LeaderboardRow>,
|
||||
w2l_leaderboard: Vec<LeaderboardRow>,
|
||||
}
|
||||
|
||||
async fn query_leaderboard(mysql_pool: &MySqlPool) -> AnyhowResult<LeaderboardInfo> {
|
||||
// TODO: There has to be a better way of doing this in parallel.
|
||||
// Some more intelligent DB connection pool. (What did jOOQ in Java do? Surely not this insanity!)
|
||||
let mysql_connection_1 = mysql_pool.acquire();
|
||||
let mysql_connection_2 = mysql_pool.acquire();
|
||||
let mut mysql_connection = mysql_pool.acquire().await?;
|
||||
|
||||
let mut mysql_connection_1 = mysql_connection_1.await?;
|
||||
let mut mysql_connection_2 = mysql_connection_2.await?;
|
||||
|
||||
let maybe_tts_results =
|
||||
calculate_tts_model_leaderboard(&mut mysql_connection_1);
|
||||
|
||||
let maybe_w2l_results =
|
||||
calculate_w2l_template_leaderboard(&mut mysql_connection_2);
|
||||
|
||||
let tts_results = maybe_tts_results
|
||||
let tts_results = calculate_tts_model_leaderboard(&mut mysql_connection)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| LeaderboardRow {
|
||||
creator_user_token: record.creator_user_token.clone(), // NB: Cloned because of ref use
|
||||
username: record.username.to_string(), // NB: Cloned because of ref use for avatar below
|
||||
display_name: record.display_name.to_string(), // Cloned because of `UserDetailsLight`
|
||||
creator_user_token: record.creator_user_token.clone(),
|
||||
username: record.username.to_string(),
|
||||
display_name: record.display_name.to_string(),
|
||||
gravatar_hash: record.gravatar_hash.to_string(),
|
||||
default_avatar_index: default_avatar_from_username(&record.username),
|
||||
default_avatar_color_index: default_avatar_color_from_username(&record.username),
|
||||
user: UserDetailsLight {
|
||||
user_token: UserToken::new_from_str(&record.creator_user_token),
|
||||
username: record.username.to_string(), // NB: Cloned because of ref use for avatar below
|
||||
display_name: record.display_name,
|
||||
gravatar_hash: record.gravatar_hash,
|
||||
default_avatar: UserDefaultAvatarInfo::from_username(&record.username),
|
||||
},
|
||||
uploaded_count: record.uploaded_count,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let w2l_results = maybe_w2l_results
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| LeaderboardRow {
|
||||
creator_user_token: record.creator_user_token.clone(), // NB: Cloned because of ref use
|
||||
username: record.username.to_string(), // NB: Cloned because of ref use for avatar below
|
||||
display_name: record.display_name.clone(), // Cloned because of ref use.
|
||||
gravatar_hash: record.gravatar_hash.clone(), // Cloned because of ref use.
|
||||
default_avatar_index: default_avatar_from_username(&record.username),
|
||||
default_avatar_color_index: default_avatar_color_from_username(&record.username),
|
||||
user: UserDetailsLight {
|
||||
user_token: UserToken::new_from_str(&record.creator_user_token),
|
||||
username: record.username.to_string(), // NB: Cloned because of ref use for avatar below
|
||||
username: record.username.to_string(),
|
||||
display_name: record.display_name,
|
||||
gravatar_hash: record.gravatar_hash,
|
||||
default_avatar: UserDefaultAvatarInfo::from_username(&record.username),
|
||||
@@ -230,6 +195,5 @@ async fn query_leaderboard(mysql_pool: &MySqlPool) -> AnyhowResult<LeaderboardIn
|
||||
|
||||
Ok(LeaderboardInfo {
|
||||
tts_leaderboard: tts_results,
|
||||
w2l_leaderboard: w2l_results,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,5 +13,4 @@ pub mod mocap;
|
||||
pub mod moderation;
|
||||
pub mod stubs;
|
||||
pub mod vocoders;
|
||||
pub mod w2l;
|
||||
pub mod workflows;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pub mod pending_w2l_templates;
|
||||
|
||||
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::delete_w2l_inference_result_as_mod;
|
||||
use mysql_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::delete_w2l_inference_result_as_user;
|
||||
use mysql_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::undelete_w2l_inference_result_as_mod;
|
||||
use mysql_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::undelete_w2l_inference_result_as_user;
|
||||
use mysql_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::delete_role_disambiguation;
|
||||
use crate::util::delete_role_disambiguation::DeleteRole;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteW2lInferenceResultPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteW2lInferenceResultRequest {
|
||||
set_delete: bool,
|
||||
/// NB: this is only to disambiguate when a user is both a mod and an author.
|
||||
as_mod: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeleteW2lInferenceResultError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for DeleteW2lInferenceResultError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
DeleteW2lInferenceResultError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
DeleteW2lInferenceResultError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
DeleteW2lInferenceResultError::NotFound => StatusCode::NOT_FOUND,
|
||||
DeleteW2lInferenceResultError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
DeleteW2lInferenceResultError::BadInput(reason) => reason.to_string(),
|
||||
DeleteW2lInferenceResultError::NotAuthorized => "unauthorized".to_string(),
|
||||
DeleteW2lInferenceResultError::NotFound => "not found".to_string(),
|
||||
DeleteW2lInferenceResultError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for DeleteW2lInferenceResultError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_w2l_inference_result_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<DeleteW2lInferenceResultPathInfo>,
|
||||
request: web::Json<DeleteW2lInferenceResultRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, DeleteW2lInferenceResultError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
DeleteW2lInferenceResultError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(DeleteW2lInferenceResultError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// NB: First permission check.
|
||||
// Only mods should see deleted models (both user_* and mod_* deleted).
|
||||
let is_mod_that_can_see_deleted = user_session.can_delete_other_users_w2l_results;
|
||||
|
||||
let inference_result_query_result = select_w2l_result_by_token(
|
||||
&path.token,
|
||||
is_mod_that_can_see_deleted,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let w2l_inference_result = match inference_result_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(DeleteW2lInferenceResultError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(DeleteW2lInferenceResultError::NotFound),
|
||||
Ok(Some(inference_result)) => inference_result,
|
||||
};
|
||||
|
||||
// NB: Second set of permission checks
|
||||
let is_author = w2l_inference_result.maybe_creator_user_token
|
||||
.as_deref()
|
||||
.map(|creator_token| creator_token == user_session.user_token.as_str())
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_mod = user_session.can_delete_other_users_w2l_results;
|
||||
|
||||
if !is_author && !is_mod {
|
||||
warn!("user is not allowed to delete inference results: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lInferenceResultError::NotAuthorized);
|
||||
}
|
||||
|
||||
let delete_role = delete_role_disambiguation(is_mod, is_author, request.as_mod);
|
||||
|
||||
let query_result = if request.set_delete {
|
||||
match delete_role {
|
||||
DeleteRole::ErrorDoNotDelete => {
|
||||
warn!("user is not allowed to delete inference results: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lInferenceResultError::NotAuthorized);
|
||||
}
|
||||
DeleteRole::AsUser => {
|
||||
delete_w2l_inference_result_as_user(
|
||||
&path.token,
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
DeleteRole::AsMod => {
|
||||
delete_w2l_inference_result_as_mod(
|
||||
&path.token,
|
||||
user_session.user_token.as_str(),
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match delete_role {
|
||||
DeleteRole::ErrorDoNotDelete => {
|
||||
warn!("user is not allowed to delete inference results: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lInferenceResultError::NotAuthorized);
|
||||
}
|
||||
DeleteRole::AsUser => {
|
||||
// NB: Technically only mods can see their own inference_results here
|
||||
undelete_w2l_inference_result_as_user(
|
||||
&path.token,
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
DeleteRole::AsMod => {
|
||||
undelete_w2l_inference_result_as_mod(
|
||||
&path.token,
|
||||
user_session.user_token.as_str(),
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match query_result {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
warn!("Update w2l mod approval status DB error: {:?}", err);
|
||||
return Err(DeleteW2lInferenceResultError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::w2l::w2l_templates::delete_w2l_template_various_scopes::delete_w2l_template_as_mod;
|
||||
use mysql_queries::queries::w2l::w2l_templates::delete_w2l_template_various_scopes::delete_w2l_template_as_user;
|
||||
use mysql_queries::queries::w2l::w2l_templates::delete_w2l_template_various_scopes::undelete_w2l_template_as_mod;
|
||||
use mysql_queries::queries::w2l::w2l_templates::delete_w2l_template_various_scopes::undelete_w2l_template_as_user;
|
||||
use mysql_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::delete_role_disambiguation;
|
||||
use crate::util::delete_role_disambiguation::DeleteRole;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteW2lTemplatePathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteW2lTemplateRequest {
|
||||
set_delete: bool,
|
||||
/// NB: this is only to disambiguate when a user is both a mod and an author.
|
||||
as_mod: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeleteW2lTemplateError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for DeleteW2lTemplateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
DeleteW2lTemplateError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
DeleteW2lTemplateError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
DeleteW2lTemplateError::NotFound => StatusCode::NOT_FOUND,
|
||||
DeleteW2lTemplateError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
DeleteW2lTemplateError::BadInput(reason) => reason.to_string(),
|
||||
DeleteW2lTemplateError::NotAuthorized => "unauthorized".to_string(),
|
||||
DeleteW2lTemplateError::NotFound => "not found".to_string(),
|
||||
DeleteW2lTemplateError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for DeleteW2lTemplateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_w2l_template_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<DeleteW2lTemplatePathInfo>,
|
||||
request: web::Json<DeleteW2lTemplateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, DeleteW2lTemplateError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
DeleteW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(DeleteW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// NB: First permission check.
|
||||
// Only mods should see deleted models (both user_* and mod_* deleted).
|
||||
let is_mod_that_can_see_deleted = user_session.can_delete_other_users_w2l_templates;
|
||||
|
||||
let template_query_result = select_w2l_template_by_token(
|
||||
&path.token,
|
||||
is_mod_that_can_see_deleted,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let w2l_template = match template_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(DeleteW2lTemplateError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(DeleteW2lTemplateError::NotFound),
|
||||
Ok(Some(template)) => template,
|
||||
};
|
||||
|
||||
// NB: Second set of permission checks
|
||||
let is_author = w2l_template.creator_user_token == user_session.user_token.as_str();
|
||||
let is_mod = user_session.can_delete_other_users_w2l_templates;
|
||||
|
||||
if !is_author && !is_mod {
|
||||
warn!("user is not allowed to delete templates: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
|
||||
if !is_mod {
|
||||
if w2l_template.is_locked_from_user_modification || w2l_template.is_locked_from_use {
|
||||
warn!("user is not allowed to delete templates (locked): {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let delete_role = delete_role_disambiguation(is_mod, is_author, request.as_mod);
|
||||
|
||||
let query_result = if request.set_delete {
|
||||
match delete_role {
|
||||
DeleteRole::ErrorDoNotDelete => {
|
||||
warn!("user is not allowed to delete template: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
DeleteRole::AsUser => {
|
||||
delete_w2l_template_as_user(
|
||||
&path.token,
|
||||
&ip_address,
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
DeleteRole::AsMod => {
|
||||
delete_w2l_template_as_mod(
|
||||
&path.token,
|
||||
user_session.user_token.as_str(),
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match delete_role {
|
||||
DeleteRole::ErrorDoNotDelete => {
|
||||
warn!("user is not allowed to delete template: {:?}", user_session.user_token);
|
||||
return Err(DeleteW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
DeleteRole::AsUser => {
|
||||
// NB: Technically only mods can see their own templates here
|
||||
undelete_w2l_template_as_user(
|
||||
&path.token,
|
||||
&ip_address,
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
DeleteRole::AsMod => {
|
||||
undelete_w2l_template_as_mod(
|
||||
&path.token,
|
||||
user_session.user_token.as_str(),
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match query_result {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
warn!("Update w2l mod approval status DB error: {:?}", err);
|
||||
return Err(DeleteW2lTemplateError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{error, warn};
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::w2l::w2l_results::edit_w2l_result::{edit_w2l_result, CreatorOrModFields, EditW2lResultArgs};
|
||||
use mysql_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditW2lResultPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditW2lResultRequest {
|
||||
// ========== Author + Moderator options ==========
|
||||
pub creator_set_visibility: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditW2lResultError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ResultNotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for EditW2lResultError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EditW2lResultError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EditW2lResultError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EditW2lResultError::ResultNotFound => StatusCode::NOT_FOUND,
|
||||
EditW2lResultError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
EditW2lResultError::BadInput(reason) => reason.to_string(),
|
||||
EditW2lResultError::NotAuthorized=> "unauthorized".to_string(),
|
||||
EditW2lResultError::ResultNotFound => "not found".to_string(),
|
||||
EditW2lResultError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for EditW2lResultError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_w2l_inference_result_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<EditW2lResultPathInfo>,
|
||||
request: web::Json<EditW2lResultRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EditW2lResultError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
EditW2lResultError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(EditW2lResultError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// NB: Moderators can see deleted results.
|
||||
// Original creators cannot see them (unless they're moderators!)
|
||||
let show_deleted_results = user_session.can_delete_other_users_w2l_results;
|
||||
|
||||
// Moderators get to see all the fields.
|
||||
let is_moderator = user_session.can_delete_other_users_w2l_results
|
||||
|| user_session.can_edit_other_users_w2l_templates; // TODO: Not an exact permission fit
|
||||
|
||||
let inference_result_query_result = select_w2l_result_by_token(
|
||||
&path.token,
|
||||
show_deleted_results,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let inference_result = match inference_result_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(EditW2lResultError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(EditW2lResultError::ResultNotFound),
|
||||
Ok(Some(inference_result)) => inference_result,
|
||||
};
|
||||
|
||||
// NB: Second set of permission checks
|
||||
let mut is_author = false;
|
||||
if let Some(creator_user_token) = inference_result.maybe_creator_user_token.as_deref() {
|
||||
is_author = creator_user_token == user_session.user_token.as_str();
|
||||
}
|
||||
|
||||
if !is_author && !is_moderator {
|
||||
warn!("user is not allowed to edit result: {:?}", user_session.user_token);
|
||||
return Err(EditW2lResultError::NotAuthorized);
|
||||
}
|
||||
|
||||
// Author + Mod fields.
|
||||
// These fields must be present on all requests.
|
||||
let mut creator_set_visibility = Visibility::Public;
|
||||
|
||||
if let Some(visibility) = request.creator_set_visibility.as_deref() {
|
||||
creator_set_visibility = Visibility::from_str(visibility)
|
||||
.map_err(|_| EditW2lResultError::BadInput("bad record visibility".to_string()))?;
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let args = EditW2lResultArgs {
|
||||
w2l_result_token: &inference_result.w2l_result_token,
|
||||
creator_set_visibility,
|
||||
role_dependent_fields: if is_author {
|
||||
CreatorOrModFields::CreatorFields {
|
||||
creator_ip_address: &ip_address,
|
||||
}
|
||||
} else {
|
||||
CreatorOrModFields::ModFields {
|
||||
mod_user_token: user_session.user_token.as_str(),
|
||||
}
|
||||
},
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
};
|
||||
|
||||
edit_w2l_result(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Update W2L result DB error: {:?}", err);
|
||||
EditW2lResultError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
-211
@@ -1,211 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{error, info, warn};
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use markdown::simple_markdown_to_html::simple_markdown_to_html;
|
||||
use mysql_queries::queries::w2l::w2l_templates::edit_w2l_template::{edit_w2l_template, CreatorOrModFields, EditW2lTemplateArgs, ModFields};
|
||||
use mysql_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token;
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditW2lTemplatePathInfo {
|
||||
template_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditW2lTemplateRequest {
|
||||
// ========== Author + Moderator options ==========
|
||||
pub title: Option<String>,
|
||||
pub description_markdown: Option<String>,
|
||||
pub creator_set_visibility: Option<String>,
|
||||
//pub updatable_slug: Option<String>,
|
||||
|
||||
// ========== Moderator options ==========
|
||||
|
||||
pub is_public_listing_approved: Option<bool>,
|
||||
pub is_locked_from_user_modification: Option<bool>,
|
||||
pub is_locked_from_use: Option<bool>,
|
||||
pub maybe_mod_comments: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditW2lTemplateError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
TemplateNotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for EditW2lTemplateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EditW2lTemplateError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EditW2lTemplateError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EditW2lTemplateError::TemplateNotFound => StatusCode::NOT_FOUND,
|
||||
EditW2lTemplateError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
EditW2lTemplateError::BadInput(reason) => reason.to_string(),
|
||||
EditW2lTemplateError::NotAuthorized=> "unauthorized".to_string(),
|
||||
EditW2lTemplateError::TemplateNotFound => "not found".to_string(),
|
||||
EditW2lTemplateError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for EditW2lTemplateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_w2l_template_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<EditW2lTemplatePathInfo>,
|
||||
request: Json<EditW2lTemplateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EditW2lTemplateError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
EditW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(EditW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// NB: First permission check.
|
||||
// Only mods should see deleted templates (both user_* and mod_* deleted).
|
||||
let is_mod_that_can_see_deleted = user_session.can_delete_other_users_w2l_templates;
|
||||
|
||||
let template_lookup_result = select_w2l_template_by_token(
|
||||
&path.template_token,
|
||||
is_mod_that_can_see_deleted,
|
||||
&server_state.mysql_pool).await;
|
||||
|
||||
let template_record = match template_lookup_result {
|
||||
Ok(Some(result)) => {
|
||||
info!("Found template: {}", result.template_token);
|
||||
result
|
||||
},
|
||||
Ok(None) => {
|
||||
warn!("could not find template");
|
||||
return Err(EditW2lTemplateError::TemplateNotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("error looking up template: {:?}", err);
|
||||
return Err(EditW2lTemplateError::TemplateNotFound);
|
||||
},
|
||||
};
|
||||
|
||||
// NB: Second set of permission checks
|
||||
let is_author = template_record.creator_user_token == user_session.user_token.as_str();
|
||||
let is_mod = user_session.can_edit_other_users_w2l_templates ;
|
||||
|
||||
if !is_author && !is_mod {
|
||||
return Err(EditW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
|
||||
if !is_mod {
|
||||
if template_record.is_locked_from_user_modification || template_record.is_locked_from_use {
|
||||
return Err(EditW2lTemplateError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
|
||||
// Author + Mod fields.
|
||||
// These fields must be present on all requests.
|
||||
let mut title = None;
|
||||
let mut description_markdown = None;
|
||||
let mut description_html = None;
|
||||
let mut creator_set_visibility = Visibility::Public;
|
||||
|
||||
if let Some(payload) = request.title.as_deref() {
|
||||
if contains_slurs(payload) {
|
||||
return Err(EditW2lTemplateError::BadInput("title contains slurs".to_string()));
|
||||
}
|
||||
|
||||
title = Some(payload.to_string());
|
||||
}
|
||||
|
||||
if let Some(markdown) = request.description_markdown.as_deref() {
|
||||
if contains_slurs(markdown) {
|
||||
return Err(EditW2lTemplateError::BadInput("description contains slurs".to_string()));
|
||||
}
|
||||
|
||||
let markdown = markdown.trim().to_string();
|
||||
let html = simple_markdown_to_html(&markdown);
|
||||
|
||||
description_markdown = Some(markdown);
|
||||
description_html = Some(html);
|
||||
}
|
||||
|
||||
if let Some(visibility) = request.creator_set_visibility.as_deref() {
|
||||
creator_set_visibility = Visibility::from_str(visibility)
|
||||
.map_err(|_| EditW2lTemplateError::BadInput("bad record visibility".to_string()))?;
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let args = EditW2lTemplateArgs {
|
||||
w2l_template_token: &template_record.template_token,
|
||||
title: title.as_deref(),
|
||||
description_markdown: description_markdown.as_deref(),
|
||||
description_rendered_html: description_html.as_deref(),
|
||||
creator_set_visibility,
|
||||
role_dependent_fields: if is_author {
|
||||
CreatorOrModFields::CreatorFields {
|
||||
creator_ip_address: &ip_address,
|
||||
}
|
||||
} else {
|
||||
CreatorOrModFields::ModFields(ModFields {
|
||||
mod_user_token: user_session.user_token.as_str(),
|
||||
is_public_listing_approved: request.is_public_listing_approved.unwrap_or(false),
|
||||
is_locked_from_user_modification: request.is_locked_from_user_modification.unwrap_or(false),
|
||||
is_locked_from_use: request.is_locked_from_use.unwrap_or(false),
|
||||
maybe_mod_comments: request.maybe_mod_comments.as_deref(),
|
||||
})
|
||||
},
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
};
|
||||
|
||||
edit_w2l_template(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Update W2L template DB error: {:?}", err);
|
||||
EditW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{info, warn};
|
||||
use redis::Commands;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::insert_w2l_inference_job::{insert_w2l_inference_job, InsertW2lInferenceJobArgs};
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InferW2lRequest {
|
||||
w2l_template_token: Option<String>,
|
||||
tts_inference_result_token: Option<String>,
|
||||
creator_set_visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct InferW2lSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InferW2lError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl ResponseError for InferW2lError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
InferW2lError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
InferW2lError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
InferW2lError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
InferW2lError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
InferW2lError::BadInput(reason) => reason.to_string(),
|
||||
InferW2lError::ServerError => "server error".to_string(),
|
||||
InferW2lError::NotAuthorized => "not authorized".to_string(),
|
||||
InferW2lError::RateLimited => "rate limited".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for InferW2lError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn infer_w2l_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<InferW2lRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, InferW2lError>
|
||||
{
|
||||
|
||||
if true {
|
||||
unimplemented!("this isn't finished");
|
||||
}
|
||||
|
||||
let maybe_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_session_light(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
InferW2lError::ServerError
|
||||
})?;
|
||||
|
||||
// TODO
|
||||
//if let Err(_err) = server_state.redis_rate_limiter.rate_limit_request(&http_request) {
|
||||
// return Err(InferW2lError::RateLimited);
|
||||
//}
|
||||
|
||||
let maybe_user_token : Option<String> = maybe_session
|
||||
.as_ref()
|
||||
.map(|user_session| user_session.user_token.to_string());
|
||||
|
||||
info!("Enqueue infer w2l by user token: {:?}", maybe_user_token);
|
||||
|
||||
let w2l_template_token = match &request.w2l_template_token {
|
||||
None => {
|
||||
// TODO: Allow image uploads.
|
||||
return Err(InferW2lError::BadInput("w2l token is required".to_string()));
|
||||
},
|
||||
Some(t) => {
|
||||
// TODO: CHECK DATABASE!
|
||||
t.to_string()
|
||||
},
|
||||
};
|
||||
|
||||
let tts_inference_result_token = match &request.tts_inference_result_token {
|
||||
None => {
|
||||
// TODO: Allow audio uploads.
|
||||
return Err(InferW2lError::BadInput("tts token is required".to_string()));
|
||||
},
|
||||
Some(t) => {
|
||||
// TODO: CHECK DATABASE!
|
||||
t.to_string()
|
||||
},
|
||||
};
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
InferW2lError::ServerError
|
||||
})?;
|
||||
|
||||
let redis_count_key = RedisKeys::w2l_template_usage_count(&w2l_template_token);
|
||||
|
||||
redis.incr::<_, _, ()>(&redis_count_key, 1)
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
InferW2lError::ServerError
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let creator_set_visibility = "public".to_string();
|
||||
|
||||
if true {
|
||||
unimplemented!("this isn't finished");
|
||||
}
|
||||
|
||||
// FIXME: NB: This is an old query that was somewhat modernized when moved.
|
||||
// All the same, do not copy this example!
|
||||
|
||||
insert_w2l_inference_job(InsertW2lInferenceJobArgs {
|
||||
w2l_template_token: &w2l_template_token,
|
||||
tts_inference_result_token: &tts_inference_result_token,
|
||||
maybe_user_token: maybe_user_token.as_deref(),
|
||||
ip_address: &ip_address,
|
||||
creator_set_visibility: &creator_set_visibility,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await
|
||||
.map_err(|_err| {
|
||||
InferW2lError::ServerError
|
||||
})?;
|
||||
|
||||
//info!("new w2l inference job id: {}", record_id);
|
||||
|
||||
let response = InferW2lSuccessResponse {
|
||||
success: true,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| InferW2lError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-315
@@ -1,315 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::BytesMut;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
|
||||
use futures::TryStreamExt;
|
||||
use log::{error, info, warn};
|
||||
use redis::Commands;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::insert_w2l_inference_job_extended::{insert_w2l_inference_job_extended, InsertW2lInferenceJobExtendedArgs};
|
||||
use mysql_queries::queries::w2l::w2l_templates::check_w2l_template_exists::check_w2l_template_exists;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::web_utils::read_multipart_field_bytes::checked_read_multipart_bytes;
|
||||
use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_text;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const BUCKET_AUDIO_FILE_NAME : &str = "input_audio_file";
|
||||
const BUCKET_IMAGE_FILE_NAME: &str = "input_image_file";
|
||||
const BUCKET_VIDEO_FILE_NAME : &str = "input_video_file";
|
||||
|
||||
const MIN_BYTES : usize = 10;
|
||||
const MAX_BYTES : usize = 1024 * 1024 * 20;
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct InferW2lWithUploadSuccessResponse {
|
||||
pub success: bool,
|
||||
/// This is how frontend clients can request the job execution status.
|
||||
pub inference_job_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InferW2lWithUploadError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
EmptyFileUploaded,
|
||||
ServerError,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl ResponseError for InferW2lWithUploadError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
InferW2lWithUploadError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
InferW2lWithUploadError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
InferW2lWithUploadError::EmptyFileUploaded => StatusCode::BAD_REQUEST,
|
||||
InferW2lWithUploadError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
InferW2lWithUploadError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
InferW2lWithUploadError::BadInput(reason) => reason.to_string(),
|
||||
InferW2lWithUploadError::NotAuthorized => "unauthorized".to_string(),
|
||||
InferW2lWithUploadError::EmptyFileUploaded => "empty file uploaded".to_string(),
|
||||
InferW2lWithUploadError::ServerError => "server error".to_string(),
|
||||
InferW2lWithUploadError::RateLimited => "rate limited".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for InferW2lWithUploadError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// This handles audio uploads w/ W2L templates.
|
||||
pub async fn enqueue_infer_w2l_with_uploads(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
mut payload: Multipart
|
||||
) -> Result<HttpResponse, InferW2lWithUploadError> {
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
// ==================== READ SESSION ==================== //
|
||||
|
||||
let maybe_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== BANNED USERS ==================== //
|
||||
|
||||
if let Some(ref user) = maybe_session {
|
||||
if user.is_banned {
|
||||
return Err(InferW2lWithUploadError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RATE LIMIT ==================== //
|
||||
|
||||
let rate_limiter = match maybe_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref _user) => &server_state.redis_rate_limiters.logged_in,
|
||||
};
|
||||
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
|
||||
return Err(InferW2lWithUploadError::RateLimited);
|
||||
}
|
||||
|
||||
// ==================== SESSION DETAILS ==================== //
|
||||
|
||||
let maybe_user_token : Option<String> = maybe_session
|
||||
.as_ref()
|
||||
.map(|user_session| user_session.user_token.as_str().to_string());
|
||||
|
||||
let maybe_user_preferred_visibility : Option<Visibility> = maybe_session
|
||||
.as_ref()
|
||||
.map(|user_session| user_session.preferred_tts_result_visibility);
|
||||
|
||||
let set_visibility = maybe_user_preferred_visibility
|
||||
.unwrap_or(Visibility::Public);
|
||||
|
||||
info!("Enqueue infer w2l by user token: {:?}", maybe_user_token);
|
||||
|
||||
// ==================== READ MULTIPART REQUEST ==================== //
|
||||
|
||||
info!("Reading multipart request...");
|
||||
|
||||
let mut maybe_uuid_idempotency_token: Option<String> = None;
|
||||
let mut maybe_template_token: Option<String> = None;
|
||||
let mut maybe_audio_file_name : Option<String> = None;
|
||||
let mut audio_bytes = BytesMut::with_capacity(0);
|
||||
|
||||
while let Ok(Some(mut field)) = payload.try_next().await {
|
||||
let mut field_name = "".to_string();
|
||||
let mut filename = "".to_string();
|
||||
|
||||
if let content_disposition = field.content_disposition() {
|
||||
field_name = content_disposition.get_name()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
filename = content_disposition.get_filename()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
}
|
||||
|
||||
match field_name.as_ref() {
|
||||
"uuid_idempotency_token" => {
|
||||
// Form text field.
|
||||
maybe_uuid_idempotency_token = read_multipart_field_as_text(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading idempotency token: {:}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
},
|
||||
"template_token" => {
|
||||
// Form text field.
|
||||
maybe_template_token = read_multipart_field_as_text(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading template token: {:}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
},
|
||||
"audio" => {
|
||||
// Form binary data.
|
||||
maybe_audio_file_name = Some(filename.to_string());
|
||||
|
||||
let maybe_bytes = checked_read_multipart_bytes(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading audio upload: {:}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
audio_bytes = match maybe_bytes {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
warn!("Empty file uploaded");
|
||||
return Err(InferW2lWithUploadError::EmptyFileUploaded); // Nothing was uploaded!
|
||||
},
|
||||
};
|
||||
},
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
info!("Saved file: {}.", &filename);
|
||||
}
|
||||
|
||||
// ==================== CHECK REQUEST ==================== //
|
||||
|
||||
let template_token = match &maybe_template_token {
|
||||
Some(ref token) => token.to_string(),
|
||||
None => {
|
||||
return Err(InferW2lWithUploadError::BadInput("No template selected".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let uuid_idempotency_token = match maybe_uuid_idempotency_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Err(InferW2lWithUploadError::BadInput("No uuid idempotency token".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let exists = check_w2l_template_exists(&template_token, &server_state.mysql_pool).await
|
||||
.map_err(|e| {
|
||||
warn!("error checking tmpl existence : {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
if !exists {
|
||||
return Err(InferW2lWithUploadError::BadInput("Template does not exist".to_string()));
|
||||
}
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
let redis_count_key = RedisKeys::w2l_template_usage_count(&template_token);
|
||||
|
||||
redis.incr::<_, _, ()>(&redis_count_key, 1)
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== ANALYZE AND UPLOAD AUDIO FILE ==================== //
|
||||
|
||||
let mut audio_type = "application/octet-stream".to_string();
|
||||
|
||||
if let Some(maybe_type) = infer::get(audio_bytes.as_ref()) {
|
||||
audio_type = maybe_type.mime_type().to_string();
|
||||
}
|
||||
|
||||
let upload_uuid = generate_random_uuid();
|
||||
|
||||
let audio_upload_bucket_hash = upload_uuid.clone();
|
||||
|
||||
//let audio_upload_bucket_path = hash_to_bucket_path_string(
|
||||
// &upload_uuid,
|
||||
// Some(&server_state.audio_uploads_bucket_root)
|
||||
//).map_err(|e| {
|
||||
// warn!("Hash bucket path error: {:?}", e);
|
||||
// InferW2lWithUploadError::ServerError
|
||||
//})?;
|
||||
|
||||
let audio_upload_bucket_path = "THIS_IS_BROKEN_BECAUSE_W2L_IS_DEAD";
|
||||
|
||||
info!("Uploading audio to bucket...");
|
||||
server_state.private_bucket_client.upload_file_with_content_type(
|
||||
&audio_upload_bucket_path,
|
||||
audio_bytes.as_ref(),
|
||||
&audio_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Upload audio bytes to bucket error: {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== SAVE JOB RECORD ==================== //
|
||||
|
||||
info!("Creating w2l inference job record...");
|
||||
|
||||
let job_token = insert_w2l_inference_job_extended(InsertW2lInferenceJobExtendedArgs {
|
||||
uuid_idempotency_token: &uuid_idempotency_token,
|
||||
maybe_template_token: maybe_template_token.as_deref(),
|
||||
audio_upload_bucket_hash: Some(&audio_upload_bucket_hash),
|
||||
audio_upload_bucket_path: Some(&audio_upload_bucket_path),
|
||||
maybe_audio_file_name: maybe_audio_file_name.as_deref(),
|
||||
audio_type: Some(&audio_type),
|
||||
maybe_user_token: maybe_user_token.as_deref(),
|
||||
ip_address: &ip_address,
|
||||
set_visibility,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("error inserting w2l inference job: {:?}", err);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
server_state.firehose_publisher.enqueue_w2l_inference(maybe_user_token.as_deref(), &job_token, &template_token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error publishing event: {:?}", e);
|
||||
InferW2lWithUploadError::ServerError
|
||||
})?;
|
||||
|
||||
let response = InferW2lWithUploadSuccessResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token.to_string(),
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| InferW2lWithUploadError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use config::is_bad_video_download_url::is_bad_video_download_url;
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::w2l::w2l_template_upload_jobs::insert_w2l_template_upload_job::{insert_w2l_template_upload_job, InsertW2lTemplateUploadJobArgs};
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
|
||||
use crate::http_server::validations::validate_model_title::validate_model_title;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum W2lTemplateType {
|
||||
/// unknown
|
||||
Unknown,
|
||||
/// video
|
||||
Video,
|
||||
/// image
|
||||
Image,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UploadW2lTemplateRequest {
|
||||
idempotency_token: String,
|
||||
title: String,
|
||||
download_url: String,
|
||||
template_type: Option<W2lTemplateType>,
|
||||
creator_set_visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UploadW2lTemplateSuccessResponse {
|
||||
pub success: bool,
|
||||
/// This is how frontend clients can request the job execution status.
|
||||
pub job_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UploadW2lTemplateError {
|
||||
BadInput(String),
|
||||
MustBeLoggedIn,
|
||||
ServerError,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl ResponseError for UploadW2lTemplateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
UploadW2lTemplateError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
UploadW2lTemplateError::MustBeLoggedIn => StatusCode::UNAUTHORIZED,
|
||||
UploadW2lTemplateError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
UploadW2lTemplateError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
UploadW2lTemplateError::BadInput(reason) => reason.to_string(),
|
||||
UploadW2lTemplateError::MustBeLoggedIn => "user must be logged in".to_string(),
|
||||
UploadW2lTemplateError::ServerError => "server error".to_string(),
|
||||
UploadW2lTemplateError::RateLimited => "rate limited".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for UploadW2lTemplateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_w2l_template_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<UploadW2lTemplateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, UploadW2lTemplateError>
|
||||
{
|
||||
if let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(&http_request).await {
|
||||
return Err(UploadW2lTemplateError::RateLimited);
|
||||
}
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
UploadW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(UploadW2lTemplateError::MustBeLoggedIn);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token(&request.idempotency_token) {
|
||||
return Err(UploadW2lTemplateError::BadInput(reason));
|
||||
}
|
||||
|
||||
if let Err(reason) = validate_model_title(&request.title) {
|
||||
return Err(UploadW2lTemplateError::BadInput(reason));
|
||||
}
|
||||
|
||||
if is_bad_video_download_url(&request.download_url).unwrap_or(true) {
|
||||
return Err(UploadW2lTemplateError::BadInput("url is invalid".to_string()));
|
||||
}
|
||||
|
||||
if contains_slurs(&request.title) {
|
||||
return Err(UploadW2lTemplateError::BadInput("title contains slurs".to_string()));
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let uuid = request.idempotency_token.to_string();
|
||||
let title = request.title.to_string();
|
||||
let download_url = request.download_url.to_string();
|
||||
|
||||
let template_type = "unknown".to_string();
|
||||
let creator_set_visibility = "public".to_string();
|
||||
|
||||
let job_token = insert_w2l_template_upload_job(InsertW2lTemplateUploadJobArgs {
|
||||
uuid: &uuid,
|
||||
creator_user_token: user_session.user_token.as_str(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: &creator_set_visibility,
|
||||
title: &title,
|
||||
template_type: &template_type,
|
||||
download_url: &download_url,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
warn!("New w2l template upload creation DB error: {:?}", err);
|
||||
UploadW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
server_state.firehose_publisher.enqueue_w2l_template_upload(user_session.user_token.as_str(), &job_token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error publishing event: {:?}", e);
|
||||
UploadW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
let response = UploadW2lTemplateSuccessResponse {
|
||||
success: true,
|
||||
job_token: job_token.to_string(),
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| UploadW2lTemplateError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
fn validate_idempotency_token(token: &str) -> Result<(), String> {
|
||||
if token.len() != 36 {
|
||||
return Err("idempotency token should be 36 characters".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use redis::Commands;
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_inference_jobs::get_w2l_inference_job_status::get_w2l_inference_job_status;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetW2lInferenceStatusPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lInferenceJobStatusForResponse {
|
||||
pub job_token: String,
|
||||
|
||||
/// Primary status from the database (a state machine).
|
||||
pub status: String,
|
||||
|
||||
/// Extra, temporary status from Redis.
|
||||
/// This can denote inference progress, and the Python code can write to it.
|
||||
pub maybe_extra_status_description: Option<String>,
|
||||
|
||||
pub attempt_count: u8,
|
||||
pub maybe_result_token: Option<String>,
|
||||
pub maybe_public_bucket_video_path: Option<String>,
|
||||
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub w2l_template_type: String,
|
||||
pub title: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetW2lInferenceStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
pub state: W2lInferenceJobStatusForResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetW2lInferenceStatusError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lInferenceJobStatusRecord {
|
||||
pub job_token: String,
|
||||
|
||||
pub status: String,
|
||||
pub attempt_count: i32,
|
||||
pub maybe_result_token: Option<String>,
|
||||
pub maybe_public_bucket_video_path: Option<String>,
|
||||
|
||||
pub maybe_w2l_template_token: Option<String>,
|
||||
pub w2l_template_type: String,
|
||||
|
||||
pub title: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ResponseError for GetW2lInferenceStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetW2lInferenceStatusError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetW2lInferenceStatusError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetW2lInferenceStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_w2l_inference_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetW2lInferenceStatusPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetW2lInferenceStatusError>
|
||||
{
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_status = get_w2l_inference_job_status(
|
||||
&path.token,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let record = maybe_status
|
||||
.map_err(|err| {
|
||||
warn!("w2l template query error: {:?}", err);
|
||||
GetW2lInferenceStatusError::ServerError
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
GetW2lInferenceStatusError::ServerError // TODO: Not found
|
||||
})?;
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
GetW2lInferenceStatusError::ServerError
|
||||
})?;
|
||||
|
||||
let extra_status_key = RedisKeys::w2l_inference_extra_status_info(&path.token);
|
||||
let maybe_extra_status_description : Option<String> = match redis.get(&extra_status_key) {
|
||||
Ok(Some(status)) => Some(status),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
warn!("redis error: {:?}", e);
|
||||
None // Fail open
|
||||
},
|
||||
};
|
||||
|
||||
let template_for_response = W2lInferenceJobStatusForResponse {
|
||||
job_token: record.job_token.clone(),
|
||||
status: record.status.clone(),
|
||||
maybe_extra_status_description,
|
||||
attempt_count: record.attempt_count as u8,
|
||||
maybe_result_token: record.maybe_result_token.clone(),
|
||||
maybe_public_bucket_video_path: record.maybe_public_bucket_video_path.clone(),
|
||||
maybe_w2l_template_token: record.maybe_w2l_template_token.clone(),
|
||||
w2l_template_type: record.w2l_template_type.clone(),
|
||||
title: record.title.clone(),
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
};
|
||||
|
||||
let response = GetW2lInferenceStatusSuccessResponse {
|
||||
success: true,
|
||||
state: template_for_response,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| GetW2lInferenceStatusError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token;
|
||||
use mysql_queries::queries::w2l::w2l_results::query_w2l_result::W2lResultRecordForResponse;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetW2lResultPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetW2lResultSuccessResponse {
|
||||
pub success: bool,
|
||||
pub result: W2lResultRecordForResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetW2lResultError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for GetW2lResultError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetW2lResultError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetW2lResultError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetW2lResultError::ServerError => "server error".to_string(),
|
||||
GetW2lResultError::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetW2lResultError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_w2l_inference_result_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetW2lResultPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, GetW2lResultError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GetW2lResultError::ServerError
|
||||
})?;
|
||||
|
||||
let mut show_deleted_results = false;
|
||||
let mut is_moderator = false;
|
||||
|
||||
if let Some(user_session) = maybe_user_session {
|
||||
// NB: Moderators can see deleted results.
|
||||
// Original creators cannot see them (unless they're moderators!)
|
||||
show_deleted_results = user_session.can_delete_other_users_w2l_results;
|
||||
// Moderators get to see all the fields.
|
||||
is_moderator = user_session.can_delete_other_users_w2l_results
|
||||
|| user_session.can_edit_other_users_w2l_templates;
|
||||
}
|
||||
|
||||
let inference_result_query_result = select_w2l_result_by_token(
|
||||
&path.token,
|
||||
show_deleted_results,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let mut inference_result = match inference_result_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(GetW2lResultError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(GetW2lResultError::NotFound),
|
||||
Ok(Some(inference_result)) => inference_result,
|
||||
};
|
||||
|
||||
if let Some(moderator_fields) = inference_result.maybe_moderator_fields.as_ref() {
|
||||
// NB: The moderator fields will always be present before removal
|
||||
// We don't want non-mods seeing stuff made by banned users.
|
||||
if (moderator_fields.template_creator_is_banned || moderator_fields.result_creator_is_banned_if_user)
|
||||
&& !is_moderator{
|
||||
return Err(GetW2lResultError::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_moderator {
|
||||
inference_result.maybe_moderator_fields = None;
|
||||
}
|
||||
|
||||
let response = GetW2lResultSuccessResponse {
|
||||
success: true,
|
||||
result: inference_result,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| GetW2lResultError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token;
|
||||
use mysql_queries::queries::w2l::w2l_templates::get_w2l_template::W2lTemplateRecordForResponse;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetW2lTemplatePathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetW2lTemplateSuccessResponse {
|
||||
pub success: bool,
|
||||
pub template: W2lTemplateRecordForResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetW2lTemplateError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for GetW2lTemplateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetW2lTemplateError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetW2lTemplateError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetW2lTemplateError::ServerError => "server error".to_string(),
|
||||
GetW2lTemplateError::NotFound=> "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetW2lTemplateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_w2l_template_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetW2lTemplatePathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetW2lTemplateError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GetW2lTemplateError::ServerError
|
||||
})?;
|
||||
|
||||
let mut show_deleted_templates = false;
|
||||
let mut is_moderator = false;
|
||||
|
||||
if let Some(user_session) = maybe_user_session {
|
||||
// NB: Moderators can see deleted templates.
|
||||
// Original creators cannot see them (unless they're moderators!)
|
||||
show_deleted_templates = user_session.can_delete_other_users_w2l_templates;
|
||||
// Moderators get to see all the fields.
|
||||
is_moderator = user_session.can_delete_other_users_w2l_results
|
||||
|| user_session.can_edit_other_users_w2l_templates;
|
||||
}
|
||||
|
||||
let template_query_result = select_w2l_template_by_token(
|
||||
&path.token,
|
||||
show_deleted_templates,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let mut template = match template_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(GetW2lTemplateError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(GetW2lTemplateError::NotFound),
|
||||
Ok(Some(template)) => template,
|
||||
};
|
||||
|
||||
if let Some(moderator_fields) = template.maybe_moderator_fields.as_ref() {
|
||||
// NB: The moderator fields will always be present before removal
|
||||
// We don't want non-mods seeing stuff made by banned users.
|
||||
if moderator_fields.creator_is_banned && !is_moderator {
|
||||
return Err(GetW2lTemplateError::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_moderator {
|
||||
template.maybe_moderator_fields = None;
|
||||
}
|
||||
|
||||
let response = GetW2lTemplateSuccessResponse {
|
||||
success: true,
|
||||
template,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| GetW2lTemplateError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use derive_more::Display;
|
||||
use log::warn;
|
||||
use redis::Commands;
|
||||
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetW2lTemplateUseCountPathInfo {
|
||||
template_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetW2lTemplateUseCountSuccessResponse {
|
||||
pub success: bool,
|
||||
pub count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum GetW2lTemplateUseCountError {
|
||||
ServerError,
|
||||
NotFoundError,
|
||||
}
|
||||
|
||||
impl ResponseError for GetW2lTemplateUseCountError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetW2lTemplateUseCountError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetW2lTemplateUseCountError::NotFoundError => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetW2lTemplateUseCountError::ServerError => "server error".to_string(),
|
||||
GetW2lTemplateUseCountError::NotFoundError => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_w2l_template_use_count_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetW2lTemplateUseCountPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetW2lTemplateUseCountError>
|
||||
{
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
GetW2lTemplateUseCountError::ServerError
|
||||
})?;
|
||||
|
||||
let redis_count_key = RedisKeys::w2l_template_usage_count(&path.template_token);
|
||||
|
||||
let count : Option<u64> = redis.get(&redis_count_key)
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
GetW2lTemplateUseCountError::ServerError
|
||||
})?;
|
||||
|
||||
let response = GetW2lTemplateUseCountSuccessResponse {
|
||||
success: true,
|
||||
count,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| GetW2lTemplateUseCountError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::Display;
|
||||
use log::warn;
|
||||
use redis::Commands;
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_template_upload_jobs::get_w2l_template_upload_job_status::get_w2l_template_upload_job_status;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetW2lUploadTemplateStatusPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lUploadTemplateJobStatusForResponse {
|
||||
pub job_token: String,
|
||||
|
||||
/// Primary status from the database (a state machine).
|
||||
pub status: String,
|
||||
|
||||
/// Extra, temporary status from Redis.
|
||||
/// This can denote inference progress, and the Python code can write to it.
|
||||
pub maybe_extra_status_description: Option<String>,
|
||||
|
||||
/// If the job failed or permanently died, we might have a reason.
|
||||
pub maybe_failure_reason: Option<String>,
|
||||
|
||||
pub attempt_count: u8,
|
||||
pub maybe_template_token: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetW2lUploadTemplateStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
pub state: W2lUploadTemplateJobStatusForResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum GetW2lUploadTemplateStatusError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct W2lUploadTemplateJobStatusRecord {
|
||||
pub job_token: String,
|
||||
|
||||
pub status: String,
|
||||
pub attempt_count: i32,
|
||||
pub maybe_template_token: Option<String>,
|
||||
|
||||
pub maybe_failure_reason: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ResponseError for GetW2lUploadTemplateStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetW2lUploadTemplateStatusError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetW2lUploadTemplateStatusError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_w2l_upload_template_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetW2lUploadTemplateStatusPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetW2lUploadTemplateStatusError>
|
||||
{
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let record = get_w2l_template_upload_job_status(&path.token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("database error: {:?}", e);
|
||||
GetW2lUploadTemplateStatusError::ServerError
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
warn!("no w2l template upload job found with token: {}", &path.token);
|
||||
GetW2lUploadTemplateStatusError::ServerError
|
||||
})?;
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("redis error: {:?}", e);
|
||||
GetW2lUploadTemplateStatusError::ServerError
|
||||
})?;
|
||||
|
||||
let extra_status_key = RedisKeys::w2l_download_extra_status_info(&path.token);
|
||||
let maybe_extra_status_description : Option<String> = match redis.get(&extra_status_key) {
|
||||
Ok(Some(status)) => Some(status),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
warn!("redis error: {:?}", e);
|
||||
None // Fail open
|
||||
},
|
||||
};
|
||||
|
||||
let template_for_response = W2lUploadTemplateJobStatusForResponse {
|
||||
job_token: record.job_token.clone(),
|
||||
status: record.status.clone(),
|
||||
maybe_extra_status_description,
|
||||
maybe_failure_reason: record.maybe_failure_reason.clone(),
|
||||
attempt_count: record.attempt_count as u8,
|
||||
maybe_template_token: record.maybe_template_token.clone(),
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
};
|
||||
|
||||
let response = GetW2lUploadTemplateStatusSuccessResponse {
|
||||
success: true,
|
||||
state: template_for_response,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| GetW2lUploadTemplateStatusError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-188
@@ -1,188 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Path, Query};
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use log::{info, warn};
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_results::list_w2l_inference_results_query_builder::ListW2lResultsQueryBuilder;
|
||||
use mysql_queries::queries::w2l::w2l_results::list_w2l_inference_results_query_builder::W2lInferenceRecordForList;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListW2lInferenceResultsForUserPathInfo {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListW2lInferenceResultsForUserQuery {
|
||||
pub sort_ascending: Option<bool>,
|
||||
pub limit: Option<u16>,
|
||||
pub cursor: Option<String>,
|
||||
pub cursor_is_reversed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListW2lInferenceResultsForUserSuccessResponse {
|
||||
pub success: bool,
|
||||
pub results: Vec<W2lInferenceRecordForList>,
|
||||
pub cursor_next: Option<String>,
|
||||
pub cursor_previous: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ListW2lInferenceResultsForUserError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ListW2lInferenceResultsForUserError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListW2lInferenceResultsForUserError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListW2lInferenceResultsForUserError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for ListW2lInferenceResultsForUserError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_user_w2l_inference_results_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<ListW2lInferenceResultsForUserPathInfo>,
|
||||
query: Query<ListW2lInferenceResultsForUserQuery>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListW2lInferenceResultsForUserError>
|
||||
{
|
||||
return Ok(HttpResponse::Gone()
|
||||
.content_type(ContentType::plaintext())
|
||||
.body("This endpoint has been removed."))
|
||||
}
|
||||
|
||||
pub async fn original_list_user_w2l_inference_results_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<ListW2lInferenceResultsForUserPathInfo>,
|
||||
query: Query<ListW2lInferenceResultsForUserQuery>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListW2lInferenceResultsForUserError>
|
||||
{
|
||||
info!("Fetching inference results for user: {}", &path.username);
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListW2lInferenceResultsForUserError::ServerError
|
||||
})?;
|
||||
|
||||
let mut is_author = false;
|
||||
let mut is_mod_that_can_see_deleted = false;
|
||||
let mut is_mod_that_can_approve_w2l_templates = false;
|
||||
|
||||
match maybe_user_session {
|
||||
None => {},
|
||||
Some(session) => {
|
||||
is_author = session.username == path.username;
|
||||
is_mod_that_can_see_deleted = session.can_delete_other_users_w2l_results;
|
||||
is_mod_that_can_approve_w2l_templates = session.can_approve_w2l_templates;
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: Enforce real maximums and defaults
|
||||
let limit = query.limit.unwrap_or(25);
|
||||
//let limit = std::cmp::max(limit, 100);
|
||||
|
||||
let sort_ascending = query.sort_ascending.unwrap_or(false);
|
||||
let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false);
|
||||
|
||||
let cursor = if let Some(cursor) = query.cursor.as_deref() {
|
||||
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListW2lInferenceResultsForUserError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let include_user_hidden = is_author || is_mod_that_can_see_deleted;
|
||||
let include_unapproved = is_author || is_mod_that_can_approve_w2l_templates;
|
||||
|
||||
let mut query_builder = ListW2lResultsQueryBuilder::new()
|
||||
.sort_ascending(sort_ascending)
|
||||
.scope_creator_username(Some(path.username.as_ref()))
|
||||
.include_user_hidden(include_user_hidden)
|
||||
.include_user_deleted_results(is_mod_that_can_see_deleted)
|
||||
.include_mod_deleted_results(is_mod_that_can_see_deleted)
|
||||
.include_templates_not_approved_for_public_listing(include_unapproved)
|
||||
.limit(limit)
|
||||
.cursor_is_reversed(cursor_is_reversed)
|
||||
.offset(cursor);
|
||||
|
||||
let query_results = query_builder.perform_query_for_page(&server_state.mysql_pool)
|
||||
.await;
|
||||
|
||||
let results_page = match query_results {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(ListW2lInferenceResultsForUserError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_next = if let Some(id) = results_page.last_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListW2lInferenceResultsForUserError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cursor_previous = if let Some(id) = results_page.first_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListW2lInferenceResultsForUserError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = ListW2lInferenceResultsForUserSuccessResponse {
|
||||
success: true,
|
||||
results: results_page.inference_records,
|
||||
cursor_next,
|
||||
cursor_previous,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListW2lInferenceResultsForUserError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use log::{info, warn};
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::{list_w2l_templates, W2lTemplateRecordForList};
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetProfilePathInfo {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListW2lTemplatesForUserSuccessResponse {
|
||||
pub success: bool,
|
||||
pub templates: Vec<W2lTemplateRecordForList>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ListW2lTemplatesForUserError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ListW2lTemplatesForUserError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListW2lTemplatesForUserError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListW2lTemplatesForUserError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for ListW2lTemplatesForUserError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_user_w2l_templates_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetProfilePathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListW2lTemplatesForUserError>
|
||||
{
|
||||
return Ok(HttpResponse::Gone()
|
||||
.content_type(ContentType::plaintext())
|
||||
.body("This endpoint has been removed."))
|
||||
}
|
||||
|
||||
pub async fn original_list_user_w2l_templates_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetProfilePathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListW2lTemplatesForUserError>
|
||||
{
|
||||
info!("Fetching templates for user: {}", &path.username);
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListW2lTemplatesForUserError::ServerError
|
||||
})?;
|
||||
|
||||
let mut viewer_is_original_user = false;
|
||||
let mut viewer_is_moderator = false;
|
||||
|
||||
match maybe_user_session {
|
||||
None => {},
|
||||
Some(session) => {
|
||||
viewer_is_moderator = session.can_delete_other_users_w2l_templates;
|
||||
viewer_is_original_user = &session.username == &path.username;
|
||||
},
|
||||
};
|
||||
|
||||
// The original user can see all their uploads, as can mods.
|
||||
// Once they've been approved, everyone can see them.
|
||||
let require_mod_approved = !(viewer_is_original_user || viewer_is_moderator);
|
||||
|
||||
let query_results = list_w2l_templates(
|
||||
&server_state.mysql_pool,
|
||||
Some(path.username.as_ref()),
|
||||
require_mod_approved
|
||||
).await;
|
||||
|
||||
let templates = match query_results {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(ListW2lTemplatesForUserError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let response = ListW2lTemplatesForUserSuccessResponse {
|
||||
success: true,
|
||||
templates,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListW2lTemplatesForUserError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use log::{error, warn};
|
||||
|
||||
use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::{list_w2l_templates, W2lTemplateRecordForList};
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListW2lTemplatesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub templates: Vec<W2lTemplateRecordForList>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ListW2lTemplatesError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ListW2lTemplatesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListW2lTemplatesError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListW2lTemplatesError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for ListW2lTemplatesError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_w2l_templates_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, ListW2lTemplatesError>
|
||||
{
|
||||
// TODO(bt): Perhaps we should build a second endpoint to allow fetching a user's W2L templates
|
||||
// and including them in the global list (before they're mod approved!) just for the uploader.
|
||||
|
||||
let maybe_templates = server_state
|
||||
.caches
|
||||
.ephemeral
|
||||
.w2l_template_list
|
||||
.grab_copy_without_bump_if_unexpired()
|
||||
.map_err(|e| {
|
||||
error!("Error consulting cache: {:?}", e);
|
||||
ListW2lTemplatesError::ServerError
|
||||
})?;
|
||||
|
||||
let templates = match maybe_templates {
|
||||
Some(templates) => {
|
||||
templates
|
||||
},
|
||||
None => {
|
||||
const NO_CREATOR_SCOPING_HERE : Option<&'static str> = None;
|
||||
|
||||
let query_results = list_w2l_templates(
|
||||
&server_state.mysql_pool,
|
||||
NO_CREATOR_SCOPING_HERE,
|
||||
true,
|
||||
).await;
|
||||
|
||||
let templates = match query_results {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("w2l template list query error: {:?}", e);
|
||||
return Err(ListW2lTemplatesError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
server_state.caches.ephemeral.w2l_template_list.store_copy(&templates)
|
||||
.map_err(|e| {
|
||||
error!("error storing cache: {:?}", e);
|
||||
ListW2lTemplatesError::ServerError
|
||||
})?;
|
||||
|
||||
templates
|
||||
},
|
||||
};
|
||||
|
||||
let response = ListW2lTemplatesSuccessResponse {
|
||||
success: true,
|
||||
templates,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListW2lTemplatesError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pub mod delete_w2l_result;
|
||||
pub mod delete_w2l_template;
|
||||
pub mod edit_w2l_result;
|
||||
pub mod edit_w2l_template;
|
||||
pub mod enqueue_infer_w2l;
|
||||
pub mod enqueue_infer_w2l_with_uploads;
|
||||
pub mod enqueue_upload_w2l_template;
|
||||
pub mod get_w2l_inference_job_status;
|
||||
pub mod get_w2l_result;
|
||||
pub mod get_w2l_template;
|
||||
pub mod get_w2l_template_use_count;
|
||||
pub mod get_w2l_upload_template_job_status;
|
||||
pub mod list_user_w2l_inference_results;
|
||||
pub mod list_user_w2l_templates;
|
||||
pub mod list_w2l_templates;
|
||||
pub mod set_w2l_template_mod_approval;
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token;
|
||||
use mysql_queries::queries::w2l::w2l_templates::set_w2l_template_mod_approval::set_w2l_template_mod_approval;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetW2lTemplateModApprovalPathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetW2lTemplateModApprovalRequest {
|
||||
is_approved: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SetW2lTemplateModApprovalSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SetW2lTemplateModApprovalError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for SetW2lTemplateModApprovalError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
SetW2lTemplateModApprovalError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
SetW2lTemplateModApprovalError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
SetW2lTemplateModApprovalError::NotFound => StatusCode::NOT_FOUND,
|
||||
SetW2lTemplateModApprovalError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
SetW2lTemplateModApprovalError::BadInput(reason) => reason.to_string(),
|
||||
SetW2lTemplateModApprovalError::NotAuthorized => "unauthorized".to_string(),
|
||||
SetW2lTemplateModApprovalError::NotFound => "not found".to_string(),
|
||||
SetW2lTemplateModApprovalError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for SetW2lTemplateModApprovalError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_w2l_template_mod_approval_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<SetW2lTemplateModApprovalPathInfo>,
|
||||
request: web::Json<SetW2lTemplateModApprovalRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, SetW2lTemplateModApprovalError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
SetW2lTemplateModApprovalError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(SetW2lTemplateModApprovalError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
if !user_session.can_approve_w2l_templates {
|
||||
warn!("user is not allowed to approve templates: {:?}", user_session.user_token);
|
||||
return Err(SetW2lTemplateModApprovalError::NotAuthorized);
|
||||
}
|
||||
|
||||
let template_query_result = select_w2l_template_by_token(
|
||||
&path.token,
|
||||
true, // Only mods can perform this action
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let w2l_template = match template_query_result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(SetW2lTemplateModApprovalError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(SetW2lTemplateModApprovalError::NotFound),
|
||||
Ok(Some(template)) => template,
|
||||
};
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let query_result = set_w2l_template_mod_approval(
|
||||
&path.token,
|
||||
user_session.user_token.as_str(),
|
||||
request.is_approved,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
warn!("Update w2l mod approval status DB error: {:?}", err);
|
||||
return Err(SetW2lTemplateModApprovalError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let response = SetW2lTemplateModApprovalSuccessResponse {
|
||||
success: true,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| SetW2lTemplateModApprovalError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod get_tts_inference_queue_count;
|
||||
pub mod get_w2l_inference_queue_count;
|
||||
pub mod kill_tts_inference_jobs;
|
||||
pub mod moderation_get_job_by_token_handler;
|
||||
pub mod user;
|
||||
-17
@@ -3,7 +3,6 @@ use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
use crate::http_server::deprecated_endpoints::moderation::approval::pending_w2l_templates::get_pending_w2l_templates_handler;
|
||||
use crate::http_server::deprecated_endpoints::moderation::categories::delete_category::delete_category_handler;
|
||||
use crate::http_server::deprecated_endpoints::moderation::categories::edit_category::edit_category_handler;
|
||||
use crate::http_server::deprecated_endpoints::moderation::categories::list_tts_categories_for_moderation::list_tts_categories_for_moderation_handler;
|
||||
@@ -27,7 +26,6 @@ use crate::http_server::endpoints::moderation::wallets::moderator_add_banked_bal
|
||||
use crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler;
|
||||
use crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler;
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler;
|
||||
@@ -243,11 +241,6 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::post().to(kill_tts_inference_jobs_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/w2l_inference_queue_stats")
|
||||
.route(web::get().to(get_w2l_inference_queue_count_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/kill_generic")
|
||||
.route(web::post().to(kill_generic_inference_jobs_handler))
|
||||
@@ -256,16 +249,6 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
)
|
||||
.service(
|
||||
web::scope("/pending")
|
||||
.service(
|
||||
web::resource("/w2l_templates")
|
||||
.route(web::get().to(get_pending_w2l_templates_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/w2l_inference_queue_stats")
|
||||
.route(web::get().to(get_w2l_inference_queue_count_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(
|
||||
web::scope("/stats")
|
||||
|
||||
-14
@@ -4,8 +4,6 @@ use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse};
|
||||
use actix_web::error::Error;
|
||||
use actix_web::{web, App, HttpResponse};
|
||||
|
||||
use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_inference_results::list_user_w2l_inference_results_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler;
|
||||
use crate::http_server::endpoints::tts::list_user_tts_inference_results::list_user_tts_inference_results_handler;
|
||||
use crate::http_server::endpoints::tts::list_user_tts_models::list_user_tts_models_handler;
|
||||
use crate::http_server::endpoints::users::change_password_handler::change_password_handler;
|
||||
@@ -150,17 +148,5 @@ pub fn add_user_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::get().to(list_user_tts_inference_results_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
// NB: Removed endpoint
|
||||
.service(
|
||||
web::resource("/{username}/w2l_templates")
|
||||
.route(web::get().to(list_user_w2l_templates_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
// NB: Removed endpoint
|
||||
.service(
|
||||
web::resource("/{username}/w2l_results")
|
||||
.route(web::get().to(list_user_w2l_inference_results_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+1
-100
@@ -28,19 +28,6 @@ use crate::http_server::deprecated_endpoints::media_uploads::upload_media::uploa
|
||||
use crate::http_server::deprecated_endpoints::mocap::enqueue_mocapnet::enqueue_mocapnet_handler;
|
||||
use crate::http_server::deprecated_endpoints::vocoders::get_vocoder::get_vocoder_handler;
|
||||
use crate::http_server::deprecated_endpoints::vocoders::list_vocoders::list_vocoders_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::delete_w2l_result::delete_w2l_inference_result_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::delete_w2l_template::delete_w2l_template_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::edit_w2l_result::edit_w2l_inference_result_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::edit_w2l_template::edit_w2l_template_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::enqueue_infer_w2l_with_uploads::enqueue_infer_w2l_with_uploads;
|
||||
use crate::http_server::deprecated_endpoints::w2l::enqueue_upload_w2l_template::upload_w2l_template_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::get_w2l_inference_job_status::get_w2l_inference_job_status_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::get_w2l_result::get_w2l_inference_result_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::get_w2l_template::get_w2l_template_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::get_w2l_template_use_count::get_w2l_template_use_count_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::get_w2l_upload_template_job_status::get_w2l_upload_template_job_status_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::list_w2l_templates::list_w2l_templates_handler;
|
||||
use crate::http_server::deprecated_endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler;
|
||||
use crate::http_server::endpoints::app_state::get_app_state_handler::get_app_state_handler;
|
||||
use crate::http_server::endpoints::download_job::enqueue_generic_download::enqueue_generic_download_handler;
|
||||
use crate::http_server::endpoints::download_job::get_generic_upload_job_status::get_generic_download_job_status_handler;
|
||||
@@ -95,9 +82,7 @@ where
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
let mut app = add_w2l_routes(app);
|
||||
|
||||
app = add_vocoder_routes(app); // /vocoder
|
||||
let mut app = add_vocoder_routes(app); // /vocoder
|
||||
app = add_remote_download_routes(app); // /v1/remote_downloads (prev. /retrieval, aka. "generic_download_jobs")
|
||||
app = add_category_routes(app); // /category
|
||||
app = add_api_token_routes(app); // /api_tokens
|
||||
@@ -205,90 +190,6 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== WAV2LIP ROUTES ====================
|
||||
|
||||
|
||||
fn add_w2l_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app.service(
|
||||
web::scope("/w2l")
|
||||
.service(
|
||||
web::resource("/upload")
|
||||
.route(web::post().to(upload_w2l_template_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/inference")
|
||||
.route(web::post().to(enqueue_infer_w2l_with_uploads))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/list")
|
||||
.route(web::get().to(list_w2l_templates_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/template/{token}")
|
||||
.route(web::get().to(get_w2l_template_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/template/{template_token}/count")
|
||||
.route(web::get().to(get_w2l_template_use_count_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/template/{template_token}/edit")
|
||||
.route(web::post().to(edit_w2l_template_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/template/{token}/moderate")
|
||||
.route(web::post().to(set_w2l_template_mod_approval_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/template/{token}/delete")
|
||||
.route(web::post().to(delete_w2l_template_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/result/{token}")
|
||||
.route(web::get().to(get_w2l_inference_result_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/result/{token}/edit")
|
||||
.route(web::post().to(edit_w2l_inference_result_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/result/{token}/delete")
|
||||
.route(web::post().to(delete_w2l_inference_result_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/job/{token}")
|
||||
.route(web::get().to(get_w2l_inference_job_status_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/upload_template_job/{token}")
|
||||
.route(web::get().to(get_w2l_upload_template_job_status_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== RETRIEVAL ROUTES ("GENERIC_DOWNLOAD_JOBS") ====================
|
||||
|
||||
fn add_remote_download_routes<T, B> (app: App<T>) -> App<T>
|
||||
|
||||
@@ -146,10 +146,6 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult<SetupResu
|
||||
easyenv::get_env_duration_seconds_or_default("DATABASE_TTS_CATEGORY_LIST_CACHE_TTL_SECONDS", Duration::from_secs(60))
|
||||
);
|
||||
|
||||
let w2l_template_cache = SingleItemTtlCache::create_with_duration(
|
||||
easyenv::get_env_duration_seconds_or_default("W2L_TEMPLATE_LIST_CACHE_TTL_SECONDS", Duration::from_secs(300))
|
||||
);
|
||||
|
||||
let tts_queue_length_cache = SingleItemTtlCache::create_with_duration(
|
||||
easyenv::get_env_duration_seconds_or_default("TTS_QUEUE_LENGTH_CACHE_TTL_SECONDS", Duration::from_secs(30))
|
||||
);
|
||||
@@ -291,7 +287,6 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult<SetupResu
|
||||
easyenv::get_env_duration_seconds_or_default(
|
||||
"VOICE_CONVERSION_MODEL_LIST_CACHE_TTL_SECONDS",
|
||||
Duration::from_secs(60))),
|
||||
w2l_template_list: w2l_template_cache,
|
||||
database_tts_category_list: database_tts_category_list_cache,
|
||||
tts_queue_length: tts_queue_length_cache,
|
||||
tts_model_category_assignments: tts_model_category_assignments_cache,
|
||||
|
||||
@@ -35,7 +35,6 @@ use mysql_queries::queries::generic_inference::web::get_pending_inference_job_co
|
||||
use mysql_queries::queries::media_files::list::list_featured_media_files::FeaturedMediaFileListPage;
|
||||
use mysql_queries::queries::model_categories::list_categories_query_builder::CategoryList;
|
||||
use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult;
|
||||
use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList;
|
||||
use opaque_cursors::v2::opaque_cursor_encoder_v2::OpaqueCursorEncoderV2;
|
||||
use pager::client::pager::Pager;
|
||||
use redis::Client;
|
||||
@@ -201,9 +200,6 @@ pub struct EphemeralInMemoryCaches {
|
||||
/// Contains a list of all voice conversion models.
|
||||
pub voice_conversion_model_list: SingleItemTtlCache<Vec<VoiceConversionModel>>,
|
||||
|
||||
/// Contains a list of all W2L templates.
|
||||
pub w2l_template_list: SingleItemTtlCache<Vec<W2lTemplateRecordForList>>,
|
||||
|
||||
/// Contains a list of all TTS categories in the database
|
||||
/// (before any enrichment with synthetic categories)
|
||||
/// This is used in several places (list categories, computed category assignments)
|
||||
|
||||
Reference in New Issue
Block a user