add: store avatars and banners in uploads
This commit is contained in:
parent
1e50ace8b2
commit
dbed2b2457
36 changed files with 211 additions and 363 deletions
|
@ -1,17 +1,12 @@
|
|||
use axum::{
|
||||
Extension, Json,
|
||||
body::Body,
|
||||
extract::{Path, Query},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum::{Extension, Json, response::IntoResponse};
|
||||
use crate::cookie::CookieJar;
|
||||
use pathbufd::{PathBufD, pathd};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs::{File, exists},
|
||||
io::Read,
|
||||
use pathbufd::PathBufD;
|
||||
use std::{fs::File, io::Read};
|
||||
use tetratto_core::model::{
|
||||
permissions::FinePermission,
|
||||
uploads::{MediaType, MediaUpload},
|
||||
ApiReturn, Error,
|
||||
};
|
||||
use tetratto_core::model::{permissions::FinePermission, ApiReturn, Error};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
|
@ -29,139 +24,6 @@ pub fn read_image(path: PathBufD) -> Vec<u8> {
|
|||
bytes
|
||||
}
|
||||
|
||||
#[derive(Deserialize, PartialEq, Eq)]
|
||||
pub enum AvatarSelectorType {
|
||||
#[serde(alias = "username")]
|
||||
Username,
|
||||
#[serde(alias = "id")]
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AvatarSelectorQuery {
|
||||
pub selector_type: AvatarSelectorType,
|
||||
}
|
||||
|
||||
/// Get a profile's avatar image
|
||||
/// `/api/v1/auth/user/{id}/avatar`
|
||||
pub async fn avatar_request(
|
||||
Path(selector): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
Query(req): Query<AvatarSelectorQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
|
||||
let user = match if req.selector_type == AvatarSelectorType::Id {
|
||||
data.get_user_by_id(match selector.parse::<usize>() {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-avatar.svg",
|
||||
]))),
|
||||
));
|
||||
}
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
data.get_user_by_username(&selector).await
|
||||
} {
|
||||
Ok(ua) => ua,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-avatar.svg",
|
||||
]))),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mime = if user.settings.avatar_mime.is_empty() {
|
||||
"image/avif"
|
||||
} else {
|
||||
&user.settings.avatar_mime
|
||||
};
|
||||
|
||||
let path = PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"avatars",
|
||||
&format!("{}.{}", &(user.id as i64), mime.replace("image/", "")),
|
||||
]);
|
||||
|
||||
if !exists(&path).unwrap() {
|
||||
return Err((
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-avatar.svg",
|
||||
]))),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
[("Content-Type".to_string(), mime.to_owned())],
|
||||
Body::from(read_image(path)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Get a profile's banner image
|
||||
/// `/api/v1/auth/user/{id}/banner`
|
||||
pub async fn banner_request(
|
||||
Path(username): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
|
||||
let user = match data.get_user_by_username(&username).await {
|
||||
Ok(ua) => ua,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-banner.svg",
|
||||
]))),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mime = if user.settings.banner_mime.is_empty() {
|
||||
"image/avif"
|
||||
} else {
|
||||
&user.settings.banner_mime
|
||||
};
|
||||
|
||||
let path = PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"banners",
|
||||
&format!("{}.{}", &(user.id as i64), mime.replace("image/", "")),
|
||||
]);
|
||||
|
||||
if !exists(&path).unwrap() {
|
||||
return Err((
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-banner.svg",
|
||||
]))),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
[("Content-Type".to_string(), mime.to_owned())],
|
||||
Body::from(read_image(path)),
|
||||
))
|
||||
}
|
||||
|
||||
pub const MAXIMUM_FILE_SIZE: usize = 8388608;
|
||||
pub const MAXIMUM_GIF_FILE_SIZE: usize = 2097152;
|
||||
|
||||
|
@ -173,44 +35,57 @@ pub async fn upload_avatar_request(
|
|||
) -> impl IntoResponse {
|
||||
// get user from token
|
||||
let data = &(data.read().await).0;
|
||||
let mut auth_user = match get_user_from_token!(jar, data) {
|
||||
let auth_user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if img.1 == "image/gif" && !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||
return Json(Error::RequiresSupporter.into());
|
||||
}
|
||||
|
||||
let mime = if img.1 == "image/gif" {
|
||||
"image/gif"
|
||||
if img.1 == "image/gif" {
|
||||
if !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||
return Json(Error::RequiresSupporter.into());
|
||||
} else {
|
||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"image/avif"
|
||||
};
|
||||
|
||||
if auth_user.settings.avatar_mime != mime {
|
||||
// mime changed; delete old image
|
||||
let path = pathd!(
|
||||
"{}/avatars/{}.{}",
|
||||
data.0.0.dirs.media,
|
||||
&auth_user.id,
|
||||
auth_user.settings.avatar_mime.replace("image/", "")
|
||||
);
|
||||
|
||||
if std::fs::exists(&path).unwrap() {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
}
|
||||
|
||||
let path = pathd!(
|
||||
"{}/avatars/{}.{}",
|
||||
data.0.0.dirs.media,
|
||||
&auth_user.id,
|
||||
mime.replace("image/", "")
|
||||
// delete old upload
|
||||
if let Ok(u) = data
|
||||
.2
|
||||
.get_upload_by_id_bucket(auth_user.id, "avatars")
|
||||
.await
|
||||
{
|
||||
if let Err(e) = data.2.delete_upload_with_bucket(u.id, "avatars").await {
|
||||
return Json(Error::MiscError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// create new upload
|
||||
let mut new_upload = MediaUpload::new(
|
||||
if img.1 == "image/gif" {
|
||||
MediaType::Gif
|
||||
} else {
|
||||
MediaType::Avif
|
||||
},
|
||||
auth_user.id,
|
||||
"avatars".to_string(),
|
||||
);
|
||||
|
||||
new_upload.id = auth_user.id;
|
||||
|
||||
let upload = match data.2.create_upload(new_upload).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
// upload image (gif)
|
||||
if mime == "image/gif" {
|
||||
let path = upload.path(&data.2.0.0.directory);
|
||||
if img.1 == "image/gif" {
|
||||
// gif image, don't encode
|
||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
|
@ -218,15 +93,6 @@ pub async fn upload_avatar_request(
|
|||
|
||||
std::fs::write(&path, img.0).unwrap();
|
||||
|
||||
// update user settings
|
||||
auth_user.settings.avatar_mime = "image/gif".to_string();
|
||||
if let Err(e) = data
|
||||
.update_user_settings(auth_user.id, auth_user.settings)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// ...
|
||||
return Json(ApiReturn {
|
||||
ok: true,
|
||||
|
@ -235,28 +101,8 @@ pub async fn upload_avatar_request(
|
|||
});
|
||||
}
|
||||
|
||||
// check file size
|
||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
|
||||
// update user settings
|
||||
auth_user.settings.avatar_mime = "image/avif".to_string();
|
||||
if let Err(e) = data
|
||||
.update_user_settings(auth_user.id, auth_user.settings)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// upload image
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
for byte in img.0 {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
match save_buffer(&path, bytes, image::ImageFormat::Avif) {
|
||||
match save_buffer(&path.to_string(), img.0.to_vec(), image::ImageFormat::Avif) {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Avatar uploaded. It might take a bit to update".to_string(),
|
||||
|
@ -274,44 +120,57 @@ pub async fn upload_banner_request(
|
|||
) -> impl IntoResponse {
|
||||
// get user from token
|
||||
let data = &(data.read().await).0;
|
||||
let mut auth_user = match get_user_from_token!(jar, data) {
|
||||
let auth_user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if img.1 == "image/gif" && !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||
return Json(Error::RequiresSupporter.into());
|
||||
}
|
||||
|
||||
let mime = if img.1 == "image/gif" {
|
||||
"image/gif"
|
||||
if img.1 == "image/gif" {
|
||||
if !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||
return Json(Error::RequiresSupporter.into());
|
||||
} else {
|
||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"image/avif"
|
||||
};
|
||||
|
||||
if auth_user.settings.banner_mime != mime {
|
||||
// mime changed; delete old image
|
||||
let path = pathd!(
|
||||
"{}/banners/{}.{}",
|
||||
data.0.0.dirs.media,
|
||||
&auth_user.id,
|
||||
auth_user.settings.banner_mime.replace("image/", "")
|
||||
);
|
||||
|
||||
if std::fs::exists(&path).unwrap() {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
}
|
||||
|
||||
let path = pathd!(
|
||||
"{}/banners/{}.{}",
|
||||
data.0.0.dirs.media,
|
||||
&auth_user.id,
|
||||
mime.replace("image/", "")
|
||||
// delete old upload
|
||||
if let Ok(u) = data
|
||||
.2
|
||||
.get_upload_by_id_bucket(auth_user.id, "banners")
|
||||
.await
|
||||
{
|
||||
if let Err(e) = data.2.delete_upload_with_bucket(u.id, "banners").await {
|
||||
return Json(Error::MiscError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// create new upload
|
||||
let mut new_upload = MediaUpload::new(
|
||||
if img.1 == "image/gif" {
|
||||
MediaType::Gif
|
||||
} else {
|
||||
MediaType::Avif
|
||||
},
|
||||
auth_user.id,
|
||||
"banners".to_string(),
|
||||
);
|
||||
|
||||
new_upload.id = auth_user.id;
|
||||
|
||||
let upload = match data.2.create_upload(new_upload).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
// upload image (gif)
|
||||
if mime == "image/gif" {
|
||||
let path = upload.path(&data.2.0.0.directory);
|
||||
if img.1 == "image/gif" {
|
||||
// gif image, don't encode
|
||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
|
@ -319,15 +178,6 @@ pub async fn upload_banner_request(
|
|||
|
||||
std::fs::write(&path, img.0).unwrap();
|
||||
|
||||
// update user settings
|
||||
auth_user.settings.banner_mime = "image/gif".to_string();
|
||||
if let Err(e) = data
|
||||
.update_user_settings(auth_user.id, auth_user.settings)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// ...
|
||||
return Json(ApiReturn {
|
||||
ok: true,
|
||||
|
@ -336,28 +186,8 @@ pub async fn upload_banner_request(
|
|||
});
|
||||
}
|
||||
|
||||
// check file size
|
||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
|
||||
// update user settings
|
||||
auth_user.settings.avatar_mime = "image/avif".to_string();
|
||||
if let Err(e) = data
|
||||
.update_user_settings(auth_user.id, auth_user.settings)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// upload image
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
for byte in img.0 {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
match save_buffer(&path, bytes, image::ImageFormat::Avif) {
|
||||
match save_buffer(&path.to_string(), img.0.to_vec(), image::ImageFormat::Avif) {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Banner uploaded. It might take a bit to update".to_string(),
|
||||
|
|
|
@ -306,8 +306,6 @@ pub fn routes() -> Router {
|
|||
"/auth/user/me/policy_consent",
|
||||
post(auth::profile::policy_consent_request),
|
||||
)
|
||||
.route("/auth/user/{id}/avatar", get(auth::images::avatar_request))
|
||||
.route("/auth/user/{id}/banner", get(auth::images::banner_request))
|
||||
.route("/auth/user/{id}/follow", post(auth::social::follow_request))
|
||||
.route(
|
||||
"/auth/user/{id}/follow/toggle",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue