From 68d4294049b16e2d164469f1ccf51c85688dc4a4 Mon Sep 17 00:00:00 2001 From: minco Date: Wed, 29 Apr 2026 19:26:59 +0900 Subject: [PATCH] feat: add uploader --- Cargo.lock | 12 ++++++++ Cargo.toml | 2 +- backend/src/handlers.rs | 10 +++++++ backend/src/main.rs | 2 +- backend/src/storage.rs | 21 +++++++++++++ uploader/Cargo.toml | 12 ++++++++ uploader/src/lib.rs | 65 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 uploader/Cargo.toml create mode 100644 uploader/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 03d491e..a65cf73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,6 +1091,18 @@ dependencies = [ "serde", ] +[[package]] +name = "klog-uploader" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "klog-types", + "reqwest", + "sha2 0.11.0", + "tokio", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 005d9cb..a578bad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["types", "cli", "backend"] +members = ["types", "cli", "backend", "uploader"] [workspace.dependencies] diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index fd53bf6..2a1acc8 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -84,6 +84,16 @@ pub async fn delete_files( 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, diff --git a/backend/src/main.rs b/backend/src/main.rs index 213e83c..8f37373 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -32,7 +32,7 @@ async fn main() { .route("/admin/files", get(handlers::list_all_files_meta)) .route("/admin/files/get", post(handlers::download_files)) .route("/admin/files", delete(handlers::delete_files)) - .route("/files", post(handlers::upload_file)) + .route("/files", get(handlers::list_my_files).post(handlers::upload_file)) .with_state(state); let addr = format!("0.0.0.0:{port}"); diff --git a/backend/src/storage.rs b/backend/src/storage.rs index 9d91734..308f916 100644 --- a/backend/src/storage.rs +++ b/backend/src/storage.rs @@ -54,6 +54,27 @@ pub async fn hash_file(path: &Path) -> io::Result { Ok(hex::encode(hasher.finalize())) } +pub async fn list_user_files(data_dir: &Path, username: &str) -> io::Result> { + let user_dir = files_root(data_dir).join(username); + let mut result = Vec::new(); + + let mut files = match tokio::fs::read_dir(&user_dir).await { + Ok(d) => d, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(result), + Err(e) => return Err(e), + }; + + while let Some(entry) = files.next_entry().await? { + if !entry.file_type().await?.is_file() { + continue; + } + let filename = entry.file_name().to_string_lossy().to_string(); + let sha256 = hash_file(&user_dir.join(&filename)).await?; + result.push(FileInfo { username: username.to_string(), filename, sha256 }); + } + Ok(result) +} + pub async fn read_file(data_dir: &Path, username: &str, filename: &str) -> io::Result> { tokio::fs::read(files_root(data_dir).join(username).join(filename)).await } diff --git a/uploader/Cargo.toml b/uploader/Cargo.toml new file mode 100644 index 0000000..0424a07 --- /dev/null +++ b/uploader/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "klog-uploader" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +hex = "0.4.3" +klog-types = { version = "0.1.0", path = "../types" } +reqwest = { version = "0.13.3", features = ["json", "multipart"] } +sha2 = "0.11.0" +tokio = { version = "1.52.1", features = ["fs"] } diff --git a/uploader/src/lib.rs b/uploader/src/lib.rs new file mode 100644 index 0000000..564df53 --- /dev/null +++ b/uploader/src/lib.rs @@ -0,0 +1,65 @@ +use anyhow::{Context, Result}; +use sha2::{Digest, Sha256}; +use std::{collections::HashMap, path::Path}; +use klog_types::FilesMetaResponse; + +pub async fn upload_changed(base_url: &str, token: &str, folder: &Path) -> Result> { + let base_url = base_url.trim_end_matches('/'); + let client = reqwest::Client::new(); + + let remote: FilesMetaResponse = client + .get(format!("{base_url}/files")) + .bearer_auth(token) + .send() + .await + .context("GET /files request failed")? + .error_for_status() + .context("GET /files returned error status")? + .json() + .await + .context("GET /files response parse failed")?; + + let remote_hashes: HashMap = remote + .files + .into_iter() + .map(|f| (f.filename, f.sha256)) + .collect(); + + let mut uploaded = Vec::new(); + let mut entries = tokio::fs::read_dir(folder) + .await + .with_context(|| format!("Cannot read folder: {}", folder.display()))?; + + while let Some(entry) = entries.next_entry().await? { + if !entry.file_type().await?.is_file() { + continue; + } + let filename = entry.file_name().to_string_lossy().to_string(); + let data = tokio::fs::read(entry.path()).await?; + let local_hash = hex::encode(Sha256::digest(&data)); + + let needs_upload = remote_hashes + .get(&filename) + .map_or(true, |h| h != &local_hash); + + if !needs_upload { + continue; + } + + let part = reqwest::multipart::Part::bytes(data).file_name(filename.clone()); + let form = reqwest::multipart::Form::new().part("file", part); + client + .post(format!("{base_url}/files")) + .bearer_auth(token) + .multipart(form) + .send() + .await + .with_context(|| format!("Upload failed for {filename}"))? + .error_for_status() + .with_context(|| format!("Upload error status for {filename}"))?; + + uploaded.push(filename); + } + + Ok(uploaded) +}