add file upload
parent
79a2f2839f
commit
752ce3f16c
|
|
@ -1,9 +1,12 @@
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool::{
|
use crate::{
|
||||||
|
lib::domain::intro_tool::{
|
||||||
models::guild::{self, GetUserError, GuildId, IntroId, User},
|
models::guild::{self, GetUserError, GuildId, IntroId, User},
|
||||||
ports::{IntroToolRepository, IntroToolService},
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
|
},
|
||||||
|
media,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::models;
|
use super::models;
|
||||||
|
|
@ -98,14 +101,28 @@ where
|
||||||
req: guild::AddIntroToGuildRequest,
|
req: guild::AddIntroToGuildRequest,
|
||||||
) -> Result<IntroId, guild::AddIntroToGuildError> {
|
) -> Result<IntroId, guild::AddIntroToGuildError> {
|
||||||
let file_name = match &req.data {
|
let file_name = match &req.data {
|
||||||
guild::IntroRequestData::Data(items) => todo!(),
|
guild::IntroRequestData::Data(bytes) => {
|
||||||
|
// TODO: put this behind an interface
|
||||||
|
let uuid = Uuid::new_v4().to_string();
|
||||||
|
let temp_path = format!("./sounds/temp/{uuid}");
|
||||||
|
let dest_path = format!("./sounds/{uuid}.mp3");
|
||||||
|
|
||||||
|
// Write original file so its ready for codec conversion
|
||||||
|
std::fs::write(&temp_path, bytes).context("failed to write temp file")?;
|
||||||
|
media::normalize(&temp_path, &dest_path)
|
||||||
|
.await
|
||||||
|
.context("failed to normalize file")?;
|
||||||
|
std::fs::remove_file(&temp_path).context("failed to remove temp file")?;
|
||||||
|
|
||||||
|
dest_path
|
||||||
|
}
|
||||||
guild::IntroRequestData::Url(url) => {
|
guild::IntroRequestData::Url(url) => {
|
||||||
let uuid = Uuid::new_v4().to_string();
|
let uuid = Uuid::new_v4().to_string();
|
||||||
let file_name = format!("sounds/{uuid}");
|
let file_name = format!("sounds/{uuid}");
|
||||||
|
|
||||||
// TODO: put this behind an interface
|
// TODO: put this behind an interface
|
||||||
let child = tokio::process::Command::new("yt-dlp")
|
let child = tokio::process::Command::new("yt-dlp")
|
||||||
.arg(&url)
|
.arg(url)
|
||||||
.args(["-o", &file_name])
|
.args(["-o", &file_name])
|
||||||
.args(["-x", "--audio-format", "mp3"])
|
.args(["-x", "--audio-format", "mp3"])
|
||||||
.spawn()
|
.spawn()
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,10 @@ where
|
||||||
.route("/login", get(page::login))
|
.route("/login", get(page::login))
|
||||||
.route("/guild/:guild_id", get(page::guild_dashboard))
|
.route("/guild/:guild_id", get(page::guild_dashboard))
|
||||||
.route("/v2/intros/:guild/add", get(handlers::add_guild_intro))
|
.route("/v2/intros/:guild/add", get(handlers::add_guild_intro))
|
||||||
|
.route(
|
||||||
|
"/v2/intros/:guild/upload",
|
||||||
|
post(handlers::upload_guild_intro),
|
||||||
|
)
|
||||||
|
|
||||||
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
||||||
// .route(
|
// .route(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Multipart, Path, Query, State},
|
||||||
http::{HeaderMap, HeaderValue},
|
http::{HeaderMap, HeaderValue},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,27 +17,33 @@ use crate::lib::{
|
||||||
};
|
};
|
||||||
|
|
||||||
trait FromApi<T, P>: Sized {
|
trait FromApi<T, P>: Sized {
|
||||||
fn from_api(value: T, params: P) -> Result<Self, ApiError>;
|
async fn from_api(value: T, params: P) -> Result<Self, ApiError>;
|
||||||
}
|
}
|
||||||
trait IntoDomain<T, P> {
|
trait IntoDomain<T, P> {
|
||||||
fn into_domain(self, params: P) -> Result<T, ApiError>;
|
async fn into_domain(self, params: P) -> Result<T, ApiError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, O: FromApi<I, P>, P> IntoDomain<O, P> for I {
|
impl<I, O: FromApi<I, P>, P> IntoDomain<O, P> for I {
|
||||||
fn into_domain(self, params: P) -> Result<O, ApiError> {
|
async fn into_domain(self, params: P) -> Result<O, ApiError> {
|
||||||
O::from_api(self, params)
|
O::from_api(self, params).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromApi<HashMap<String, String>, GuildId> for AddIntroToGuildRequest {
|
impl FromApi<HashMap<String, String>, GuildId> for AddIntroToGuildRequest {
|
||||||
fn from_api(value: HashMap<String, String>, params: GuildId) -> Result<Self, ApiError> {
|
async fn from_api(value: HashMap<String, String>, params: GuildId) -> Result<Self, ApiError> {
|
||||||
let Some(url) = value.get("url") else {
|
let Some(url) = value.get("url") else {
|
||||||
return Err(ApiError::bad_request("url is required"));
|
return Err(ApiError::bad_request("url is required"));
|
||||||
};
|
};
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("url cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
let Some(name) = value.get("name") else {
|
let Some(name) = value.get("name") else {
|
||||||
return Err(ApiError::bad_request("name is required"));
|
return Err(ApiError::bad_request("name is required"));
|
||||||
};
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
guild_id: params,
|
guild_id: params,
|
||||||
|
|
@ -48,13 +54,93 @@ impl FromApi<HashMap<String, String>, GuildId> for AddIntroToGuildRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromApi<Multipart, GuildId> for AddIntroToGuildRequest {
|
||||||
|
async fn from_api(mut form_data: Multipart, params: GuildId) -> Result<Self, ApiError> {
|
||||||
|
let mut name = None;
|
||||||
|
let mut file = None;
|
||||||
|
|
||||||
|
while let Ok(Some(field)) = form_data.next_field().await {
|
||||||
|
let Some(field_name) = field.name() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("name") {
|
||||||
|
name = Some(field.text().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected text for name: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("file") {
|
||||||
|
file = Some(field.bytes().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected bytes for file: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name) = name else {
|
||||||
|
return Err(ApiError::bad_request("name is required"));
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = file else {
|
||||||
|
return Err(ApiError::bad_request("file is required"));
|
||||||
|
};
|
||||||
|
if file.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("file cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guild_id: params,
|
||||||
|
name: name.to_string(),
|
||||||
|
volume: 0,
|
||||||
|
data: IntroRequestData::Data(file.to_vec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn add_guild_intro<S: IntroToolService>(
|
pub(super) async fn add_guild_intro<S: IntroToolService>(
|
||||||
State(state): State<ApiState<S>>,
|
State(state): State<ApiState<S>>,
|
||||||
Path(guild_id): Path<u64>,
|
Path(guild_id): Path<u64>,
|
||||||
Query(params): Query<HashMap<String, String>>,
|
Query(params): Query<HashMap<String, String>>,
|
||||||
user: User,
|
user: User,
|
||||||
) -> Result<HeaderMap, ApiError> {
|
) -> Result<HeaderMap, ApiError> {
|
||||||
let req = params.into_domain(guild_id.into())?;
|
let req = params.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intro_tool_service.add_intro_to_guild(req).await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn upload_guild_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
user: User,
|
||||||
|
form_data: Multipart,
|
||||||
|
) -> Result<HeaderMap, ApiError> {
|
||||||
|
let req = form_data.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
let user_guilds = state
|
let user_guilds = state
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue