mirror of https://github.com/raftario/filite.git
Added PUT routes
This commit is contained in:
parent
ea56031235
commit
901bf9bdac
|
@ -2,6 +2,7 @@ PORT=8080
|
|||
DATABASE_URL=target/database.db
|
||||
POOL_SIZE=4
|
||||
FILES_DIR=target/static/
|
||||
MAX_FILESIZE=50000000
|
||||
|
||||
RUST_LOG=actix_web=debug
|
||||
LOG_FORMAT='[%r] (%D ms) : [%s] (%b B)'
|
||||
|
|
|
@ -588,6 +588,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"actix-files 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"actix-web 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -7,6 +7,7 @@ edition = "2018"
|
|||
[dependencies]
|
||||
actix-files = "0.1.5"
|
||||
actix-web = "1.0.8"
|
||||
base64 = "0.10.1"
|
||||
cfg-if = "0.1.10"
|
||||
chrono = "0.4.9"
|
||||
dirs = "2.0.2"
|
||||
|
|
209
src/main.rs
209
src/main.rs
|
@ -1,18 +1,20 @@
|
|||
#[macro_use]
|
||||
extern crate cfg_if;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use filite::queries::{self, SelectFilters, SelectQuery};
|
||||
use filite::setup::{self, Config};
|
||||
use filite::Pool;
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use actix_web::error::BlockingError;
|
||||
use actix_web::{http, web, App, Error, FromRequest, HttpResponse, HttpServer, Responder};
|
||||
use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
|
||||
use futures::future::{self, Either};
|
||||
use futures::Future;
|
||||
use std::process;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, num, process};
|
||||
|
||||
/// Performs the initial setup
|
||||
#[cfg(not(debug_assertions))]
|
||||
|
@ -67,8 +69,18 @@ fn not_found() -> Error {
|
|||
|
||||
/// Parses a base 36 ID
|
||||
#[inline(always)]
|
||||
fn parse_id(s: &str) -> i32 {
|
||||
i32::from_str_radix(s, 36).unwrap_or(-1)
|
||||
fn id_from_b36(s: &str) -> Result<i32, num::ParseIntError> {
|
||||
i32::from_str_radix(s, 36)
|
||||
}
|
||||
|
||||
/// Parses an ID and errors if it fails
|
||||
macro_rules! parse_id {
|
||||
($s:expr) => {
|
||||
match id_from_b36($s) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Either::B(future::err(HttpResponse::BadRequest().finish().into())),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// Formats a timestamp to the "Last-Modified" header format
|
||||
|
@ -84,19 +96,21 @@ fn get_file(
|
|||
pool: web::Data<Pool>,
|
||||
config: web::Data<Config>,
|
||||
) -> impl Future<Item = NamedFile, Error = Error> {
|
||||
let id = parse_id(&path);
|
||||
let id = parse_id!(&path);
|
||||
let files_dir = config.files_dir.clone();
|
||||
web::block(move || queries::files::find(id, pool)).then(|result| match result {
|
||||
Ok(file) => {
|
||||
let mut path = files_dir;
|
||||
path.push(file.filepath);
|
||||
match NamedFile::open(&path) {
|
||||
Ok(nf) => Ok(nf),
|
||||
Err(_) => Err(HttpResponse::NotFound().finish().into()),
|
||||
Either::A(
|
||||
web::block(move || queries::files::find(id, pool)).then(|result| match result {
|
||||
Ok(file) => {
|
||||
let mut path = files_dir;
|
||||
path.push(file.filepath);
|
||||
match NamedFile::open(&path) {
|
||||
Ok(nf) => Ok(nf),
|
||||
Err(_) => Err(HttpResponse::NotFound().finish().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Err(not_found()),
|
||||
})
|
||||
Err(_) => Err(not_found()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// GET a link entry and redirect to it
|
||||
|
@ -104,14 +118,16 @@ fn get_link(
|
|||
path: web::Path<String>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let id = parse_id(&path);
|
||||
web::block(move || queries::links::find(id, pool)).then(|result| match result {
|
||||
Ok(link) => Ok(HttpResponse::Found()
|
||||
.header("Location", link.forward)
|
||||
.header("Last-Modified", timestamp_to_last_modified(link.updated))
|
||||
.finish()),
|
||||
Err(_) => Err(not_found()),
|
||||
})
|
||||
let id = parse_id!(&path);
|
||||
Either::A(
|
||||
web::block(move || queries::links::find(id, pool)).then(|result| match result {
|
||||
Ok(link) => Ok(HttpResponse::Found()
|
||||
.header("Location", link.forward)
|
||||
.header("Last-Modified", timestamp_to_last_modified(link.updated))
|
||||
.finish()),
|
||||
Err(_) => Err(not_found()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// GET a text entry and display it
|
||||
|
@ -119,13 +135,128 @@ fn get_text(
|
|||
path: web::Path<String>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let id = parse_id(&path);
|
||||
web::block(move || queries::texts::find(id, pool)).then(|result| match result {
|
||||
Ok(text) => Ok(HttpResponse::Ok()
|
||||
.header("Last-Modified", timestamp_to_last_modified(text.updated))
|
||||
.body(text.contents)),
|
||||
Err(_) => Err(not_found()),
|
||||
})
|
||||
let id = parse_id!(&path);
|
||||
Either::A(
|
||||
web::block(move || queries::texts::find(id, pool)).then(|result| match result {
|
||||
Ok(text) => Ok(HttpResponse::Ok()
|
||||
.header("Last-Modified", timestamp_to_last_modified(text.updated))
|
||||
.body(text.contents)),
|
||||
Err(_) => Err(not_found()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Request body when PUTting files
|
||||
#[derive(Deserialize)]
|
||||
struct PutFile {
|
||||
base64: String,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
/// Request body when PUTting links
|
||||
#[derive(Deserialize)]
|
||||
struct PutLink {
|
||||
forward: String,
|
||||
}
|
||||
|
||||
/// Request body when PUTting texts
|
||||
#[derive(Deserialize)]
|
||||
struct PutText {
|
||||
contents: String,
|
||||
}
|
||||
|
||||
macro_rules! put_then {
|
||||
($f:expr) => {
|
||||
$f.then(|result| match result {
|
||||
Ok(x) => Ok(HttpResponse::Created().json(x)),
|
||||
Err(_) => Err(HttpResponse::InternalServerError().finish().into()),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// PUT a new file entry
|
||||
fn put_file(
|
||||
(path, body): (web::Path<String>, web::Json<PutFile>),
|
||||
config: web::Data<Config>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let id = parse_id!(&path);
|
||||
Either::A(
|
||||
web::block(move || {
|
||||
let mut path = config.files_dir.clone();
|
||||
let mut relative_path = PathBuf::new();
|
||||
|
||||
let current_time = Utc::now();
|
||||
let current_date = current_time.date().naive_utc();
|
||||
path.push(&format!("{:04}", current_date.year()));
|
||||
relative_path.push(&format!("{:04}", current_date.year()));
|
||||
path.push(&format!("{:02}", current_date.month()));
|
||||
relative_path.push(&format!("{:02}", current_date.month()));
|
||||
path.push(&format!("{:02}", current_date.day()));
|
||||
relative_path.push(&format!("{:02}", current_date.day()));
|
||||
|
||||
if fs::create_dir_all(&path).is_err() {
|
||||
return Err(http::StatusCode::from_u16(500).unwrap());
|
||||
}
|
||||
|
||||
let mut filename = body.filename.clone();
|
||||
let timestamp = format!("{:x}.", current_time.timestamp());
|
||||
filename.insert_str(0, ×tamp);
|
||||
path.push(&filename);
|
||||
relative_path.push(&filename);
|
||||
|
||||
let relative_path = match relative_path.to_str() {
|
||||
Some(rp) => rp,
|
||||
None => return Err(http::StatusCode::from_u16(500).unwrap()),
|
||||
};
|
||||
|
||||
let contents = match base64::decode(&body.base64) {
|
||||
Ok(contents) => contents,
|
||||
Err(_) => return Err(http::StatusCode::from_u16(400).unwrap()),
|
||||
};
|
||||
if fs::write(&path, contents).is_err() {
|
||||
return Err(http::StatusCode::from_u16(500).unwrap());
|
||||
}
|
||||
|
||||
match queries::files::replace(id, relative_path, pool) {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => Err(http::StatusCode::from_u16(500).unwrap()),
|
||||
}
|
||||
})
|
||||
.then(|result| match result {
|
||||
Ok(file) => Ok(HttpResponse::Created().json(file)),
|
||||
Err(e) => match e {
|
||||
BlockingError::Error(sc) => Err(HttpResponse::new(sc).into()),
|
||||
BlockingError::Canceled => Err(HttpResponse::InternalServerError().finish().into()),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// PUT a new link entry
|
||||
fn put_link(
|
||||
(path, body): (web::Path<String>, web::Json<PutLink>),
|
||||
pool: web::Data<Pool>,
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let id = parse_id!(&path);
|
||||
Either::A(put_then!(web::block(move || queries::links::replace(
|
||||
id,
|
||||
&body.forward,
|
||||
pool
|
||||
))))
|
||||
}
|
||||
|
||||
/// PUT a new text entry
|
||||
fn put_text(
|
||||
(path, body): (web::Path<String>, web::Json<PutText>),
|
||||
pool: web::Data<Pool>,
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let id = parse_id!(&path);
|
||||
Either::A(put_then!(web::block(move || queries::texts::replace(
|
||||
id,
|
||||
&body.contents,
|
||||
pool
|
||||
))))
|
||||
}
|
||||
|
||||
/// GET the config info
|
||||
|
@ -152,6 +283,7 @@ fn main() {
|
|||
let pool = setup::create_pool(&config.database_url, config.pool_size);
|
||||
|
||||
let port = config.port;
|
||||
let max_filesize = (config.max_filesize as f64 * 1.37) as usize;
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
|
@ -166,6 +298,15 @@ fn main() {
|
|||
.route("/f/{id}", web::get().to_async(get_file))
|
||||
.route("/l/{id}", web::get().to_async(get_link))
|
||||
.route("/t/{id}", web::get().to_async(get_text))
|
||||
.service(
|
||||
web::resource("/f/{id}")
|
||||
.data(web::Json::<PutFile>::configure(|cfg| {
|
||||
cfg.limit(max_filesize)
|
||||
}))
|
||||
.route(web::put().to_async(put_file)),
|
||||
)
|
||||
.service(web::resource("/l/{id}").route(web::put().to_async(put_link)))
|
||||
.service(web::resource("/t/{id}").route(web::put().to_async(put_text)))
|
||||
})
|
||||
.bind(&format!("localhost:{}", port))
|
||||
.unwrap_or_else(|e| {
|
||||
|
|
|
@ -129,16 +129,18 @@ pub mod files {
|
|||
|
||||
find!(files, File);
|
||||
|
||||
/// INSERT a file entry
|
||||
pub fn insert(i_id: i32, p_filepath: &str, pool: Data<Pool>) -> QueryResult<File> {
|
||||
/// REPLACE a file entry
|
||||
pub fn replace(r_id: i32, r_filepath: &str, pool: Data<Pool>) -> QueryResult<File> {
|
||||
let conn: &SqliteConnection = &pool.get().unwrap();
|
||||
let new_file = NewFile {
|
||||
id: i_id,
|
||||
filepath: p_filepath,
|
||||
id: r_id,
|
||||
filepath: r_filepath,
|
||||
};
|
||||
diesel::insert_into(table).values(&new_file).execute(conn)?;
|
||||
diesel::replace_into(table)
|
||||
.values(&new_file)
|
||||
.execute(conn)?;
|
||||
|
||||
find(i_id, pool)
|
||||
find(r_id, pool)
|
||||
}
|
||||
|
||||
/// UPDATE a file entry
|
||||
|
@ -199,16 +201,18 @@ pub mod links {
|
|||
|
||||
find!(links, Link);
|
||||
|
||||
/// INSERT a link entry
|
||||
pub fn insert(i_id: i32, p_forward: &str, pool: Data<Pool>) -> QueryResult<Link> {
|
||||
/// REPLACE a link entry
|
||||
pub fn replace(r_id: i32, r_forward: &str, pool: Data<Pool>) -> QueryResult<Link> {
|
||||
let conn: &SqliteConnection = &pool.get().unwrap();
|
||||
let new_link = NewLink {
|
||||
id: i_id,
|
||||
forward: p_forward,
|
||||
id: r_id,
|
||||
forward: r_forward,
|
||||
};
|
||||
diesel::insert_into(table).values(&new_link).execute(conn)?;
|
||||
diesel::replace_into(table)
|
||||
.values(&new_link)
|
||||
.execute(conn)?;
|
||||
|
||||
find(i_id, pool)
|
||||
find(r_id, pool)
|
||||
}
|
||||
|
||||
/// UPDATE a link entry
|
||||
|
@ -267,16 +271,18 @@ pub mod texts {
|
|||
|
||||
find!(texts, Text);
|
||||
|
||||
/// INSERT a text entry
|
||||
pub fn insert(i_id: i32, p_contents: &str, pool: Data<Pool>) -> QueryResult<Text> {
|
||||
/// REPLACE a text entry
|
||||
pub fn replace(r_id: i32, r_contents: &str, pool: Data<Pool>) -> QueryResult<Text> {
|
||||
let conn: &SqliteConnection = &pool.get().unwrap();
|
||||
let new_text = NewText {
|
||||
id: i_id,
|
||||
contents: p_contents,
|
||||
id: r_id,
|
||||
contents: r_contents,
|
||||
};
|
||||
diesel::insert_into(table).values(&new_text).execute(conn)?;
|
||||
diesel::replace_into(table)
|
||||
.values(&new_text)
|
||||
.execute(conn)?;
|
||||
|
||||
find(i_id, pool)
|
||||
find(r_id, pool)
|
||||
}
|
||||
|
||||
/// UPDATE a text entry
|
||||
|
|
64
src/setup.rs
64
src/setup.rs
|
@ -28,6 +28,20 @@ fn get_config_path() -> PathBuf {
|
|||
path
|
||||
}
|
||||
|
||||
/// Returns an environment variable and panic if it isn't found
|
||||
macro_rules! get_env {
|
||||
($k:literal) => {
|
||||
env::var($k).expect(&format!("Can't find {} environment variable.", $k));
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a parsed environment variable and panic if it isn't found or is not parsable
|
||||
macro_rules! parse_env {
|
||||
($k:literal) => {
|
||||
get_env!($k).parse().expect(&format!("Invalid {}.", $k))
|
||||
};
|
||||
}
|
||||
|
||||
/// Application configuration
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
|
@ -40,6 +54,8 @@ pub struct Config {
|
|||
pub pool_size: u32,
|
||||
/// Directory where to store static files
|
||||
pub files_dir: PathBuf,
|
||||
/// Maximum allowed file size
|
||||
pub max_filesize: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -58,12 +74,14 @@ impl Default for Config {
|
|||
path.push("files");
|
||||
path
|
||||
};
|
||||
let max_filesize = 10_000_000;
|
||||
|
||||
Config {
|
||||
port,
|
||||
database_url,
|
||||
pool_size,
|
||||
files_dir,
|
||||
max_filesize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,10 +96,36 @@ impl Config {
|
|||
return Err("Can't read config file.");
|
||||
};
|
||||
let result = toml::from_str(&contents);
|
||||
match result {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err("Invalid config file."),
|
||||
|
||||
if result.is_err() {
|
||||
return Err("Invalid config file.");
|
||||
}
|
||||
let mut result: Config = result.unwrap();
|
||||
|
||||
if result.files_dir.is_absolute() {
|
||||
if let Err(_) = fs::create_dir_all(&result.files_dir) {
|
||||
return Err("Can't create files_dir.");
|
||||
}
|
||||
|
||||
result.files_dir = match result.files_dir.canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return Err("Invalid files_dir."),
|
||||
}
|
||||
} else {
|
||||
let mut data_dir = get_data_dir();
|
||||
data_dir.push(&result.files_dir);
|
||||
|
||||
if let Err(_) = fs::create_dir_all(&data_dir) {
|
||||
return Err("Can't create files_dir.");
|
||||
}
|
||||
|
||||
result.files_dir = match data_dir.canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return Err("Invalid files_dir."),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Serialize the config file
|
||||
|
@ -99,15 +143,11 @@ impl Config {
|
|||
pub fn debug() -> Self {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let get_env = |k: &str| -> String {
|
||||
env::var(k).expect(&format!("Can't parse {} environment variable.", k))
|
||||
};
|
||||
|
||||
let port = get_env("PORT").parse().expect("Invalid PORT.");
|
||||
let database_url = get_env("DATABASE_URL");
|
||||
let pool_size = get_env("POOL_SIZE").parse().expect("Invalid POOL_SIZE.");
|
||||
let port = parse_env!("PORT");
|
||||
let database_url = get_env!("DATABASE_URL");
|
||||
let pool_size = parse_env!("POOL_SIZE");
|
||||
let files_dir = {
|
||||
let files_dir = get_env("FILES_DIR");
|
||||
let files_dir = get_env!("FILES_DIR");
|
||||
let path = PathBuf::from_str(&files_dir).expect("Can't convert files dir to path");
|
||||
if path.is_absolute() {
|
||||
path.canonicalize().expect("Invalid FILES_DIR")
|
||||
|
@ -121,12 +161,14 @@ impl Config {
|
|||
.expect("Invalid FILES_DIR")
|
||||
}
|
||||
};
|
||||
let max_filesize = parse_env!("MAX_FILESIZE");
|
||||
|
||||
Config {
|
||||
port,
|
||||
database_url,
|
||||
pool_size,
|
||||
files_dir,
|
||||
max_filesize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue