feat: add klog

This commit is contained in:
2026-04-29 17:57:41 +09:00
commit f9f009fcd2
18 changed files with 3923 additions and 0 deletions

151
backend/src/handlers.rs Normal file
View File

@@ -0,0 +1,151 @@
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 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()
}
}