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>, auth: AuthUser, ) -> Result, 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>, auth: AuthUser, Json(req): Json, ) -> Result { 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>, auth: AuthUser, Json(req): Json, ) -> Result, 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>, auth: AuthUser, ) -> Result, 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>, auth: AuthUser, mut multipart: Multipart, ) -> Result, AppError> { let mut uploaded: Vec = 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() } }