Merge pull request #96 from storytold/echelon-get-media-file-endpoint

v1 of media_file GET endpoint.
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-10-13 02:40:39 -04:00
commit 9b83caece1
8 files changed
+481 -4

No files matched your search

@@ -37,7 +37,6 @@ and status IN ('pending', 'started', 'complete_failure', 'attempt_failed')
group by maybe_creator_user_token, maybe_creator_username, creator_ip_address
order by attempts desc;
-- Detailed report on most recent jobs, ordered by worst performing.
-- TODO: Determine if the problem is in downloading models. Make sure the job timer doesn't include
-- sections where models / files get downloaded.
@@ -71,8 +70,8 @@ from (
m.token = jobs.maybe_input_source_token
where
jobs.status != 'pending'
AND jobs.created_at > NOW() - INTERVAL 2000 MINUTE
AND jobs.maybe_model_type IN ('sad_talker')
AND jobs.created_at > NOW() - INTERVAL 20 MINUTE
AND jobs.maybe_model_type IN ('so_vits_svc')
order by id desc
limit 200
) as t
@@ -0,0 +1,196 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
#[derive(Serialize)]
pub struct MediaFile {
pub token: MediaFileToken,
pub media_type: MediaFileType,
// TODO: Bucket hash bits.
// TODO: Other media details (file size, mime type, dimensions, duration, etc.)
// TODO: Provenance data (product, upload vs inference, model details and foreign keys)
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
pub maybe_creator_display_name: Option<String>,
pub maybe_creator_gravatar_hash: Option<String>,
pub creator_set_visibility: Visibility,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// pub maybe_moderator_fields: Option<MediaFileModeratorFields>,
}
// /// "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 MediaFileModeratorFields {
// pub model_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>>,
// pub maybe_mod_user_token: Option<String>,
// }
#[derive(Serialize)]
pub struct MediaFileRaw {
pub token: MediaFileToken,
pub media_type: MediaFileType,
// TODO: Bucket hash bits.
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
pub maybe_creator_display_name: Option<String>,
pub maybe_creator_gravatar_hash: Option<String>,
pub creator_set_visibility: Visibility,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
//pub model_is_mod_approved: bool, // converted
//pub maybe_mod_user_token: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn get_media_file(
media_file_token: &MediaFileToken,
can_see_deleted: bool,
mysql_pool: &MySqlPool
) -> AnyhowResult<Option<MediaFile>> {
let record = if can_see_deleted {
select_including_deleted(media_file_token, mysql_pool).await
} else {
select_without_deleted(media_file_token, mysql_pool).await
};
let record = match record {
Ok(record) => record,
Err(ref err) => {
return match err {
sqlx::Error::RowNotFound => Ok(None),
_ => Err(anyhow!("database error: {:?}", err)),
}
}
};
Ok(Some(MediaFile {
token: record.token,
media_type: record.media_type,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_username: record.maybe_creator_username,
maybe_creator_display_name: record.maybe_creator_display_name,
maybe_creator_gravatar_hash: record.maybe_creator_gravatar_hash,
creator_set_visibility: record.creator_set_visibility,
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
created_at: record.created_at,
updated_at: record.updated_at,
}))
}
async fn select_including_deleted(
media_file_token: &MediaFileToken,
mysql_pool: &MySqlPool
) -> Result<MediaFileRaw, sqlx::Error> {
sqlx::query_as!(
MediaFileRaw,
r#"
SELECT
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.created_at,
m.updated_at
FROM media_files AS m
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
WHERE
m.token = ?
"#,
media_file_token
)
.fetch_one(mysql_pool)
.await
}
async fn select_without_deleted(
media_file_token: &MediaFileToken,
mysql_pool: &MySqlPool
) -> Result<MediaFileRaw, sqlx::Error> {
sqlx::query_as!(
MediaFileRaw,
r#"
SELECT
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.created_at,
m.updated_at
FROM media_files AS m
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
WHERE
m.token = ?
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
"#,
media_file_token
)
.fetch_one(mysql_pool)
.await
}
@@ -1 +1,2 @@
pub mod get_media_file;
pub mod insert_media_file_from_face_animation;
@@ -28,6 +28,45 @@ pub struct UserDetailsLight {
// In the future, we'll also support user-uploaded avatars that we store on our servers.
}
impl UserDetailsLight {
pub fn from_optional_db_fields(
maybe_user_token: Option<&UserToken>,
maybe_username: Option<&str>,
maybe_display_name: Option<&str>,
maybe_gravatar_hash: Option<&str>,
) -> Option<Self> {
Self::from_optional_db_fields_owned(
maybe_user_token.map(|u| u.clone()),
maybe_username.map(|s|s.to_string()),
maybe_display_name.map(|s|s.to_string()),
maybe_gravatar_hash.map(|s|s.to_string()),
)
}
pub fn from_optional_db_fields_owned(
maybe_user_token: Option<UserToken>,
maybe_username: Option<String>,
maybe_display_name: Option<String>,
maybe_gravatar_hash: Option<String>,
) -> Option<Self> {
match (maybe_user_token, maybe_username, maybe_display_name, maybe_gravatar_hash) {
(Some(user_token), Some(username), Some(display_name), Some(gravatar_hash)) => {
Some(Self {
default_avatar: DefaultAvatarInfo::from_username(&username),
user_token,
username,
display_name,
gravatar_hash,
})
}
_ => {
None
}
}
}
}
#[derive(Clone, Serialize)]
pub struct DefaultAvatarInfo {
pub image_index: u8,
@@ -45,3 +84,34 @@ impl DefaultAvatarInfo {
}
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
#[test]
fn test_from_optional_db_fields() {
let user_token = UserToken::new_from_str("token");
let username = "username";
let display_name = "display_name";
let gravatar_hash= "adsf";
let user_details = UserDetailsLight::from_optional_db_fields(
Some(&user_token),
Some(username),
Some(display_name),
Some(gravatar_hash)
);
let user_details = user_details.expect("Should not be optional.");
assert_eq!(user_details.user_token, user_token);
assert_eq!(user_details.username, username);
assert_eq!(user_details.display_name, display_name);
assert_eq!(user_details.gravatar_hash, gravatar_hash);
assert_eq!(user_details.default_avatar.color_index, 5);
assert_eq!(user_details.default_avatar.image_index, 16);
}
}
@@ -0,0 +1,186 @@
use std::fmt;
use std::sync::Arc;
use actix_web::{HttpMessage, HttpRequest, HttpResponse, web};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use chrono::{DateTime, Utc};
use log::warn;
use buckets::public::media_files::original_file::MediaFileBucketPath;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use mysql_queries::queries::media_files::get_media_file::get_media_file;
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct GetMediaFilePathInfo {
token: MediaFileToken,
}
#[derive(Serialize)]
pub struct GetMediaFileSuccessResponse {
pub success: bool,
pub media_file: MediaFile,
}
#[derive(Serialize)]
pub struct MediaFile {
pub token: MediaFileToken,
/// Type of media will dictate which fields are populated and what
/// the frontend should display (eg. video player vs audio player).
pub media_type: MediaFileType,
/// URL to the media file
pub public_bucket_path: String,
//// Provenance Data
//pub origin_product: MediaFileOriginProductCategory,
//pub origin_category: MediaFileOriginCategory,
//pub maybe_origin_model_type: Option<MediaFileOriginModelType>,
//pub maybe_origin_model_token: Option<String>,
//pub maybe_origin_filename: Option<String>,
// User
pub maybe_creator_user: Option<UserDetailsLight>,
pub creator_set_visibility: Visibility,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
//pub maybe_moderator_fields: Option<ModeratorFields>,
}
//#[derive(Serialize)]
//pub struct ModeratorFields {
// pub creator_ip_address: String,
// pub creator_is_banned_if_user: bool,
// pub maybe_creator_deleted_at: Option<DateTime<Utc>>,
// pub maybe_mod_deleted_at: Option<DateTime<Utc>>,
//}
#[derive(Debug)]
pub enum GetMediaFileError {
ServerError,
NotFound,
}
impl ResponseError for GetMediaFileError {
fn status_code(&self) -> StatusCode {
match *self {
GetMediaFileError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
GetMediaFileError::NotFound=> StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
GetMediaFileError::ServerError => "server error".to_string(),
GetMediaFileError::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 GetMediaFileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn get_media_file_handler(
http_request: HttpRequest,
path: Path<GetMediaFilePathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetMediaFileError>
{
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);
GetMediaFileError::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_tts_results;
// Moderators get to see all the fields.
is_moderator = user_session.can_delete_other_users_tts_results
|| user_session.can_edit_other_users_tts_models;
}
let result = get_media_file(
&path.token,
show_deleted_results,
&server_state.mysql_pool
).await;
let result = match result {
Err(e) => {
warn!("query error: {:?}", e);
return Err(GetMediaFileError::ServerError);
}
Ok(None) => return Err(GetMediaFileError::NotFound),
Ok(Some(result)) => result,
};
//if let Some(moderator_fields) = 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.model_creator_is_banned || moderator_fields.result_creator_is_banned_if_user)
// && !is_moderator{
// return Err(GetMediaFileError::NotFound);
// }
//}
//if !is_moderator {
// result.maybe_moderator_fields = None;
//}
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&result.public_bucket_directory_hash,
result.maybe_public_bucket_prefix.as_deref(),
result.maybe_public_bucket_extension.as_deref())
.get_full_object_path_str()
.to_string();
let response = GetMediaFileSuccessResponse {
success: true,
media_file: MediaFile {
token: result.token,
media_type: result.media_type,
public_bucket_path,
maybe_creator_user: UserDetailsLight::from_optional_db_fields_owned(
result.maybe_creator_user_token,
result.maybe_creator_username,
result.maybe_creator_display_name,
result.maybe_creator_gravatar_hash,
),
creator_set_visibility: result.creator_set_visibility,
created_at: result.created_at,
updated_at: result.updated_at,
},
};
let body = serde_json::to_string(&response)
.map_err(|e| GetMediaFileError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -0,0 +1 @@
pub mod get_media_file;
@@ -8,6 +8,7 @@ pub mod flags;
pub mod inference_job;
pub mod investor_demo;
pub mod leaderboard;
pub mod media_files;
pub mod media_uploads;
pub mod misc;
pub mod moderation;
@@ -23,5 +24,5 @@ pub mod user_ratings;
pub mod vocoders;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod w2l;
pub mod voice_designer;
@@ -34,6 +34,7 @@ use crate::http_server::endpoints::inference_job::get_pending_inference_job_coun
use crate::http_server::endpoints::investor_demo::disable_demo_mode_handler::disable_demo_mode_handler;
use crate::http_server::endpoints::investor_demo::enable_demo_mode_handler::enable_demo_mode_handler;
use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler;
use crate::http_server::endpoints::media_files::get_media_file::get_media_file_handler;
use crate::http_server::endpoints::media_uploads::list_user_media_uploads_of_type::list_user_media_uploads_of_type_handler;
use crate::http_server::endpoints::media_uploads::upload_audio::upload_audio_handler;
use crate::http_server::endpoints::media_uploads::upload_image::upload_image_handler;
@@ -163,6 +164,7 @@ pub fn add_routes<T, B> (app: App<T>) -> App<T>
app = add_investor_demo_routes(app); /* /demo_mode */ // TODO: DEFINITELY TEMPORARY
app = add_flag_routes(app); /* /flag */
app = add_desktop_app_routes(app); /* /v1/vc/... */
app = add_media_file_routes(app); /* /v1/media_files/... */
app = add_media_upload_routes(app); /* /v1/media_upload/... */
app = add_trending_routes(app); /* /v1/trending/... */
app = add_user_rating_routes(app); /* /v1/user_rating/... */
@@ -956,6 +958,27 @@ fn add_desktop_app_routes<T, B> (app: App<T>) -> App<T>
)
}
// ==================== MEDIA FILE ROUTES ====================
fn add_media_file_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("/v1/media_files")
.service(web::resource("/file/{token}")
.route(web::get().to(get_media_file_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
// ==================== MEDIA UPLOAD ROUTES ====================
fn add_media_upload_routes<T, B> (app: App<T>) -> App<T>