mirror of https://github.com/wfrsk/violetta.git
[feat] basic file uploader
This commit is contained in:
parent
49d9b7cb8a
commit
669899425d
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/images
|
|
@ -911,6 +911,7 @@ dependencies = [
|
|||
"rocket_codegen",
|
||||
"rocket_http",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"state",
|
||||
"tempfile",
|
||||
"time",
|
||||
|
@ -1020,6 +1021,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.5"
|
||||
|
@ -1342,6 +1349,29 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
"sha1_smol",
|
||||
"uuid-macro-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "548f7181a5990efa50237abb7ebca410828b57a8955993334679f8b50b35c97d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -1359,6 +1389,8 @@ name = "violetta"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rocket",
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -3,10 +3,16 @@ name = "violetta"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.0-rc.2", features = ["secrets"] }
|
||||
rocket = { version = "0.5.0-rc.2", features = ["serde_json", "json"] }
|
||||
serde = "1.0.144"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.1.2"
|
||||
features = [
|
||||
"v5", # Lets you generate random UUIDs
|
||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
||||
]
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
[default]
|
||||
address = "0.0.0.0"
|
||||
port =
|
||||
port = 8080
|
||||
|
||||
|
||||
[default.tls]
|
||||
certs = "path/to/cert-chain.pem"
|
||||
key = "path/to/key.pem"
|
||||
|
||||
[default.limits]
|
||||
form = "64 kB"
|
||||
|
@ -22,5 +20,7 @@ mercy = 5
|
|||
port = 3000
|
||||
|
||||
limits = { json = "10MiB" }
|
||||
|
||||
[release]
|
||||
port = 8443
|
||||
secret_key = "THIS IS A PLACEHOLDER, GENERATE YOUR OWN KEYS."
|
206
src/main.rs
206
src/main.rs
|
@ -1,24 +1,208 @@
|
|||
mod test;
|
||||
use rocket::{
|
||||
data::ToByteUnit,
|
||||
fairing::AdHoc,
|
||||
figment::{
|
||||
providers::{Env, Format, Serialized, Toml},
|
||||
Figment, Profile,
|
||||
},
|
||||
fs::{relative, NamedFile},
|
||||
http::{ContentType, Status},
|
||||
request::{FromRequest, Outcome},
|
||||
serde::json::Json,
|
||||
serde::*,
|
||||
tokio::{fs::File, io::AsyncWriteExt},
|
||||
*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use rocket::*;
|
||||
#[derive(Default)]
|
||||
struct RequiresAuthentication;
|
||||
|
||||
#[get("/hello/<name>")]
|
||||
fn greet_user( name: String ) -> String {
|
||||
format!("Hello, {name}!")
|
||||
#[rocket::async_trait]
|
||||
impl<'a> FromRequest<'a> for RequiresAuthentication {
|
||||
type Error = UploadResponse;
|
||||
|
||||
async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let expected_key = &req
|
||||
.guard::<&State<UploaderConfiguration>>()
|
||||
.await
|
||||
.unwrap()
|
||||
.inner()
|
||||
.auth_key;
|
||||
match expected_key {
|
||||
Some(actual_key @ _) => {
|
||||
if let Some(extern_key) = req.headers().get_one("Authorization") {
|
||||
if actual_key == extern_key {
|
||||
Outcome::Success(RequiresAuthentication::default())
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, UploadResponse::error()))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, UploadResponse::error()))
|
||||
}
|
||||
}
|
||||
None => Outcome::Success(RequiresAuthentication::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UploaderConfiguration {
|
||||
pub max_size: u32,
|
||||
pub auth_key: Option<String>,
|
||||
pub allowed_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct UploadResponse {
|
||||
successful: bool,
|
||||
file_id: Option<String>,
|
||||
}
|
||||
|
||||
impl UploadResponse {
|
||||
fn new(successful: bool, file_id: Option<String>) -> Self {
|
||||
Self {
|
||||
successful,
|
||||
file_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn error() -> Self {
|
||||
Self {
|
||||
successful: false,
|
||||
file_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/", data = "<file>")]
|
||||
async fn upload_media(
|
||||
file: Data<'_>,
|
||||
config: &State<UploaderConfiguration>,
|
||||
_a: RequiresAuthentication,
|
||||
content_type: &ContentType,
|
||||
) -> std::io::Result<Json<UploadResponse>> {
|
||||
match content_type.extension().map(|ext| ext.as_str()) {
|
||||
ext @ Some("png") | ext @ Some("mp4") => {
|
||||
let ext = ext.unwrap();
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let raw_data = file
|
||||
.open(config.max_size.mebibytes())
|
||||
.into_bytes()
|
||||
.await?
|
||||
.into_inner();
|
||||
let file_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, &raw_data);
|
||||
let mut file = File::create(current_dir.join(format!(
|
||||
"{}/{}.{ext}",
|
||||
relative!("images"),
|
||||
file_id
|
||||
)))
|
||||
.await?;
|
||||
file.write_all(&raw_data.as_slice()).await?;
|
||||
Ok(Json(UploadResponse::new(true, Some(file_id.to_string()))))
|
||||
}
|
||||
_ => Ok(Json(UploadResponse::error())),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<file_id>")]
|
||||
async fn retrieve_media(file_id: &str, config: &State<UploaderConfiguration>) -> Option<NamedFile> {
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let ext: Option<String> = {
|
||||
let mut corresponding_ext: Option<String> = None;
|
||||
for ext in config.allowed_extensions.iter() {
|
||||
let corresponding_path =
|
||||
current_dir.join(format!("{}/{}.{ext}", relative!("images"), file_id));
|
||||
|
||||
if corresponding_path.is_file() {
|
||||
corresponding_ext = Some(ext.clone());
|
||||
}
|
||||
}
|
||||
corresponding_ext
|
||||
};
|
||||
if let Some(ext) = ext {
|
||||
Some(
|
||||
NamedFile::open(current_dir.join(format!("{}/{file_id}.{ext}", relative!("images"))))
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index( ) -> &'static str {
|
||||
fn index() -> &'static str {
|
||||
"USAGE
|
||||
GET /hello/:name
|
||||
simply, greets you.
|
||||
GET /
|
||||
what are you reading right now
|
||||
|
||||
PUT /
|
||||
upload a file, send as raw body
|
||||
|
||||
GET /:id
|
||||
gets the file <:id>
|
||||
"
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
fn unauthorized() -> Json<UploadResponse> {
|
||||
Json(UploadResponse::error())
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
fn internal_server_err() -> Json<UploadResponse> {
|
||||
Json(UploadResponse::error())
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
fn not_found() -> Json<UploadResponse> {
|
||||
Json(UploadResponse::error())
|
||||
}
|
||||
|
||||
#[launch]
|
||||
pub fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.mount(
|
||||
"/", routes![ index, greet_user ]
|
||||
rocket::custom(
|
||||
Figment::from(rocket::Config::default())
|
||||
.merge(Serialized::defaults(Config::default()))
|
||||
.merge(Toml::file("violetta.toml").nested())
|
||||
.merge(Env::prefixed("VIOLETTA_").global())
|
||||
.select(Profile::from_env_or("VIOLETTA_PROFILE", "default")),
|
||||
)
|
||||
.mount("/", routes![index, upload_media, retrieve_media])
|
||||
.register("/", catchers![not_found, unauthorized, internal_server_err])
|
||||
.attach(AdHoc::config::<UploaderConfiguration>())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::*;
|
||||
use rocket::local::blocking::Client;
|
||||
|
||||
macro_rules! client {
|
||||
() => {
|
||||
Client::tracked(rocket()).expect("valid rocket instance")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! request {
|
||||
( $client:ident, $method:ident, $path:expr ) => {
|
||||
$client.$method($path)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_file() {
|
||||
let client = client!();
|
||||
let response = request!(client, put, "/")
|
||||
.header(ContentType::PNG)
|
||||
.body(&[0u8; 1024])
|
||||
.dispatch();
|
||||
let response_body = response.into_json::<UploadResponse>().unwrap();
|
||||
assert_eq!(response_body.successful, true);
|
||||
assert!(response_body.file_id.is_some());
|
||||
let response = request!(client, get, format!( "/{}", response_body.file_id.unwrap() )).dispatch();
|
||||
assert_eq!(response.into_bytes().unwrap().as_slice(), [0u8; 1024]);
|
||||
}
|
||||
}
|
||||
|
|
14
src/test.rs
14
src/test.rs
|
@ -1,14 +0,0 @@
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use rocket::local::blocking::Client;
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn does_it_greet_us( ) {
|
||||
let client = Client::tracked( rocket() )
|
||||
.expect( "invalid rocket instance" );
|
||||
|
||||
let response = client.get( "/hello/World" ).dispatch( );
|
||||
assert_eq!( response.into_string( ).unwrap( ), "Hello, World!" );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
[default]
|
||||
max_size = 16 # in mebibytes [MiB]
|
||||
allowed_extensions = [ "png", "jpg", "mp4" ]
|
Loading…
Reference in New Issue