162 lines
4.8 KiB
Rust
162 lines
4.8 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::{Multipart, State},
|
|
http::{HeaderValue, StatusCode},
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use std::{io::Write, sync::Arc};
|
|
use klog_types::{BatchFilesRequest, FilesMetaResponse};
|
|
use serde_json::json;
|
|
use crate::{auth::AuthUser, storage, AppState};
|
|
|
|
pub async fn list_all_files_meta(
|
|
State(state): State<Arc<AppState>>,
|
|
auth: AuthUser,
|
|
) -> Result<Json<FilesMetaResponse>, AppError> {
|
|
require_admin(&auth)?;
|
|
let files = storage::list_all_files(&state.config.data_dir)
|
|
.await
|
|
.map_err(AppError::Io)?;
|
|
Ok(Json(FilesMetaResponse { files }))
|
|
}
|
|
|
|
pub async fn download_files(
|
|
State(state): State<Arc<AppState>>,
|
|
auth: AuthUser,
|
|
Json(req): Json<BatchFilesRequest>,
|
|
) -> Result<Response, AppError> {
|
|
require_admin(&auth)?;
|
|
|
|
let cursor = std::io::Cursor::new(Vec::new());
|
|
let mut zip = zip::ZipWriter::new(cursor);
|
|
let options = zip::write::SimpleFileOptions::default();
|
|
|
|
for user_files in &req.users {
|
|
for filename in &user_files.files {
|
|
if !storage::validate_filename(filename) {
|
|
continue;
|
|
}
|
|
let data = match storage::read_file(&state.config.data_dir, &user_files.username, filename).await {
|
|
Ok(d) => d,
|
|
Err(_) => continue,
|
|
};
|
|
let zip_path = format!("{}/{}", user_files.username, filename);
|
|
zip.start_file(&zip_path, options)
|
|
.map_err(|e| AppError::Internal(e.to_string()))?;
|
|
zip.write_all(&data)
|
|
.map_err(|e| AppError::Internal(e.to_string()))?;
|
|
}
|
|
}
|
|
|
|
let cursor = zip.finish().map_err(|e| AppError::Internal(e.to_string()))?;
|
|
let bytes = cursor.into_inner();
|
|
|
|
let mut response = Response::new(Body::from(bytes));
|
|
response
|
|
.headers_mut()
|
|
.insert("Content-Type", HeaderValue::from_static("application/zip"));
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn delete_files(
|
|
State(state): State<Arc<AppState>>,
|
|
auth: AuthUser,
|
|
Json(req): Json<BatchFilesRequest>,
|
|
) -> Result<Json<serde_json::Value>, AppError> {
|
|
require_admin(&auth)?;
|
|
|
|
let mut deleted = 0u32;
|
|
for user_files in &req.users {
|
|
for filename in &user_files.files {
|
|
if !storage::validate_filename(filename) {
|
|
continue;
|
|
}
|
|
if storage::delete_file(&state.config.data_dir, &user_files.username, filename)
|
|
.await
|
|
.unwrap_or(false)
|
|
{
|
|
deleted += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Json(json!({ "deleted": deleted })))
|
|
}
|
|
|
|
pub async fn list_my_files(
|
|
State(state): State<Arc<AppState>>,
|
|
auth: AuthUser,
|
|
) -> Result<Json<FilesMetaResponse>, AppError> {
|
|
let files = storage::list_user_files(&state.config.data_dir, &auth.username)
|
|
.await
|
|
.map_err(AppError::Io)?;
|
|
Ok(Json(FilesMetaResponse { files }))
|
|
}
|
|
|
|
pub async fn upload_file(
|
|
State(state): State<Arc<AppState>>,
|
|
auth: AuthUser,
|
|
mut multipart: Multipart,
|
|
) -> Result<Json<serde_json::Value>, AppError> {
|
|
let mut uploaded: Vec<String> = Vec::new();
|
|
|
|
while let Some(field) = multipart
|
|
.next_field()
|
|
.await
|
|
.map_err(|e| AppError::BadRequest(e.to_string()))?
|
|
{
|
|
let filename = field
|
|
.file_name()
|
|
.or_else(|| field.name())
|
|
.ok_or_else(|| AppError::BadRequest("Missing filename".to_string()))?
|
|
.to_string();
|
|
|
|
if !storage::validate_filename(&filename) {
|
|
return Err(AppError::BadRequest(format!("Invalid filename: {filename}")));
|
|
}
|
|
|
|
let data = field.bytes().await.map_err(|e| AppError::BadRequest(e.to_string()))?;
|
|
storage::write_file(&state.config.data_dir, &auth.username, &filename, &data)
|
|
.await
|
|
.map_err(AppError::Io)?;
|
|
|
|
uploaded.push(filename);
|
|
}
|
|
|
|
Ok(Json(json!({ "uploaded": uploaded })))
|
|
}
|
|
|
|
fn require_admin(auth: &AuthUser) -> Result<(), AppError> {
|
|
if auth.is_admin {
|
|
Ok(())
|
|
} else {
|
|
Err(AppError::Forbidden)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AppError {
|
|
Forbidden,
|
|
BadRequest(String),
|
|
Internal(String),
|
|
Io(std::io::Error),
|
|
}
|
|
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> Response {
|
|
let (status, msg) = match self {
|
|
AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden".to_string()),
|
|
AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
|
|
AppError::Internal(m) => (StatusCode::INTERNAL_SERVER_ERROR, m),
|
|
AppError::Io(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
|
};
|
|
if status.is_server_error() {
|
|
tracing::error!(status = status.as_u16(), "{msg}");
|
|
} else {
|
|
tracing::warn!(status = status.as_u16(), "{msg}");
|
|
}
|
|
(status, msg).into_response()
|
|
}
|
|
}
|