forked from Anvilcraft/jmserver
Implement upload endpoint
This commit is contained in:
parent
ac17b66a8d
commit
b6abcd7c90
3 changed files with 119 additions and 16 deletions
|
@ -18,8 +18,12 @@ pub enum APIError {
|
||||||
Sql(#[from] sqlx::Error),
|
Sql(#[from] sqlx::Error),
|
||||||
#[error("Multipart form error: {0}")]
|
#[error("Multipart form error: {0}")]
|
||||||
Multipart(#[from] MultipartError),
|
Multipart(#[from] MultipartError),
|
||||||
#[error("Bad request: {0}")]
|
#[error("{0}")]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Unauthorized(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Forbidden(String),
|
||||||
#[error("IPFS error: {0}")]
|
#[error("IPFS error: {0}")]
|
||||||
IPFS(#[from] IPFSError),
|
IPFS(#[from] IPFSError),
|
||||||
}
|
}
|
||||||
|
@ -47,6 +51,8 @@ impl IntoResponse for APIError {
|
||||||
},
|
},
|
||||||
APIError::Multipart(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
APIError::Multipart(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||||
APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)),
|
APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)),
|
||||||
|
APIError::Unauthorized(err) => ErrorResponse::new(StatusCode::UNAUTHORIZED, Some(err)),
|
||||||
|
APIError::Forbidden(err) => ErrorResponse::new(StatusCode::FORBIDDEN, Some(err)),
|
||||||
APIError::IPFS(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
APIError::IPFS(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||||
};
|
};
|
||||||
let status = res.status.clone();
|
let status = res.status.clone();
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
use crate::config::ConfVars;
|
use crate::config::ConfVars;
|
||||||
use crate::ipfs::IPFSFile;
|
use crate::ipfs::IPFSFile;
|
||||||
|
use crate::lib::ExtractIP;
|
||||||
use crate::v1::models::*;
|
use crate::v1::models::*;
|
||||||
|
|
||||||
use axum::extract::{ContentLengthLimit, Extension, Multipart, Query};
|
use axum::extract::{ContentLengthLimit, Extension, Multipart, Query};
|
||||||
use axum::handler::{get, post};
|
use axum::handler::{get, post};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::BoxRoute;
|
use axum::routing::BoxRoute;
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
|
use hyper::StatusCode;
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use super::error::APIError;
|
use super::error::APIError;
|
||||||
|
@ -97,8 +100,70 @@ async fn upload(
|
||||||
ContentLengthLimit(mut form): ContentLengthLimit<Multipart, { 1024 * 1024 * 1024 }>,
|
ContentLengthLimit(mut form): ContentLengthLimit<Multipart, { 1024 * 1024 * 1024 }>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
Extension(db_pool): Extension<MySqlPool>,
|
||||||
Extension(vars): Extension<ConfVars>,
|
Extension(vars): Extension<ConfVars>,
|
||||||
|
ExtractIP(ip): ExtractIP,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
Ok(())
|
let mut category: Option<String> = None;
|
||||||
|
let mut token: Option<String> = None;
|
||||||
|
let mut files: Vec<IPFSFile> = vec![];
|
||||||
|
|
||||||
|
let ipfs = vars.ipfs_client()?;
|
||||||
|
|
||||||
|
while let Some(field) = form.next_field().await? {
|
||||||
|
match field.name().ok_or(APIError::BadRequest(
|
||||||
|
"A multipart-form field is missing a name".to_string(),
|
||||||
|
))? {
|
||||||
|
"token" => token = Some(field.text().await?),
|
||||||
|
"category" => category = Some(field.text().await?),
|
||||||
|
"file" | "file[]" => {
|
||||||
|
let filename = field
|
||||||
|
.file_name()
|
||||||
|
.ok_or(APIError::BadRequest(
|
||||||
|
"A file field has no filename".to_string(),
|
||||||
|
))?
|
||||||
|
.to_string();
|
||||||
|
let file = ipfs.add(field.bytes().await?, filename).await?;
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = token.ok_or(APIError::Unauthorized("Missing token".to_string()))?;
|
||||||
|
let category = category.ok_or(APIError::BadRequest("Missing category".to_string()))?;
|
||||||
|
let user = User::check_token(token, &db_pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(APIError::Forbidden("token not existing".to_string()))?;
|
||||||
|
let total = (user.dayuploads as isize) + (files.len() as isize);
|
||||||
|
|
||||||
|
if total > 20 {
|
||||||
|
return Err(APIError::Forbidden("Upload limit reached".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cat = Category::get(&category, &db_pool).await?;
|
||||||
|
|
||||||
|
let ip = ip.to_string();
|
||||||
|
|
||||||
|
let mut links: Vec<String> = vec![];
|
||||||
|
|
||||||
|
for f in files {
|
||||||
|
let res = cat.add_meme(&user, &f, &ip, &db_pool).await?;
|
||||||
|
ipfs.pin(f.hash).await?;
|
||||||
|
links.push(format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
vars.cdn,
|
||||||
|
user.id.clone(),
|
||||||
|
f.name.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(UploadResponse {
|
||||||
|
status: 201,
|
||||||
|
error: None,
|
||||||
|
files: Some(links),
|
||||||
|
}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Implement upload endpoint
|
//TODO: Implement upload endpoint
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::ipfs::IPFSFile;
|
||||||
use crate::v1::models::{Category, Meme, MemeFilterQuery, User, UserIDQuery};
|
use crate::v1::models::{Category, Meme, MemeFilterQuery, User, UserIDQuery};
|
||||||
use sqlx::mysql::MySqlRow;
|
use sqlx::mysql::MySqlRow;
|
||||||
use sqlx::{MySqlPool, Result, Row};
|
use sqlx::{MySqlPool, Result, Row};
|
||||||
|
@ -25,8 +26,8 @@ impl Meme {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(id: i32, pool: &MySqlPool, cdn: String) -> Result<Meme> {
|
pub async fn get(id: i32, pool: &MySqlPool, cdn: String) -> Result<Meme> {
|
||||||
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id)
|
let q: Self = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id)
|
||||||
.map(|row: MySqlRow| Meme::new(DBMeme {
|
.map(|row: MySqlRow| Self::new(DBMeme {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
filename: row.get("filename"),
|
filename: row.get("filename"),
|
||||||
user: row.get("name"),
|
user: row.get("name"),
|
||||||
|
@ -43,12 +44,12 @@ impl Meme {
|
||||||
params: MemeFilterQuery,
|
params: MemeFilterQuery,
|
||||||
pool: &MySqlPool,
|
pool: &MySqlPool,
|
||||||
cdn: String,
|
cdn: String,
|
||||||
) -> Result<Vec<Meme>> {
|
) -> Result<Vec<Self>> {
|
||||||
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY memes.id")
|
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY memes.id")
|
||||||
.bind(params.category.unwrap_or(String::from("%")))
|
.bind(params.category.unwrap_or(String::from("%")))
|
||||||
.bind(format!("%{}%", params.user.unwrap_or(String::from(""))))
|
.bind(format!("%{}%", params.user.unwrap_or(String::from(""))))
|
||||||
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
|
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
|
||||||
.map(|row: MySqlRow| Meme::new(DBMeme {
|
.map(|row: MySqlRow| Self::new(DBMeme {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
filename: row.get("filename"),
|
filename: row.get("filename"),
|
||||||
user: row.get("name"),
|
user: row.get("name"),
|
||||||
|
@ -65,12 +66,12 @@ impl Meme {
|
||||||
params: MemeFilterQuery,
|
params: MemeFilterQuery,
|
||||||
pool: &MySqlPool,
|
pool: &MySqlPool,
|
||||||
cdn: String,
|
cdn: String,
|
||||||
) -> Result<Meme> {
|
) -> Result<Self> {
|
||||||
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY RAND() LIMIT 1")
|
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY RAND() LIMIT 1")
|
||||||
.bind(params.category.unwrap_or(String::from("%")))
|
.bind(params.category.unwrap_or(String::from("%")))
|
||||||
.bind(format!("%{}%", params.user.unwrap_or(String::from(""))))
|
.bind(format!("%{}%", params.user.unwrap_or(String::from(""))))
|
||||||
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
|
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
|
||||||
.map(|row: MySqlRow| Meme::new(DBMeme {
|
.map(|row: MySqlRow| Self::new(DBMeme {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
filename: row.get("filename"),
|
filename: row.get("filename"),
|
||||||
user: row.get("name"),
|
user: row.get("name"),
|
||||||
|
@ -85,10 +86,10 @@ impl Meme {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Category {
|
impl Category {
|
||||||
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Category> {
|
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Self> {
|
||||||
let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?")
|
let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.map(|row: MySqlRow| Category {
|
.map(|row: MySqlRow| Self {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
})
|
})
|
||||||
|
@ -97,9 +98,9 @@ impl Category {
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Category>> {
|
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
|
||||||
let q: Vec<Category> = sqlx::query("SELECT * FROM categories ORDER BY num")
|
let q: Vec<Category> = sqlx::query("SELECT * FROM categories ORDER BY num")
|
||||||
.map(|row: MySqlRow| Category {
|
.map(|row: MySqlRow| Self {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
})
|
})
|
||||||
|
@ -107,15 +108,32 @@ impl Category {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_meme(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
file: &IPFSFile,
|
||||||
|
ip: &String,
|
||||||
|
pool: &MySqlPool,
|
||||||
|
) -> Result<u64> {
|
||||||
|
let q = sqlx::query("INSERT INTO memes (filename, user, category, timestamp, ip, cid) VALUES (?, ?, ?, NOW(), ?, ?)")
|
||||||
|
.bind(&file.name)
|
||||||
|
.bind(&user.id)
|
||||||
|
.bind(&self.id)
|
||||||
|
.bind(ip)
|
||||||
|
.bind(&file.hash)
|
||||||
|
.execute(pool).await?;
|
||||||
|
Ok(q)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub async fn get(params: UserIDQuery, pool: &MySqlPool) -> Result<User> {
|
pub async fn get(params: UserIDQuery, pool: &MySqlPool) -> Result<Self> {
|
||||||
let q: User = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid AND (users.id LIKE ? OR token LIKE ? OR name LIKE ?) UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'")
|
let q: User = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid AND (users.id LIKE ? OR token LIKE ? OR name LIKE ?) UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'")
|
||||||
.bind(params.id.unwrap_or(String::from("")))
|
.bind(params.id.unwrap_or(String::from("")))
|
||||||
.bind(params.token.unwrap_or(String::from("")))
|
.bind(params.token.unwrap_or(String::from("")))
|
||||||
.bind(params.name.unwrap_or(String::from("")))
|
.bind(params.name.unwrap_or(String::from("")))
|
||||||
.map(|row: MySqlRow| User {
|
.map(|row: MySqlRow| Self {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
userdir: row.get("id"),
|
userdir: row.get("id"),
|
||||||
|
@ -126,9 +144,9 @@ impl User {
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<User>> {
|
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
|
||||||
let q: Vec<User> = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'")
|
let q: Vec<User> = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'")
|
||||||
.map(|row: MySqlRow| User {
|
.map(|row: MySqlRow| Self {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
userdir: row.get("id"),
|
userdir: row.get("id"),
|
||||||
|
@ -138,4 +156,18 @@ impl User {
|
||||||
.fetch_all(pool).await?;
|
.fetch_all(pool).await?;
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_token(token: String, pool: &MySqlPool) -> Result<Option<Self>> {
|
||||||
|
let q: Option<User> = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid AND token = ?")
|
||||||
|
.bind(token)
|
||||||
|
.map(|row: MySqlRow| Self {
|
||||||
|
id: row.get("id"),
|
||||||
|
name: row.get("name"),
|
||||||
|
userdir: row.get("id"),
|
||||||
|
tokenhash: row.get("hash"),
|
||||||
|
dayuploads: row.get("uploads"),
|
||||||
|
})
|
||||||
|
.fetch_optional(pool).await?;
|
||||||
|
Ok(q)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue