Compare commits

...
Author SHA1 Message Date
Brandon Thomas 1de0715b72 remove old wav2lip code 2026-05-21 16:10:13 -04:00
67 changed files with 9 additions and 8207 deletions
Generated
-63
View File
@@ -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"
-2
View File
@@ -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;
@@ -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;
@@ -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()))
}
@@ -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))
}
}
}
}
@@ -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)),
}
}
}
@@ -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(())
}
@@ -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;
@@ -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))
}
}
}
}
}
@@ -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)),
}
}
@@ -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
}
@@ -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)),
}
}
}
@@ -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;
@@ -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)
}
@@ -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(())
}
@@ -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))
}
}
@@ -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
}
@@ -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>,
}
@@ -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;
@@ -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,
&region_name,
&private_bucket_name,
&s3_compatible_endpoint_url,
None,
Some(bucket_timeout),
)?;
let public_bucket_client = BucketClient::create(
&access_key,
&secret_key,
&region_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(())
}
@@ -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(())
}
}
@@ -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(())
}
}
@@ -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;
@@ -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,
&region_name,
&private_bucket_name,
&s3_compatible_endpoint_url,
None,
Some(bucket_timeout),
)?;
let public_bucket_client = BucketClient::create(
&access_key,
&secret_key,
&region_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;
@@ -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(())
}
}
@@ -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 @@
pub mod pending_w2l_templates;
@@ -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())
}
@@ -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())
}
@@ -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())
}
@@ -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())
}
@@ -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))
}
@@ -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))
}
@@ -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(())
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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;
@@ -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;
@@ -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")
@@ -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()))
)
)
}
@@ -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)