135 lines
4.2 KiB
Rust
135 lines
4.2 KiB
Rust
use std::{
|
|
ffi::OsStr,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
|
|
use axum::{
|
|
body::Body,
|
|
extract::{Query, State},
|
|
response::{IntoResponse as _, Response},
|
|
};
|
|
use axum_extra::TypedHeader;
|
|
use headers::ContentLength;
|
|
use http::{HeaderValue, StatusCode};
|
|
use serde::Deserialize;
|
|
use serde_with::{DurationSeconds, serde_as};
|
|
use tracing::error;
|
|
|
|
use crate::engine::{Engine, ProcessOutcome};
|
|
|
|
fn default_keep_exif() -> bool {
|
|
false
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Deserialize)]
|
|
pub struct NewRequest {
|
|
name: String,
|
|
key: Option<String>,
|
|
|
|
#[serde(rename = "lastfor")]
|
|
#[serde_as(as = "Option<DurationSeconds>")]
|
|
last_for: Option<Duration>,
|
|
|
|
#[serde(rename = "keepexif", default = "default_keep_exif")]
|
|
keep_exif: bool,
|
|
}
|
|
|
|
/// The request handler for the /new path.
|
|
/// This handles all new uploads.
|
|
pub async fn new(
|
|
State(engine): State<Arc<Engine>>,
|
|
Query(req): Query<NewRequest>,
|
|
TypedHeader(ContentLength(content_length)): TypedHeader<ContentLength>,
|
|
body: Body,
|
|
) -> Result<Response, StatusCode> {
|
|
// check upload key, if i need to
|
|
if !engine.cfg.upload_key.is_empty() && req.key.unwrap_or_default() != engine.cfg.upload_key {
|
|
return Err(StatusCode::FORBIDDEN);
|
|
}
|
|
|
|
// the original file name wasn't given, so i can't work out what the extension should be
|
|
if req.name.is_empty() {
|
|
return Err(StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
// -- try to figure out a file extension..
|
|
|
|
fn extension(pb: &Path) -> Option<String> {
|
|
pb.extension().and_then(OsStr::to_str).map(str::to_string)
|
|
}
|
|
|
|
let pb = PathBuf::from(req.name);
|
|
let mut ext = extension(&pb);
|
|
|
|
// common extensions that usually have a second extension before themselves
|
|
const ADDITIVE: &[&str] = &["gz", "xz", "bz2", "lz4", "zst"];
|
|
|
|
// if the extension is one of those, try to find that second extension
|
|
if ext
|
|
.as_ref()
|
|
.is_some_and(|ext| ADDITIVE.contains(&ext.as_str()))
|
|
{
|
|
// try to parse out another extension
|
|
let stem = pb.file_stem().unwrap(); // SAFETY: if extension is Some(), this will also be
|
|
|
|
if let Some(second_ext) = extension(&PathBuf::from(stem)) {
|
|
// there is another extension,
|
|
// try to make sure it's one we want
|
|
// 4 is enough for most common file extensions
|
|
// and not many false positives, hopefully
|
|
if second_ext.len() <= 4 {
|
|
// seems ok so combine them
|
|
ext = ext.as_ref().map(|first_ext| second_ext + "." + first_ext);
|
|
}
|
|
}
|
|
}
|
|
|
|
// turn body into stream
|
|
let stream = Body::into_data_stream(body);
|
|
|
|
// pass it off to the engine to be processed
|
|
// --
|
|
// also, error responses here don't get presented properly in ShareX most of the time
|
|
// they don't expect the connection to close before they're done uploading, i think
|
|
// so it will just present the user with a "connection closed" error
|
|
match engine
|
|
.process(ext, content_length, stream, req.last_for, req.keep_exif)
|
|
.await
|
|
{
|
|
Ok(outcome) => match outcome {
|
|
// 200 OK
|
|
ProcessOutcome::Success { url, deletion_url } => {
|
|
let mut res = url.into_response();
|
|
|
|
// insert deletion url header if needed
|
|
if let Some(deletion_url) = deletion_url {
|
|
let deletion_url = HeaderValue::from_str(&deletion_url)
|
|
.expect("deletion url contains invalid chars");
|
|
|
|
let headers = res.headers_mut();
|
|
headers.insert("Breeze-Deletion-Url", deletion_url);
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
// 413 Payload Too Large
|
|
ProcessOutcome::UploadTooLarge | ProcessOutcome::TemporaryUploadTooLarge => {
|
|
Err(StatusCode::PAYLOAD_TOO_LARGE)
|
|
}
|
|
|
|
// 400 Bad Request
|
|
ProcessOutcome::TemporaryUploadLifetimeTooLong => Err(StatusCode::BAD_REQUEST),
|
|
},
|
|
|
|
// 500 Internal Server Error
|
|
Err(err) => {
|
|
error!("failed to process upload!! {err:#}");
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|