Schema changes, endpoints, routes, queries, etc.

This commit is contained in:
Brandon Thomas committed 2022-08-09 22:06:36 -04:00
1 parent f8175d7666
commit 08c05cbcf2
18 files changed
+360 -191

No files matched your search

Generated
+2
View File
@@ -1297,6 +1297,7 @@ dependencies = [
"log",
"once_cell",
"regex",
"reusable_types",
"serde",
"serde_derive",
"serde_json",
@@ -3893,6 +3894,7 @@ dependencies = [
"memory_caching",
"native-tls",
"newrelic-telemetry",
"once_cell",
"pulldown-cmark",
"r2d2_redis",
"rand 0.8.4",
@@ -18,7 +18,11 @@ CREATE TABLE generic_download_jobs (
-- If the job completes successfully, this is the result token.
-- This is only populated on a successful result.
on_success_result_token VARCHAR(32) DEFAULT NULL,
on_success_downloaded_entity_token VARCHAR(32) DEFAULT NULL,
-- The type of the object will vary based on the type of the upload,
-- and we may include heuristics that auto-detect types in the future
on_success_downloaded_entity_type VARCHAR(32) DEFAULT NULL,
-- ========== UPLOAD DETAILS ==========
@@ -98,7 +102,6 @@ CREATE TABLE generic_download_jobs (
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (uuid_idempotency_token),
KEY fk_on_success_result_token (on_success_result_token),
KEY fk_on_download_type (download_type),
KEY fk_creator_user_token (creator_user_token),
KEY index_status (status),
+1
View File
@@ -17,6 +17,7 @@ path = "src/lib.rs"
# Internal
config = { path = "../config" }
container_common = { path = "../container_common" }
reusable_types = { path = "../reusable_types" }
# External
anyhow = "1.0.40"
@@ -24,6 +24,9 @@ enum FirehoseEvent {
W2lInferenceStarted,
W2lInferenceCompleted,
GenericDownloadStarted,
GenericDownloadCompleted,
TwitterMention,
TwitterRetweet,
DiscordJoin,
@@ -51,6 +54,8 @@ impl FirehoseEvent {
FirehoseEvent::DiscordMessage => "discord_message",
FirehoseEvent::TwitchSubscribe => "twitch_subscribe",
FirehoseEvent::TwitchFollow => "twitch_follow",
FirehoseEvent::GenericDownloadStarted => "generic_download_started",
FirehoseEvent::GenericDownloadCompleted => "generic_download_completed",
}
}
}
@@ -172,6 +177,26 @@ impl FirehosePublisher {
Ok(())
}
pub async fn enqueue_generic_download(&self, user_token: &str, job_token: &str) -> AnyhowResult<()> {
let _record_id = self.insert(
FirehoseEvent::GenericDownloadStarted,
Some(user_token),
None,
Some(job_token)
).await?;
Ok(())
}
pub async fn publish_generic_download_finished(&self, user_token: &str, template_token: &str) -> AnyhowResult<()> {
let _record_id = self.insert(
FirehoseEvent::GenericDownloadCompleted,
Some(user_token),
Some(template_token),
Some(template_token)
).await?;
Ok(())
}
// =======================================================================
async fn insert(
@@ -0,0 +1,59 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::warn;
use sqlx::MySqlPool;
use container_common::anyhow_result::AnyhowResult;
use reusable_types::entity_visibility::EntityVisibility;
use reusable_types::generic_download_type::GenericDownloadType;
pub struct GenericDownloadJobStatus {
pub job_token: String,
pub status: String,
pub attempt_count: i32,
pub maybe_downloaded_entity_type: Option<String>,
pub maybe_downloaded_entity_token: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Look up job status.
/// Returns Ok(None) when the record cannot be found.
pub async fn get_generic_download_job_status(job_token: &str, mysql_pool: &MySqlPool)
-> AnyhowResult<Option<GenericDownloadJobStatus>>
{
let maybe_status = sqlx::query_as!(
GenericDownloadJobStatus,
r#"
SELECT
jobs.token as job_token,
jobs.status,
jobs.attempt_count,
jobs.on_success_downloaded_entity_token as maybe_downloaded_entity_token,
jobs.on_success_downloaded_entity_type as maybe_downloaded_entity_type,
jobs.created_at,
jobs.updated_at
FROM generic_download_jobs as jobs
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),
_ => {
warn!("error querying job record: {:?}", err);
Err(anyhow!("error querying job record: {:?}", err))
}
}
}
}
@@ -0,0 +1,58 @@
use anyhow::anyhow;
use sqlx::MySqlPool;
use container_common::anyhow_result::AnyhowResult;
use reusable_types::entity_visibility::EntityVisibility;
use reusable_types::generic_download_type::GenericDownloadType;
pub struct Args <'a> {
pub job_token: &'a str,
pub uuid_idempotency_token: &'a str,
pub download_type: GenericDownloadType,
pub download_url: &'a str,
pub title: &'a str,
pub creator_user_token: &'a str,
pub creator_ip_address: &'a str,
pub creator_set_visibility: EntityVisibility,
pub mysql_pool: &'a MySqlPool,
}
pub async fn insert_generic_download_job(args: Args<'_>) -> AnyhowResult<u64> {
let query = sqlx::query!(
r#"
INSERT INTO generic_download_jobs
SET
token = ?,
uuid_idempotency_token = ?,
download_type = ?,
download_url = ?,
title = ?,
creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
status = "pending"
"#,
args.job_token,
args.uuid_idempotency_token,
args.download_type,
args.download_url,
args.title,
args.creator_user_token,
args.creator_ip_address,
args.creator_set_visibility,
);
let query_result = query.execute(args.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
return Err(anyhow!("error inserting new generic download job: {:?}", err));
}
};
Ok(record_id)
}
@@ -0,0 +1,2 @@
pub mod get_generic_download_job_status;
pub mod insert_generic_download_job;
@@ -1,4 +1,5 @@
pub mod api_tokens;
pub mod generic_download;
pub mod health_check;
pub mod ip_bans;
pub mod model_categories;
@@ -25,6 +25,12 @@ impl RedisKeys {
format!("ttsDownloadExtraStatus:{}", inference_job_token)
}
/// We write extra in-progress status information to keys.
/// These keys should have a TTL.
pub fn generic_download_extra_status_info(inference_job_token: &str) -> String {
format!("genericDownloadExtraStatus:{}", inference_job_token)
}
/// We write extra in-progress status information to keys.
/// These keys should have a TTL.
pub fn tts_inference_extra_status_info(inference_job_token: &str) -> String {
@@ -1,10 +1,10 @@
/// Our "generic uploads" pipeline supports a wide variety of ML models and other media.
/// Each type of upload is identified by the following enum variants.
/// Our "generic downloads" pipeline supports a wide variety of ML models and other media.
/// Each type of download is identified by the following enum variants.
/// These types are present in the HTTP API and database columns as serialized here.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)]
pub enum GenericUploadType {
pub enum GenericDownloadType {
#[serde(rename = "hifigan")]
#[sqlx(rename = "hifigan")]
HifiGan,
@@ -12,11 +12,11 @@ pub enum GenericUploadType {
#[cfg(test)]
mod tests {
use crate::generic_upload_type::GenericUploadType;
use crate::generic_download_type::GenericDownloadType;
use crate::test_helpers::assert_serialization;
#[test]
fn test_serialization() {
assert_serialization(GenericUploadType::HifiGan, "hifigan");
assert_serialization(GenericDownloadType::HifiGan, "hifigan");
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
// Okay to toggle
//#![forbid(unused_imports)]
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
@@ -14,6 +14,6 @@
#[macro_use] extern crate serde_derive;
pub mod entity_visibility;
pub mod generic_upload_type;
pub mod generic_download_type;
#[cfg(test)] pub mod test_helpers;
@@ -46,6 +46,7 @@ lazy_static = "1.4.0"
lexical-sort = "0.3.1"
lfu = "0.2.5"
log = "0.4.14"
once_cell = "1.9.0"
rand = "0.8.3"
regex = "1.5.4"
time = "0.2.26"
@@ -12,71 +12,63 @@ use crate::server_state::ServerState;
use crate::validations::model_uploads::validate_model_title;
use crate::validations::passwords::validate_passwords;
use crate::validations::username::validate_username;
use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use database_queries::queries::generic_download::insert_generic_download_job::{Args, insert_generic_download_job};
use database_queries::tokens::Tokens;
use log::{info, warn, log};
use regex::Regex;
use reusable_types::generic_upload_type::GenericUploadType;
use reusable_types::entity_visibility::EntityVisibility;
use reusable_types::generic_download_type::GenericDownloadType;
use sqlx::error::DatabaseError;
use sqlx::error::Error::Database;
use sqlx::mysql::MySqlDatabaseError;
use std::fmt;
use std::sync::Arc;
#[deprecated(note = "Use `RecordVisibility` instead!")]
#[derive(Deserialize)]
pub enum CreatorSetVisibility {
/// public
Public,
/// hidden
Hidden,
/// private
Private,
}
#[derive(Deserialize)]
pub struct UploadTtsModelRequest {
pub struct EnqueueGenericDownloadRequest {
idempotency_token: String,
title: String,
download_url: String,
generic_upload_type: GenericUploadType,
generic_download_type: GenericDownloadType,
/// Not all upload types will have an associated visibility, so supplying this is optional.
/// If an upload type supports it and it is not supplied, assume a reasonable default or user
/// setting.
creator_set_visibility: Option<CreatorSetVisibility>,
creator_set_visibility: Option<EntityVisibility>,
}
#[derive(Serialize)]
pub struct UploadTtsModelSuccessResponse {
pub struct EnqueueGenericDownloadSuccessResponse {
pub success: bool,
/// This is how frontend clients can request the job execution status.
pub job_token: String,
}
#[derive(Debug)]
pub enum UploadTtsModelError {
pub enum EnqueueGenericDownloadError {
BadInput(String),
MustBeLoggedIn,
ServerError,
RateLimited,
}
impl ResponseError for UploadTtsModelError {
impl ResponseError for EnqueueGenericDownloadError {
fn status_code(&self) -> StatusCode {
match *self {
UploadTtsModelError::BadInput(_) => StatusCode::BAD_REQUEST,
UploadTtsModelError::MustBeLoggedIn => StatusCode::UNAUTHORIZED,
UploadTtsModelError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
UploadTtsModelError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
EnqueueGenericDownloadError::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueGenericDownloadError::MustBeLoggedIn => StatusCode::UNAUTHORIZED,
EnqueueGenericDownloadError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueGenericDownloadError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
UploadTtsModelError::BadInput(reason) => reason.to_string(),
UploadTtsModelError::MustBeLoggedIn => "user must be logged in".to_string(),
UploadTtsModelError::ServerError => "server error".to_string(),
UploadTtsModelError::RateLimited => "rate limited".to_string(),
EnqueueGenericDownloadError::BadInput(reason) => reason.to_string(),
EnqueueGenericDownloadError::MustBeLoggedIn => "user must be logged in".to_string(),
EnqueueGenericDownloadError::ServerError => "server error".to_string(),
EnqueueGenericDownloadError::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
@@ -84,19 +76,19 @@ impl ResponseError for UploadTtsModelError {
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for UploadTtsModelError {
impl fmt::Display for EnqueueGenericDownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn upload_tts_model_handler(
pub async fn enqueue_generic_download_handler(
http_request: HttpRequest,
request: web::Json<UploadTtsModelRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, UploadTtsModelError>
request: web::Json<EnqueueGenericDownloadRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EnqueueGenericDownloadError>
{
if let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(&http_request) {
return Err(UploadTtsModelError::RateLimited);
return Err(EnqueueGenericDownloadError::RateLimited);
}
let maybe_user_session = server_state
@@ -105,133 +97,87 @@ pub async fn upload_tts_model_handler(
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
UploadTtsModelError::ServerError
EnqueueGenericDownloadError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(UploadTtsModelError::MustBeLoggedIn);
return Err(EnqueueGenericDownloadError::MustBeLoggedIn);
}
};
if let Err(reason) = validate_idempotency_token(&request.idempotency_token) {
return Err(UploadTtsModelError::BadInput(reason));
}
if let Err(reason) = validate_model_title(&request.title) {
return Err(UploadTtsModelError::BadInput(reason));
}
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 title = request.title.trim().to_string();
let download_url = request.download_url.trim().to_string();
let creator_set_visibility = request.creator_set_visibility
.unwrap_or(EntityVisibility::Public);
if let Err(reason) = validate_idempotency_token_format(&uuid) {
return Err(EnqueueGenericDownloadError::BadInput(reason));
}
if let Err(reason) = validate_model_title(&title) {
return Err(EnqueueGenericDownloadError::BadInput(reason));
}
match is_bad_tts_model_download_url(&download_url) {
Ok(false) => {} // Ok case
Ok(true) => {
return Err(UploadTtsModelError::BadInput("Bad model download URL".to_string()));
return Err(EnqueueGenericDownloadError::BadInput("Bad model download URL".to_string()));
}
Err(err) => {
warn!("Error parsing url: {:?}", err);
return Err(UploadTtsModelError::BadInput("Bad model download URL".to_string()));
return Err(EnqueueGenericDownloadError::BadInput("Bad model download URL".to_string()));
}
}
let tts_model_type = "tacotron2".to_string();
let creator_set_visibility = "public".to_string();
// This token is returned to the client.
let job_token = Tokens::new_tts_model_upload_job()
.map_err(|e| {
warn!("Error creating token");
UploadTtsModelError::ServerError
EnqueueGenericDownloadError::ServerError
})?;
let query_result = sqlx::query!(
r#"
INSERT INTO tts_model_upload_jobs
SET
token = ?,
uuid_idempotency_token = ?,
creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
title = ?,
tts_model_type = ?,
download_url = ?,
status = "pending"
"#,
job_token.to_string(),
uuid.to_string(),
user_session.user_token.to_string(),
ip_address.to_string(),
creator_set_visibility.to_string(),
title.to_string(),
tts_model_type,
download_url,
)
.execute(&server_state.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
warn!("New tts model upload creation DB error: {:?}", err);
let record_id = insert_generic_download_job(Args {
job_token: &job_token,
uuid_idempotency_token: &uuid,
download_type: request.generic_download_type,
download_url: &download_url,
title: &title,
creator_user_token: &user_session.user_token,
creator_ip_address: &ip_address,
creator_set_visibility,
mysql_pool: &server_state.mysql_pool,
})
.await
.map_err(|err| {
warn!("New generic download creation DB error: {:?}", err);
EnqueueGenericDownloadError::ServerError
})?;
// NB: SQLSTATE[23000]: Integrity constraint violation
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
match err {
Database(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(UploadTtsModelError::ServerError);
}
};
info!("new generic download job id: {}", record_id);
info!("new model upload job id: {}", record_id);
server_state.firehose_publisher.enqueue_tts_model_upload(&user_session.user_token, &job_token)
server_state.firehose_publisher.enqueue_generic_download(&user_session.user_token, &job_token)
.await
.map_err(|e| {
warn!("error publishing event: {:?}", e);
UploadTtsModelError::ServerError
EnqueueGenericDownloadError::ServerError
})?;
let response = UploadTtsModelSuccessResponse {
let response = EnqueueGenericDownloadSuccessResponse {
success: true,
job_token: job_token.to_string(),
};
let body = serde_json::to_string(&response)
.map_err(|e| UploadTtsModelError::ServerError)?;
.map_err(|e| EnqueueGenericDownloadError::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,16 +1,17 @@
use actix_http::Error;
use actix_http::http::header;
use actix_web::cookie::Cookie;
use actix_web::HttpResponseBuilder;
use actix_web::cookie::Cookie;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use actix_web::{Responder, web, HttpResponse, error, HttpRequest, HttpMessage};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use crate::AnyhowResult;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
use derive_more::{Display, Error};
use database_queries::queries::generic_download::get_generic_download_job_status::{GenericDownloadJobStatus, get_generic_download_job_status};
use log::{info, warn, log};
use r2d2_redis::redis::Commands;
use redis_common::redis_keys::RedisKeys;
@@ -19,46 +20,46 @@ use sqlx::MySqlPool;
use sqlx::error::DatabaseError;
use sqlx::error::Error::Database;
use sqlx::mysql::MySqlDatabaseError;
use std::fmt;
use std::sync::Arc;
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct GetTtsUploadModelStatusPathInfo {
pub struct GetGenericDownloadJobStatusPathInfo {
token: String,
}
#[derive(Serialize)]
pub struct TtsUploadModelJobStatusForResponse {
pub struct GetGenericDownloadJobStatusForResponse {
pub job_token: String,
/// Primary status from the database (a state machine).
pub status: String,
// TODO: Not yet used.
/// 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_model_token: Option<String>,
pub maybe_downloaded_entity_type: Option<String>,
pub maybe_downloaded_entity_token: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize)]
pub struct GetTtsUploadModelStatusSuccessResponse {
pub struct GetGenericDownloadJobStatusSuccessResponse {
pub success: bool,
pub state: TtsUploadModelJobStatusForResponse,
pub state: GetGenericDownloadJobStatusForResponse,
}
#[derive(Debug, Display)]
pub enum GetTtsUploadModelStatusError {
#[derive(Debug)]
pub enum GetGenericDownloadJobStatusError {
NotFoundError,
ServerError,
}
#[derive(Serialize)]
pub struct TtsUploadModelJobStatusRecord {
pub struct GetGenericDownloadJobStatusRecord {
pub job_token: String,
pub status: String,
@@ -69,63 +70,52 @@ pub struct TtsUploadModelJobStatusRecord {
pub updated_at: DateTime<Utc>,
}
impl ResponseError for GetTtsUploadModelStatusError {
impl ResponseError for GetGenericDownloadJobStatusError {
fn status_code(&self) -> StatusCode {
match *self {
GetTtsUploadModelStatusError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
GetGenericDownloadJobStatusError::NotFoundError => StatusCode::NOT_FOUND,
GetGenericDownloadJobStatusError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
GetTtsUploadModelStatusError::ServerError => "server error".to_string(),
GetGenericDownloadJobStatusError::NotFoundError=> "not found".to_string(),
GetGenericDownloadJobStatusError::ServerError => "server error".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
pub async fn get_tts_upload_model_job_status_handler(
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GetGenericDownloadJobStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn get_generic_download_job_status_handler(
http_request: HttpRequest,
path: Path<GetTtsUploadModelStatusPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetTtsUploadModelStatusError>
path: Path<GetGenericDownloadJobStatusPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetGenericDownloadJobStatusError>
{
// NB: Lookup failure is Err(RowNotFound).
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_status = sqlx::query_as!(
TtsUploadModelJobStatusRecord,
r#"
SELECT
jobs.token as job_token,
let maybe_job_status = get_generic_download_job_status(
&path.token,
&server_state.mysql_pool
)
.await
.map_err(|err| {
warn!("error querying job record: {:?}", err);
return GetGenericDownloadJobStatusError::ServerError
})?;
jobs.status,
jobs.attempt_count,
jobs.on_success_result_token as maybe_model_token,
jobs.created_at,
jobs.updated_at
FROM tts_model_upload_jobs as jobs
WHERE jobs.token = ?
"#,
&path.token
)
.fetch_one(&server_state.mysql_pool)
.await; // TODO: This will return error if it doesn't exist
let record : TtsUploadModelJobStatusRecord = match maybe_status {
Ok(record) => record,
Err(err) => {
match err {
sqlx::Error::RowNotFound => {
return Err(GetTtsUploadModelStatusError::ServerError);
},
_ => {
warn!("tts template query error: {:?}", err);
return Err(GetTtsUploadModelStatusError::ServerError);
}
}
let job_status = match maybe_job_status {
Some(record) => record,
None => {
warn!("job record not found");
return Err(GetGenericDownloadJobStatusError::NotFoundError);
}
};
@@ -133,10 +123,10 @@ WHERE jobs.token = ?
.get()
.map_err(|e| {
warn!("redis error: {:?}", e);
GetTtsUploadModelStatusError::ServerError
GetGenericDownloadJobStatusError::ServerError
})?;
let extra_status_key = RedisKeys::tts_download_extra_status_info(&path.token);
let extra_status_key = RedisKeys::generic_download_extra_status_info(&path.token);
let maybe_extra_status_description : Option<String> = match redis.get(&extra_status_key) {
Ok(Some(status)) => {
Some(status)
@@ -148,23 +138,24 @@ WHERE jobs.token = ?
},
};
let model_for_response = TtsUploadModelJobStatusForResponse {
job_token: record.job_token.clone(),
status: record.status.clone(),
let model_for_response = GetGenericDownloadJobStatusForResponse {
job_token: job_status.job_token,
status: job_status.status,
maybe_extra_status_description,
attempt_count: record.attempt_count as u8,
maybe_model_token: record.maybe_model_token.clone(),
created_at: record.created_at.clone(),
updated_at: record.updated_at.clone(),
attempt_count: job_status.attempt_count as u8,
maybe_downloaded_entity_token: job_status.maybe_downloaded_entity_token,
maybe_downloaded_entity_type: job_status.maybe_downloaded_entity_type,
created_at: job_status.created_at.clone(),
updated_at: job_status.updated_at.clone(),
};
let response = GetTtsUploadModelStatusSuccessResponse {
let response = GetGenericDownloadJobStatusSuccessResponse {
success: true,
state: model_for_response,
};
let body = serde_json::to_string(&response)
.map_err(|e| GetTtsUploadModelStatusError::ServerError)?;
.map_err(|e| GetGenericDownloadJobStatusError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
@@ -1,3 +1,2 @@
pub mod enqueue_generic_download;
pub mod generic_upload_type;
pub mod get_generic_upload_job_status;
@@ -12,6 +12,8 @@ use crate::http_server::endpoints::categories::create_category::create_category_
use crate::http_server::endpoints::categories::get_category::get_category_handler;
use crate::http_server::endpoints::categories::list_tts_categories::list_tts_categories_handler;
use crate::http_server::endpoints::categories::list_tts_model_assigned_categories::list_tts_model_assigned_categories_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;
use crate::http_server::endpoints::events::list_events::list_events_handler;
use crate::http_server::endpoints::flags::design_refresh_flag::disable_design_refresh_flag_handler::disable_design_refresh_flag_handler;
use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_refresh_flag_handler::enable_design_refresh_flag_handler;
@@ -106,6 +108,7 @@ pub fn add_routes<T, B> (app: App<T, B>) -> App<T, B>
let mut app = add_moderator_routes(app); /* /moderation */
app = add_tts_routes(app); /* /tts */
app = add_w2l_routes(app); /* /w2l */
app = add_retrieval_routes(app); /* /retrieval (aka. "generic_download_jobs") */
app = add_category_routes(app); /* /category */
app = add_user_profile_routes(app); /* /user */
app = add_api_token_routes(app); /* /api_tokens */
@@ -469,6 +472,34 @@ fn add_w2l_routes<T, B> (app: App<T, B>) -> App<T, B>
)
}
// ==================== RETRIEVAL ROUTES ("GENERIC_DOWNLOAD_JOBS") ====================
fn add_retrieval_routes<T, B> (app: App<T, B>) -> App<T, B>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
app.service(
web::scope("/retrieval")
.service(
web::resource("/enqueue")
.route(web::post().to(enqueue_generic_download_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
web::resource("/job_status/{token}")
.route(web::get().to(get_generic_download_job_status_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
// ==================== CATEGORY ROUTES ====================
fn add_category_routes<T, B> (app: App<T, B>) -> App<T, B>
@@ -8,4 +8,5 @@ pub mod twitch_username;
pub mod twitter_username;
pub mod username;
pub mod username_reservations;
pub mod validate_idempotency_token_format;
pub mod website_url;
@@ -0,0 +1,43 @@
use once_cell::sync::Lazy;
use regex::Regex;
static EXPECTED_FORMAT : Lazy<Regex> = Lazy::new(|| {
// Support UUID with or without the dashes.
Regex::new(r"^(([a-zA-Z0-9]{32})|([a-zA-Z0-9\-]{36}))$").expect("should be valid regex")
});
/// Validate the idempotency token
/// Idempotency tokens are supposed to be random entropy from the frontend to prevent duplicate
/// creation/insertion requests from being sent and acknowledged by the database.
/// We expect them to be in UUID format.
pub fn validate_idempotency_token_format(token: &str) -> Result<(), String> {
if !EXPECTED_FORMAT.is_match(token) {
return Err("idempotency token does not match the expected format".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format;
#[test]
fn valid_idempotency_tokens() {
// UUID with the dashes
assert!(validate_idempotency_token_format("bf12126a-2de0-4527-bbbf-156c4c45ae21").is_ok());
assert!(validate_idempotency_token_format("220ace1b-db32-42c4-bb87-49176c85a907").is_ok());
// UUID without the dashes
assert!(validate_idempotency_token_format("220ace1bdb3242c4bb8749176c85a907").is_ok());
}
#[test]
fn invalid_idempotency_tokens() {
assert!(!validate_idempotency_token_format("").is_ok());
assert!(!validate_idempotency_token_format(" ").is_ok());
assert!(!validate_idempotency_token_format("\n\n\n\n").is_ok());
assert!(!validate_idempotency_token_format("bob").is_ok());
assert!(!validate_idempotency_token_format("220ace1bdb324").is_ok());
assert!(!validate_idempotency_token_format("220ace1bdb3242c4bb8749176c85a907ace0b0def0ff0").is_ok());
}
}