🧹 Remove unused legacy modules and functions

- Deleted quota.rs module (quota management not needed via HTTP)
- Removed legacy get_id_by_token GraphQL function
- Removed unused set_user_quota and increase_user_quota methods
- Cleaned up unused imports and legacy structs
- Simplified handlers/mod.rs to only expose universal_handler

Architecture now focused on core functionality:
- GET / (user info)
- GET /<filename> (file serving)
- POST / (file upload)
This commit is contained in:
2025-09-02 11:27:48 +03:00
parent 6c03863a86
commit d3bee5144f
9 changed files with 119 additions and 331 deletions

View File

@@ -1,3 +1,22 @@
## [0.5.3] - 2025-09-02
### 🔄 Архитектурные изменения
- **УПРОЩЕНО**: Убран сложный роутинг Actix-web в пользу универсального обработчика
- **ДОБАВЛЕНО**: Прямое определение HTTP методов (GET/POST) в единой точке
- **УБРАНО**: HTTP API для управления квотами (quota endpoints)
- **СОХРАНЕНО**: ACME challenge поддержка для SSL сертификатов
### 📋 API Структура
- `GET /` - авторизованная информация о персональном хранилище
- `GET /<filename>` - статические файлы с миниатюрами
- `POST /` - авторизованная загрузка файлов
### 🔧 Технические детали
- Единый `universal_handler` для всех запросов
- Определение метода через `req.method()`
- Маршрутизация по пути через `req.path()`
- CORS и middleware сохранены
## [0.5.2] - 2025-09-02 ## [0.5.2] - 2025-09-02
### Fixed ### Fixed

48
Cargo.lock generated
View File

