feat: Bunny CDN integration for images #17
4 changed files with 65 additions and 13 deletions
|
@ -36,6 +36,7 @@ diesel = { version = "2.2", features = ["uuid"] }
|
||||||
diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] }
|
diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
|
actix-multipart = "0.7.2"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.44"
|
version = "1.44"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::{get, patch, web, HttpRequest, HttpResponse};
|
use actix_web::{get, patch, web, HttpRequest, HttpResponse};
|
||||||
|
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data};
|
use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data};
|
||||||
|
@ -18,7 +19,7 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
|
||||||
Ok(HttpResponse::Ok().json(me))
|
Ok(HttpResponse::Ok().json(me))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct NewInfo {
|
struct NewInfo {
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
@ -26,8 +27,15 @@ struct NewInfo {
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
struct UploadForm {
|
||||||
|
#[multipart(limit = "100MB")]
|
||||||
|
avatar: Option<TempFile>,
|
||||||
|
json: Option<MpJson<NewInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[patch("/me")]
|
#[patch("/me")]
|
||||||
pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
|
pub async fn update(req: HttpRequest, MultipartForm(form): MultipartForm<UploadForm>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
|
|
||||||
let auth_header = get_auth_header(headers)?;
|
let auth_header = get_auth_header(headers)?;
|
||||||
|
@ -36,8 +44,17 @@ pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::D
|
||||||
|
|
||||||
let uuid = check_access_token(auth_header, &mut conn).await?;
|
let uuid = check_access_token(auth_header, &mut conn).await?;
|
||||||
|
|
||||||
let me = Me::get(&mut conn, uuid).await?;
|
let mut me = Me::get(&mut conn, uuid).await?;
|
||||||
|
|
||||||
|
if let Some(avatar) = form.avatar {
|
||||||
|
let bytes = tokio::fs::read(avatar.file).await?;
|
||||||
|
|
||||||
|
let byte_slice: &[u8] = &bytes;
|
||||||
|
|
||||||
|
me.set_avatar(&data.bunny_cdn, &mut conn, data.config.bunny.cdn_url.clone(), byte_slice.into()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_info) = form.json {
|
||||||
if let Some(username) = &new_info.username {
|
if let Some(username) = &new_info.username {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
@ -53,6 +70,7 @@ pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::D
|
||||||
if let Some(email) = &new_info.email {
|
if let Some(email) = &new_info.email {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
Ok(HttpResponse::Ok().finish())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub fn web() -> Scope {
|
||||||
web::scope("/users")
|
web::scope("/users")
|
||||||
.service(res)
|
.service(res)
|
||||||
.service(me::res)
|
.service(me::res)
|
||||||
|
.service(me::update)
|
||||||
.service(uuid::res)
|
.service(uuid::res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -667,6 +667,38 @@ impl Me {
|
||||||
|
|
||||||
Ok(me)
|
Ok(me)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_avatar(&mut self, bunny_cdn: &bunny_api_tokio::Client, conn: &mut Conn, cdn_url: Url, avatar: BytesMut) -> Result<(), Error> {
|
||||||
|
let avatar_clone = avatar.clone();
|
||||||
|
let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??;
|
||||||
|
|
||||||
|
if let Some(avatar) = &self.avatar {
|
||||||
|
let avatar_url: Url = avatar.parse()?;
|
||||||
|
|
||||||
|
let relative_url = avatar_url
|
||||||
|
.path()
|
||||||
|
.trim_start_matches('/');
|
||||||
|
|
||||||
|
bunny_cdn.storage.delete(relative_url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = format!("avatar/{}/avatar.{}", self.uuid, image_type);
|
||||||
|
|
||||||
|
bunny_cdn.storage.upload(path.clone(), avatar.into()).await?;
|
||||||
|
|
||||||
|
let avatar_url = cdn_url.join(&path)?;
|
||||||
|
|
||||||
|
use users::dsl;
|
||||||
|
update(users::table)
|
||||||
|
.filter(dsl::uuid.eq(self.uuid))
|
||||||
|
.set(dsl::avatar.eq(avatar_url.as_str()))
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.avatar = Some(avatar_url.to_string());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue