SQL improvements
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
Timo Ley 2022-01-16 17:46:09 +01:00
parent 467c00410c
commit 9133d1ea9e
8 changed files with 245 additions and 211 deletions

View file

@ -15,6 +15,8 @@ mod config;
mod error;
mod ipfs;
mod lib;
mod models;
mod sql;
mod v1;
#[derive(StructOpt)]

41
src/models.rs Normal file
View file

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct Meme {
pub id: i32,
pub filename: String,
pub userid: String,
pub username: String,
pub category: String,
pub timestamp: i64,
pub ipfs: String,
}
#[derive(Serialize)]
pub struct Category {
pub id: String,
pub name: String,
}
#[derive(Serialize)]
pub struct User {
pub id: String,
pub name: String,
pub userdir: String,
pub tokenhash: String,
pub dayuploads: i32,
}
pub enum UserIdentifier {
Id(String),
Token(String),
Username(String),
Null,
}
#[derive(Deserialize)]
pub struct MemeFilter {
pub category: Option<String>,
pub user: Option<String>,
pub search: Option<String>,
}

145
src/sql.rs Normal file
View file

@ -0,0 +1,145 @@
use crate::ipfs::IPFSFile;
use crate::models::{Category, Meme, MemeFilter, User, UserIdentifier};
use sqlx::mysql::MySqlRow;
use sqlx::{MySqlPool, Result, Row};
impl Meme {
pub async fn get(id: i32, pool: &MySqlPool) -> Result<Option<Self>> {
let q: Option<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| Self {
id: row.get("id"),
filename: row.get("filename"),
username: row.get("name"),
userid: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
})
.fetch_optional(pool).await?;
Ok(q)
}
pub async fn get_all(filter: MemeFilter, pool: &MySqlPool) -> Result<Vec<Self>> {
let q: Vec<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 (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY memes.id")
.bind(filter.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", filter.user.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.search.unwrap_or_else(String::new)))
.map(|row: MySqlRow| Self {
id: row.get("id"),
filename: row.get("filename"),
username: row.get("name"),
userid: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
})
.fetch_all(pool).await?;
Ok(q)
}
pub async fn get_random(filter: MemeFilter, pool: &MySqlPool) -> Result<Self> {
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 (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY RAND() LIMIT 1")
.bind(filter.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", filter.user.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.search.unwrap_or_else(String::new)))
.map(|row: MySqlRow| Self {
id: row.get("id"),
filename: row.get("filename"),
username: row.get("name"),
userid: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
})
.fetch_one(pool).await?;
Ok(q)
}
}
impl Category {
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Option<Self>> {
let q: Option<Self> = sqlx::query("SELECT * FROM categories WHERE id=?")
.bind(id)
.map(|row: MySqlRow| Self {
id: row.get("id"),
name: row.get("name"),
})
.fetch_optional(pool)
.await?;
Ok(q)
}
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
let q: Vec<Self> = sqlx::query("SELECT * FROM categories ORDER BY num")
.map(|row: MySqlRow| Self {
id: row.get("id"),
name: row.get("name"),
})
.fetch_all(pool)
.await?;
Ok(q)
}
pub async fn add_meme(
&self,
user: &User,
file: &IPFSFile,
ip: &String,
pool: &MySqlPool,
) -> Result<u64> {
let mut tx = pool.begin().await?;
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(&mut tx).await?;
let id: u64 = sqlx::query("SELECT LAST_INSERT_ID() as id")
.map(|row: MySqlRow| row.get("id"))
.fetch_one(&mut tx)
.await?;
tx.commit().await?;
Ok(id)
}
}
impl User {
pub async fn get(identifier: UserIdentifier, pool: &MySqlPool) -> Result<Option<Self>> {
let query = match identifier {
UserIdentifier::Id(id) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') 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 LEFT JOIN token ON users.id = token.uid WHERE users.id = ?").bind(id),
UserIdentifier::Token(token) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') 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 LEFT JOIN token ON users.id = token.uid WHERE token = ?").bind(token),
UserIdentifier::Username(name) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') 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 LEFT JOIN token ON users.id = token.uid WHERE name = ?").bind(name),
UserIdentifier::Null => sqlx::query("SELECT id, name, '0' AS hash, 0 AS uploads FROM users WHERE id = '000'"),
};
let q: Option<Self> = query
.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)
}
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
let q: Vec<Self> = sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') 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 LEFT JOIN token ON users.id = token.uid")
.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_all(pool).await?;
Ok(q)
}
pub async fn check_token(token: &String, pool: &MySqlPool) -> Result<Option<Self>> {
let user = Self::get(UserIdentifier::Token(token.clone()), pool).await?;
Ok(user)
}
}

View file