@@ -117,7 +117,7 @@ dependencies = [
"actix-multipart-derive", "actix-multipart-derive",
"actix-utils", "actix-utils",
"actix-web", "actix-web",
"derive_more 0.99.18", "derive_more 0.99.20",
"futures-core", "futures-core",
"futures-util", "futures-util",
"httparse", "httparse",
@@ -855,9 +855,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.1" version = "8.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@@ -1106,18 +1106,18 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.13" version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
@@ -1137,9 +1137,9 @@ dependencies = [
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.10" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"darling_macro", "darling_macro",
@@ -1147,9 +1147,9 @@ dependencies = [
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.20.10" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
@@ -1161,9 +1161,9 @@ dependencies = [
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.10" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
@@ -1201,9 +1201,9 @@ dependencies = [
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.18" version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
@@ -2644,7 +2644,7 @@ dependencies = [
[[package]] [[package]]
name = "quoter" name = "quoter"
version = "0.5.1" version = "0.5.3"
dependencies = [ dependencies = [
"actix", "actix",
"actix-cors", "actix-cors",
@@ -3787,9 +3787,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.18.0" version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"js-sys", "js-sys",
@@ -4277,27 +4277,27 @@ dependencies = [
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "7.2.1" version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [ dependencies = [
"zstd-sys", "zstd-sys",
] ]
[[package]] [[package]]
name = "zstd-sys" name = "zstd-sys"
version = "2.0.13+zstd.1.5.6" version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "quoter" name = "quoter"
version = "0.5.1" version = "0.5.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -8,7 +8,6 @@ futures = "0.3.30"
serde_json = "1.0.143" serde_json = "1.0.143"
actix-web = "4.11.0" actix-web = "4.11.0"
actix-cors = "0.7.0" actix-cors = "0.7.0"
actix-files = "0.6.7"
reqwest = { version = "0.12.23", features = ["json"] } reqwest = { version = "0.12.23", features = ["json"] }
sentry = { version = "0.42", features = ["tokio"] } sentry = { version = "0.42", features = ["tokio"] }
uuid = { version = "1.18.0", features = ["v4"] } uuid = { version = "1.18.0", features = ["v4"] }

View File

@@ -179,42 +179,4 @@ impl AppState {
Ok(new_quota) Ok(new_quota)
} }
/// Устанавливает квоту пользователя в байтах (позволяет увеличить или уменьшить)
pub async fn set_user_quota(&self, user_id: &str, bytes: u64) -> Result<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Устанавливаем новое значение квоты
redis
.set::<_, u64, ()>(&quota_key, bytes)
.await
.map_err(|_| ErrorInternalServerError("Failed to set user quota in Redis"))?;
Ok(bytes)
}
/// Увеличивает квоту пользователя на указанное количество байт
pub async fn increase_user_quota(
&self,
user_id: &str,
additional_bytes: u64,
) -> Result<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Получаем текущую квоту
let current_quota: u64 = redis.get(&quota_key).await.unwrap_or(0);
// Вычисляем новую квоту
let new_quota = current_quota + additional_bytes;
// Устанавливаем новое значение
redis
.set::<_, u64, ()>(&quota_key, new_quota)
.await
.map_err(|_| ErrorInternalServerError("Failed to increase user quota in Redis"))?;
Ok(new_quota)
}
} }

View File

@@ -2,33 +2,8 @@ use actix_web::error::ErrorInternalServerError;
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
use log::{info, warn}; use log::{info, warn};
use redis::{AsyncCommands, aio::MultiplexedConnection}; use redis::{AsyncCommands, aio::MultiplexedConnection};
use reqwest::Client as HTTPClient;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use std::error::Error;
use std::{collections::HashMap, env, error::Error};
// Старые структуры для совместимости с get_id_by_token
#[derive(Deserialize)]
struct AuthResponse {
data: Option<AuthData>,
}
#[derive(Deserialize)]
struct AuthData {
validate_jwt_token: Option<ValidateJWTToken>,
}
#[derive(Deserialize)]
struct ValidateJWTToken {
is_valid: bool,
claims: Option<Claims>,
}
#[derive(Deserialize)]
struct Claims {
sub: Option<String>,
}
// Структуры для JWT токенов // Структуры для JWT токенов
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -51,57 +26,6 @@ pub struct Author {
pub device_info: Option<String>, pub device_info: Option<String>,
} }
/// Получает айди пользователя из токена в заголовке
#[allow(clippy::collapsible_if)]
pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> {
let auth_api_base = env::var("CORE_URL")?;
let query_name = "validate_jwt_token";
let operation = "ValidateToken";
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let mut variables = HashMap::<String, HashMap<String, String>>::new();
let mut params = HashMap::<String, String>::new();
params.insert("token".to_string(), token.to_string());
params.insert("token_type".to_string(), "access_token".to_string());
variables.insert("params".to_string(), params);
let gql = json!({
"query": format!("query {}($params: ValidateJWTTokenInput!) {{ {}(params: $params) {{ is_valid claims }} }}", operation, query_name),
"operationName": operation,
"variables": variables
});
let client = HTTPClient::new();
let response = client
.post(&auth_api_base)
.headers(headers)
.json(&gql)
.send()
.await?;
if response.status().is_success() {
let auth_response: AuthResponse = response.json().await?;
if let Some(auth_data) = auth_response.data {
if let Some(validate_jwt_token) = auth_data.validate_jwt_token {
if validate_jwt_token.is_valid {
if let Some(claims) = validate_jwt_token.claims {
if let Some(sub) = claims.sub {
return Ok(sub);
}
}
}
}
}
Err(Box::new(std::io::Error::other("Invalid token response")))
} else {
Err(Box::new(std::io::Error::other(format!(
"Request failed with status: {}",
response.status()
))))
}
}
/// Декодирует JWT токен и извлекает claims с проверкой истечения /// Декодирует JWT токен и извлекает claims с проверкой истечения
fn decode_jwt_token(token: &str) -> Result<TokenClaims, Box<dyn Error>> { fn decode_jwt_token(token: &str) -> Result<TokenClaims, Box<dyn Error>> {
// В реальном приложении здесь должен быть настоящий секретный ключ // В реальном приложении здесь должен быть настоящий секретный ключ

View File

@@ -1,13 +1,10 @@
mod proxy; mod proxy;
mod quota;
mod serve_file; mod serve_file;
mod upload; mod upload;
mod user; mod user;
mod universal;
pub use proxy::proxy_handler; pub use universal::universal_handler;
pub use quota::{get_quota_handler, increase_quota_handler, set_quota_handler};
pub use upload::upload_handler;
pub use user::get_current_user_handler;
// Общий лимит квоты на пользователя: 5 ГБ // Общий лимит квоты на пользователя: 12 ГБ
pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; pub const MAX_USER_QUOTA_BYTES: u64 = 12 * 1024 * 1024 * 1024;

View File

@@ -1,166 +0,0 @@
use actix_web::{HttpRequest, HttpResponse, Result, web};
use log::warn;
use serde::{Deserialize, Serialize};
use crate::app_state::AppState;
use crate::auth::get_id_by_token;
#[derive(Deserialize)]
pub struct QuotaRequest {
pub user_id: String,
pub additional_bytes: Option<u64>,
pub new_quota_bytes: Option<u64>,
}
#[derive(Serialize)]
pub struct QuotaResponse {
pub user_id: String,
pub current_quota: u64,
pub max_quota: u64,
}
/// Обработчик для получения информации о квоте пользователя
pub async fn get_quota_handler(
req: HttpRequest,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// Проверяем авторизацию
let token = req
.headers()
.get("Authorization")
.and_then(|header_value| header_value.to_str().ok());
if token.is_none() {
return Err(actix_web::error::ErrorUnauthorized("Unauthorized"));
}
let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| {
let error_msg = if e.to_string().contains("expired") {
"Admin token has expired"
} else {
"Invalid admin token"
};
actix_web::error::ErrorUnauthorized(error_msg)
})?;
// Получаем user_id из query параметров
let user_id = req
.query_string()
.split("user_id=")
.nth(1)
.and_then(|s| s.split('&').next())
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing user_id parameter"))?;
// Получаем текущую квоту пользователя
let current_quota = state.get_or_create_quota(user_id).await?;
let response = QuotaResponse {
user_id: user_id.to_string(),
current_quota,
max_quota: crate::handlers::MAX_USER_QUOTA_BYTES,
};
Ok(HttpResponse::Ok().json(response))
}
/// Обработчик для увеличения квоты пользователя
pub async fn increase_quota_handler(
req: HttpRequest,
quota_data: web::Json<QuotaRequest>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// Проверяем авторизацию
let token = req
.headers()
.get("Authorization")
.and_then(|header_value| header_value.to_str().ok());
if token.is_none() {
return Err(actix_web::error::ErrorUnauthorized("Unauthorized"));
}
let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| {
let error_msg = if e.to_string().contains("expired") {
"Admin token has expired"
} else {
"Invalid admin token"
};
actix_web::error::ErrorUnauthorized(error_msg)
})?;
let additional_bytes = quota_data
.additional_bytes
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing additional_bytes parameter"))?;
if additional_bytes == 0 {
return Err(actix_web::error::ErrorBadRequest(
"additional_bytes must be greater than 0",
));
}
// Увеличиваем квоту пользователя
let new_quota = state
.increase_user_quota(&quota_data.user_id, additional_bytes)
.await?;
warn!(
"Increased quota for user {} by {} bytes, new total: {} bytes",
quota_data.user_id, additional_bytes, new_quota
);
let response = QuotaResponse {
user_id: quota_data.user_id.clone(),
current_quota: new_quota,
max_quota: crate::handlers::MAX_USER_QUOTA_BYTES,
};
Ok(HttpResponse::Ok().json(response))
}
/// Обработчик для установки квоты пользователя
pub async fn set_quota_handler(
req: HttpRequest,
quota_data: web::Json<QuotaRequest>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// Проверяем авторизацию
let token = req
.headers()
.get("Authorization")
.and_then(|header_value| header_value.to_str().ok());
if token.is_none() {
return Err(actix_web::error::ErrorUnauthorized("Unauthorized"));
}
let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| {
let error_msg = if e.to_string().contains("expired") {
"Admin token has expired"
} else {
"Invalid admin token"
};
actix_web::error::ErrorUnauthorized(error_msg)
})?;
let new_quota_bytes = quota_data
.new_quota_bytes
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing new_quota_bytes parameter"))?;
// Устанавливаем новую квоту пользователя
let new_quota = state
.set_user_quota(&quota_data.user_id, new_quota_bytes)
.await?;
warn!(
"Set quota for user {} to {} bytes",
quota_data.user_id, new_quota
);
let response = QuotaResponse {
user_id: quota_data.user_id.clone(),
current_quota: new_quota,
max_quota: crate::handlers::MAX_USER_QUOTA_BYTES,
};
Ok(HttpResponse::Ok().json(response))
}

68
src/handlers/universal.rs Normal file
View File

@@ -0,0 +1,68 @@
use actix_web::{HttpRequest, HttpResponse, Result, web};
use actix_multipart::Multipart;
use log::{info, warn};
use crate::app_state::AppState;
/// Универсальный обработчик, который определяет HTTP метод и путь
pub async fn universal_handler(
req: HttpRequest,
payload: web::Payload,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let method = req.method().clone();
let path = req.path().to_string();
info!("Universal handler: {} {}", method, path);
// Возвращаем 404 для .well-known путей (для Let's Encrypt ACME)
if path.starts_with("/.well-known/") {
warn!("ACME challenge path requested: {}", path);
return Ok(HttpResponse::NotFound().finish());
}
match method.as_str() {
"GET" => handle_get(req, state, &path).await,
"POST" => handle_post(req, payload, state, &path).await,
_ => {
warn!("Unsupported HTTP method: {}", method);
Ok(HttpResponse::MethodNotAllowed().json(serde_json::json!({
"error": "Method not allowed"
})))
}
}
}
async fn handle_get(
req: HttpRequest,
state: web::Data<AppState>,
path: &str,
) -> Result<HttpResponse, actix_web::Error> {
if path == "/" {
// GET / - получение информации о пользователе
crate::handlers::user::get_current_user_handler(req, state).await
} else {
// GET /{path} - получение файла через proxy
let path_without_slash = path.trim_start_matches('/');
let requested_res = web::Path::from(path_without_slash.to_string());
crate::handlers::proxy::proxy_handler(req, requested_res, state).await
}
}
async fn handle_post(
req: HttpRequest,
payload: web::Payload,
state: web::Data<AppState>,
path: &str,
) -> Result<HttpResponse, actix_web::Error> {
if path == "/" {
// POST / - загрузка файла (multipart)
let multipart = Multipart::new(&req.headers(), payload);
crate::handlers::upload::upload_handler(req, multipart, state).await
} else {
warn!("Unsupported POST path: {}", path);
Ok(HttpResponse::NotFound().json(serde_json::json!({
"error": "Endpoint not found"
})))
}
}

View File

@@ -14,10 +14,7 @@ use actix_web::{
}; };
use app_state::AppState; use app_state::AppState;
use handlers::{ use handlers::universal_handler;
get_current_user_handler, get_quota_handler, increase_quota_handler, proxy_handler,
set_quota_handler, upload_handler,
};
use log::warn; use log::warn;
use std::env; use std::env;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
@@ -63,19 +60,7 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(app_state.clone())) .app_data(web::Data::new(app_state.clone()))
.wrap(cors) .wrap(cors)
.wrap(Logger::default()) .wrap(Logger::default())
.route("/", web::get().to(get_current_user_handler)) .default_service(web::to(universal_handler))
.route("/", web::post().to(upload_handler))
.route("/quota", web::get().to(get_quota_handler))
.route("/quota/increase", web::post().to(increase_quota_handler))
.route("/quota/set", web::post().to(set_quota_handler))
.service(
web::scope("/.well-known")
.service(
actix_files::Files::new("/", "/tmp/.well-known")
.show_files_listing()
)
)
.route("/{path:.*}", web::get().to(proxy_handler))
}) })
.bind(addr)? .bind(addr)?
.run() .run()