Init
This commit is contained in:
commit
039f785129
8 changed files with 3068 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.idea
|
||||
*.iml
|
2691
Cargo.lock
generated
Normal file
2691
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "jmserver"
|
||||
version = "0.1.0"
|
||||
authors = ["Timo Ley <timo@leyt.xyz>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.51"
|
||||
sqlx = { version = "0.3", features = [ "mysql" ] }
|
||||
rand = "0.8.0"
|
21
src/main.rs
Normal file
21
src/main.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use actix_web::{HttpServer, App};
|
||||
use std::{io, env};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
mod v1;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()>{
|
||||
|
||||
let database_url = env::var("DBURL").unwrap();
|
||||
let db_pool = MySqlPool::new(&database_url).await.unwrap();
|
||||
|
||||
let mut server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(db_pool.clone())
|
||||
.configure(v1::init)
|
||||
});
|
||||
|
||||
server = server.bind(env::var("LISTEN").unwrap())?;
|
||||
server.run().await
|
||||
}
|
5
src/v1/mod.rs
Normal file
5
src/v1/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod routes;
|
||||
pub mod models;
|
||||
mod sql;
|
||||
|
||||
pub use routes::init;
|
104
src/v1/models.rs
Normal file
104
src/v1/models.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Meme {
|
||||
pub id: String,
|
||||
pub link: String,
|
||||
pub category: String,
|
||||
pub user: String,
|
||||
pub timestamp: 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>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MemeResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub meme: Option<Meme>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CategoriesResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub categories: Option<Vec<Category>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CategoryResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub category: Option<Category>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UsersResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub users: Option<Vec<User>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UploadResponse {
|
||||
pub status: i32,
|
||||
pub error: Option<String>,
|
||||
pub files: Option<Vec<String>>
|
||||
}
|
||||
|
||||
//Query
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IDQuery {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MemeIDQuery {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserIDQuery {
|
||||
pub id: Option<String>,
|
||||
pub token: Option<String>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MemeFilterQuery {
|
||||
pub category: Option<String>,
|
||||
pub user: Option<String>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
|
115
src/v1/routes.rs
Normal file
115
src/v1/routes.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use actix_web::{web, get, Responder, HttpResponse};
|
||||
use crate::v1::models::*;
|
||||
use sqlx::{MySqlPool, Error};
|
||||
|
||||
#[get("/v1/meme")]
|
||||
async fn meme(params: web::Query<MemeIDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = Meme::get(params.id, db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(meme) => HttpResponse::Ok().json(MemeResponse {
|
||||
status: 200,
|
||||
error: None,
|
||||
meme: Option::from(meme)
|
||||
}),
|
||||
Err(err) => match err {
|
||||
Error::RowNotFound => HttpResponse::NotFound().json(MemeResponse {
|
||||
status: 404,
|
||||
error: Option::from(String::from("Meme not found")),
|
||||
meme: None
|
||||
}),
|
||||
_ => HttpResponse::InternalServerError().json(MemeResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
meme: None
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/memes")]
|
||||
async fn memes(params: web::Query<MemeFilterQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = Meme::get_all(params.0, db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(memes) => HttpResponse::Ok().json(MemesResponse {
|
||||
status: 200,
|
||||
error: None,
|
||||
memes: Option::from(memes)
|
||||
}),
|
||||
_ => HttpResponse::InternalServerError().json(MemesResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
memes: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/category")]
|
||||
async fn category(params: web::Query<IDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = Category::get(¶ms.id, db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(category) => HttpResponse::Ok().json(CategoryResponse { status: 200, error: None, category: Option::from(category)}),
|
||||
Err(err) => match err {
|
||||
Error::RowNotFound => HttpResponse::NotFound().json(CategoryResponse {
|
||||
status: 404,
|
||||
error: Option::from(String::from("Category not found")),
|
||||
category: None
|
||||
}),
|
||||
_ => HttpResponse::InternalServerError().json(CategoryResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
category: None
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/categories")]
|
||||
async fn categories(db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = Category::get_all(db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(categories) => HttpResponse::Ok().json(CategoriesResponse { status: 200, error: None, categories: Option::from(categories)}),
|
||||
_ => HttpResponse::InternalServerError().json(CategoriesResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
categories: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/user")]
|
||||
async fn user(params: web::Query<UserIDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = User::get(params.0,db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(user) => HttpResponse::Ok().json(UserResponse { status: 200, error: None, user: Option::from(user)}),
|
||||
_ => HttpResponse::InternalServerError().json(UserResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
user: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/users")]
|
||||
async fn users(db_pool: web::Data<MySqlPool>) -> impl Responder {
|
||||
let q = User::get_all(db_pool.get_ref()).await;
|
||||
match q {
|
||||
Ok(users) => HttpResponse::Ok().json(UsersResponse { status: 200, error: None, users: Option::from(users)}),
|
||||
_ => HttpResponse::InternalServerError().json(UsersResponse {
|
||||
status: 500,
|
||||
error: Option::from(String::from("Internal Server Error")),
|
||||
users: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Implement random meme endpoint
|
||||
//TODO: Implement upload endpoint
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(meme);
|
||||
cfg.service(memes);
|
||||
cfg.service(category);
|
||||
cfg.service(categories);
|
||||
cfg.service(user);
|
||||
cfg.service(users);
|
||||
}
|
115
src/v1/sql.rs
Normal file
115
src/v1/sql.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use crate::v1::models::{Meme, MemeFilterQuery, Category, User, UserIDQuery};
|
||||
use sqlx::{MySqlPool, Result, Row};
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use std::env;
|
||||
|
||||
pub struct DBMeme {
|
||||
pub id: i32,
|
||||
pub filename: String,
|
||||
pub user: String,
|
||||
pub userdir: String,
|
||||
pub category: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
|
||||
impl Meme {
|
||||
pub async fn get(id: i32, pool: &MySqlPool) -> Result<Meme> {
|
||||
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id)
|
||||
.map(|row: MySqlRow| Meme::from(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"),
|
||||
}))
|
||||
.fetch_one(pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_all(params: MemeFilterQuery, pool: &MySqlPool) -> Result<Vec<Meme>> {
|
||||
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts 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(format!("%{}%", params.user.unwrap_or(String::from(""))))
|
||||
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
|
||||
.map(|row: MySqlRow| Meme::from(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"),
|
||||
}))
|
||||
.fetch_all(pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl From<DBMeme> for Meme {
|
||||
fn from(meme: DBMeme) -> Self {
|
||||
Meme {
|
||||
id: meme.id.to_string(),
|
||||
link: format!("{}/{}/{}", env::var("CDNURL").unwrap(), meme.userdir, meme.filename),
|
||||
category: meme.category,
|
||||
user: meme.user,
|
||||
timestamp: meme.timestamp.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Category> {
|
||||
let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?").bind(id)
|
||||
.map(|row: MySqlRow| Category {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
})
|
||||
.fetch_one(pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Category>> {
|
||||
let q: Vec<Category> = sqlx::query("SELECT * FROM categories ORDER BY num")
|
||||
.map(|row: MySqlRow| Category {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
})
|
||||
.fetch_all(pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
|
||||
pub async fn get(params: UserIDQuery, pool: &MySqlPool) -> Result<User> {
|
||||
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.token.unwrap_or(String::from("")))
|
||||
.bind(params.name.unwrap_or(String::from("")))
|
||||
.map(|row: MySqlRow| User {
|
||||
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<User>> {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue