feat: add uploader
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -1091,6 +1091,18 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "klog-uploader"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"hex",
|
||||||
|
"klog-types",
|
||||||
|
"reqwest",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["types", "cli", "backend"]
|
members = ["types", "cli", "backend", "uploader"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ pub async fn delete_files(
|
|||||||
Ok(Json(json!({ "deleted": deleted })))
|
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(
|
pub async fn upload_file(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
auth: AuthUser,
|
auth: AuthUser,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async fn main() {
|
|||||||
.route("/admin/files", get(handlers::list_all_files_meta))
|
.route("/admin/files", get(handlers::list_all_files_meta))
|
||||||
.route("/admin/files/get", post(handlers::download_files))
|
.route("/admin/files/get", post(handlers::download_files))
|
||||||
.route("/admin/files", delete(handlers::delete_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);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = format!("0.0.0.0:{port}");
|
let addr = format!("0.0.0.0:{port}");
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ pub async fn hash_file(path: &Path) -> io::Result<String> {
|
|||||||
Ok(hex::encode(hasher.finalize()))
|
Ok(hex::encode(hasher.finalize()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_user_files(data_dir: &Path, username: &str) -> io::Result<Vec<FileInfo>> {
|
||||||
|
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<Vec<u8>> {
|
pub async fn read_file(data_dir: &Path, username: &str, filename: &str) -> io::Result<Vec<u8>> {
|
||||||
tokio::fs::read(files_root(data_dir).join(username).join(filename)).await
|
tokio::fs::read(files_root(data_dir).join(username).join(filename)).await
|
||||||
}
|
}
|
||||||
|
|||||||
12
uploader/Cargo.toml
Normal file
12
uploader/Cargo.toml
Normal file
@@ -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"] }
|
||||||
65
uploader/src/lib.rs
Normal file
65
uploader/src/lib.rs
Normal file
@@ -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<Vec<String>> {
|
||||||
|
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<String, String> = 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user