@ -25,6 +25,8 @@ pub enum APIError {
#[error("{0}")]
Forbidden(String),
#[error("{0}")]
NotFound(String),
#[error("{0}")]
Internal(String),
#[error("IPFS error: {0}")]
Ipfs(#[from] IPFSError),
@ -57,6 +59,7 @@ impl IntoResponse for APIError {
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::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)),
APIError::Internal(err) => {
ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err))
}

View file

@ -1,7 +1,6 @@
mod error;
pub mod models;
mod routes;
mod sql;
use async_trait::async_trait;
use axum::extract::{FromRequest, RequestParts};

View file

@ -1,6 +1,8 @@
use reqwest::StatusCode;
use serde::{Deserialize, Serialize, Serializer};
use crate::models::{Category, Meme, User, UserIdentifier};
fn serialize_status<S>(x: &StatusCode, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@ -9,7 +11,7 @@ where
}
#[derive(Serialize)]
pub struct Meme {
pub struct V1Meme {
pub id: String,
pub link: String,
pub category: String,
@ -18,35 +20,20 @@ pub struct Meme {
pub ipfs: String,
}
#[derive(Serialize)]
pub struct Category {
pub id: String,
pub name: String,
}
#[derive(Serialize)]
pub struct User {
pub id: String,
pub name: String,
pub userdir: String,
pub tokenhash: String,
pub dayuploads: i32,
}
//Responses
#[derive(Serialize)]
pub struct MemesResponse {
pub status: i32,
pub error: Option<String>,
pub memes: Option<Vec<Meme>>,
pub memes: Option<Vec<V1Meme>>,
}
#[derive(Serialize)]
pub struct MemeResponse {
pub status: i32,
pub error: Option<String>,
pub meme: Option<Meme>,
pub meme: Option<V1Meme>,
}
#[derive(Serialize)]
@ -111,9 +98,29 @@ pub struct UserIDQuery {
pub name: Option<String>,
}
#[derive(Deserialize)]
pub struct MemeFilterQuery {
pub category: Option<String>,
pub user: Option<String>,
pub search: Option<String>,
impl V1Meme {
pub fn new(meme: Meme, cdn: String) -> Self {
Self {
id: meme.id.to_string(),
link: format!("{}/{}/{}", cdn, meme.userid, meme.filename),
category: meme.category,
user: meme.username,
timestamp: meme.timestamp.to_string(),
ipfs: meme.ipfs,
}
}
}
impl From<UserIDQuery> for UserIdentifier {
fn from(query: UserIDQuery) -> Self {
if let Some(id) = query.id {
Self::Id(id)
} else if let Some(token) = query.token {
Self::Token(token)
} else if let Some(name) = query.name {
Self::Username(name)
} else {
Self::Null
}
}
}

View file

@ -1,6 +1,7 @@
use crate::config::ConfVars;
use crate::ipfs::IPFSFile;
use crate::lib::ExtractIP;
use crate::models::{Category, Meme, MemeFilter, User};
use crate::v1::models::*;
use axum::extract::{ContentLengthLimit, Extension, Multipart};
@ -19,7 +20,12 @@ async fn meme(
Extension(db_pool): Extension<MySqlPool>,
Extension(vars): Extension<ConfVars>,
) -> Result<impl IntoResponse, APIError> {
let meme = Meme::get(params.id, &db_pool, vars.cdn).await?;
let meme = V1Meme::new(
Meme::get(params.id, &db_pool)
.await?
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
vars.cdn,
);
Ok(Json(MemeResponse {
status: 200,
error: None,
@ -28,11 +34,15 @@ async fn meme(
}
async fn memes(
Query(params): Query<MemeFilterQuery>,
Query(params): Query<MemeFilter>,
Extension(db_pool): Extension<MySqlPool>,
Extension(vars): Extension<ConfVars>,
) -> Result<impl IntoResponse, APIError> {
let memes = Meme::get_all(params, &db_pool, vars.cdn).await?;
let memes = Meme::get_all(params, &db_pool)
.await?
.into_iter()
.map(|meme| V1Meme::new(meme, vars.cdn.clone()))
.collect();
Ok(Json(MemesResponse {
status: 200,
error: None,
@ -44,7 +54,9 @@ async fn category(
Query(params): Query<IDQuery>,
Extension(db_pool): Extension<MySqlPool>,
) -> Result<impl IntoResponse, APIError> {
let category = Category::get(&params.id, &db_pool).await?;
let category = Category::get(&params.id, &db_pool)
.await?
.ok_or_else(|| APIError::NotFound("Category not found".to_string()))?;
Ok(Json(CategoryResponse {
status: 200,
error: None,
@ -67,7 +79,9 @@ async fn user(
Query(params): Query<UserIDQuery>,
Extension(db_pool): Extension<MySqlPool>,
) -> Result<impl IntoResponse, APIError> {
let user = User::get(params, &db_pool).await?;
let user = User::get(params.into(), &db_pool)
.await?
.ok_or_else(|| APIError::NotFound("User not found".to_string()))?;
Ok(Json(UserResponse {
status: 200,
error: None,
@ -85,11 +99,11 @@ async fn users(Extension(db_pool): Extension<MySqlPool>) -> Result<impl IntoResp
}
async fn random(
Query(params): Query<MemeFilterQuery>,
Query(params): Query<MemeFilter>,
Extension(db_pool): Extension<MySqlPool>,
Extension(vars): Extension<ConfVars>,
) -> Result<impl IntoResponse, APIError> {
let random = Meme::get_random(params, &db_pool, vars.cdn).await?;
let random = V1Meme::new(Meme::get_random(params, &db_pool).await?, vars.cdn);
Ok(Json(MemeResponse {
status: 200,
error: None,
@ -140,7 +154,9 @@ async fn upload(
return Err(APIError::Forbidden("Upload limit reached".to_string()));
}
let cat = Category::get(&category, &db_pool).await?;
let cat = Category::get(&category, &db_pool)
.await?
.ok_or_else(|| APIError::BadRequest("Category not existing".to_string()))?;
let ip = ip.to_string();

View file

@ -1,179 +0,0 @@
use crate::ipfs::IPFSFile;
use crate::v1::models::{Category, Meme, MemeFilterQuery, User, UserIDQuery};
use sqlx::mysql::MySqlRow;
use sqlx::{MySqlPool, Result, Row};
pub struct DBMeme {
pub id: i32,
pub filename: String,
pub user: String,
pub userdir: String,
pub category: String,
pub timestamp: i64,
pub ipfs: String,
}
impl Meme {
pub fn new(meme: DBMeme, cdn: String) -> Self {
Self {
id: meme.id.to_string(),
link: format!("{}/{}/{}", cdn, meme.userdir, meme.filename),
category: meme.category,
user: meme.user,
timestamp: meme.timestamp.to_string(),
ipfs: meme.ipfs,
}
}
pub async fn get(id: i32, pool: &MySqlPool, cdn: String) -> Result<Meme> {
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| Self::new(DBMeme {
id: row.get("id"),
filename: row.get("filename"),
user: row.get("name"),
userdir: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
}, cdn.clone()))
.fetch_one(pool).await?;
Ok(q)
}
pub async fn get_all(
params: MemeFilterQuery,
pool: &MySqlPool,
cdn: String,
) -> 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")
.bind(params.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", params.user.unwrap_or_else(String::new)))
.bind(format!("%{}%", params.search.unwrap_or_else(String::new)))
.map(|row: MySqlRow| Self::new(DBMeme {
id: row.get("id"),
filename: row.get("filename"),
user: row.get("name"),
userdir: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
}, cdn.clone()))
.fetch_all(pool).await?;
Ok(q)
}
pub async fn get_random(
params: MemeFilterQuery,
pool: &MySqlPool,
cdn: String,
) -> 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")
.bind(params.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", params.user.unwrap_or_else(String::new)))
.bind(format!("%{}%", params.search.unwrap_or_else(String::new)))
.map(|row: MySqlRow| Self::new(DBMeme {
id: row.get("id"),
filename: row.get("filename"),
user: row.get("name"),
userdir: row.get("user"),
category: row.get("category"),
timestamp: row.get("ts"),
ipfs: row.get("cid"),
}, cdn.clone()))
.fetch_one(pool).await?;
Ok(q)
}
}
impl Category {
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Self> {
let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?")
.bind(id)
.map(|row: MySqlRow| Self {
id: row.get("id"),
name: row.get("name"),
})
.fetch_one(pool)
.await?;
Ok(q)
}
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
let q: Vec<Category> = sqlx::query("SELECT * FROM categories ORDER BY num")
.map(|row: MySqlRow| Self {
id: row.get("id"),
name: row.get("name"),
})
.fetch_all(pool)
.await?;
Ok(q)
}
pub async fn add_meme(
&self,
user: &User,
file: &IPFSFile,
ip: &String,
pool: &MySqlPool,
) -> Result<u64> {
let mut tx = pool.begin().await?;
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(&mut tx).await?;
let id: u64 = sqlx::query("SELECT LAST_INSERT_ID() as id")
.map(|row: MySqlRow| row.get("id"))
.fetch_one(&mut tx)
.await?;
tx.commit().await?;
Ok(id)
}
}
impl 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'")
.bind(params.id.unwrap_or_else(String::new))
.bind(params.token.unwrap_or_else(String::new))
.bind(params.name.unwrap_or_else(String::new))
.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_one(pool).await?;
Ok(q)
}
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'")
.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_all(pool).await?;
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)
}
}