diff --git a/Dockerfile b/Dockerfile index d1cfb8f0..a08bb47a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,11 @@ RUN cargo install cargo-chef WORKDIR /app FROM chef AS planner -COPY ./pentaract . +WORKDIR /app +COPY pentaract/Cargo.toml . +COPY pentaract/Cargo.lock . +COPY pentaract/src ./src + RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder diff --git a/pentaract/Cargo.toml b/pentaract/Cargo.toml index 13eed2df..56fd093b 100644 --- a/pentaract/Cargo.toml +++ b/pentaract/Cargo.toml @@ -3,23 +3,19 @@ name = "pentaract" version = "0.1.0" edition = "2021" -[profile.release] -strip = true -codegen-units = 1 - [dependencies] # routing -axum = { version = "0.6.20", features = ["headers", "tracing", "multipart"]} +axum = { version = "0.6.20", features = ["headers", "tracing", "multipart"] } mime_guess = "2.0.4" -tower = { version = "0.4.13", features = ["limit"], default-features = false} -tower-http = { version = "0.4.4", features = ["fs", "trace", "cors"], default_features = false } +tower = { version = "0.4.13", features = ["limit"], default-features = false } +tower-http = { version = "0.4.4", features = ["fs", "trace", "cors"], default-features = false } # serialization/deserialization serde = { version = "1.0.189", features = ["derive"] } # auth pwhash = "1.0.0" -jsonwebtoken = { version = "9", default-features = false } +jsonwebtoken = { version = "9", default-features = false, features = ["use_pem"] } # async tokio = { version = "1.33.0", features = ["full"] } @@ -28,10 +24,13 @@ futures = "0.3.29" # logging tracing = "0.1.40" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"]} +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } # others sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid"] } thiserror = "1.0.50" uuid = { version = "1.5.0", features = ["serde", "v4"] } -reqwest = { version = "0.11.22", features = ["multipart", "json"] } + +# HTTP client (без native-tls — ок для musl) +reqwest = { version = "0.11.22", default-features = false, features = ["multipart", "json", "rustls-tls"] } +percent-encoding = "2.3" diff --git a/pentaract/src/common/telegram_api/bot_api.rs b/pentaract/src/common/telegram_api/bot_api.rs index 793b3591..f6145a68 100644 --- a/pentaract/src/common/telegram_api/bot_api.rs +++ b/pentaract/src/common/telegram_api/bot_api.rs @@ -2,7 +2,8 @@ use reqwest::multipart; use uuid::Uuid; use crate::{ - common::types::ChatId, errors::PentaractResult, + common::types::ChatId, + errors::{PentaractError, PentaractResult}, services::storage_workers_scheduler::StorageWorkersScheduler, }; @@ -27,15 +28,19 @@ impl<'t> TelegramBotApi<'t> { chat_id: ChatId, storage_id: Uuid, ) -> PentaractResult { - let chat_id = { - // inserting 100 between minus sign and chat id - // cause telegram devs are complete retards and it works this way only - // - // https://stackoverflow.com/a/65965402/12255756 + tracing::debug!( + "[TELEGRAM API] Uploading chunk: chat_id={}, file_size={}", + chat_id, + file.len() + ); - let n = chat_id.abs().checked_ilog10().unwrap_or(0) + 1; - chat_id - (100 * ChatId::from(10).pow(n)) - }; + if chat_id < 0 && chat_id > -10000000000 { + tracing::info!( + "[TELEGRAM API] Using regular group (chat_id={}). If bot can't find the chat, \ + make sure the bot is added and has permissions.", + chat_id + ); + } let token = self.scheduler.get_token(storage_id).await?; let url = self.build_url("", "sendDocument", token); @@ -51,10 +56,27 @@ impl<'t> TelegramBotApi<'t> { .send() .await?; - match response.error_for_status() { - // https://stackoverflow.com/a/32679930/12255756 - Ok(r) => Ok(r.json::().await?.result.document), - Err(e) => Err(e.into()), + let status = response.status(); + if !status.is_success() { + let error_text = response.text().await.unwrap_or_else(|_| "Unable to read error body".to_string()); + tracing::error!( + "[TELEGRAM API] Upload failed: status={}, response={}", + status, + error_text + ); + return Err(PentaractError::TelegramAPIError(format!( + "Status {}: {}", + status, + error_text + ))); + } + + match response.json::().await { + Ok(body) => Ok(body.result.document), + Err(e) => { + tracing::error!("[TELEGRAM API] Failed to parse response: {}", e); + Err(e.into()) + } } } diff --git a/pentaract/src/repositories/access.rs b/pentaract/src/repositories/access.rs index 63dbfe04..af065046 100644 --- a/pentaract/src/repositories/access.rs +++ b/pentaract/src/repositories/access.rs @@ -24,6 +24,13 @@ impl<'d> AccessRepository<'d> { ) -> PentaractResult<()> { let id = Uuid::new_v4(); + tracing::debug!( + "[ACCESS REPO] Attempting to grant access: storage_id={}, user_email={}, access_type={:?}", + storage_id, + grant_access.user_email, + grant_access.access_type + ); + let result = sqlx::query( format!( " @@ -54,13 +61,28 @@ impl<'d> AccessRepository<'d> { } })?; + tracing::debug!( + "[ACCESS REPO] Query affected {} rows", + result.rows_affected() + ); + if result.rows_affected() == 0 { + tracing::error!( + "[ACCESS REPO] User with email \"{}\" not found in users table", + grant_access.user_email + ); return Err(PentaractError::DoesNotExist(format!( "user with email \"{}\"", grant_access.user_email ))); } + tracing::debug!( + "[ACCESS REPO] Successfully granted access to user {} for storage {}", + grant_access.user_email, + storage_id + ); + Ok(()) } diff --git a/pentaract/src/repositories/storages.rs b/pentaract/src/repositories/storages.rs index c2161715..dbdf5996 100644 --- a/pentaract/src/repositories/storages.rs +++ b/pentaract/src/repositories/storages.rs @@ -46,14 +46,19 @@ impl<'d> StoragesRepository<'d> { } pub async fn list_by_user_id(&self, user_id: Uuid) -> PentaractResult> { - sqlx::query_as( + tracing::debug!( + "[STORAGES REPO] Fetching storages for user_id={}", + user_id + ); + + let result = sqlx::query_as( format!( " SELECT s.*, COUNT(f.id) AS files_amount, COALESCE(SUM(f.size), 0)::BigInt as size FROM {TABLE} s JOIN {ACCESS_TABLE} a ON s.id = a.storage_id - LEFT JOIN {FILES_TABLE} f ON s.id = f.storage_id - WHERE a.user_id = $1 AND (f.path NOT LIKE '%/' OR f.path IS NULL) + LEFT JOIN {FILES_TABLE} f ON s.id = f.storage_id AND f.path NOT LIKE '%/' + WHERE a.user_id = $1 GROUP by s.id " ) @@ -62,7 +67,15 @@ impl<'d> StoragesRepository<'d> { .bind(user_id) .fetch_all(self.db) .await - .map_err(|e| map_not_found(e, "storages")) + .map_err(|e| map_not_found(e, "storages"))?; + + tracing::debug!( + "[STORAGES REPO] Found {} storages for user_id={}", + result.len(), + user_id + ); + + Ok(result) } pub async fn get_by_id(&self, id: Uuid) -> PentaractResult { diff --git a/pentaract/src/routers/files.rs b/pentaract/src/routers/files.rs index 07d88150..442e6fce 100644 --- a/pentaract/src/routers/files.rs +++ b/pentaract/src/routers/files.rs @@ -9,6 +9,7 @@ use axum::{ routing::{get, post}, Extension, Json, Router, }; +use percent_encoding::percent_decode_str; use reqwest::header; use tokio_util::bytes::Bytes; use uuid::Uuid; @@ -100,7 +101,13 @@ impl FilesRouter { file = Some(data); filename = Some(field_filename); } - "path" => path = Some(String::from_utf8(data.to_vec()).unwrap()), + "path" => { + let raw_path = String::from_utf8(data.to_vec()).unwrap(); + let decoded = percent_decode_str(&raw_path) + .decode_utf8() + .unwrap_or(std::borrow::Cow::Borrowed(&raw_path)); + path = Some(decoded.to_string()); + } // don't give a fuck about other fields _ => (), } @@ -143,7 +150,13 @@ impl FilesRouter { .get("path") .map(|path| String::from_utf8(path.to_vec()).map_err(|_| "Path cannot be parsed")) .unwrap_or(Err("Path is required")) - .map_err(|e| (StatusCode::BAD_REQUEST, e.to_owned()))?; + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_owned())) + .map(|raw_path| { + percent_decode_str(&raw_path) + .decode_utf8() + .unwrap_or(std::borrow::Cow::Borrowed(&raw_path)) + .to_string() + })?; let file = body_parts .get("file") diff --git a/pentaract/src/routers/storages.rs b/pentaract/src/routers/storages.rs index 7d12b6fc..3ef25992 100644 --- a/pentaract/src/routers/storages.rs +++ b/pentaract/src/routers/storages.rs @@ -66,6 +66,10 @@ impl StoragesRouter { .list(&user) .await .map(|s| StoragesListSchema::new(s))?; + tracing::debug!( + "[STORAGES ROUTER] Returning {} storages to client", + storages.storages.len() + ); Ok::<_, (StatusCode, String)>(Json(storages)) } diff --git a/pentaract/src/services/storages.rs b/pentaract/src/services/storages.rs index bbcca3b5..7bd84c32 100644 --- a/pentaract/src/services/storages.rs +++ b/pentaract/src/services/storages.rs @@ -44,6 +44,13 @@ impl<'d> StoragesService<'d> { // creating storage let in_model = InStorage::new(in_schema.name, in_schema.chat_id); let storage = self.repo.create(in_model).await?; + + tracing::debug!( + "[STORAGES SERVICE] Created storage id={}, name={}, chat_id={}", + storage.id, + storage.name, + storage.chat_id + ); // setting user as the storage admin let access_schema = GrantAccess::new(user.email.clone(), AccessType::A); @@ -51,15 +58,37 @@ impl<'d> StoragesService<'d> { .access_repo .create_or_update(storage.id, access_schema) .await; - if result.is_err() { - // fallback - self.repo.delete_storage(storage.id).await? + + match &result { + Ok(_) => { + tracing::debug!( + "[STORAGES SERVICE] Successfully granted access to user {} for storage {}", + user.email, + storage.id + ); + } + Err(e) => { + tracing::error!( + "[STORAGES SERVICE] Failed to grant access to user {} for storage {}: {:?}. Rolling back storage creation.", + user.email, + storage.id, + e + ); + // fallback + let _ = self.repo.delete_storage(storage.id).await; + } } result.map(|_| storage) } pub async fn list(&self, user: &AuthUser) -> PentaractResult> { - self.repo.list_by_user_id(user.id).await + let storages = self.repo.list_by_user_id(user.id).await?; + tracing::debug!( + "[STORAGES SERVICE] Listed {} storages for user_id={}", + storages.len(), + user.id + ); + Ok(storages) } pub async fn get(&self, id: Uuid, user: &AuthUser) -> PentaractResult { diff --git a/ui/src/pages/Storages/StorageCreateForm.jsx b/ui/src/pages/Storages/StorageCreateForm.jsx index 4ad1f6a3..4752695c 100644 --- a/ui/src/pages/Storages/StorageCreateForm.jsx +++ b/ui/src/pages/Storages/StorageCreateForm.jsx @@ -48,10 +48,12 @@ const StorageCreateForm = () => { let err = null if (value > 0) { - err = 'Chat id must be a valid negative integer' + err = 'Chat id must be a negative integer' } else if (value === '') { - err = 'Chat id is required and must be a valid negative integer' + err = 'Chat id is required' } + // No additional validation - accept any negative number + // Both regular groups (-XXXXXXXXX) and supergroups (-100XXXXXXXXXX) are valid setChatIdErr(err) } @@ -108,7 +110,10 @@ const StorageCreateForm = () => { type="number" variant="standard" onChange={validateChatId} - helperText={chatIdErr} + helperText={ + chatIdErr() || + 'Get chat ID via @userinfobot or @getidsbot. Use the ID exactly as provided.' + } error={typeof chatIdErr() === 'string'} fullWidth required diff --git a/ui/src/pages/Storages/index.jsx b/ui/src/pages/Storages/index.jsx index c3980524..2b43169c 100644 --- a/ui/src/pages/Storages/index.jsx +++ b/ui/src/pages/Storages/index.jsx @@ -24,6 +24,8 @@ const Storages = () => { onMount(async () => { const storagesSchema = await API.storages.listStorages() + console.log('[STORAGES] Received from API:', storagesSchema) + console.log('[STORAGES] Number of storages:', storagesSchema.storages?.length || 0) setStorages(storagesSchema.storages) })