feat(uploader): add ryzencdn local-disk uploader (POST multipart + GET serve)
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Shirokami-API source uploads to Ryzumi S3 (s3.ryzumi.vip) which is DNS-dead
from this VPS. Implemented a local-disk uploader keeping the source's response
shape {success, url, fileName, size}:
- POST /uploader/ryzencdn (multipart 'file' field, magic-byte ext detection)
- GET /uploader/file/{name} (path-traversal safe, MIME by extension)
Files stored under /var/lib/scraper/uploads (owned by scraper user).
This commit is contained in:
@@ -9,4 +9,5 @@ pub mod proxy;
|
||||
pub mod search;
|
||||
pub mod stalk;
|
||||
pub mod tools;
|
||||
pub mod uploader;
|
||||
pub mod weebs;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Axum handlers for the uploader.
|
||||
//!
|
||||
//! POST /uploader/ryzencdn — multipart file upload, saved to local disk.
|
||||
//! GET /uploader/file/:name — serve an uploaded file.
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Multipart, Path};
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::Response;
|
||||
use axum::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::application::uploader as use_cases;
|
||||
use crate::presentation::error::AppError;
|
||||
|
||||
/// POST /uploader/ryzencdn — accept a multipart file upload.
|
||||
pub async fn ryzencdn_handler(mut multipart: Multipart) -> Result<Json<Value>, AppError> {
|
||||
// Read the first file field.
|
||||
let mut file_name = String::from("");
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| AppError::BadRequest(format!("multipart: {e}")))?
|
||||
{
|
||||
let name = field.name().unwrap_or("file").to_string();
|
||||
if name == "file" {
|
||||
file_name = field.file_name().unwrap_or("").to_string();
|
||||
let content = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| AppError::BadRequest(format!("bytes: {e}")))?;
|
||||
data = content.to_vec();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"No file field named 'file' in multipart body".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let result = use_cases::upload(&data, &file_name)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /uploader/file/:name — serve an uploaded file.
|
||||
pub async fn serve_file_handler(Path(name): Path<String>) -> Result<Response<Body>, AppError> {
|
||||
let (bytes, mime) = use_cases::serve(&name)?;
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime)
|
||||
.header(header::CACHE_CONTROL, "public, max-age=86400")
|
||||
.body(Body::from(bytes))
|
||||
.unwrap())
|
||||
}
|
||||
@@ -382,6 +382,15 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
|
||||
"/image/brat/animated",
|
||||
axum::routing::get(crate::presentation::handler::image::brat_animated_handler),
|
||||
)
|
||||
// Uploader routes
|
||||
.route(
|
||||
"/uploader/ryzencdn",
|
||||
axum::routing::post(crate::presentation::handler::uploader::ryzencdn_handler),
|
||||
)
|
||||
.route(
|
||||
"/uploader/file/{name}",
|
||||
axum::routing::get(crate::presentation::handler::uploader::serve_file_handler),
|
||||
)
|
||||
// Health
|
||||
.route(
|
||||
"/health",
|
||||
|
||||
Reference in New Issue
Block a user