Fix code quality: trailing whitespace, unused imports, clippy warnings
- Fix trailing whitespace in kex.rs and s3.rs - Add missing KexProposal import in kex_complete.rs - Auto-fix clippy warnings across all crates - All 153 tests pass
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use rusqlite::{Connection, params};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -46,10 +46,10 @@ impl DownloadDb {
|
||||
Self::init_tables(&conn)?;
|
||||
conn
|
||||
};
|
||||
|
||||
|
||||
Ok(DownloadDb { conn })
|
||||
}
|
||||
|
||||
|
||||
fn init_tables(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS products (
|
||||
@@ -74,63 +74,70 @@ impl DownloadDb {
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_product_files_product_id ON product_files(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_series ON products(series);
|
||||
"
|
||||
",
|
||||
)?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_product(&mut self, product_name: &str, series: &str, description: Option<&str>) -> Result<i64> {
|
||||
|
||||
pub fn create_product(
|
||||
&mut self,
|
||||
product_name: &str,
|
||||
series: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO products (product_name, series, description, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![product_name, series, description, now],
|
||||
)?;
|
||||
|
||||
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
|
||||
pub fn get_all_products(&self) -> Result<Vec<Product>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, product_name, series, description, created_at FROM products ORDER BY series, product_name"
|
||||
)?;
|
||||
|
||||
let products = stmt.query_map([], |row| {
|
||||
Ok(Product {
|
||||
id: row.get(0)?,
|
||||
product_name: row.get(1)?,
|
||||
series: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
let products = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(Product {
|
||||
id: row.get(0)?,
|
||||
product_name: row.get(1)?,
|
||||
series: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
|
||||
pub fn get_products_by_series(&self, series: &str) -> Result<Vec<Product>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, product_name, series, description, created_at FROM products
|
||||
WHERE series = ?1 ORDER BY product_name"
|
||||
WHERE series = ?1 ORDER BY product_name",
|
||||
)?;
|
||||
|
||||
let products = stmt.query_map([series], |row| {
|
||||
Ok(Product {
|
||||
id: row.get(0)?,
|
||||
product_name: row.get(1)?,
|
||||
series: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
let products = stmt
|
||||
.query_map([series], |row| {
|
||||
Ok(Product {
|
||||
id: row.get(0)?,
|
||||
product_name: row.get(1)?,
|
||||
series: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
|
||||
pub fn get_series_stats(&self) -> Result<Vec<SeriesStats>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT
|
||||
@@ -141,106 +148,118 @@ impl DownloadDb {
|
||||
FROM products p
|
||||
LEFT JOIN product_files pf ON p.id = pf.product_id
|
||||
GROUP BY p.series
|
||||
ORDER BY p.series"
|
||||
ORDER BY p.series",
|
||||
)?;
|
||||
|
||||
let stats = stmt.query_map([], |row| {
|
||||
Ok(SeriesStats {
|
||||
series: row.get(0)?,
|
||||
product_count: row.get(1)?,
|
||||
file_count: row.get(2)?,
|
||||
total_size: row.get::<_, i64>(3)? as u64,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
let stats = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(SeriesStats {
|
||||
series: row.get(0)?,
|
||||
product_count: row.get(1)?,
|
||||
file_count: row.get(2)?,
|
||||
total_size: row.get::<_, i64>(3)? as u64,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
pub fn add_file_to_product(&mut self, product_id: i64, file_path: &str, file_name: &str, file_size: u64, file_hash: Option<&str>) -> Result<i64> {
|
||||
|
||||
pub fn add_file_to_product(
|
||||
&mut self,
|
||||
product_id: i64,
|
||||
file_path: &str,
|
||||
file_name: &str,
|
||||
file_size: u64,
|
||||
file_hash: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO product_files (product_id, file_path, file_name, file_size, file_hash, uploaded_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![product_id, file_path, file_name, file_size as i64, file_hash, now],
|
||||
)?;
|
||||
|
||||
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
|
||||
pub fn get_files_by_product(&self, product_id: i64) -> Result<Vec<ProductFile>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at
|
||||
FROM product_files WHERE product_id = ?1 ORDER BY file_name"
|
||||
)?;
|
||||
|
||||
let files = stmt.query_map([product_id], |row| {
|
||||
Ok(ProductFile {
|
||||
id: row.get(0)?,
|
||||
product_id: row.get(1)?,
|
||||
file_path: row.get(2)?,
|
||||
file_name: row.get(3)?,
|
||||
file_size: row.get::<_, i64>(4)? as u64,
|
||||
file_hash: row.get(5)?,
|
||||
download_count: row.get(6)?,
|
||||
uploaded_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
let files = stmt
|
||||
.query_map([product_id], |row| {
|
||||
Ok(ProductFile {
|
||||
id: row.get(0)?,
|
||||
product_id: row.get(1)?,
|
||||
file_path: row.get(2)?,
|
||||
file_name: row.get(3)?,
|
||||
file_size: row.get::<_, i64>(4)? as u64,
|
||||
file_hash: row.get(5)?,
|
||||
download_count: row.get(6)?,
|
||||
uploaded_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
|
||||
pub fn increment_download_count(&mut self, file_id: i64) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE product_files SET download_count = download_count + 1 WHERE id = ?1",
|
||||
params![file_id],
|
||||
)?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn get_all_files(&self) -> Result<Vec<ProductFile>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at
|
||||
FROM product_files ORDER BY uploaded_at DESC"
|
||||
)?;
|
||||
|
||||
let files = stmt.query_map([], |row| {
|
||||
Ok(ProductFile {
|
||||
id: row.get(0)?,
|
||||
product_id: row.get(1)?,
|
||||
file_path: row.get(2)?,
|
||||
file_name: row.get(3)?,
|
||||
file_size: row.get::<_, i64>(4)? as u64,
|
||||
file_hash: row.get(5)?,
|
||||
download_count: row.get(6)?,
|
||||
uploaded_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
let files = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(ProductFile {
|
||||
id: row.get(0)?,
|
||||
product_id: row.get(1)?,
|
||||
file_path: row.get(2)?,
|
||||
file_name: row.get(3)?,
|
||||
file_size: row.get::<_, i64>(4)? as u64,
|
||||
file_hash: row.get(5)?,
|
||||
download_count: row.get(6)?,
|
||||
uploaded_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
|
||||
pub fn delete_product_with_files(&mut self, product_id: i64) -> Result<(i64, i64)> {
|
||||
// 先删除关联的文件映射
|
||||
self.conn.execute(
|
||||
"DELETE FROM product_files WHERE product_id = ?1",
|
||||
params![product_id],
|
||||
)?;
|
||||
|
||||
|
||||
let deleted_files = self.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// 再删除产品记录
|
||||
self.conn.execute(
|
||||
"DELETE FROM products WHERE id = ?1",
|
||||
params![product_id],
|
||||
)?;
|
||||
|
||||
let deleted_product = if self.conn.last_insert_rowid() > 0 { 1 } else { 0 };
|
||||
|
||||
self.conn
|
||||
.execute("DELETE FROM products WHERE id = ?1", params![product_id])?;
|
||||
|
||||
let deleted_product = if self.conn.last_insert_rowid() > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok((deleted_files, deleted_product))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,19 @@ use axum::{
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use crate::server::AppState;
|
||||
use crate::download::db::DownloadDb;
|
||||
use crate::server::AppState;
|
||||
|
||||
pub async fn download_file(
|
||||
Path(file_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(mut db) => {
|
||||
// 获取文件信息
|
||||
@@ -24,48 +28,65 @@ pub async fn download_file(
|
||||
if files.is_empty() {
|
||||
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
||||
}
|
||||
|
||||
|
||||
let file_info = &files[0];
|
||||
|
||||
|
||||
// 更新下载统计
|
||||
db.increment_download_count(file_info.id).ok();
|
||||
|
||||
|
||||
// 构建文件路径(使用配置的db_dir)
|
||||
let base_path = state.db_dir.replace("users", "Downloads");
|
||||
let file_path = std::path::Path::new(&base_path).join(&file_info.file_path);
|
||||
|
||||
|
||||
if !file_path.exists() {
|
||||
return (StatusCode::NOT_FOUND, "File not found on disk").into_response();
|
||||
}
|
||||
|
||||
|
||||
// 读取文件内容
|
||||
match File::open(&file_path) {
|
||||
Ok(mut file) => {
|
||||
let mut buffer = Vec::new();
|
||||
match file.read_to_end(&mut buffer) {
|
||||
Ok(_) => {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", file_info.file_name)
|
||||
)
|
||||
.header("X-File-Hash", file_info.file_hash.clone().unwrap_or_default())
|
||||
.header("X-File-Size", file_info.file_size)
|
||||
.body(buffer.into())
|
||||
.unwrap()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response()
|
||||
Ok(_) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", file_info.file_name),
|
||||
)
|
||||
.header(
|
||||
"X-File-Hash",
|
||||
file_info.file_hash.clone().unwrap_or_default(),
|
||||
)
|
||||
.header("X-File-Size", file_info.file_size)
|
||||
.body(buffer.into())
|
||||
.unwrap(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error reading file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response()
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error opening file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response()
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response()
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,69 +101,84 @@ pub async fn download_file_by_path(
|
||||
// User files are in Downloads/user_id/
|
||||
format!("/Users/accusys/Downloads/{}", user_id)
|
||||
};
|
||||
|
||||
|
||||
let full_path = std::path::Path::new(&base_path).join(&file_path);
|
||||
|
||||
|
||||
if !full_path.exists() {
|
||||
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
||||
}
|
||||
|
||||
let filename = file_path.split('/').last().unwrap_or("unknown");
|
||||
|
||||
|
||||
let filename = file_path.split('/').next_back().unwrap_or("unknown");
|
||||
|
||||
match File::open(&full_path) {
|
||||
Ok(mut file) => {
|
||||
let mut buffer = Vec::new();
|
||||
match file.read_to_end(&mut buffer) {
|
||||
Ok(_) => {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", filename)
|
||||
)
|
||||
.body(buffer.into())
|
||||
.unwrap()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response()
|
||||
Ok(_) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
)
|
||||
.body(buffer.into())
|
||||
.unwrap(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error reading file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response()
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error opening file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_download_stats(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
pub async fn get_download_stats(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(db) => {
|
||||
match db.get_all_files() {
|
||||
Ok(files) => {
|
||||
let total_downloads: i64 = files.iter().map(|f| f.download_count).sum();
|
||||
let top_files: Vec<_> = files.iter()
|
||||
.filter(|f| f.download_count > 0)
|
||||
.take(10)
|
||||
.map(|f| serde_json::json!({
|
||||
Ok(db) => match db.get_all_files() {
|
||||
Ok(files) => {
|
||||
let total_downloads: i64 = files.iter().map(|f| f.download_count).sum();
|
||||
let top_files: Vec<_> = files
|
||||
.iter()
|
||||
.filter(|f| f.download_count > 0)
|
||||
.take(10)
|
||||
.map(|f| {
|
||||
serde_json::json!({
|
||||
"file_name": f.file_name,
|
||||
"download_count": f.download_count
|
||||
}))
|
||||
.collect();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"total_files": files.len(),
|
||||
"total_downloads": total_downloads,
|
||||
"top_files": top_files
|
||||
}))
|
||||
)
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"total_files": files.len(),
|
||||
"total_downloads": total_downloads,
|
||||
"top_files": top_files
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,35 +187,41 @@ pub async fn download_product_file(
|
||||
) -> impl IntoResponse {
|
||||
let base_path = format!("/Users/accusys/markbase/data/downloads/{}/", product_series);
|
||||
let full_path = std::path::Path::new(&base_path).join(&file_path);
|
||||
|
||||
|
||||
if !full_path.exists() {
|
||||
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
||||
}
|
||||
|
||||
|
||||
if full_path.is_dir() {
|
||||
return (StatusCode::BAD_REQUEST, "Path is a directory, not a file").into_response();
|
||||
}
|
||||
|
||||
let filename = file_path.split('/').last().unwrap_or("unknown");
|
||||
|
||||
|
||||
let filename = file_path.split('/').next_back().unwrap_or("unknown");
|
||||
|
||||
match File::open(&full_path) {
|
||||
Ok(mut file) => {
|
||||
let mut buffer = Vec::new();
|
||||
match file.read_to_end(&mut buffer) {
|
||||
Ok(_) => {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", filename)
|
||||
)
|
||||
.body(buffer.into())
|
||||
.unwrap()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response()
|
||||
Ok(_) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
)
|
||||
.body(buffer.into())
|
||||
.unwrap(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error reading file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response()
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error opening file: {}", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{Html, IntoResponse, Json},
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::server::AppState;
|
||||
use crate::download::storage;
|
||||
|
||||
pub async fn list_uploaded_files(
|
||||
Path(user_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn list_uploaded_files(Path(user_id): Path<String>) -> impl IntoResponse {
|
||||
let file_list = storage::scan_uploaded_files(&user_id);
|
||||
(StatusCode::OK, Json(file_list))
|
||||
}
|
||||
|
||||
pub async fn get_file_info(
|
||||
Path((user_id, filename)): Path<(String, String)>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn get_file_info(Path((user_id, filename)): Path<(String, String)>) -> impl IntoResponse {
|
||||
let base_path = format!("/Users/accusys/Downloads/{}", user_id);
|
||||
let file_path = PathBuf::from(&base_path).join(&filename);
|
||||
|
||||
|
||||
if !file_path.exists() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -29,7 +24,7 @@ pub async fn get_file_info(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
|
||||
let metadata = std::fs::metadata(&file_path).unwrap();
|
||||
let file_size = metadata.len();
|
||||
let file_hash = if file_size > 0 {
|
||||
@@ -37,7 +32,7 @@ pub async fn get_file_info(
|
||||
} else {
|
||||
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string())
|
||||
};
|
||||
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
@@ -49,4 +44,4 @@ pub async fn get_file_info(
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
pub mod models;
|
||||
pub mod db;
|
||||
pub mod handlers;
|
||||
pub mod storage;
|
||||
pub mod product_handlers;
|
||||
pub mod download_handler;
|
||||
pub mod handlers;
|
||||
pub mod models;
|
||||
pub mod product_handlers;
|
||||
pub mod storage;
|
||||
|
||||
pub use models::*;
|
||||
pub use db::{DownloadDb, Product, ProductFile, SeriesStats};
|
||||
pub use download_handler::*;
|
||||
pub use handlers::*;
|
||||
pub use models::*;
|
||||
pub use product_handlers::*;
|
||||
pub use download_handler::*;
|
||||
@@ -39,4 +39,4 @@ pub struct DownloadStats {
|
||||
pub total_files: i64,
|
||||
pub total_downloads: i64,
|
||||
pub series_stats: Vec<ProductSeries>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{Json, IntoResponse},
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::download::db::DownloadDb;
|
||||
use crate::server::AppState;
|
||||
use crate::download::db::{DownloadDb, Product, ProductFile, SeriesStats};
|
||||
|
||||
pub async fn list_all_products(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
pub async fn list_all_products(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(db) => {
|
||||
match db.get_all_products() {
|
||||
Ok(products) => (StatusCode::OK, Json(json!({
|
||||
Ok(db) => match db.get_all_products() {
|
||||
Ok(products) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"products": products,
|
||||
"total": products.len()
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,47 +44,67 @@ pub async fn list_products_by_series(
|
||||
Path(series): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(db) => {
|
||||
match db.get_products_by_series(&series) {
|
||||
Ok(products) => (StatusCode::OK, Json(json!({
|
||||
Ok(db) => match db.get_products_by_series(&series) {
|
||||
Ok(products) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"series": series,
|
||||
"products": products,
|
||||
"total": products.len()
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_series_stats(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
pub async fn get_series_stats(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(db) => {
|
||||
match db.get_series_stats() {
|
||||
Ok(stats) => (StatusCode::OK, Json(json!({
|
||||
Ok(db) => match db.get_series_stats() {
|
||||
Ok(stats) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"series_stats": stats,
|
||||
"total_series": stats.len()
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,25 +112,36 @@ pub async fn get_product_files(
|
||||
Path(product_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(db) => {
|
||||
match db.get_files_by_product(product_id) {
|
||||
Ok(files) => (StatusCode::OK, Json(json!({
|
||||
Ok(db) => match db.get_files_by_product(product_id) {
|
||||
Ok(files) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"product_id": product_id,
|
||||
"files": files,
|
||||
"total_files": files.len(),
|
||||
"total_size": files.iter().map(|f| f.file_size).sum::<u64>()
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,29 +149,40 @@ pub async fn create_product_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
let product_name = payload["product_name"].as_str().unwrap_or("");
|
||||
let series = payload["series"].as_str().unwrap_or("");
|
||||
let description = payload["description"].as_str();
|
||||
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(mut db) => {
|
||||
match db.create_product(product_name, series, description) {
|
||||
Ok(product_id) => (StatusCode::OK, Json(json!({
|
||||
Ok(mut db) => match db.create_product(product_name, series, description) {
|
||||
Ok(product_id) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"product_name": product_name,
|
||||
"series": series
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,48 +191,62 @@ pub async fn assign_files_to_product(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
let files_vec = payload["files"].as_array().cloned().unwrap_or_default();
|
||||
let files = files_vec.as_slice();
|
||||
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(mut db) => {
|
||||
let mut assigned_count = 0;
|
||||
let mut errors = vec![];
|
||||
|
||||
|
||||
for file in files {
|
||||
let file_path = file["file_path"].as_str().unwrap_or("");
|
||||
let file_name = file["file_name"].as_str().unwrap_or("");
|
||||
let file_size = file["file_size"].as_u64().unwrap_or(0);
|
||||
let file_hash = file["file_hash"].as_str();
|
||||
|
||||
match db.add_file_to_product(product_id, file_path, file_name, file_size, file_hash) {
|
||||
|
||||
match db.add_file_to_product(product_id, file_path, file_name, file_size, file_hash)
|
||||
{
|
||||
Ok(_) => assigned_count += 1,
|
||||
Err(e) => {
|
||||
errors.push(format!("Failed to assign {}: {}", file_path, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if errors.is_empty() {
|
||||
(StatusCode::OK, Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"assigned_count": assigned_count
|
||||
})))
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"assigned_count": assigned_count
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
(StatusCode::PARTIAL_CONTENT, Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"assigned_count": assigned_count,
|
||||
"errors": errors
|
||||
})))
|
||||
(
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"assigned_count": assigned_count,
|
||||
"errors": errors
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,24 +254,35 @@ pub async fn delete_product(
|
||||
Path(product_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite");
|
||||
|
||||
let db_path = format!(
|
||||
"{}{}",
|
||||
state.db_dir.replace("users", "downloads"),
|
||||
"/products.sqlite"
|
||||
);
|
||||
|
||||
match DownloadDb::new(&db_path) {
|
||||
Ok(mut db) => {
|
||||
match db.delete_product_with_files(product_id) {
|
||||
Ok((deleted_files, deleted_product)) => (StatusCode::OK, Json(json!({
|
||||
Ok(mut db) => match db.delete_product_with_files(product_id) {
|
||||
Ok((deleted_files, deleted_product)) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"product_id": product_id,
|
||||
"deleted_files": deleted_files,
|
||||
"deleted_product": deleted_product
|
||||
}))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": e.to_string()
|
||||
}))),
|
||||
})),
|
||||
),
|
||||
},
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": e.to_string()
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileInfo {
|
||||
@@ -23,17 +23,17 @@ pub struct FileListResponse {
|
||||
pub fn scan_uploaded_files(user_id: &str) -> FileListResponse {
|
||||
let base_path = format!("/Users/accusys/Downloads/{}", user_id);
|
||||
let path = Path::new(&base_path);
|
||||
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut total_size = 0u64;
|
||||
|
||||
|
||||
if path.exists() {
|
||||
scan_directory_recursive(path, path, &mut files, &mut total_size);
|
||||
}
|
||||
|
||||
|
||||
FileListResponse {
|
||||
user_id: user_id.to_string(),
|
||||
base_path: base_path,
|
||||
base_path,
|
||||
total_files: files.len(),
|
||||
total_size,
|
||||
files,
|
||||
@@ -49,24 +49,25 @@ fn scan_directory_recursive(
|
||||
if let Ok(entries) = std::fs::read_dir(current) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let file_size = entry.metadata()
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
let relative_path = path.strip_prefix(base)
|
||||
|
||||
let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let relative_path = path
|
||||
.strip_prefix(base)
|
||||
.ok()
|
||||
.and_then(|p| p.to_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| filename.clone());
|
||||
|
||||
let upload_time = entry.metadata()
|
||||
|
||||
let upload_time = entry
|
||||
.metadata()
|
||||
.ok()
|
||||
.and_then(|m| m.modified().ok())
|
||||
.and_then(|t| {
|
||||
@@ -75,13 +76,16 @@ fn scan_directory_recursive(
|
||||
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
|
||||
})
|
||||
.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string());
|
||||
|
||||
|
||||
let file_hash = if file_size > 0 {
|
||||
compute_file_hash(&path).ok()
|
||||
} else {
|
||||
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string())
|
||||
Some(
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
.to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
files.push(FileInfo {
|
||||
filename,
|
||||
file_size,
|
||||
@@ -90,7 +94,7 @@ fn scan_directory_recursive(
|
||||
relative_path,
|
||||
upload_time,
|
||||
});
|
||||
|
||||
|
||||
*total_size += file_size;
|
||||
} else if path.is_dir() {
|
||||
scan_directory_recursive(base, &path, files, total_size);
|
||||
@@ -102,11 +106,11 @@ fn scan_directory_recursive(
|
||||
pub fn compute_file_hash(path: &Path) -> Result<String, std::io::Error> {
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
|
||||
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0u8; 8192];
|
||||
|
||||
|
||||
loop {
|
||||
let bytes_read = file.read(&mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
@@ -114,6 +118,6 @@ pub fn compute_file_hash(path: &Path) -> Result<String, std::io::Error> {
|
||||
}
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user