mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad5fdc5c8c | ||
|
|
9476589fc0 |
Generated
+27
@@ -3934,6 +3934,20 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mp4"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"num-rational 0.4.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "my-workspace-hack"
|
||||
version = "0.1.0"
|
||||
@@ -4260,6 +4274,7 @@ dependencies = [
|
||||
"num-bigint 0.4.4",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6441,6 +6456,7 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid 0.8.2",
|
||||
"videos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7771,6 +7787,17 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "videos"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"log",
|
||||
"mp4",
|
||||
"my-workspace-hack",
|
||||
"testing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
|
||||
@@ -48,6 +48,7 @@ members = [
|
||||
"crates/lib/files/images",
|
||||
"crates/lib/files/media",
|
||||
"crates/lib/files/mimetypes",
|
||||
"crates/lib/files/videos",
|
||||
"crates/lib/idempotency",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/logging",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files DROP COLUMN maybe_sample_rate;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN maybe_sample_rate FLOAT DEFAULT NULL
|
||||
AFTER maybe_duration_millis;
|
||||
@@ -90,6 +90,9 @@ CREATE TABLE media_files (
|
||||
-- The duration in millis, if audio or video.
|
||||
maybe_duration_millis INT(10) DEFAULT NULL,
|
||||
|
||||
-- Frame rate for video, sampling rate for audio (if present)
|
||||
maybe_sample_rate FLOAT DEFAULT NULL,
|
||||
|
||||
-- Audio encoder details
|
||||
-- Only present if the file was audio or a video that had audio.
|
||||
maybe_audio_encoding VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "videos"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
# NB: Public "videos" crate doesn't exist; "video" crate looks like it's not useful
|
||||
name = "videos"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Internal
|
||||
errors = { path = "../../errors" }
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
my-workspace-hack = { version = "0.1", path = "../../../my-workspace-hack" }
|
||||
|
||||
# External
|
||||
mp4 = "0.14.0"
|
||||
log = "0.4.20"
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../testing" }
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
||||
use log::warn;
|
||||
use mp4::TrackType;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct Mp4Info {
|
||||
/// Framerate of the longest video track.
|
||||
pub framerate: f64,
|
||||
/// Duration of the entire mp4.
|
||||
pub duration_millis: u128,
|
||||
}
|
||||
|
||||
pub fn get_mp4_info<T: Seek + Read>(reader: T, file_size: u64) -> AnyhowResult<Mp4Info> {
|
||||
let mp4 = mp4::Mp4Reader::read_header(reader, file_size)?;
|
||||
|
||||
let mut framerate = 0.0;
|
||||
let mut longest_duration = Duration::from_secs(0);
|
||||
|
||||
for track in mp4.tracks().values() {
|
||||
match track.track_type() {
|
||||
Ok(TrackType::Video) => {
|
||||
if track.duration() > longest_duration {
|
||||
longest_duration = track.duration();
|
||||
framerate = track.frame_rate();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error determining track type: {:?}", err);
|
||||
continue;
|
||||
},
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Mp4Info {
|
||||
framerate,
|
||||
duration_millis: mp4.duration().as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
use crate::get_mp4_info::get_mp4_info;
|
||||
|
||||
fn test_file(path_from_repo_root: &str) -> PathBuf {
|
||||
// https://doc.rust-lang.org/cargo/reference/environment-variables.html
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push(format!("../../../{}", path_from_repo_root));
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_decode_mp4() {
|
||||
let filename = test_file_path("test_data/video/mp4/golden_sun_garoh.mp4")
|
||||
.expect("path should exist");
|
||||
|
||||
let file = File::open(filename).expect("file should open");
|
||||
let size = file.metadata().expect("should be able to grab metadata").len();
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let info = get_mp4_info(reader, size).expect("mp4 reader should work");
|
||||
|
||||
assert_eq!(info.framerate, 30.0);
|
||||
assert_eq!(info.duration_millis, 15168);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! videos
|
||||
//!
|
||||
//! Library functions related to processing videos.
|
||||
//!
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
|
||||
|
||||
// Okay to toggle
|
||||
#![forbid(unreachable_patterns)]
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod get_mp4_info;
|
||||
+5
@@ -34,6 +34,7 @@ pub struct InsertMediaFileFromUploadArgs<'a> {
|
||||
pub maybe_mime_type: Option<&'a str>,
|
||||
pub file_size_bytes: u64,
|
||||
pub duration_millis: u64,
|
||||
pub maybe_sample_rate: Option<f32>,
|
||||
pub sha256_checksum: &'a str,
|
||||
|
||||
// TODO: Media duration.
|
||||
@@ -98,6 +99,8 @@ SET
|
||||
maybe_mime_type = ?,
|
||||
file_size_bytes = ?,
|
||||
|
||||
maybe_sample_rate = ?,
|
||||
|
||||
checksum_sha2 = ?,
|
||||
|
||||
public_bucket_directory_hash = ?,
|
||||
@@ -122,6 +125,8 @@ SET
|
||||
args.maybe_mime_type,
|
||||
args.file_size_bytes,
|
||||
|
||||
args.maybe_sample_rate,
|
||||
|
||||
args.sha256_checksum,
|
||||
|
||||
args.public_bucket_directory_hash,
|
||||
|
||||
@@ -53,6 +53,7 @@ tts_common = { path = "../../../lib/tts_common" }
|
||||
twitch_common = { path = "../../../lib/api_clients/twitch_common" }
|
||||
url_config = { path = "../../../lib/url_config" }
|
||||
user_input_common = { path = "../../../lib/user_input_common" }
|
||||
videos = { path = "../../../lib/files/videos" }
|
||||
|
||||
# Internal Components
|
||||
billing_component = { path = "../../plugins/billing" }
|
||||
|
||||
+2
-1
@@ -200,6 +200,7 @@ pub async fn create_scene_handler(
|
||||
media_file_type: MediaFileType::SceneRon,
|
||||
maybe_mime_type: Some(mime_type),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_sample_rate: None,
|
||||
duration_millis: 0, // None
|
||||
sha256_checksum: &hash,
|
||||
public_bucket_directory_hash: public_upload_path.get_object_hash(),
|
||||
@@ -209,7 +210,7 @@ pub async fn create_scene_handler(
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("New generic download creation DB error: {:?}", err);
|
||||
warn!("New file creation DB error: {:?}", err);
|
||||
CreateSceneError::ServerError
|
||||
})?;
|
||||
|
||||
|
||||
+1
@@ -332,6 +332,7 @@ pub async fn process_upload_media_file(
|
||||
media_file_type,
|
||||
maybe_mime_type: Some(mime_type),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_sample_rate: None,
|
||||
duration_millis: 0, // TODO
|
||||
sha256_checksum: &hash,
|
||||
public_bucket_directory_hash: public_upload_path.get_object_hash(),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod list_featured_media_files;
|
||||
pub mod list_media_files;
|
||||
pub mod list_media_files_by_batch_token;
|
||||
pub mod list_media_files_for_user;
|
||||
@@ -1,10 +1,7 @@
|
||||
pub mod delete_media_file;
|
||||
pub mod drain_multipart_request;
|
||||
pub mod get_media_file;
|
||||
pub mod list_featured_media_files;
|
||||
pub mod list_media_files;
|
||||
pub mod list_media_files_by_batch_token;
|
||||
pub mod list_media_files_for_user;
|
||||
pub mod list;
|
||||
pub mod update_media_file;
|
||||
pub mod upload;
|
||||
pub mod upload_media_file;
|
||||
pub mod upload_video;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod drain_multipart_request;
|
||||
pub mod process_upload_media_file;
|
||||
pub mod upload_error;
|
||||
pub mod upload_media_file_handler;
|
||||
|
||||
+1
@@ -333,6 +333,7 @@ pub async fn process_upload_media_file(
|
||||
maybe_mime_type: Some(mime_type),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
duration_millis: 0, // TODO
|
||||
maybe_sample_rate: None,
|
||||
sha256_checksum: &hash,
|
||||
public_bucket_directory_hash: public_upload_path.get_object_hash(),
|
||||
maybe_public_bucket_prefix: PREFIX,
|
||||
|
||||
+14
-12
@@ -4,27 +4,29 @@ use futures::TryStreamExt;
|
||||
use log::warn;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken;
|
||||
|
||||
use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text};
|
||||
|
||||
pub struct UploadMediaRequest {
|
||||
pub struct MediaFileUploadData {
|
||||
pub uuid_idempotency_token: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_bytes: Option<BytesMut>,
|
||||
pub media_source: MediaSource,
|
||||
pub media_source: MediaFileUploadSource,
|
||||
}
|
||||
|
||||
/// Where the frontend tells us the file came from.
|
||||
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||
pub enum MediaSource {
|
||||
pub enum MediaFileUploadSource {
|
||||
Unknown,
|
||||
/// Eg. from the user's filesystem
|
||||
UserFile,
|
||||
/// Eg. the web audio API in Javascript.
|
||||
UserDeviceApi,
|
||||
}
|
||||
|
||||
/// Pull common parts out of multipart media HTTP requests, typically for handling file uploads.
|
||||
pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> AnyhowResult<UploadMediaRequest> {
|
||||
pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> AnyhowResult<MediaFileUploadData> {
|
||||
let mut uuid_idempotency_token = None;
|
||||
let mut file_bytes = None;
|
||||
let mut file_name = None;
|
||||
@@ -45,7 +47,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
Some("uuid_idempotency_token") => {
|
||||
uuid_idempotency_token = read_multipart_field_as_text(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading idempotency token: {:}", e);
|
||||
warn!("Error reading idempotency token: {:}", &e);
|
||||
e
|
||||
})?;
|
||||
},
|
||||
@@ -53,14 +55,14 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
file_name = field_filename.clone();
|
||||
file_bytes = checked_read_multipart_bytes(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading audio upload: {:}", e);
|
||||
warn!("Error reading audio upload: {:}", &e);
|
||||
e
|
||||
})?;
|
||||
},
|
||||
Some("source") => {
|
||||
media_source = read_multipart_field_as_text(&mut field).await
|
||||
.map_err(|e| {
|
||||
warn!("Error reading source: {:}", e);
|
||||
warn!("Error reading source: {:}", &e);
|
||||
e
|
||||
})?;
|
||||
},
|
||||
@@ -69,18 +71,18 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
}
|
||||
|
||||
let media_source = match media_source.as_deref() {
|
||||
Some("device") => MediaSource::UserDeviceApi,
|
||||
Some("file") => MediaSource::UserFile,
|
||||
Some("device") => MediaFileUploadSource::UserDeviceApi,
|
||||
Some("file") => MediaFileUploadSource::UserFile,
|
||||
_ => {
|
||||
if file_name.as_deref() == Some("blob") {
|
||||
MediaSource::UserDeviceApi
|
||||
MediaFileUploadSource::UserDeviceApi
|
||||
} else {
|
||||
MediaSource::Unknown
|
||||
MediaFileUploadSource::Unknown
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(UploadMediaRequest {
|
||||
Ok(MediaFileUploadData {
|
||||
uuid_idempotency_token,
|
||||
file_name,
|
||||
file_bytes,
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod drain_multipart_request;
|
||||
pub mod upload_error;
|
||||
pub mod upload_video_media_file_handler;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::HttpResponse;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum VideoMediaFileUploadError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
MustBeLoggedIn,
|
||||
ServerError,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl ResponseError for VideoMediaFileUploadError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
VideoMediaFileUploadError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
VideoMediaFileUploadError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
VideoMediaFileUploadError::MustBeLoggedIn => StatusCode::UNAUTHORIZED,
|
||||
VideoMediaFileUploadError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
VideoMediaFileUploadError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VideoMediaFileUploadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
use std::collections::HashSet;
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use buckets::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
|
||||
use mimetypes::mimetype_to_extension::mimetype_to_extension;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::create::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use videos::get_mp4_info::get_mp4_info;
|
||||
|
||||
use crate::http_server::endpoints::engine::create_scene_handler::{CreateSceneError, CreateSceneSuccessResponse};
|
||||
use crate::http_server::endpoints::media_files::upload_video::drain_multipart_request::drain_multipart_request;
|
||||
use crate::http_server::endpoints::media_files::upload_video::drain_multipart_request::MediaFileUploadSource;
|
||||
use crate::http_server::endpoints::media_files::upload_video::upload_error::VideoMediaFileUploadError;
|
||||
use crate::server_state::ServerState;
|
||||
use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UploadVideoMediaSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
|
||||
static ALLOWED_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
// Video
|
||||
"video/mp4", // NB: Only mp4 for now.
|
||||
])
|
||||
});
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/media_files/upload/video",
|
||||
responses(
|
||||
(status = 200, description = "Success Update", body = UploadVideoMediaSuccessResponse),
|
||||
(status = 400, description = "Bad input", body = MediaFileUploadError),
|
||||
(status = 401, description = "Not authorized", body = MediaFileUploadError),
|
||||
(status = 429, description = "Too many requests", body = MediaFileUploadError),
|
||||
(status = 500, description = "Server error", body = MediaFileUploadError),
|
||||
),
|
||||
params(
|
||||
("request" = (), description = "Ask Brandon. This is form-multipart."),
|
||||
)
|
||||
)]
|
||||
pub async fn upload_video_media_file_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
mut multipart_payload: Multipart,
|
||||
) -> Result<HttpResponse, VideoMediaFileUploadError> {
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("MySql pool error: {:?}", err);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== READ SESSION ==================== //
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Session checker error: {:?}", e);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// ==================== BANNED USERS ==================== //
|
||||
|
||||
if let Some(ref user) = maybe_user_session {
|
||||
if user.is_banned {
|
||||
return Err(VideoMediaFileUploadError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RATE LIMIT ==================== //
|
||||
|
||||
let rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.file_upload_logged_out,
|
||||
Some(ref _session) => &server_state.redis_rate_limiters.file_upload_logged_in,
|
||||
};
|
||||
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
|
||||
return Err(VideoMediaFileUploadError::RateLimited);
|
||||
}
|
||||
|
||||
// ==================== READ MULTIPART REQUEST ==================== //
|
||||
|
||||
let upload_media_request = drain_multipart_request(multipart_payload)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// TODO: Error handling could be nicer.
|
||||
VideoMediaFileUploadError::BadInput("bad request".to_string())
|
||||
})?;
|
||||
|
||||
// TODO(bt, 2024-02-26): This should be a transaction.
|
||||
let uuid_idempotency_token = upload_media_request.uuid_idempotency_token
|
||||
.ok_or(VideoMediaFileUploadError::BadInput("no uuid".to_string()))?;
|
||||
|
||||
// ==================== HANDLE IDEMPOTENCY ==================== //
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&uuid_idempotency_token) {
|
||||
return Err(VideoMediaFileUploadError::BadInput(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&uuid_idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
VideoMediaFileUploadError::BadInput("invalid idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
// ==================== UPLOAD METADATA ==================== //
|
||||
|
||||
let creator_set_visibility = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|user_session| user_session.preferred_tts_result_visibility) // TODO: We need a new type of visibility control.
|
||||
.unwrap_or(Visibility::default());
|
||||
|
||||
let upload_type = match upload_media_request.media_source {
|
||||
MediaFileUploadSource::Unknown => UploadType::Filesystem,
|
||||
MediaFileUploadSource::UserFile => UploadType::Filesystem,
|
||||
MediaFileUploadSource::UserDeviceApi => UploadType::DeviceCaptureApi,
|
||||
};
|
||||
|
||||
// ==================== USER DATA ==================== //
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
let file_bytes = match upload_media_request.file_bytes {
|
||||
None => return Err(VideoMediaFileUploadError::BadInput("missing file contents".to_string())),
|
||||
Some(bytes) => bytes,
|
||||
};
|
||||
|
||||
let mut maybe_mimetype = get_mimetype_for_bytes(&file_bytes);
|
||||
|
||||
let mimetype = match maybe_mimetype {
|
||||
None => return Err(VideoMediaFileUploadError::BadInput("unknown mimetype".to_string())),
|
||||
Some(mimetype) => if ALLOWED_MIME_TYPES.contains(mimetype) {
|
||||
mimetype
|
||||
} else {
|
||||
return Err(VideoMediaFileUploadError::BadInput("unsupported mimetype".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
|
||||
let hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|io_error| {
|
||||
error!("Problem hashing bytes: {:?}", io_error);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== FRAME RATE ==================== //
|
||||
|
||||
let reader = BufReader::new(file_bytes.as_ref());
|
||||
|
||||
let mp4_info = get_mp4_info(&reader, file_size_bytes as u64)
|
||||
.map_err(|err| {
|
||||
warn!("Error reading mp4 info: {:?}", err);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== UPLOAD AND SAVE ==================== //
|
||||
|
||||
// TODO(bt,2024-02-26): We statically know the extension if it was an allowed mimetype. Fix this.
|
||||
let mut extension = mimetype_to_extension(mimetype)
|
||||
.map(|extension| format!(".{extension}"))
|
||||
.unwrap_or(".mp4".to_string());
|
||||
|
||||
const PREFIX : Option<&str> = Some("video_");
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension));
|
||||
|
||||
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
|
||||
|
||||
server_state.public_bucket_client.upload_file_with_content_type(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
mimetype)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Upload media bytes to bucket error: {:?}", e);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// TODO(bt, 2024-02-22): This should be a transaction.
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
upload_type,
|
||||
media_file_type: MediaFileType::Video,
|
||||
maybe_mime_type: Some(mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_sample_rate: Some(mp4_info.framerate as f32),
|
||||
duration_millis: mp4_info.duration_millis as u64,
|
||||
sha256_checksum: &hash,
|
||||
public_bucket_directory_hash: public_upload_path.get_object_hash(),
|
||||
maybe_public_bucket_prefix: PREFIX,
|
||||
maybe_public_bucket_extension: Some(&extension),
|
||||
pool: &server_state.mysql_pool,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("New file creation DB error: {:?}", err);
|
||||
VideoMediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
info!("new media file id: {} token: {:?}", record_id, &token);
|
||||
|
||||
let response = CreateSceneSuccessResponse {
|
||||
success: true,
|
||||
media_file_token: token,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| VideoMediaFileUploadError::ServerError)?;
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body));
|
||||
}
|
||||
@@ -46,12 +46,12 @@ use crate::http_server::endpoints::investor_demo::enable_demo_mode_handler::enab
|
||||
use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler;
|
||||
use crate::http_server::endpoints::media_files::delete_media_file::delete_media_file_handler;
|
||||
use crate::http_server::endpoints::media_files::get_media_file::get_media_file_handler;
|
||||
use crate::http_server::endpoints::media_files::list_featured_media_files::list_featured_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::list_media_files::list_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::list_media_files_by_batch_token::list_media_files_by_batch_token_handler;
|
||||
use crate::http_server::endpoints::media_files::list_media_files_for_user::list_media_files_for_user_handler;
|
||||
use crate::http_server::endpoints::media_files::list::list_featured_media_files::list_featured_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::list::list_media_files::list_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token::list_media_files_by_batch_token_handler;
|
||||
use crate::http_server::endpoints::media_files::list::list_media_files_for_user::list_media_files_for_user_handler;
|
||||
use crate::http_server::endpoints::media_files::update_media_file::update_media_file_handler;
|
||||
use crate::http_server::endpoints::media_files::upload_media_file::upload_media_file_handler;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_media_file_handler::upload_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;
|
||||
@@ -1087,6 +1087,10 @@ fn add_media_file_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::post().to(upload_media_file_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/upload/video")
|
||||
.route(web::post().to(upload_media_file_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user