wip create w2l inference job

This commit is contained in:
Brandon Thomas committed 2021-05-28 01:00:36 -04:00
1 parent eecaa07638
commit 074b985222
11 files changed
+566 -2

No files matched your search

Generated
+39
View File
@@ -138,6 +138,24 @@ dependencies = [
"syn",
]
[[package]]
name = "actix-multipart"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3"
dependencies = [
"actix-service",
"actix-utils 2.0.0",
"actix-web",
"bytes 0.5.6",
"derive_more",
"futures-util",
"httparse",
"log",
"mime",
"twoway",
]
[[package]]
name = "actix-router"
version = "0.2.7"
@@ -2967,6 +2985,7 @@ dependencies = [
"actix-cors",
"actix-files",
"actix-http",
"actix-multipart",
"actix-rt 2.2.0",
"actix-utils 3.0.0",
"actix-web",
@@ -3001,6 +3020,7 @@ dependencies = [
"time 0.2.26",
"tokio 0.2.25",
"toml",
"uuid",
]
[[package]]
@@ -3359,6 +3379,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "twoway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
dependencies = [
"memchr",
"unchecked-index",
]
[[package]]
name = "typenum"
version = "1.13.0"
@@ -3371,6 +3401,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]]
name = "unicase"
version = "2.6.0"
@@ -3450,6 +3486,9 @@ name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.2",
]
[[package]]
name = "v_escape"
+2
View File
@@ -45,6 +45,7 @@ log = "0.4.14"
rand = "0.8.3"
regex = "1.5.4"
time = "0.2.26"
uuid = { version = "0.8.2", features = ["v4"] }
# Runtime
# NB(1): We switched to Tokio from `async-std` for broader ecosystem support.
@@ -79,6 +80,7 @@ tempdir = "0.3.7"
actix-cors = "0.5.4"
actix-files= "0.5"
actix-http = "2.2.0"
actix-multipart = "0.3.0" # TODO: Ready for 0.4 upgrade
actix-rt = "2.2.0"
actix-utils = "3.0.0"
actix-web = "3.3.2"
@@ -104,7 +104,7 @@ CREATE TABLE w2l_template_upload_jobs (
-- Idempotency token
uuid_idempotency_token VARCHAR(36) NOT NULL,
-- Foreign key to user
-- Foreign key to user
creator_user_token VARCHAR(32) NOT NULL,
-- For abuse tracking.
@@ -3,6 +3,12 @@
CREATE TABLE tts_inference_jobs (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Idempotency token from client
uuid_idempotency_token VARCHAR(36) NOT NULL,
-- ========== INFERENCE DETAILS ==========
-- The model to use.
@@ -55,6 +61,8 @@ CREATE TABLE tts_inference_jobs (
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (uuid_idempotency_token),
KEY fk_model_token (model_token),
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
KEY index_status (status),
@@ -65,6 +73,12 @@ CREATE TABLE tts_inference_jobs (
CREATE TABLE w2l_inference_jobs (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Idempotency token from client
uuid_idempotency_token VARCHAR(36) NOT NULL,
-- ========== INFERENCE DETAILS ==========
-- The W2L template to use
@@ -100,6 +114,10 @@ CREATE TABLE w2l_inference_jobs (
'private'
) NOT NULL DEFAULT 'public',
-- Flags to disable branding
disable_end_bump BOOL NOT NULL DEFAULT false,
disable_watermark BOOL NOT NULL DEFAULT false,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
@@ -124,6 +142,8 @@ CREATE TABLE w2l_inference_jobs (
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (uuid_idempotency_token),
KEY fk_maybe_w2l_template_token (maybe_w2l_template_token),
KEY fk_maybe_tts_inference_result_token (maybe_tts_inference_result_token),
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
@@ -0,0 +1,347 @@
//use bytes::{Bytes, BytesMut};
use actix_http::http::header;
use actix_multipart::{Multipart, Field};
use actix_web::body::ResponseBody::Body;
use actix_web::dev::HttpResponseBuilder;
use actix_web::http::StatusCode;
use actix_web::http::header::ContentDisposition;
use actix_web::web::{Data, Json, BytesMut};
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer, HttpRequest, Either, ResponseError};
use anyhow::anyhow;
use crate::buckets::bucket_client::BucketClient;
use crate::buckets::bucket_paths::hash_to_bucket_path;
use crate::http_server::web_utils::ip_address::get_request_ip;
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_boolean;
use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_text;
use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_bytes;
use crate::server_state::ServerState;
use crate::util::anyhow_result::AnyhowResult;
use crate::util::random_prefix_crockford_token::random_prefix_crockford_token;
use crate::util::random_token::random_token;
use crate::util::random_uuid::generate_random_uuid;
use derive_more::{Display, Error};
use futures::{StreamExt, TryStreamExt};
use log::{warn, info};
use sqlx::MySqlPool;
use sqlx::error::Error::Database;
use std::io::Write;
use std::sync::Arc;
use uuid::Uuid;
const BUCKET_AUDIO_FILE_NAME : &'static str = "input_audio_file";
const BUCKET_IMAGE_FILE_NAME: &'static str = "input_image_file";
const BUCKET_VIDEO_FILE_NAME : &'static str = "input_video_file";
const MIN_BYTES : usize = 10;
const MAX_BYTES : usize = 1024 * 1024 * 20;
/// Just to query for existence
#[derive(Serialize)]
pub struct W2lTemplateExistenceRecord {
pub template_token: String,
}
#[derive(Serialize)]
pub struct InferW2lWithUploadSuccessResponse {
pub success: bool,
pub job_token: String,
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub success: bool,
pub error_reason: String,
}
#[derive(Debug, Display)]
pub enum InferW2lWithUploadError {
BadInput(String),
EmptyFileUploaded,
ServerError,
}
impl ResponseError for InferW2lWithUploadError {
fn status_code(&self) -> StatusCode {
match *self {
InferW2lWithUploadError::BadInput(_) => StatusCode::BAD_REQUEST,
InferW2lWithUploadError::EmptyFileUploaded => StatusCode::BAD_REQUEST,
InferW2lWithUploadError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
InferW2lWithUploadError::BadInput(reason) => reason.to_string(),
InferW2lWithUploadError::EmptyFileUploaded => "empty file uploaded".to_string(),
InferW2lWithUploadError::ServerError => "server error".to_string(),
};
let response = ErrorResponse {
success: false,
error_reason,
};
let body = match serde_json::to_string(&response) {
Ok(json) => json,
Err(_) => "{}".to_string(),
};
HttpResponseBuilder::new(self.status_code())
.set_header(header::CONTENT_TYPE, "application/json")
.body(body)
}
}
/// 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<Json<UploadResponse>, HttpResponse>
-> Result<HttpResponse, InferW2lWithUploadError>
{
// ==================== TODO: READ IP AND RATE LIMIT ==================== //
let ip_address = get_request_ip(&http_request);
// TODO: Check IP address holds
// TODO: Rate Limiting!
/*let mut hold_exists = handler_state.redis_client.get_ip_hold(&ip_address)
.map_err(|err| HttpResponse::InternalServerError()
.body(format!("Error: {:?}", err)))?;
match ip_address.as_ref() {
"127.0.0.1" => {
warn!("Allowing IP address hold bypass for {}", &ip_address);
hold_exists = false
},
_ => {},
}
if hold_exists {
return Err(HttpResponse::TooManyRequests().body("you already have pending requests").into())
}*/
// ==================== READ 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;
while let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition: ContentDisposition = field.content_disposition().unwrap();
let field_name = content_disposition.get_name().unwrap_or("");
let filename = content_disposition.get_filename().unwrap_or("");
match field_name {
"uuid_idempotency_token" => {
// Form text field.
maybe_uuid_idempotency_token = read_multipart_field_as_text(&mut field).await
.map_err(|_| InferW2lWithUploadError::ServerError)?;
},
"template_token" => {
// Form text field.
maybe_template_token = read_multipart_field_as_text(&mut field).await
.map_err(|_| 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(|_| InferW2lWithUploadError::ServerError)?;
let bytes = match maybe_bytes {
Some(bytes) => bytes,
None => {
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_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()));
}
// ==================== READ SESSION ==================== //
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);
InferW2lWithUploadError::ServerError
})?;
let mut maybe_user_token : Option<String> = maybe_user_session
.as_ref()
.map(|user_session| user_session.user_token.to_string());
// ==================== TODO: UPLOAD AUDIO FILE ==================== //
let upload_uuid = generate_random_uuid();
let audio_upload_bucket_path = hash_to_bucket_path(
&upload_uuid,
Some("/audio_uploads") // TODO: pull from configs
).map_err(|e| {
warn!("Hash bucket path error: {:?}", e);
InferW2lWithUploadError::ServerError
})?;
// TODO: Upload audio to bucket!
// ==================== SAVE JOB RECORD ==================== //
// This token is returned to the client.
let job_token = random_prefix_crockford_token("TIJ:", 32)
.map_err(|e| {
warn!("Error creating token");
InferW2lWithUploadError::ServerError
})?;
info!("Creating w2l inference job record...");
let query_result = sqlx::query!(
r#"
INSERT INTO w2l_inference_jobs
SET
token = ?,
uuid_idempotency_token = ?,
maybe_w2l_template_token = ?,
maybe_audio_bucket_location = ?,
maybe_creator_user_token = ?,
creator_ip_address = ?,
disable_end_bump = false,
creator_set_visibility = "public",
status = "pending"
"#,
job_token.to_string(),
uuid_idempotency_token.to_string(),
maybe_template_token.clone(),
Some(audio_upload_bucket_path.clone()), // TODO THIS ISN'T WIDE ENOUGH
maybe_user_token.clone(),
ip_address.to_string()
)
.execute(&server_state.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
warn!("New w2l template upload 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(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(InferW2lWithUploadError::ServerError);
}
};
// TODO: IP Address holds.
//handler_state.redis_client.set_ip_hold(&ip_address)
// .map_err(|err| HttpResponse::InternalServerError()
// .body(format!("Redis Err: {:?}", err)))?;
let response = InferW2lWithUploadSuccessResponse {
success: true,
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))
}
async fn check_template_exists(template_token: &str, mysql_pool: &MySqlPool) -> AnyhowResult<bool>
{
// NB: Lookup failure is Err(RowNotFound).
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_template = sqlx::query_as!(
W2lTemplateExistenceRecord,
r#"
SELECT
token as template_token
FROM w2l_templates
WHERE token = ?
AND deleted_at IS NULL
"#,
&template_token
)
.fetch_one(mysql_pool)
.await; // TODO: This will return error if it doesn't exist
let record_exists = match maybe_template {
Ok(record) => {
true
},
Err(err) => {
match err {
RowNotFound => {
false
},
_ => {
warn!("Infer w2l query error: {:?}", err);
return Err(anyhow!("infer w2l query error: {:?}", err));
}
}
}
};
Ok(record_exists)
}
+1
View File
@@ -1,4 +1,5 @@
pub mod enqueue_infer_w2l;
pub mod enqueue_infer_w2l_with_uploads;
pub mod enqueue_upload_w2l_template;
pub mod get_w2l_template;
pub mod list_w2l_templates;
+1
View File
@@ -1,3 +1,4 @@
pub mod cookie_manager;
pub mod ip_address;
pub mod read_multipart_field_bytes;
pub mod session_checker;
@@ -0,0 +1,66 @@
//use bytes::BytesMut;
use actix_multipart::Field;
use actix_web::web::BytesMut;
use anyhow::anyhow;
use futures::{StreamExt, TryStreamExt};
use log::warn;
const MIN_BYTES : usize = 10;
const MAX_BYTES : usize = 1024 * 1024 * 20;
/// Read any field from a multipart form as bytes, but do some error checking.
pub async fn checked_read_multipart_bytes(field: &mut Field) -> anyhow::Result<Option<BytesMut>> {
let bytes = read_multipart_field_bytes(field).await?;
if bytes.is_empty() {
// This field is empty
return Ok(None);
}
if bytes.len() < MIN_BYTES {
warn!("Bytes of upload too few: {}", bytes.len());
return Err(anyhow!("Too few bytes"));
}
if bytes.len() > MAX_BYTES {
warn!("Bytes of upload too much: {}", bytes.len());
return Err(anyhow!("Too many bytes"));
}
Ok(Some(bytes))
}
/// Read any field from a multipart form as bytes.
pub async fn read_multipart_field_bytes(field: &mut Field) -> anyhow::Result<BytesMut> {
let mut bytes = BytesMut::new();
while let Some(chunk) = field.next().await {
let chunk_data = chunk.map_err(|err| anyhow!("Error: {:?}", err))?;
bytes.extend_from_slice(&chunk_data);
}
Ok(bytes)
}
/// This reads inputs, text boxes, etc. because form-multipart is hard.
pub async fn read_multipart_field_as_text(field: &mut Field) -> anyhow::Result<Option<String>> {
let bytes = read_multipart_field_bytes(field).await?;
let value = String::from_utf8(bytes.to_vec())?;
if value.is_empty() || value == "none" {
return Ok(None);
}
Ok(Some(value))
}
/// Read a html form boolean (eg. input type=checkbox); assumes default is false.
pub async fn read_multipart_field_as_boolean(field: &mut Field) -> anyhow::Result<bool> {
let result = read_multipart_field_as_text(field).await?;
Ok(match result.as_ref().map(String::as_str) {
Some("true") => true,
Some("TRUE") => true,
Some("false") => false,
Some("FALSE") => false,
None => false,
_ => true,
})
}
+3 -1
View File
@@ -1,4 +1,6 @@
pub mod anyhow_result;
pub mod email_to_gravatar;
pub mod filesystem;
pub mod random_prefix_crockford_token;
pub mod random_token;
pub mod email_to_gravatar;
pub mod random_uuid;
+77
View File
@@ -0,0 +1,77 @@
use rand::Rng;
use anyhow::anyhow;
use crate::util::anyhow_result::AnyhowResult;
// Crockford characters
const CROCKFORD_CHARSET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
// Random part can't be any less than this.
const MIN_ENTROPY_LENGTH : usize = 8;
pub fn random_prefix_crockford_token(prefix: &str, length: usize) -> AnyhowResult<String> {
let mut prefix = prefix.trim().to_string();
if prefix.is_empty() {
return Err(anyhow!("prefix not set; use other crockford functions"));
}
if !prefix.ends_with(":") {
prefix = format!("{}:", prefix);
}
let entropy_length : usize = length.saturating_sub(prefix.len());
if entropy_length < MIN_ENTROPY_LENGTH {
return Err(anyhow!("not enough entropy in token"));
}
let mut rng = rand::thread_rng();
let entropy_part: String = (0..entropy_length)
.map(|_| {
let idx = rng.gen_range(0..CROCKFORD_CHARSET.len());
CROCKFORD_CHARSET[idx] as char
})
.collect();
let token = format!("{}{}", prefix, entropy_part);
Ok(token)
}
#[cfg(test)]
mod tests {
use crate::util::random_prefix_crockford_token::random_prefix_crockford_token;
#[test]
fn no_prefix_error() {
assert!(random_prefix_crockford_token("", 0).is_err());
assert!(random_prefix_crockford_token("", 10).is_err());
assert!(random_prefix_crockford_token("", 32).is_err());
}
#[test]
fn not_enough_entropy_error() {
assert!(random_prefix_crockford_token("foo", 0).is_err());
assert!(random_prefix_crockford_token("foo", 5).is_err());
assert!(random_prefix_crockford_token("foo", 11).is_err());
assert!(random_prefix_crockford_token("foo:", 11).is_err());
}
#[test]
fn starts_with() {
assert_eq!(random_prefix_crockford_token("w", 10).unwrap().starts_with("w:"));
assert_eq!(random_prefix_crockford_token("w", 10).unwrap().starts_with("w:"));
assert_eq!(random_prefix_crockford_token("prefix", 16).unwrap().starts_with("prefix:"));
assert_eq!(random_prefix_crockford_token("prefix:", 16).unwrap().starts_with("prefix:"));
}
#[test]
fn length() {
assert_eq!(random_prefix_crockford_token("w", 10).len(), 10);
assert_eq!(random_prefix_crockford_token("w:", 10).len(), 10);
assert_eq!(random_prefix_crockford_token("prefix", 16).len(), 16);
assert_eq!(random_prefix_crockford_token("prefix:", 16).len(), 16);
assert_eq!(random_prefix_crockford_token("asdf:", 32).len(), 32);
assert_eq!(random_prefix_crockford_token("asdf", 32).len(), 32);
}
}
+9
View File
@@ -0,0 +1,9 @@
use uuid::Uuid;
pub fn generate_random_uuid() -> String {
let uuid = Uuid::new_v4();
let mut buffer = Uuid::encode_buffer();
let hyphenated_uuid = uuid.to_hyphenated()
.encode_lower(&mut buffer);
hyphenated_uuid.to_string()
}