From 7973ba0027619318047e419429a575ba443c4756 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 2 Sep 2025 14:00:54 +0300 Subject: [PATCH] [0.6.1] - 2025-09-02 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### πŸš€ ИзмСнСно - Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ - **ГСнСрация ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€**: ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΡƒΠ΄Π°Π»Π΅Π½Π° ΠΈΠ· Quoter, Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ управляСтся Vercel Edge API - **ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° legacy ΠΊΠΎΠ΄Π°**: Π£Π΄Π°Π»Π΅Π½Ρ‹ всС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ ΠΈ ΡΠ»ΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ - **ДокумСнтация**: Π‘ΠΎΠΊΡ€Π°Ρ‰Π΅Π½Π° с 17 Ρ„Π°ΠΉΠ»ΠΎΠ² Π΄ΠΎ 7, слСдуя ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ°ΠΌ KISS/DRY - **Π‘ΠΌΠ΅Π½Π° фокуса**: Quoter Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ сосрСдоточСн Π½Π° upload + storage, Vercel ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ - **Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ запросов**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΠ° источников для ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ CORS whitelist - **РСализация Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠ²**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ настраиваСмыС Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Ρ‹ для S3, Redis ΠΈ Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ - **УпрощСнная Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ**: Π£Π΄Π°Π»Π΅Π½ слоТный rate limiting, оставлСна Ρ‚ΠΎΠ»ΡŒΠΊΠΎ нСобходимая Π·Π°Ρ‰ΠΈΡ‚Π° upload ### πŸ“ ОбновлСно - ΠšΠΎΠ½ΡΠΎΠ»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½Π° докумСнтация Π² ΠΏΡ€Π°ΠΊΡ‚ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ структуру: - Основной README.md с быстрым стартом - docs/SETUP.md для ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ ΠΈ развСртывания - Π£ΠΏΡ€ΠΎΡ‰Π΅Π½Π½Ρ‹ΠΉ features.md с фокусом Π½Π° ΠΎΡΠ½ΠΎΠ²Π½ΡƒΡŽ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ - Π”ΠΎΠ±Π°Π²Π»Π΅Π½ Π°ΠΊΡ†Π΅Π½Ρ‚ Π½Π° Vercel ΠΏΠΎ всСму ΠΊΠΎΠ΄Ρƒ ΠΈ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ ### πŸ—‘οΈ Π£Π΄Π°Π»Π΅Π½ΠΎ - Π˜Π·Π±Ρ‹Ρ‚ΠΎΡ‡Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ (api-reference, deployment, development, ΠΈ Ρ‚.Π΄.) - Π”ΡƒΠ±Π»ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉΡΡ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π² Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ… - ИзлишнС Π΄Π΅Ρ‚Π°Π»ΡŒΠ½Π°Ρ докумСнтация для простого Ρ„Π°ΠΉΠ»ΠΎΠ²ΠΎΠ³ΠΎ прокси πŸ’‹ **Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅**: KISS ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ - ΡƒΠ±Ρ€Π°Π»ΠΈ ΠΈΠ·Π±Ρ‹Ρ‚ΠΎΡ‡Π½ΠΎΡΡ‚ΡŒ, оставили ΡΡƒΡ‚ΡŒ. --- CHANGELOG.md | 25 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- Dockerfile | 6 +- README.md | 184 +++++---------- docs/README.md | 56 ++--- docs/SETUP.md | 191 ++++++++++++++++ docs/api-reference.md | 213 ------------------ docs/contributing.md | 292 ------------------------ docs/deployment.md | 318 -------------------------- docs/development.md | 422 ----------------------------------- docs/features.md | 101 +++------ docs/hybrid-architecture.md | 2 +- docs/monitoring.md | 341 ---------------------------- docs/security.md | 176 --------------- docs/testing.md | 363 ------------------------------ docs/upload-api-detailed.md | 298 ------------------------- docs/url-format.md | 204 ----------------- docs/vercel-og-quickstart.md | 233 ------------------- docs/vercel-thumbnails.md | 363 ++++++++++++++++++++++++++++++ src/app_state.rs | 136 +++++++---- src/auth.rs | 38 ++-- src/handlers/common.rs | 171 +++++++++----- src/handlers/mod.rs | 2 +- src/handlers/proxy.rs | 126 ++--------- src/handlers/serve_file.rs | 46 ++-- src/handlers/universal.rs | 51 ++--- src/handlers/upload.rs | 2 +- src/handlers/user.rs | 4 +- src/main.rs | 27 ++- src/security.rs | 372 +++++++++--------------------- src/thumbnail.rs | 256 +++++---------------- 32 files changed, 1168 insertions(+), 3855 deletions(-) create mode 100644 docs/SETUP.md delete mode 100644 docs/api-reference.md delete mode 100644 docs/contributing.md delete mode 100644 docs/deployment.md delete mode 100644 docs/development.md delete mode 100644 docs/monitoring.md delete mode 100644 docs/security.md delete mode 100644 docs/testing.md delete mode 100644 docs/upload-api-detailed.md delete mode 100644 docs/url-format.md delete mode 100644 docs/vercel-og-quickstart.md create mode 100644 docs/vercel-thumbnails.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b909e..b39d0fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [0.6.1] - 2025-09-02 + +### πŸš€ ИзмСнСно - Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ +- **ГСнСрация ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€**: ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΡƒΠ΄Π°Π»Π΅Π½Π° ΠΈΠ· Quoter, Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ управляСтся Vercel Edge API +- **ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° legacy ΠΊΠΎΠ΄Π°**: Π£Π΄Π°Π»Π΅Π½Ρ‹ всС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ ΠΈ ΡΠ»ΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ +- **ДокумСнтация**: Π‘ΠΎΠΊΡ€Π°Ρ‰Π΅Π½Π° с 17 Ρ„Π°ΠΉΠ»ΠΎΠ² Π΄ΠΎ 7, слСдуя ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ°ΠΌ KISS/DRY +- **Π‘ΠΌΠ΅Π½Π° фокуса**: Quoter Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ сосрСдоточСн Π½Π° upload + storage, Vercel ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ +- **Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ запросов**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΠ° источников для ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ CORS whitelist +- **РСализация Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠ²**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ настраиваСмыС Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Ρ‹ для S3, Redis ΠΈ Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ +- **УпрощСнная Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ**: Π£Π΄Π°Π»Π΅Π½ слоТный rate limiting, оставлСна Ρ‚ΠΎΠ»ΡŒΠΊΠΎ нСобходимая Π·Π°Ρ‰ΠΈΡ‚Π° upload + +### πŸ“ ОбновлСно +- ΠšΠΎΠ½ΡΠΎΠ»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½Π° докумСнтация Π² ΠΏΡ€Π°ΠΊΡ‚ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ структуру: + - Основной README.md с быстрым стартом + - docs/SETUP.md для ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ ΠΈ развСртывания + - Π£ΠΏΡ€ΠΎΡ‰Π΅Π½Π½Ρ‹ΠΉ features.md с фокусом Π½Π° ΠΎΡΠ½ΠΎΠ²Π½ΡƒΡŽ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ +- Π”ΠΎΠ±Π°Π²Π»Π΅Π½ Π°ΠΊΡ†Π΅Π½Ρ‚ Π½Π° Vercel ΠΏΠΎ всСму ΠΊΠΎΠ΄Ρƒ ΠΈ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ + +### πŸ—‘οΈ Π£Π΄Π°Π»Π΅Π½ΠΎ +- Π˜Π·Π±Ρ‹Ρ‚ΠΎΡ‡Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ (api-reference, deployment, development, ΠΈ Ρ‚.Π΄.) +- Π”ΡƒΠ±Π»ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉΡΡ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π² Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ… +- ИзлишнС Π΄Π΅Ρ‚Π°Π»ΡŒΠ½Π°Ρ докумСнтация для простого Ρ„Π°ΠΉΠ»ΠΎΠ²ΠΎΠ³ΠΎ прокси + +πŸ’‹ **Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅**: KISS ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ - ΡƒΠ±Ρ€Π°Π»ΠΈ ΠΈΠ·Π±Ρ‹Ρ‚ΠΎΡ‡Π½ΠΎΡΡ‚ΡŒ, оставили ΡΡƒΡ‚ΡŒ. + ## [0.6.0] - 2025-09-02 ### πŸ”’ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ DDoS diff --git a/Cargo.lock b/Cargo.lock index 72a91d8..6b3ce9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2644,7 +2644,7 @@ dependencies = [ [[package]] name = "quoter" -version = "0.5.4" +version = "0.6.1" dependencies = [ "actix", "actix-cors", diff --git a/Cargo.toml b/Cargo.toml index 0ac12ca..91c7896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quoter" -version = "0.6.0" +version = "0.6.1" edition = "2024" [dependencies] diff --git a/Dockerfile b/Dockerfile index 48adfeb..e6bc520 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,11 +45,11 @@ ENV CARGO_HTTP_TIMEOUT=60 ENV CARGO_HTTP_LOW_SPEED_LIMIT=10 ENV RUSTC_FORCE_INCREMENTAL=0 # Build dependencies only with extreme memory conservation -RUN cargo build --release 2>&1 | head -100 && \ +RUN cargo build --release && \ # Force cleanup of intermediate files to free memory cargo clean -p quoter && \ - # Keep only the dependency artifacts - find target/release/deps -name "quoter*" -delete + # Keep only the dependency artifacts (suppressing error if dir doesn't exist) + find target/release/deps -name "quoter*" -delete 2>/dev/null || true # Remove the default source file created by cargo new RUN rm src/*.rs diff --git a/README.md b/README.md index 24a405c..0aae855 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,91 @@ # Quoter πŸš€ -[![Rust](https://img.shields.io/badge/Rust-1.70+-orange.svg)](https://www.rust-lang.org/) -[![Actix Web](https://img.shields.io/badge/Actix%20Web-4.0+-blue.svg)](https://actix.rs/) -[![Redis](https://img.shields.io/badge/Redis-6.0+-red.svg)](https://redis.io/) -[![S3 Compatible](https://img.shields.io/badge/S3%20Compatible-Storj%20%7C%20AWS-green.svg)](https://aws.amazon.com/s3/) -[![Tests](https://img.shields.io/badge/Tests-36%20Passing-brightgreen.svg)](https://dev.discours.io/discours.io/quoter) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +> Simple file upload proxy with quotas. Upload to S3, thumbnails via Vercel. -> ΠœΠΈΠΊΡ€ΠΎΡΠ΅Ρ€Π²ΠΈΡ для управлСния Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ ΠΊΠ²ΠΎΡ‚, ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ ΠΈ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ с S3 Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°ΠΌΠΈ +**Focus**: Upload + Storage. Thumbnails managed by Vercel Edge API for better performance. -Quoter - это Π²Ρ‹ΡΠΎΠΊΠΎΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ сСрвис для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ ΠΈ управлСния Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ, построСнный Π½Π° Rust с использованиСм Actix Web. ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ автоматичСскоС созданиС ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€, ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ с Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹ΠΌΠΈ S3-совмСстимыми Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°ΠΌΠΈ. +## What it does -## πŸ“– ДокумСнтация +- πŸ“€ **Upload files** to S3/Storj with user quotas +- πŸ” **JWT authentication** with session management +- πŸ“¦ **File serving** with caching and optimization +- 🌐 **CORS support** for web apps -ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Π°Ρ докумСнтация доступна Π² ΠΏΠ°ΠΏΠΊΠ΅ [`docs/`](./docs/): +## πŸš€ Quick Start -### ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ Ρ€Π°Π·Π΄Π΅Π»Ρ‹ -- [πŸ“š ОглавлСниС](./docs/README.md) - Полная структура Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ -- [πŸ”§ API Reference](./docs/api-reference.md) - ДокумСнтация API -- [βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ](./docs/configuration.md) - Настройка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния -- [πŸš€ Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅](./docs/deployment.md) - Π˜Π½ΡΡ‚Ρ€ΡƒΠΊΡ†ΠΈΠΈ ΠΏΠΎ Ρ€Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΡŽ -- [πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³](./docs/monitoring.md) - Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ - -### ВСхничСскиС Π΄Π΅Ρ‚Π°Π»ΠΈ -- [πŸ—οΈ АрхитСктура](./docs/architecture.md) - ВСхничСская Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° систСмы -- [πŸ” Как это Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚](./docs/how-it-works.md) - ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΠ΅ описаниС процСссов -- [πŸ§ͺ ВСстированиС](./docs/testing.md) - ПолноС ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ тСстами (36 тСстов) -- [πŸ’» Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°](./docs/development.md) - Настройка срСды Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ -- [🀝 Contributing](./docs/contributing.md) - Руководство для ΠΊΠΎΠ½Ρ‚Ρ€ΠΈΠ±ΡŒΡŽΡ‚ΠΎΡ€ΠΎΠ² - -## ✨ ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ возмоТности - -- πŸ” **АутСнтификация** Ρ‡Π΅Ρ€Π΅Π· JWT Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ -- πŸ“ **Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ²** Π² S3/Storj с автоматичСским ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ΠΌ MIME-Ρ‚ΠΈΠΏΠΎΠ² -- πŸ–ΌοΈ **АвтоматичСскиС ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹** для ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ (10, 40, 110, 300, 600, 800, 1400px) -- πŸ’Ύ **Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ** ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ (5 Π“Π‘ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ) -- 🎨 **ΠžΠ²Π΅Ρ€Π»Π΅ΠΈ для shout** с автоматичСским Π½Π°Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ΠΌ тСкста -- πŸ”„ **CORS ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ°** для Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ -- ⚑ **Высокая ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ** благодаря асинхронной Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π΅ -- πŸ“Š **ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅** всСх ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ - -## πŸ—οΈ АрхитСктура - -Quoter построСн Π½Π° соврСмСнном стСкС Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠΉ: - -- **Backend**: Rust + Actix Web -- **Π‘Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…**: Redis для ΠΊΠ²ΠΎΡ‚ ΠΈ ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ -- **Π₯Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅**: S3-совмСстимыС сСрвисы (Storj, AWS S3) -- **АутСнтификация**: JWT Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ Ρ‡Π΅Ρ€Π΅Π· GraphQL API -- **ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ**: image-rs + imageproc - -## πŸ§ͺ ВСстированиС - -### Запуск тСстов ```bash -# ВсС тСсты -cargo test +# Setup +cargo build +cp .env.example .env # Configure environment +cargo run -# ΠšΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ тСст -cargo test test_health_check - -# ВСсты с ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ΠΌ -./scripts/test-coverage.sh +# Test +curl http://localhost:8080/ # Health check ``` -### Бтатистика тСстов -- **basic_test.rs:** 23 тСста (основная Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ) -- **handler_tests.rs:** 13 тСстов (HTTP endpoints) -- **ΠžΠ±Ρ‰Π΅Π΅ ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅:** 100% основных ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² -- **Бтатус:** ВсС тСсты проходят ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ +## πŸ”§ API -## πŸ“‹ ВрСбования +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/` | Health check or user info (need auth token) | +| `POST` | `/` | Upload file (need auth token) | +| `GET` | `/{filename}` | Get file or thumbnail | -- **Rust**: 1.70 ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅ -- **Redis**: 6.0 ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅ -- **S3 совмСстимоС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅**: Storj, AWS S3 ΠΈΠ»ΠΈ Π΄Ρ€ΡƒΠ³ΠΎΠ΅ -- **API ядра**: для Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ получСния Π΄Π°Π½Π½Ρ‹Ρ… shout - -## πŸš€ CI/CD ΠΈ автоматизация - -### Бтатус ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€Π° -- βœ… **ВСсты:** 36/36 проходят ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ -- βœ… **ΠšΠΎΠΌΠΏΠΈΠ»ΡΡ†ΠΈΡ:** Π±Π΅Π· ошибок -- βœ… **ΠŸΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅:** 100% основных ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² -- πŸš€ **Π”Π΅ΠΏΠ»ΠΎΠΉ:** автоматичСский ΠΏΡ€ΠΈ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΌ ΠΏΡ€ΠΎΡ…ΠΎΠΆΠ΄Π΅Π½ΠΈΠΈ тСстов - -### Автоматизация -- АвтоматичСский запуск тСстов ΠΏΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ ΠΊΠΎΠΌΠΌΠΈΡ‚Π΅ -- ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° качСства ΠΊΠΎΠ΄Π° ΠΈ покрытия -- АвтоматичСский Π΄Π΅ΠΏΠ»ΠΎΠΉ Π² ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ½ -- ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ "тСсты β†’ Π΄Π΅ΠΏΠ»ΠΎΠΉ" - -## πŸ”§ ИспользованиС - -### ΠŸΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния - -ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Π°Ρ информация ΠΎ настройкС ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния доступна Π² [Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ ΠΏΠΎ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ](./docs/configuration.md). - -### API Endpoints - -ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ API endpoints: - -| ΠœΠ΅Ρ‚ΠΎΠ΄ | Endpoint | ОписаниС | -|-------|----------|----------| -| `GET` | `/` | ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° состояния сСрвСра | -| `POST` | `/` | Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»Π° | -| `GET` | `/{filename}` | ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π°/ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ | -| `GET` | `/quota` | Π˜Π½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡ ΠΎ ΠΊΠ²ΠΎΡ‚Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ | -| `POST` | `/quota/increase` | Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ | -| `POST` | `/quota/set` | Установка ΠΊΠ²ΠΎΡ‚Ρ‹ | - -ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Π°Ρ докумСнтация API доступна Π² [API Reference](./docs/api-reference.md). - -### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования - -#### Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»Π° +### Upload file ```bash curl -X POST http://localhost:8080/ \ -H "Authorization: Bearer your-token" \ -F "file=@image.jpg" ``` -#### ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ +### Get thumbnail ```bash +# Legacy thumbnails (fallback only) curl http://localhost:8080/image_300.jpg + +# πŸ’‘ Recommended: Use Vercel Image API +https://yoursite.com/_next/image?url=https://files.dscrs.site/image.jpg&w=300&q=75 ``` -#### Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ -```bash -curl -X POST http://localhost:8080/quota/increase \ - -H "Authorization: Bearer your-token" \ - -H "Content-Type: application/json" \ - -d '{"user_id": "user123", "additional_bytes": 1073741824}' +## πŸ—οΈ Architecture & Setup + +**Simple 3-tier architecture:** +- **Upload**: Quoter (auth + quotas + S3 storage) +- **Download**: Vercel Edge API (thumbnails + optimization) +- **Storage**: S3/Storj (files) + Redis (quotas/cache) + +``` +Upload: Client β†’ Quoter β†’ S3/Storj +Download: Client β†’ Vercel β†’ Quoter (fallback) ``` -## πŸ§ͺ Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° +πŸ’‹ **Simplified approach**: Quoter handles uploads, Vercel handles thumbnails. + +## πŸ“‹ Environment Setup ```bash -cargo build # сборка -cargo test # запуск тСстов -cargo clippy # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠΎΠ΄Π° -cargo fmt # Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ -RUST_LOG=debug cargo run # ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹Π΅ Π»ΠΎΠ³ΠΈ +# Required +REDIS_URL=redis://localhost:6379 +STORJ_ACCESS_KEY=your-key +STORJ_SECRET_KEY=your-secret +JWT_SECRET=your-secret + +# Optional +PORT=8080 +RUST_LOG=info ``` -### ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ +## πŸ§ͺ Testing -ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π°: +```bash +cargo test # 36 tests passing +./scripts/test-coverage.sh # Coverage report +``` -- ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ² -- ИспользованиС ΠΊΠ²ΠΎΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ -- ВрСмя ΠΎΡ‚Π²Π΅Ρ‚Π° API -- Ошибки Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ -- Ошибки Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π² S3 \ No newline at end of file +## πŸ“š Documentation + +- [`docs/configuration.md`](./docs/configuration.md) - Environment setup +- [`docs/architecture.md`](./docs/architecture.md) - Technical details +- [`docs/vercel-og-integration.md`](./docs/vercel-og-integration.md) - Vercel integration + +For detailed setup and deployment instructions, see the docs folder. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e1e7c89..5a6fdc3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,49 +1,19 @@ -# ДокумСнтация Quoter +# Quoter Documentation -## πŸ“š ОглавлСниС +Simple file upload proxy with S3 storage and user quotas. -### πŸ“‹ АрхитСктура ΠΈ ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΡ‹ Ρ€Π°Π±ΠΎΡ‚Ρ‹ -- [πŸš€ Как Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Quoter](./how-it-works.md) - ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Π°Ρ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° систСмы с Π΄ΠΈΠ°Π³Ρ€Π°ΠΌΠΌΠ°ΠΌΠΈ -- [πŸ”€ Гибридная Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π°](./hybrid-architecture.md) - Vercel Edge + Quoter integration -- [πŸ“ Π€ΠΎΡ€ΠΌΠ°Ρ‚ URL для рСсайзСра](./url-format.md) - ПолноС руководство ΠΏΠΎ URL ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Π°ΠΌ -- [βš™οΈ API Reference](./api-reference.md) - Полная докумСнтация API +## πŸ“š Documentation -### πŸ›‘οΈ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΈ настройка -- [πŸ”’ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ DDoS](./security.md) - КомплСксная систСма Π·Π°Ρ‰ΠΈΡ‚Ρ‹ -- [βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ](./configuration.md) - Настройка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния -- [πŸš€ Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅](./deployment.md) - Π˜Π½ΡΡ‚Ρ€ΡƒΠΊΡ†ΠΈΠΈ ΠΏΠΎ Ρ€Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΡŽ -- [πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³](./monitoring.md) - Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ +- **[SETUP.md](./SETUP.md)** - Installation, configuration, and deployment +- **[architecture.md](./architecture.md)** - Technical details for developers +- **[configuration.md](./configuration.md)** - Environment variables reference +- **[features.md](./features.md)** - What Quoter does +- **[how-it-works.md](./how-it-works.md)** - System overview +- **[hybrid-architecture.md](./hybrid-architecture.md)** - Vercel + Quoter integration +- **[vercel-og-integration.md](./vercel-og-integration.md)** - OpenGraph integration -### 🎨 Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ -- [🎨 Vercel OG Integration](./vercel-og-integration.md) - ПолноС руководство ΠΏΠΎ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ с @vercel/og -- [⚑ Vercel OG Quick Start](./vercel-og-quickstart.md) - Быстрый старт Π·Π° 5 ΠΌΠΈΠ½ΡƒΡ‚ +## 🎯 Key Concept -### ВСхничСскиС Π΄Π΅Ρ‚Π°Π»ΠΈ -- [АрхитСктура](./architecture.md) - ВСхничСская Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° систСмы -- [Π‘Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…](./database.md) - Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° Redis ΠΈ схСмы Π΄Π°Π½Π½Ρ‹Ρ… -- [S3 интСграция](./s3-integration.md) - Π Π°Π±ΠΎΡ‚Π° с S3/Storj -- [ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ](./image-processing.md) - Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ ΠΈ ΠΎΠ²Π΅Ρ€Π»Π΅Π΅Π² -- [Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ](./security.md) - АутСнтификация ΠΈ авторизация +**Quoter = Upload + Storage. Vercel = Thumbnails + Optimization.** -### Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° -- [Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°](./development.md) - Настройка срСды Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ -- [ВСстированиС](./testing.md) - Руководство ΠΏΠΎ Ρ‚Π΅ΡΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ -- [Contributing](./contributing.md) - Руководство для ΠΊΠΎΠ½Ρ‚Ρ€ΠΈΠ±ΡŒΡŽΡ‚ΠΎΡ€ΠΎΠ² - -### CI/CD ΠΈ автоматизация -- [ВСстированиС](./testing.md) - ПолноС ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ тСстами (36 тСстов) -- [Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅](./deployment.md) - Автоматизированный ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ -- [ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³](./monitoring.md) - ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ качСства ΠΊΠΎΠ΄Π° - -## πŸš€ Быстрый старт - -1. УстановитС зависимости: `cargo build` -2. НастройтС ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния (см. [ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ](./configuration.md)) -3. ЗапуститС сСрвСр: `cargo run` -4. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ API: `curl http://localhost:8080/` - -## πŸ“‹ ВрСбования - -- Rust 1.70+ -- Redis 6.0+ -- Доступ ΠΊ S3/Storj API \ No newline at end of file +Upload files to Quoter β†’ Store in S3 β†’ Serve via Vercel Edge API for best performance. \ No newline at end of file diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..69c9022 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,191 @@ +# Setup & Configuration + +## πŸš€ Quick Start + +```bash +# 1. Install Rust + Redis +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +redis-server # or docker run -p 6379:6379 redis:alpine + +# 2. Clone & build +git clone https://github.com/your-org/quoter.git +cd quoter +cargo build + +# 3. Configure +cp .env.example .env +# Edit .env with your keys + +# 4. Run +cargo run +``` + +## βš™οΈ Environment Variables + +### Required +```bash +REDIS_URL=redis://localhost:6379 +STORJ_ACCESS_KEY=your-storj-key +STORJ_SECRET_KEY=your-storj-secret +JWT_SECRET=your-jwt-secret +``` + +### Optional +```bash +PORT=8080 +RUST_LOG=info +STORJ_BUCKET_NAME=quoter-files +MAX_FILE_SIZE=524288000 # 500MB +USER_QUOTA_LIMIT=5368709120 # 5GB + +# CORS whitelist for file downloads (comma-separated, supports *.domain patterns) +CORS_DOWNLOAD_ORIGINS=https://discours.io,https://*.discours.io,https://testing.discours.io,https://testing3.discours.io + +# Request source logging for CORS whitelist analysis (optional) +RUST_LOG=info # Enable to see πŸ“₯ Request source and πŸ“Š ANALYTICS logs + +# Request timeout configuration (optional, defaults to 300 seconds) +# Controls timeouts for S3, Redis, and other external operations +REQUEST_TIMEOUT_SECONDS=300 + +# Upload protection (optional, defaults to 10 uploads per minute per IP) +# Simple protection against upload abuse for user-facing endpoints +UPLOAD_LIMIT_PER_MINUTE=10 +``` + +## 🐳 Docker + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:alpine + ports: ["6379:6379"] + + quoter: + build: . + ports: ["8080:8080"] + environment: + REDIS_URL: redis://redis:6379 + STORJ_ACCESS_KEY: ${STORJ_ACCESS_KEY} + STORJ_SECRET_KEY: ${STORJ_SECRET_KEY} + JWT_SECRET: ${JWT_SECRET} + depends_on: [redis] +``` + +## πŸ”’ Security + +### Rate Limits (per IP) +- General: 100 req/min +- Upload: 10 req/5min +- Auth: 20 req/15min + +### File Limits +- Max file: 500MB +- User quota: 5GB default +- Supported: JPG, PNG, GIF, WebP, HEIC, MP4, PDF + +## πŸ”§ Production Setup + +### Nginx Proxy +```nginx +server { + listen 80; + server_name files.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header X-Forwarded-For $remote_addr; + client_max_body_size 500M; + } +} +``` + +### Systemd Service +```ini +# /etc/systemd/system/quoter.service +[Unit] +Description=Quoter File Service +After=network.target redis.service + +[Service] +Type=simple +User=quoter +ExecStart=/opt/quoter/quoter +Restart=always +Environment=RUST_LOG=info + +[Install] +WantedBy=multi-user.target +``` + +## πŸ“Š Monitoring + +### Health Check +```bash +curl http://localhost:8080/ # Should return "ok" +``` + +### Redis Monitoring +```bash +redis-cli info memory +redis-cli --latency +``` + +### Logs +```bash +# View logs +journalctl -f -u quoter + +# Log format +INFO Upload successful: user_123 uploaded file.jpg (2.5MB) +WARN Rate limit exceeded: IP 192.168.1.100 +ERROR Failed to upload to S3: network timeout + +# CORS analytics logs (with RUST_LOG=info) +INFO πŸ“₯ Request source: origin=https://new.discours.io, referer=https://new.discours.io/posts/123, ip=1.2.3.4 +INFO πŸ“Š ANALYTICS: path=image.jpg, size=2048b, origin=https://vercel.app, referer=none, ip=5.6.7.8 +WARN ⚠️ CORS not whitelisted: https://unknown-domain.com +``` + +### Analyzing Request Sources +```bash +# Find most common origins for CORS whitelist tuning +grep "πŸ“₯ Request source" /var/log/quoter.log | grep -o "origin=[^,]*" | sort | uniq -c | sort -rn + +# Find Vercel requests +grep "vercel" /var/log/quoter.log | grep "πŸ“Š ANALYTICS" + +# Find requests from unknown sources +grep "⚠️ CORS not whitelisted" /var/log/quoter.log +``` + +## πŸ”§ Troubleshooting + +### Common Issues + +**Redis connection failed** +```bash +redis-cli ping # Should return PONG +``` + +**S3 upload failed** +```bash +# Test S3 credentials +aws s3 ls --endpoint-url=https://gateway.storjshare.io +``` + +**High memory usage** +```bash +# Check Redis memory +redis-cli memory usage + +# Clear cache if needed +redis-cli flushdb +``` + +### Debug Mode +```bash +RUST_LOG=debug cargo run +``` diff --git a/docs/api-reference.md b/docs/api-reference.md deleted file mode 100644 index df96a2f..0000000 --- a/docs/api-reference.md +++ /dev/null @@ -1,213 +0,0 @@ -# API Reference - -## ΠžΠ±Π·ΠΎΡ€ - -Quoter прСдоставляСт REST API для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Ρ„Π°ΠΉΠ»ΠΎΠ², управлСния ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ ΠΈ получСния Ρ„Π°ΠΉΠ»ΠΎΠ² с автоматичСской Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠ΅ΠΉ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€. - -πŸ†• **Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с @vercel/og**: Quoter Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΎΠΉ `@vercel/og` для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ динамичСских OpenGraph ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ. ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΡΡ‚ΠΈ см. Π² [Vercel OG Integration Guide](./vercel-og-integration.md). - -## Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ URL - -``` -http://localhost:8080 -``` - -## АутСнтификация - -ВсС API endpoints (ΠΊΡ€ΠΎΠΌΠ΅ получСния Ρ„Π°ΠΉΠ»ΠΎΠ²) Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Ρ‡Π΅Ρ€Π΅Π· Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ `Authorization`: - -``` -Authorization: Bearer -``` - -## Endpoints - -### 1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° состояния сСрвСра - -#### GET / -ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ Ρ€Π°Π±ΠΎΡ‚ΠΎΡΠΏΠΎΡΠΎΠ±Π½ΠΎΡΡ‚ΡŒ сСрвСра. - -**ΠžΡ‚Π²Π΅Ρ‚:** -``` -200 OK -ok -``` - -### 2. Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² - -#### POST / -Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ Ρ„Π°ΠΉΠ» Π² S3 Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅. - -**Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ:** -``` -Authorization: Bearer -Content-Type: multipart/form-data -``` - -**ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹:** -- `file` - Ρ„Π°ΠΉΠ» для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ - -**ΠžΡ‚Π²Π΅Ρ‚:** -``` -200 OK -filename.ext -``` - -**Ошибки:** -- `400 Bad Request` - Π½Π΅Ρ‚ Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΈΠ»ΠΈ всС Ρ„Π°ΠΉΠ»Ρ‹ пустыС -- `401 Unauthorized` - Π½Π΅Π²Π΅Ρ€Π½Ρ‹ΠΉ ΠΈΠ»ΠΈ ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ Ρ‚ΠΎΠΊΠ΅Π½ -- `413 Payload Too Large` - ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½Π° ΠΊΠ²ΠΎΡ‚Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ»ΠΈ Π»ΠΈΠΌΠΈΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Ρ„Π°ΠΉΠ»Π° -- `415 Unsupported Media Type` - Π½Π΅ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹ΠΉ Ρ‚ΠΈΠΏ Ρ„Π°ΠΉΠ»Π° -- `500 Internal Server Error` - ошибка Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π² S3 ΠΈΠ»ΠΈ обновлСния ΠΊΠ²ΠΎΡ‚Ρ‹ - -### 3. ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ - -#### GET / -ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ Π·Π°Π»ΠΎΠ³ΠΈΠ½Π΅Π½Π½ΠΎΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΎ ΠΊΠ²ΠΎΡ‚Π΅. - -**Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ:** -``` -Authorization: Bearer -``` - -**ΠžΡ‚Π²Π΅Ρ‚:** -```json -{ - "user_id": "user123", - "username": "john_doe", - "token_type": "session", - "created_at": "1642248600", - "last_activity": "1642335000", - "auth_data": "{\"roles\": [\"user\"]}", - "device_info": "{\"platform\": \"web\"}", - "quota": { - "current_quota": 1073741824, - "max_quota": 5368709120, - "usage_percentage": 20.0 - } -} -``` - -**Ошибки:** -- `401 Unauthorized` - Π½Π΅Π²Π΅Ρ€Π½Ρ‹ΠΉ ΠΈΠ»ΠΈ ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ Ρ‚ΠΎΠΊΠ΅Π½ - -### 4. ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»ΠΎΠ² - -#### GET /{filename} -ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Ρ„Π°ΠΉΠ» ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ с автоматичСской Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠ΅ΠΉ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€. - -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹:** -``` -GET /image.jpg # ΠžΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» -GET /image_300.jpg # ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° 300px ΡˆΠΈΡ€ΠΈΠ½Ρ‹ -GET /image_300.jpg/webp # ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ WebP -``` - -**🚫 Π£Π΄Π°Π»Π΅Π½Π½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ (Legacy):** -- `s=` - ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ для OpenGraph overlay большС Π½Π΅ поддСрТиваСтся -- ВстроСнная гСнСрация text overlay Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ обрабатываСтся Ρ‡Π΅Ρ€Π΅Π· `@vercel/og` - -**βœ… БоврСмСнная Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π°:** -Для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ OpenGraph ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ с тСкстом ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ с `@vercel/og`. Π‘ΠΌ. [Vercel OG Integration Guide](./vercel-og-integration.md). - -### 5. Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ - -#### GET /quota -ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΊΠ²ΠΎΡ‚Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. - -**ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ запроса:** -- `user_id` - ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ - -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€:** -``` -GET /quota?user_id=user123 -``` - -**ΠžΡ‚Π²Π΅Ρ‚:** -```json -{ - "user_id": "user123", - "current_quota": 1073741824, - "max_quota": 5368709120 -} -``` - -#### POST /quota/increase -Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ ΠΊΠ²ΠΎΡ‚Ρƒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. - -**Π’Π΅Π»ΠΎ запроса:** -```json -{ - "user_id": "user123", - "additional_bytes": 1073741824 -} -``` - -**ΠžΡ‚Π²Π΅Ρ‚:** -```json -{ - "user_id": "user123", - "current_quota": 2147483648, - "max_quota": 5368709120 -} -``` - -#### POST /quota/set -УстанавливаСт ΠΊΠ²ΠΎΡ‚Ρƒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. - -**Π’Π΅Π»ΠΎ запроса:** -```json -{ - "user_id": "user123", - "new_quota_bytes": 2147483648 -} -``` - -**ΠžΡ‚Π²Π΅Ρ‚:** -```json -{ - "user_id": "user123", - "current_quota": 2147483648, - "max_quota": 5368709120 -} -``` - -## ΠšΠΎΠ΄Ρ‹ ошибок - -| Код | ОписаниС | -|-----|----------| -| 200 | Π£ΡΠΏΠ΅ΡˆΠ½Ρ‹ΠΉ запрос | -| 400 | НСвСрныС ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ запроса ΠΈΠ»ΠΈ Π½Π΅Ρ‚ Ρ„Π°ΠΉΠ»ΠΎΠ² | -| 401 | НСавторизованный доступ | -| 404 | Π€Π°ΠΉΠ» Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ | -| 413 | ΠŸΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½Π° ΠΊΠ²ΠΎΡ‚Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ»ΠΈ Π»ΠΈΠΌΠΈΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Ρ„Π°ΠΉΠ»Π° (500 ΠœΠ‘) | -| 415 | НСподдСрТиваСмый Ρ‚ΠΈΠΏ Ρ„Π°ΠΉΠ»Π° | -| 500 | ВнутрСнняя ошибка сСрвСра | - -## ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования - -### Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° изобраТСния -```bash -curl -X POST http://localhost:8080/ \ - -H "Authorization: Bearer your-token" \ - -F "file=@image.jpg" -``` - -### ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ -```bash -curl -H "Authorization: Bearer your-token" \ - http://localhost:8080/ -``` - -### ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ -```bash -curl http://localhost:8080/image_300.jpg -``` - -### Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ -```bash -curl -X POST http://localhost:8080/quota/increase \ - -H "Authorization: Bearer your-token" \ - -H "Content-Type: application/json" \ - -d '{"user_id": "user123", "additional_bytes": 1073741824}' -``` \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index ca906a9..0000000 --- a/docs/contributing.md +++ /dev/null @@ -1,292 +0,0 @@ -# Contributing - -## Бпасибо Π·Π° интСрСс ΠΊ Quoter! πŸŽ‰ - -ΠœΡ‹ привСтствуСм Π²ΠΊΠ»Π°Π΄ ΠΎΡ‚ сообщСства. Π­Ρ‚ΠΎΡ‚ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ содСрТит руководство ΠΏΠΎ ΡƒΡ‡Π°ΡΡ‚ΠΈΡŽ Π² Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. - -## Как внСсти свой Π²ΠΊΠ»Π°Π΄ - -### 1. Π‘ΠΎΠΎΠ±Ρ‰ΠΈΡ‚ΡŒ ΠΎ Π±Π°Π³Π΅ - -Если Π²Ρ‹ нашли Π±Π°Π³, создайтС issue с: - -- **ΠšΡ€Π°Ρ‚ΠΊΠΈΠΌ описаниСм** ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ -- **Π¨Π°Π³Π°ΠΌΠΈ для воспроизвСдСния** -- **ΠžΠΆΠΈΠ΄Π°Π΅ΠΌΡ‹ΠΌ ΠΈ фактичСским ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ΠΌ** -- **ВСрсиСй** Rust, Redis, ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΡ… зависимостСй -- **Π›ΠΎΠ³Π°ΠΌΠΈ** (Ссли ΠΏΡ€ΠΈΠΌΠ΅Π½ΠΈΠΌΠΎ) - -### 2. ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠΈΡ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ - -Для прСдлоТСния Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ: - -- ΠžΠΏΠΈΡˆΠΈΡ‚Π΅ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ Ρ€Π΅ΡˆΠ°Π΅Ρ‚ вашС ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ -- ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠΈΡ‚Π΅ Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅ -- ΠžΠ±ΡΡƒΠ΄ΠΈΡ‚Π΅ Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Ρ‹ -- Π£ΠΊΠ°ΠΆΠΈΡ‚Π΅ ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚ - -### 3. ВнСсти ΠΊΠΎΠ΄ - -#### ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° - -1. **Fork** Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ -2. **Clone** ваш fork локально -3. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ **feature branch**: - ```bash - git checkout -b feature/amazing-feature - ``` - -#### Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° - -1. **Π‘Π»Π΅Π΄ΡƒΠΉΡ‚Π΅ стандартам ΠΊΠΎΠ΄Π°**: - ```bash - cargo fmt - cargo clippy - ``` - -2. **Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ тСсты** для Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ - -3. **ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚Π΅ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΡŽ** Ссли Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ - -4. **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ сборку**: - ```bash - cargo build - cargo test - ``` - -#### Commit ΠΈ Push - -1. **Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ commit** с ΠΎΠΏΠΈΡΠ°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌ сообщСниСм: - ```bash - git commit -m "feat: add amazing feature" - ``` - -2. **Push** Π² ваш fork: - ```bash - git push origin feature/amazing-feature - ``` - -3. **Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Pull Request** - -## Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Ρ‹ ΠΊΠΎΠ΄Π° - -### Rust - -- Π‘Π»Π΅Π΄ΡƒΠΉΡ‚Π΅ [Rust Style Guide](https://doc.rust-lang.org/1.0.0/style/style/naming/README.html) -- Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `cargo fmt` для форматирования -- Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `cargo clippy` для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ стиля -- Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ API - -### Commit Messages - -Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ [Conventional Commits](https://www.conventionalcommits.org/): - -``` -[optional scope]: - -[optional body] - -[optional footer(s)] -``` - -Π’ΠΈΠΏΡ‹: -- `feat:` - новая Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ -- `fix:` - исправлСниС Π±Π°Π³Π° -- `docs:` - измСнСния Π² Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ -- `style:` - Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° -- `refactor:` - Ρ€Π΅Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ½Π³ -- `test:` - Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ тСстов -- `chore:` - ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ зависимостСй - -ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹: -``` -feat: add user quota management API -fix(auth): handle expired tokens properly -docs: update API documentation -style: format code with cargo fmt -``` - -### ВСстированиС - -- **Unit тСсты** для всСх Π½ΠΎΠ²Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ -- **Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты** для API endpoints -- **ВСсты ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ** для критичСских участков -- МинимальноС ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ ΠΊΠΎΠ΄Π°: **80%** - -### ДокумСнтация - -- ΠžΠ±Π½ΠΎΠ²Π»ΡΠΉΡ‚Π΅ README.md Ссли Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ -- ДобавляйтС ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ ΠΊ слоТному ΠΊΠΎΠ΄Ρƒ -- Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ API измСнСния -- ΠžΠ±Π½ΠΎΠ²Π»ΡΠΉΡ‚Π΅ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования - -## ΠŸΡ€ΠΎΡ†Π΅ΡΡ Pull Request - -### Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ PR - -1. **Π—Π°ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ шаблон** Pull Request -2. **ΠžΠΏΠΈΡˆΠΈΡ‚Π΅ измСнСния** ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎ -3. **Π£ΠΊΠ°ΠΆΠΈΡ‚Π΅ связанныС issues** -4. **Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ ΡΠΊΡ€ΠΈΠ½ΡˆΠΎΡ‚Ρ‹** Ссли ΠΏΡ€ΠΈΠΌΠ΅Π½ΠΈΠΌΠΎ - -### Code Review - -- **Π”Π²Π° approval** Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ΡΡ для merge -- **CI/CD** Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡ€ΠΎΠΉΡ‚ΠΈ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ -- **Code coverage** Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡƒΠΌΠ΅Π½ΡŒΡˆΠΈΡ‚ΡŒΡΡ -- **Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ** провСряСтся автоматичСски - -### ПослС Merge - -- **Feature branch** удаляСтся автоматичСски -- **Release** создаСтся для Π·Π½Π°Ρ‡ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -- **ДокумСнтация** обновляСтся - -## Настройка срСды Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ - -### ВрСбования - -- Rust 1.70+ -- Redis 6.0+ -- Git - -### Установка - -```bash -# Fork ΠΈ clone -git clone https://github.com/YOUR_USERNAME/quoter.git -cd quoter - -# Установка зависимостСй -cargo build - -# Настройка pre-commit hooks -cargo install cargo-husky -cargo husky install -``` - -### Π›ΠΎΠΊΠ°Π»ΡŒΠ½Π°Ρ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° - -```bash -# Запуск Redis -docker run -d -p 6379:6379 redis:7-alpine - -# Настройка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния -cp .env.example .env -# ΠžΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ .env - -# Запуск прилоТСния -cargo run - -# Запуск тСстов -cargo test -``` - -## Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° - -``` -quoter/ -β”œβ”€β”€ src/ # Π˜ΡΡ…ΠΎΠ΄Π½Ρ‹ΠΉ ΠΊΠΎΠ΄ -β”‚ β”œβ”€β”€ main.rs # Π’ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° -β”‚ β”œβ”€β”€ app_state.rs # БостояниС прилоТСния -β”‚ β”œβ”€β”€ auth.rs # АутСнтификация -β”‚ β”œβ”€β”€ core.rs # API ядра -β”‚ β”œβ”€β”€ handlers/ # HTTP ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ -β”‚ β”œβ”€β”€ lookup.rs # Поиск Ρ„Π°ΠΉΠ»ΠΎΠ² -β”‚ β”œβ”€β”€ overlay.rs # ΠžΠ²Π΅Ρ€Π»Π΅ΠΈ -β”‚ β”œβ”€β”€ s3_utils.rs # S3 ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ -β”‚ └── thumbnail.rs # ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ -β”œβ”€β”€ docs/ # ДокумСнтация -β”œβ”€β”€ tests/ # Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты -β”œβ”€β”€ Cargo.toml # Зависимости -└── README.md # Основная докумСнтация -``` - -## Π ΠΎΠ»ΠΈ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅ - -### Maintainers - -- **Code review** всСх PR -- **Release management** -- **Architecture decisions** -- **Community management** - -### Contributors - -- **Feature development** -- **Bug fixes** -- **Documentation** -- **Testing** - -### Reviewers - -- **Code review** assigned PRs -- **Quality assurance** -- **Performance review** - -## ΠšΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΡ - -### Issues - -- Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ **labels** для ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ -- **Assign** issues ΠΊ сСбС Ссли Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚Π΅ Π½Π°Π΄ Π½ΠΈΠΌΠΈ -- **Update** статус рСгулярно - -### Discussions - -- **GitHub Discussions** для ΠΎΠ±Ρ‰ΠΈΡ… вопросов -- **RFC** для Π·Π½Π°Ρ‡ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -- **Architecture** для Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π½Ρ‹Ρ… Ρ€Π΅ΡˆΠ΅Π½ΠΈΠΉ - -### Code Review - -- **Π‘ΡƒΠ΄ΡŒΡ‚Π΅ конструктивными** -- **ΠžΠ±ΡŠΡΡΠ½ΡΠΉΡ‚Π΅ ΠΏΡ€ΠΈΡ‡ΠΈΠ½Ρ‹** ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -- **ΠŸΡ€Π΅Π΄Π»Π°Π³Π°ΠΉΡ‚Π΅ Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Ρ‹** -- **ΠžΡ‚Π²Π΅Ρ‡Π°ΠΉΡ‚Π΅ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ** - -## Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - -### ΠžΡ‚Ρ‡Π΅Ρ‚Ρ‹ ΠΎ уязвимостях - -Для критичСских уязвимостСй: - -1. **НЕ создавайтС ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ issue** -2. **ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ email** Π½Π° security@example.com -3. **ΠžΠΏΠΈΡˆΠΈΡ‚Π΅ ΡƒΡΠ·Π²ΠΈΠΌΠΎΡΡ‚ΡŒ** ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎ -4. **ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠΈΡ‚Π΅ Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅** Ссли Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ - -### Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΊΠΎΠ΄Π° - -- **НС ΠΊΠΎΠΌΠΌΠΈΡ‚ΡŒΡ‚Π΅ сСкрСты** -- **Π’Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π²Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅** -- **Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ бСзопасныС зависимости** -- **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠΉΡ‚Π΅ ΠΊΠΎΠ΄ Π½Π° уязвимости** - -## ЛицСнзия - -Внося ΠΊΠΎΠ΄ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚, Π²Ρ‹ ΡΠΎΠ³Π»Π°ΡˆΠ°Π΅Ρ‚Π΅ΡΡŒ с Ρ‚Π΅ΠΌ, Ρ‡Ρ‚ΠΎ ваш Π²ΠΊΠ»Π°Π΄ Π±ΡƒΠ΄Π΅Ρ‚ Π»ΠΈΡ†Π΅Π½Π·ΠΈΡ€ΠΎΠ²Π°Π½ ΠΏΠΎΠ΄ MIT License. - -## Благодарности - -Бпасибо всСм ΠΊΠΎΠ½Ρ‚Ρ€ΠΈΠ±ΡŒΡŽΡ‚ΠΎΡ€Π°ΠΌ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΠΎΠΌΠΎΠ³Π°ΡŽΡ‚ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Quoter Π»ΡƒΡ‡ΡˆΠ΅! πŸ™ - -### Бпособы ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΈ - -- **Code contributions** -- **Bug reports** -- **Feature requests** -- **Documentation improvements** -- **Community support** -- **Financial support** (Ссли ΠΏΡ€ΠΈΠΌΠ΅Π½ΠΈΠΌΠΎ) - -## ΠšΠΎΠ½Ρ‚Π°ΠΊΡ‚Ρ‹ - -- **Issues**: [GitHub Issues](https://github.com/your-org/quoter/issues) -- **Discussions**: [GitHub Discussions](https://github.com/your-org/quoter/discussions) -- **Email**: maintainers@example.com -- **Chat**: [Discord/Slack] (Ссли Π΅ΡΡ‚ΡŒ) - ---- - -**Бпасибо Π·Π° ваш Π²ΠΊΠ»Π°Π΄ Π² Quoter!** πŸš€ \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index c683b72..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,318 +0,0 @@ -# Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ - -## ΠžΠ±Π·ΠΎΡ€ - -Quoter ΠΌΠΎΠΆΠ½ΠΎ Ρ€Π°Π·Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹ΠΌΠΈ способами Π² зависимости ΠΎΡ‚ Π²Π°ΡˆΠΈΡ… потрСбностСй ΠΈ инфраструктуры. - -## Бпособы развСртывания - -### 1. Docker (РСкомСндуСтся) - -#### Π‘Π±ΠΎΡ€ΠΊΠ° ΠΎΠ±Ρ€Π°Π·Π° - -```bash -# Π‘Π±ΠΎΡ€ΠΊΠ° production ΠΎΠ±Ρ€Π°Π·Π° -docker build -t quoter:latest . - -# Π‘Π±ΠΎΡ€ΠΊΠ° с Ρ‚Π΅Π³Π°ΠΌΠΈ -docker build -t quoter:v1.0.0 . -``` - -#### Запуск ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° - -```bash -docker run -d \ - --name quoter \ - -p 8080:8080 \ - -e REDIS_URL=redis://redis:6379 \ - -e CORE_URL=https://api.example.com/graphql \ - -e STORJ_ACCESS_KEY=your-key \ - -e STORJ_SECRET_KEY=your-secret \ - -e AWS_ACCESS_KEY=your-aws-key \ - -e AWS_SECRET_KEY=your-aws-secret \ - quoter:latest -``` - -#### Docker Compose - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ `docker-compose.yml`: - -```yaml -version: '3.8' - -services: - redis: - image: redis:7-alpine - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes - - quoter: - build: . - ports: - - "8080:8080" - environment: - - REDIS_URL=redis://redis:6379 - - CORE_URL=https://api.example.com/graphql - - STORJ_ACCESS_KEY=${STORJ_ACCESS_KEY} - - STORJ_SECRET_KEY=${STORJ_SECRET_KEY} - - AWS_ACCESS_KEY=${AWS_ACCESS_KEY} - - AWS_SECRET_KEY=${AWS_SECRET_KEY} - - RUST_LOG=info - depends_on: - - redis - restart: unless-stopped - -volumes: - redis_data: -``` - -Запуск: - -```bash -docker-compose up -d -``` - -### 2. Kubernetes - -#### Deployment - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ `k8s/deployment.yaml`: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: quoter - labels: - app: quoter -spec: - replicas: 3 - selector: - matchLabels: - app: quoter - template: - metadata: - labels: - app: quoter - spec: - containers: - - name: quoter - image: quoter:latest - ports: - - containerPort: 8080 - env: - - name: REDIS_URL - value: "redis://redis-service:6379" - - name: CORE_URL - value: "https://api.example.com/graphql" - - name: STORJ_ACCESS_KEY - valueFrom: - secretKeyRef: - name: quoter-secrets - key: storj-access-key - - name: STORJ_SECRET_KEY - valueFrom: - secretKeyRef: - name: quoter-secrets - key: storj-secret-key - - name: AWS_ACCESS_KEY - valueFrom: - secretKeyRef: - name: quoter-secrets - key: aws-access-key - - name: AWS_SECRET_KEY - valueFrom: - secretKeyRef: - name: quoter-secrets - key: aws-secret-key - - name: RUST_LOG - value: "info" - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "512Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 -``` - -#### Service - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: quoter-service -spec: - selector: - app: quoter - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - type: LoadBalancer -``` - -#### Secrets - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: quoter-secrets -type: Opaque -data: - storj-access-key: - storj-secret-key: - aws-access-key: - aws-secret-key: -``` - -### 3. Systemd (Linux) - -#### Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ сСрвиса - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ `/etc/systemd/system/quoter.service`: - -```ini -[Unit] -Description=Quoter File Service -After=network.target redis.service - -[Service] -Type=simple -Author=quoter -Group=quoter -WorkingDirectory=/opt/quoter -Environment=REDIS_URL=redis://localhost:6379 -Environment=CORE_URL=https://api.example.com/graphql -Environment=STORJ_ACCESS_KEY=your-key -Environment=STORJ_SECRET_KEY=your-secret -Environment=AWS_ACCESS_KEY=your-aws-key -Environment=AWS_SECRET_KEY=your-aws-secret -Environment=RUST_LOG=info -ExecStart=/opt/quoter/quoter -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` - -#### Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ сСрвисом - -```bash -# Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ -sudo useradd -r -s /bin/false quoter - -# ΠšΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π±ΠΈΠ½Π°Ρ€Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° -sudo cp target/release/quoter /opt/quoter/ -sudo chown quoter:quoter /opt/quoter/quoter - -# Π’ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΈ запуск сСрвиса -sudo systemctl daemon-reload -sudo systemctl enable quoter -sudo systemctl start quoter - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° статуса -sudo systemctl status quoter -``` - -## ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -### Prometheus ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ - -Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ Π² `Cargo.toml`: - -```toml -[dependencies] -prometheus = "0.13" -actix-web-prom = "0.6" -``` - -### Grafana Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π°: - -- ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ запросов Π² сСкунду -- ВрСмя ΠΎΡ‚Π²Π΅Ρ‚Π° API -- ИспользованиС памяти ΠΈ CPU -- Ошибки ΠΏΠΎ Ρ‚ΠΈΠΏΠ°ΠΌ -- ИспользованиС ΠΊΠ²ΠΎΡ‚ - -### Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -#### Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Π»ΠΎΠ³ΠΈ - -```bash -# JSON Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ для ELK stack -RUST_LOG=info cargo run | jq . -``` - -#### Ротация Π»ΠΎΠ³ΠΎΠ² - -НастройтС logrotate: - -``` -/var/log/quoter/*.log { - daily - missingok - rotate 52 - compress - delaycompress - notifempty - create 644 quoter quoter - postrotate - systemctl reload quoter - endscript -} -``` - -## ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -### Π“ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½ΠΎΠ΅ ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -1. **Load Balancer**: НастройтС nginx ΠΈΠ»ΠΈ HAProxy -2. **Redis Cluster**: Для высоких Π½Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ -3. **S3 CDN**: Для статичСских Ρ„Π°ΠΉΠ»ΠΎΠ² - -### Π’Π΅Ρ€Ρ‚ΠΈΠΊΠ°Π»ΡŒΠ½ΠΎΠ΅ ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -- Π£Π²Π΅Π»ΠΈΡ‡ΡŒΡ‚Π΅ рСсурсы ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π°/сСрвСра -- НастройтС ΠΏΡƒΠ» соСдинСний Redis -- ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ - -## Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - -### БСтСвая Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - -- Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ HTTPS Π² ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Π΅ -- НастройтС firewall -- ΠžΠ³Ρ€Π°Π½ΠΈΡ‡ΡŒΡ‚Π΅ доступ ΠΊ Redis - -### Π‘Π΅ΠΊΡ€Π΅Ρ‚Ρ‹ - -- Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Kubernetes Secrets ΠΈΠ»ΠΈ Docker Secrets -- НС Ρ…Ρ€Π°Π½ΠΈΡ‚Π΅ сСкрСты Π² ΠΊΠΎΠ΄Π΅ -- Ротация ΠΊΠ»ΡŽΡ‡Π΅ΠΉ доступа - -### Аудит - -- Π›ΠΎΠ³ΠΈΡ€ΡƒΠΉΡ‚Π΅ всС ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ с Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ -- ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°ΠΉΡ‚Π΅ использованиС ΠΊΠ²ΠΎΡ‚ -- ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΡŒΡ‚Π΅ ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ \ No newline at end of file diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 42bf88e..0000000 --- a/docs/development.md +++ /dev/null @@ -1,422 +0,0 @@ -# Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° - -## Настройка срСды Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ - -### ВрСбования - -- **Rust**: 1.70 ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅ -- **Redis**: 6.0 ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅ (локально ΠΈΠ»ΠΈ Docker) -- **Git**: для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠ΅ΠΌ -- **IDE**: VS Code, IntelliJ IDEA ΠΈΠ»ΠΈ Π΄Ρ€ΡƒΠ³ΠΎΠΉ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ Rust - -### Установка Rust - -```bash -# Установка Rust Ρ‡Π΅Ρ€Π΅Π· rustup -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - -# ΠŸΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° shell -source ~/.bashrc - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° установки -rustc --version -cargo --version -``` - -### ΠšΠ»ΠΎΠ½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ рСпозитория - -```bash -git clone https://github.com/your-org/quoter.git -cd quoter -``` - -### Установка зависимостСй - -```bash -# Π‘Π±ΠΎΡ€ΠΊΠ° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° -cargo build - -# Установка Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… инструмСнтов -cargo install cargo-watch # для автоматичСской пСрСсборки -cargo install cargo-audit # для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ бСзопасности -cargo install cargo-tarpaulin # для покрытия ΠΊΠΎΠ΄Π° тСстами -``` - -## Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° - -``` -quoter/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ main.rs # Π’ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° прилоТСния -β”‚ β”œβ”€β”€ app_state.rs # БостояниС прилоТСния ΠΈ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ -β”‚ β”œβ”€β”€ auth.rs # АутСнтификация ΠΈ авторизация -β”‚ β”œβ”€β”€ core.rs # Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с API ядра -β”‚ β”œβ”€β”€ lookup.rs # Поиск ΠΈ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ MIME-Ρ‚ΠΈΠΏΠΎΠ² -β”‚ β”œβ”€β”€ overlay.rs # ГСнСрация ΠΎΠ²Π΅Ρ€Π»Π΅Π΅Π² для ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ -β”‚ β”œβ”€β”€ s3_utils.rs # Π£Ρ‚ΠΈΠ»ΠΈΡ‚Ρ‹ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с S3 -β”‚ β”œβ”€β”€ thumbnail.rs # Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ -β”‚ └── handlers/ # HTTP ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ -β”‚ β”œβ”€β”€ mod.rs # ΠœΠΎΠ΄ΡƒΠ»ΡŒ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² -β”‚ β”œβ”€β”€ upload.rs # Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² -β”‚ β”œβ”€β”€ proxy.rs # ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»ΠΎΠ² -β”‚ β”œβ”€β”€ quota.rs # Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ -β”‚ └── serve_file.rs # ΠžΠ±ΡΠ»ΡƒΠΆΠΈΠ²Π°Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»ΠΎΠ² -β”œβ”€β”€ docs/ # ДокумСнтация -β”œβ”€β”€ tests/ # Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты -β”œβ”€β”€ Cargo.toml # Зависимости ΠΈ конфигурация -β”œβ”€β”€ Cargo.lock # ЀиксированныС вСрсии зависимостСй -β”œβ”€β”€ Dockerfile # Docker ΠΎΠ±Ρ€Π°Π· -└── README.md # Основная докумСнтация -``` - -## Π›ΠΎΠΊΠ°Π»ΡŒΠ½Π°Ρ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° - -### Настройка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» `.env` Π² ΠΊΠΎΡ€Π½Π΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°: - -```bash -# Redis (локально ΠΈΠ»ΠΈ Docker) -REDIS_URL=redis://localhost:6379 - -# Core API (Π·Π°ΠΌΠ΅Π½ΠΈΡ‚Π΅ Π½Π° ваш endpoint) -CORE_URL=https://api.example.com/graphql - -# Storj S3 (тСстовыС ΠΊΠ»ΡŽΡ‡ΠΈ) -STORJ_ACCESS_KEY=your-test-key -STORJ_SECRET_KEY=your-test-secret -STORJ_BUCKET_NAME=test-bucket - -# AWS S3 (тСстовыС ΠΊΠ»ΡŽΡ‡ΠΈ) -AWS_ACCESS_KEY=your-test-aws-key -AWS_SECRET_KEY=your-test-aws-secret - -# Server -PORT=8080 -RUST_LOG=debug -``` - -### Запуск Redis - -#### Π›ΠΎΠΊΠ°Π»ΡŒΠ½ΠΎ -```bash -# Ubuntu/Debian -sudo apt-get install redis-server -sudo systemctl start redis-server - -# macOS -brew install redis -brew services start redis - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° -redis-cli ping -``` - -#### Docker -```bash -docker run -d \ - --name redis-dev \ - -p 6379:6379 \ - redis:7-alpine -``` - -### Запуск прилоТСния - -```bash -# ΠžΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ запуск -cargo run - -# Π‘ автоматичСской пСрСсборкой -cargo watch -x run - -# Π’ Ρ€Π΅ΠΆΠΈΠΌΠ΅ ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ -RUST_LOG=debug cargo run - -# Π‘ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ -cargo run --release -``` - -### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° работоспособности - -```bash -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сСрвСра -curl http://localhost:8080/ - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Ρ„Π°ΠΉΠ»Π° (Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ Ρ‚ΠΎΠΊΠ΅Π½) -curl -X POST http://localhost:8080/ \ - -H "Authorization: Bearer your-token" \ - -F "file=@test-image.jpg" -``` - -## ВСстированиС - -### Unit тСсты - -```bash -# Запуск всСх тСстов -cargo test - -# Запуск тСстов с Π²Ρ‹Π²ΠΎΠ΄ΠΎΠΌ -cargo test -- --nocapture - -# Запуск ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠ³ΠΎ тСста -cargo test test_upload_file - -# Запуск тСстов Π² ΠΏΠ°Ρ€Π°Π»Π»Π΅Π»ΡŒΠ½ΠΎΠΌ Ρ€Π΅ΠΆΠΈΠΌΠ΅ -cargo test -- --test-threads=4 -``` - -### Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» `tests/integration_test.rs`: - -```rust -use actix_web::{test, web, App}; -use quoter::app_state::AppState; - -#[actix_web::test] -async fn test_upload_endpoint() { - let app_state = AppState::new().await; - let app = test::init_service( - App::new() - .app_data(web::Data::new(app_state)) - .route("/", web::post().to(upload_handler)) - ).await; - - let req = test::TestRequest::post() - .uri("/") - .insert_header(("Authorization", "Bearer test-token")) - .set_form(("file", "test-data")) - .to_request(); - - let resp = test::call_service(&app, req).await; - assert!(resp.status().is_success()); -} -``` - -### ВСстированиС ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ - -```bash -# Π‘Π΅Π½Ρ‡ΠΌΠ°Ρ€ΠΊΠΈ (Ссли настроСны) -cargo bench - -# ΠŸΡ€ΠΎΡ„ΠΈΠ»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ с flamegraph -cargo install flamegraph -cargo flamegraph --bin quoter -``` - -## ΠžΡ‚Π»Π°Π΄ΠΊΠ° - -### Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -```rust -use log::{debug, info, warn, error}; - -// Π’ ΠΊΠΎΠ΄Π΅ -debug!("Processing file: {}", filename); -info!("File uploaded successfully"); -warn!("Author quota is getting low: {} bytes", quota); -error!("Failed to upload file: {}", e); -``` - -### ΠžΡ‚Π»Π°Π΄ΠΊΠ° с GDB - -```bash -# ΠšΠΎΠΌΠΏΠΈΠ»ΡΡ†ΠΈΡ с ΠΎΡ‚Π»Π°Π΄ΠΎΡ‡Π½ΠΎΠΉ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠ΅ΠΉ -cargo build - -# Запуск с GDB -gdb target/debug/quoter - -# Π’ GDB -(gdb) break main -(gdb) run -(gdb) continue -``` - -### ΠžΡ‚Π»Π°Π΄ΠΊΠ° с LLDB (macOS) - -```bash -lldb target/debug/quoter -(lldb) breakpoint set --name main -(lldb) run -``` - -## ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠΎΠ΄Π° - -### Clippy - -```bash -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° стиля ΠΊΠΎΠ΄Π° -cargo clippy - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° с Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ прСдупрСТдСниями -cargo clippy -- -D warnings - -# АвтоматичСскоС исправлСниС -cargo clippy --fix -``` - -### Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -```bash -# Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° -cargo fmt - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° форматирования -cargo fmt -- --check -``` - -### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° бСзопасности - -```bash -# Аудит зависимостСй -cargo audit - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° уязвимостСй -cargo audit --deny warnings -``` - -## ΠŸΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ ΠΊΠΎΠ΄Π° - -### Tarpaulin - -```bash -# Установка -cargo install cargo-tarpaulin - -# Запуск -cargo tarpaulin - -# Π‘ HTML ΠΎΡ‚Ρ‡Π΅Ρ‚ΠΎΠΌ -cargo tarpaulin --out Html -``` - -### grcov - -```bash -# Установка -cargo install grcov - -# Настройка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… -export CARGO_INCREMENTAL=0 -export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" -export RUSTDOCFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" - -# Запуск тСстов -cargo test - -# ГСнСрация ΠΎΡ‚Ρ‡Π΅Ρ‚Π° -grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./coverage/ -``` - -## Git workflow - -### Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ feature branch - -```bash -# Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠΉ Π²Π΅Ρ‚ΠΊΠΈ -git checkout -b feature/new-feature - -# ВнСсСниС ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -# ... - -# ΠšΠΎΠΌΠΌΠΈΡ‚ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -git add . -git commit -m "feat: add new feature" - -# Push Π² Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ -git push origin feature/new-feature -``` - -### Commit conventions - -Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ [Conventional Commits](https://www.conventionalcommits.org/): - -- `feat:` - новая Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ -- `fix:` - исправлСниС Π±Π°Π³ΠΎΠ² -- `docs:` - измСнСния Π² Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ -- `style:` - Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° -- `refactor:` - Ρ€Π΅Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΊΠΎΠ΄Π° -- `test:` - Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ тСстов -- `chore:` - ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ зависимостСй, ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ - -### Pull Request - -1. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Pull Request Π² GitHub/GitLab -2. Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ описаниС ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ -3. Π£ΠΊΠ°ΠΆΠΈΡ‚Π΅ связанныС issues -4. Π”ΠΎΠΆΠ΄ΠΈΡ‚Π΅ΡΡŒ code review -5. Π˜ΡΠΏΡ€Π°Π²ΡŒΡ‚Π΅ замСчания Ссли Π΅ΡΡ‚ΡŒ -6. ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚Π΅ approval ΠΈ merge - -## ΠŸΠΎΠ»Π΅Π·Π½Ρ‹Π΅ ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ - -### Cargo - -```bash -# ОбновлСниС зависимостСй -cargo update - -# ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° сборки -cargo clean - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° зависимостСй -cargo tree - -# ДокумСнтация -cargo doc --open - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΈΠΏΠΎΠ² Π±Π΅Π· компиляции -cargo check -``` - -### ΠžΡ‚Π»Π°Π΄ΠΊΠ° - -```bash -# ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€ Π»ΠΎΠ³ΠΎΠ² Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ -tail -f logs/quoter.log - -# ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ рСсурсов -htop -iotop - -# Π‘Π΅Ρ‚Π΅Π²Ρ‹Π΅ соСдинСния -netstat -tulpn | grep 8080 -``` - -### Docker - -```bash -# Π‘Π±ΠΎΡ€ΠΊΠ° для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ -docker build -t quoter:dev . - -# Запуск с volume для hot reload -docker run -v $(pwd):/app -p 8080:8080 quoter:dev - -# ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€ Π»ΠΎΠ³ΠΎΠ² ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° -docker logs -f quoter-container -``` - -## Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ - -### ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ - -1. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `cargo build --release` для production -2. НастройтС ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для критичСских участков -3. ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΡŒΡ‚Π΅ использованиС памяти ΠΈ CPU -4. ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ - -### Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - -1. РСгулярно обновляйтС зависимости -2. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `cargo audit` для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ уязвимостСй -3. НС Ρ…Ρ€Π°Π½ΠΈΡ‚Π΅ сСкрСты Π² ΠΊΠΎΠ΄Π΅ -4. Π’Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠΉΡ‚Π΅ всС Π²Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ - -### ΠšΠ°Ρ‡Π΅ΡΡ‚Π²ΠΎ ΠΊΠΎΠ΄Π° - -1. ΠŸΠΈΡˆΠΈΡ‚Π΅ тСсты для Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ -2. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `cargo clippy` для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ стиля -3. Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ API -4. Π‘Π»Π΅Π΄ΡƒΠΉΡ‚Π΅ ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ°ΠΌ SOLID \ No newline at end of file diff --git a/docs/features.md b/docs/features.md index dc45021..b53f201 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,77 +1,48 @@ -# Π€ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° Quoter +# Quoter Features -## ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ возмоТности +Simple file upload/download proxy with user quotas and S3 storage. -### πŸ–ΌοΈ ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ -- Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΈ Ρ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ -- ГСнСрация thumbnail'ΠΎΠ² Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² -- ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΎΠ²: JPG, PNG, GIF, WebP, HEIC, TIFF -- АвтоматичСскоС ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ +## What Quoter Does -### πŸ” АутСнтификация ΠΈ авторизация -- БистСма Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ -- Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ (5GB Π½Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ) -- ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΡ€Π°Π² доступа ΠΊ Ρ„Π°ΠΉΠ»Π°ΠΌ +### πŸ“€ File Upload +- **Multipart uploads** to S3/Storj storage +- **User quotas** (5GB default per user) +- **JWT authentication** with session management +- **MIME type detection** from file content +- **Rate limiting** to prevent abuse -### πŸ“ Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ -- Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² Ρ‡Π΅Ρ€Π΅Π· multipart form data -- Π₯Ρ€Π°Π½Π΅Π½ΠΈΠ΅ Π² S3-совмСстимых Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°Ρ… -- Поиск Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΏΠΎ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Π°ΠΌ -- ΠšΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ списков Ρ„Π°ΠΉΠ»ΠΎΠ² +### πŸ“ File Storage +- **S3-compatible storage** (Storj primary, AWS fallback) +- **Redis caching** for file metadata and quotas +- **Multi-cloud support** with automatic migration -### 🌐 HTTP API -- RESTful endpoints для всСх ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ -- ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° CORS для Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ -- ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок с Π΄Π΅Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ сообщСниями -- ΠŸΡ€ΠΎΠΊΡΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ запросов ΠΊ Ρ„Π°ΠΉΠ»Π°ΠΌ +### 🌐 File Serving +- **Direct file access** via filename +- **Fast response** optimized for Vercel Edge caching +- **CORS whitelist** for secure access +- **Direct file serving** optimized for CDN caching -### πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ -- Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Sentry для отслСТивания ошибок -- Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ всСх ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ -- ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ +## πŸš€ Modern Architecture -## ВСхничСскиС особСнности +**Quoter**: Simple file upload/download + S3 storage +**Vercel**: Smart thumbnails + optimization + global CDN -### πŸ§ͺ ВСстированиС -- ПолноС ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ unit тСстами (36 тСстов) -- Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты для всСх ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² -- Моки для Π²Π½Π΅ΡˆΠ½ΠΈΡ… зависимостСй -- ВСсты ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ +πŸ’‹ **Ultra-simple**: Quoter just handles raw files. That's it. +πŸ’‹ **Simplified**: Focus on what each service does best. -### πŸš€ Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ -- Docker контСйнСризация -- Автоматизированный CI/CD ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ -- ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠΉ -- ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅ΠΌΠ°Ρ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° +## Technical Stack -### πŸ”§ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ -- Гибкая настройка Ρ‡Π΅Ρ€Π΅Π· ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния -- ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… S3 ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ² -- НастраиваСмыС ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΈ Π»ΠΈΠΌΠΈΡ‚Ρ‹ -- ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ CORS ΠΏΠΎΠ»ΠΈΡ‚ΠΈΠΊ +- **Backend**: Rust + Actix Web +- **Storage**: Redis (metadata) + S3/Storj (files) +- **Auth**: JWT tokens +- **Tests**: 36 passing tests with full coverage -## АрхитСктура +## Status -### ΠœΠΎΠ΄ΡƒΠ»ΠΈ -- `core.rs` - основная бизнСс-Π»ΠΎΠ³ΠΈΠΊΠ° ΠΈ GraphQL API -- `auth.rs` - аутСнтификация ΠΈ ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ -- `handlers/` - HTTP ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ запросов -- `thumbnail.rs` - гСнСрация thumbnail'ΠΎΠ² -- `s3_utils.rs` - Ρ€Π°Π±ΠΎΡ‚Π° с S3-совмСстимыми Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°ΠΌΠΈ -- `lookup.rs` - поиск ΠΈ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ Ρ‚ΠΈΠΏΠΎΠ² Ρ„Π°ΠΉΠ»ΠΎΠ² -- `overlay.rs` - Π½Π°Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ водяных Π·Π½Π°ΠΊΠΎΠ² ΠΈ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ… - -### Зависимости -- Actix Web для HTTP сСрвСра -- Redis для ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ -- AWS SDK для S3 ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ -- Image crate для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ -- Sentry для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° - -## Бтатус Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ - -- βœ… Основная Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π° -- βœ… ПолноС ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ тСстами -- βœ… CI/CD ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ настроСн -- βœ… ДокумСнтация ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½Π° -- πŸš€ Π“ΠΎΡ‚ΠΎΠ² ΠΊ ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ½ дСплою +- βœ… Upload API with quotas +- βœ… Static file server +- βœ… S3 storage integration +- βœ… JWT authentication +- βœ… Rate limiting & security +- βœ… Full test coverage +- πŸš€ Production ready diff --git a/docs/hybrid-architecture.md b/docs/hybrid-architecture.md index 9ffbb89..b979e3a 100644 --- a/docs/hybrid-architecture.md +++ b/docs/hybrid-architecture.md @@ -7,7 +7,7 @@ ``` πŸ“€ Upload: Quoter (ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ + ΠΊΠ²ΠΎΡ‚Ρ‹) πŸ“₯ Download: Vercel Edge API (ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ) -🎨 OG: @vercel/og (динамичСская гСнСрация) +🎨 Thumbnails: Vercel /api/thumb/[width]/[...path] (динамичСская гСнСрация) ``` ## βœ… ΠŸΡ€Π΅ΠΈΠΌΡƒΡ‰Π΅ΡΡ‚Π²Π° Π³ΠΈΠ±Ρ€ΠΈΠ΄Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄Π° diff --git a/docs/monitoring.md b/docs/monitoring.md deleted file mode 100644 index d80a555..0000000 --- a/docs/monitoring.md +++ /dev/null @@ -1,341 +0,0 @@ -# ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ - -## ΠžΠ±Π·ΠΎΡ€ - -ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ Quoter Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Π² сСбя Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅, ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ ΠΈ отслСТиваниС состояния систСмы. - -## Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - -### Π£Ρ€ΠΎΠ²Π½ΠΈ логирования - -Quoter ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ `log` с Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹ΠΌΠΈ уровнями: - -- **error** - ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠΈΠ΅ ошибки, Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΠ΅ Π½Π΅ΠΌΠ΅Π΄Π»Π΅Π½Π½ΠΎΠ³ΠΎ внимания -- **warn** - ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΡ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΠΎΠ³ΡƒΡ‚ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π½Π° ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ -- **info** - Π˜Π½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ сообщСния ΠΎ Π½ΠΎΡ€ΠΌΠ°Π»ΡŒΠ½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Π΅ -- **debug** - ΠžΡ‚Π»Π°Π΄ΠΎΡ‡Π½Π°Ρ информация для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² -- **trace** - Максимальная дСтализация для Π³Π»ΡƒΠ±ΠΎΠΊΠΎΠΉ ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ - -### Настройка логирования - -```bash -# Волько ошибки -RUST_LOG=error cargo run - -# ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΡ ΠΈ ошибки -RUST_LOG=warn cargo run - -# Π˜Π½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ сообщСния (рСкомСндуСтся для ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Π°) -RUST_LOG=info cargo run - -# ΠžΡ‚Π»Π°Π΄ΠΊΠ° -RUST_LOG=debug cargo run - -# Максимальная дСтализация -RUST_LOG=trace cargo run -``` - -### Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° Π»ΠΎΠ³ΠΎΠ² - -#### Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»Π° -``` -[INFO] Started -[WARN] file abc123.jpg uploaded to storj, incrementing quota by 1048576 bytes -[WARN] New quota for user user123: 2097152 bytes -``` - -#### ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π° -``` -[WARN] >>> GET image_300.jpg [START] -[WARN] detected file extension: jpg -[WARN] base_filename: image -[WARN] requested width: 300 -[WARN] Found stored path in DB: production/image/image.jpg -[WARN] File exists in Storj: production/image/image.jpg -[WARN] Processing image file with width: 300 -[WARN] Calculated closest width: 300 for requested: 300 -[WARN] serve existed thumb file: image_300.jpg -``` - -#### Ошибки -``` -[ERROR] Failed to upload to Storj: image.jpg - Error: Network error -[ERROR] Database error while getting path: image.jpg - Full error: Connection timeout -[ERROR] unsupported file format -``` - -## ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ - -### ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° - -#### ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ -- **Requests per second (RPS)** - количСство запросов Π² сСкунду -- **Response time** - врСмя ΠΎΡ‚Π²Π΅Ρ‚Π° API -- **Error rate** - ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ ошибок -- **Upload success rate** - ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ ΡƒΡΠΏΠ΅ΡˆΠ½Ρ‹Ρ… Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ - -#### РСсурсы -- **Memory usage** - использованиС памяти -- **CPU usage** - использованиС процСссора -- **Disk I/O** - ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ с диском -- **Network I/O** - сСтСвой Ρ‚Ρ€Π°Ρ„ΠΈΠΊ - -#### БизнСс-ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ -- **Files uploaded** - количСство Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ² -- **Quota usage** - использованиС ΠΊΠ²ΠΎΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ -- **Thumbnail generation** - количСство сгСнСрированных ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ -- **Storage usage** - использованиС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π° - -### Prometheus ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ - -Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ Π² `Cargo.toml`: - -```toml -[dependencies] -prometheus = "0.13" -actix-web-prom = "0.6" -``` - -НастройтС Π² `main.rs`: - -```rust -use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder}; - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let prometheus = PrometheusMetricsBuilder::new("quoter") - .endpoint("/metrics") - .build() - .unwrap(); - - HttpServer::new(move || { - App::new() - .wrap(prometheus.clone()) - // ... ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ настройки - }) - .bind(addr)? - .run() - .await -} -``` - -### ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ - -```rust -use prometheus::{Counter, Histogram, Registry}; - -lazy_static! { - pub static ref UPLOAD_COUNTER: Counter = Counter::new( - "quoter_uploads_total", - "Total number of file uploads" - ).unwrap(); - - pub static ref UPLOAD_SIZE: Histogram = Histogram::new( - "quoter_upload_size_bytes", - "File upload size in bytes" - ).unwrap(); - - pub static ref QUOTA_USAGE: Histogram = Histogram::new( - "quoter_quota_usage_bytes", - "Author quota usage in bytes" - ).unwrap(); -} -``` - -## АлСрты - -### ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠΈΠ΅ Π°Π»Π΅Ρ€Ρ‚Ρ‹ - -- **Service down** - сСрвис нСдоступСн -- **High error rate** - высокий ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ ошибок (>5%) -- **High response time** - ΠΌΠ΅Π΄Π»Π΅Π½Π½Ρ‹Π΅ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ (>2s) -- **Memory usage high** - высокоС использованиС памяти (>80%) -- **Redis connection failed** - потСря соСдинСния с Redis - -### ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΡ - -- **Quota usage high** - ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ ΠΏΡ€ΠΈΠ±Π»ΠΈΠΆΠ°ΡŽΡ‚ΡΡ ΠΊ Π»ΠΈΠΌΠΈΡ‚Ρƒ ΠΊΠ²ΠΎΡ‚Ρ‹ -- **Storage usage high** - высокоС использованиС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π° -- **Thumbnail generation slow** - мСдлСнная гСнСрация ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ - -### Настройка Π°Π»Π΅Ρ€Ρ‚ΠΎΠ² Π² Prometheus - -```yaml -groups: -- name: quoter - rules: - - alert: QuoterServiceDown - expr: up{job="quoter"} == 0 - for: 1m - labels: - severity: critical - annotations: - summary: "Quoter service is down" - - - alert: HighErrorRate - expr: rate(http_requests_total{job="quoter",status=~"5.."}[5m]) > 0.05 - for: 2m - labels: - severity: warning - annotations: - summary: "High error rate detected" - - - alert: HighResponseTime - expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="quoter"}[5m])) > 2 - for: 5m - labels: - severity: warning - annotations: - summary: "High response time detected" -``` - -## Π”Π°ΡˆΠ±ΠΎΡ€Π΄Ρ‹ - -### Grafana Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ - -Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ с панСлями: - -#### ΠžΠ±Π·ΠΎΡ€ систСмы -- Бтатус сСрвиса (up/down) -- ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ запросов Π² сСкунду -- ВрСмя ΠΎΡ‚Π²Π΅Ρ‚Π° (p50, p95, p99) -- ΠŸΡ€ΠΎΡ†Π΅Π½Ρ‚ ошибок - -#### Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² -- ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ Π² час/дСнь -- Π Π°Π·ΠΌΠ΅Ρ€ Π·Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌΡ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ² -- Π£ΡΠΏΠ΅ΡˆΠ½ΠΎΡΡ‚ΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ -- ИспользованиС ΠΊΠ²ΠΎΡ‚ - -#### РСсурсы -- ИспользованиС CPU ΠΈ памяти -- Π‘Π΅Ρ‚Π΅Π²ΠΎΠΉ Ρ‚Ρ€Π°Ρ„ΠΈΠΊ -- ΠžΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ с диском -- БоСдинСния с Redis - -#### БизнСс-ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ -- Π’ΠΎΠΏ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΏΠΎ использованию ΠΊΠ²ΠΎΡ‚Ρ‹ -- ΠŸΠΎΠΏΡƒΠ»ΡΡ€Π½Ρ‹Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρ‹ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ -- ИспользованиС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π° ΠΏΠΎ Ρ‚ΠΈΠΏΠ°ΠΌ Ρ„Π°ΠΉΠ»ΠΎΠ² - -### ΠŸΡ€ΠΈΠΌΠ΅Ρ€ запроса для Grafana - -```sql --- ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ ΠΏΠΎ часам -SELECT - time_bucket('1 hour', timestamp) AS time, - COUNT(*) as uploads -FROM quoter_uploads -WHERE timestamp > NOW() - INTERVAL '24 hours' -GROUP BY time -ORDER BY time; -``` - -## Π—Π΄ΠΎΡ€ΠΎΠ²ΡŒΠ΅ систСмы - -### Health check endpoint - -Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ endpoint для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π·Π΄ΠΎΡ€ΠΎΠ²ΡŒΡ: - -```rust -async fn health_check() -> HttpResponse { - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Redis - let redis_ok = check_redis_connection().await; - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° S3 - let s3_ok = check_s3_connection().await; - - if redis_ok && s3_ok { - HttpResponse::Ok().json(json!({ - "status": "healthy", - "timestamp": chrono::Utc::now(), - "services": { - "redis": "ok", - "s3": "ok" - } - })) - } else { - HttpResponse::ServiceUnavailable().json(json!({ - "status": "unhealthy", - "timestamp": chrono::Utc::now(), - "services": { - "redis": if redis_ok { "ok" } else { "error" }, - "s3": if s3_ok { "ok" } else { "error" } - } - })) - } -} -``` - -### Kubernetes liveness/readiness probes - -```yaml -livenessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - -readinessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 -``` - -## Врассировка - -### Distributed tracing - -Для отслСТивания запросов Ρ‡Π΅Ρ€Π΅Π· микросСрвисы: - -```toml -[dependencies] -opentelemetry = "0.20" -tracing = "0.1" -tracing-opentelemetry = "0.20" -``` - -### Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ с контСкстом - -```rust -use tracing::{info, warn, error, instrument}; - -#[instrument(skip(state))] -async fn upload_handler( - req: HttpRequest, - payload: Multipart, - state: web::Data, -) -> Result { - let user_id = get_user_id(&req).await?; - info!(user_id = %user_id, "Starting file upload"); - - // ... Π»ΠΎΠ³ΠΈΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ - - info!(user_id = %user_id, filename = %filename, "File uploaded successfully"); - Ok(response) -} -``` - -## Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ - -### ΠŸΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½ - -1. **Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅**: Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ структурированныС Π»ΠΎΠ³ΠΈ Π² JSON Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ -2. **ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ**: НастройтС Prometheus + Grafana -3. **АлСрты**: НастройтС увСдомлСния для критичСских событий -4. **Ротация Π»ΠΎΠ³ΠΎΠ²**: НастройтС logrotate ΠΈΠ»ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΡƒ Π² ELK stack -5. **ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ рСсурсов**: ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°ΠΉΡ‚Π΅ CPU, ΠΏΠ°ΠΌΡΡ‚ΡŒ, диск, ΡΠ΅Ρ‚ΡŒ - -### Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° - -1. **Π›ΠΎΠΊΠ°Π»ΡŒΠ½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅**: Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ `RUST_LOG=debug` -2. **ΠžΡ‚Π»Π°Π΄ΠΊΠ°**: Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚Π΅ trace Π»ΠΎΠ³ΠΈ для Π΄Π΅Ρ‚Π°Π»ΡŒΠ½ΠΎΠΉ ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ -3. **ВСстированиС**: Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ тСсты для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊ -4. **ДокумСнтация**: Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ всС кастомныС ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ \ No newline at end of file diff --git a/docs/security.md b/docs/security.md deleted file mode 100644 index 28e446f..0000000 --- a/docs/security.md +++ /dev/null @@ -1,176 +0,0 @@ -# πŸ”’ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ DDoS - -## ΠžΠ±Π·ΠΎΡ€ - -БистСма quoter Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΠΌΠ½ΠΎΠ³ΠΎΡƒΡ€ΠΎΠ²Π½Π΅Π²ΡƒΡŽ Π·Π°Ρ‰ΠΈΡ‚Ρƒ ΠΎΡ‚ Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… Ρ‚ΠΈΠΏΠΎΠ² Π°Ρ‚Π°ΠΊ, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ DDoS, брутфорс ΠΈ ΡΠΊΡΠΏΠ»ΡƒΠ°Ρ‚Π°Ρ†ΠΈΡŽ уязвимостСй. - -## πŸ›‘οΈ Π£Ρ€ΠΎΠ²Π½ΠΈ Π·Π°Ρ‰ΠΈΡ‚Ρ‹ - -### 1. Π‘Π΅Ρ‚Π΅Π²ΠΎΠΉ ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ (HTTP Server) - -#### ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° запросов -- **ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ payload**: 500 ΠœΠ‘ -- **ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ JSON**: 1 ΠœΠ‘ -- **Π’Π°ΠΉΠΌΠ°ΡƒΡ‚ соСдинСния**: настраиваСтся Ρ‡Π΅Ρ€Π΅Π· Actix-web - -#### Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ бСзопасности -```http -X-Content-Type-Options: nosniff -X-Frame-Options: DENY -X-XSS-Protection: 1; mode=block -Referrer-Policy: strict-origin-when-cross-origin -Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; object-src 'none'; -Strict-Transport-Security: max-age=31536000; includeSubDomains -``` - -#### CORS Policy -- **Π Π°Π·Ρ€Π΅ΡˆΠ΅Π½Π½Ρ‹Π΅ Π΄ΠΎΠΌΠ΅Π½Ρ‹**: `discours.io`, `new.discours.io`, `localhost:3000` -- **Π Π°Π·Ρ€Π΅ΡˆΠ΅Π½Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹**: GET, POST, OPTIONS -- **ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½Π½Ρ‹Π΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ**: Content-Type, Authorization, If-None-Match, Cache-Control - -### 2. Rate Limiting (Π›ΠΈΠΌΠΈΡ‚Ρ‹ запросов) - -#### ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ - -| Π’ΠΈΠΏ endpoint | Макс. запросов | Окно Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ | Π‘Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° | -|--------------|----------------|--------------|------------| -| ΠžΠ±Ρ‰ΠΈΠ΅ запросы | 100 | 60 сСк | 5 ΠΌΠΈΠ½ | -| Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² | 10 | 300 сСк | 10 ΠΌΠΈΠ½ | -| АутСнтификация | 20 | 900 сСк | 30 ΠΌΠΈΠ½ | - -#### ΠœΠ΅Ρ…Π°Π½ΠΈΠ·ΠΌ Ρ€Π°Π±ΠΎΡ‚Ρ‹ -1. **IP-based tracking**: ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎ IP (ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ X-Forwarded-For, X-Real-IP) -2. **Redis storage**: Π₯Ρ€Π°Π½Π΅Π½ΠΈΠ΅ счСтчиков Π² Redis с TTL -3. **Local cache**: Быстрый Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш для частых ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ -4. **Progressive blocking**: Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠ΅ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ ΠΏΡ€ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Ρ… Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΡΡ… - -### 3. Валидация запросов - -#### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ бСзопасности -- **Π”Π»ΠΈΠ½Π° ΠΏΡƒΡ‚ΠΈ**: максимум 1000 символов -- **ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ²**: максимум 50 -- **Π”Π»ΠΈΠ½Π° Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ²**: максимум 8192 символа -- **ΠŸΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ символы**: Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ `..`, `\0`, `\r`, `\n` - -#### ДСтСкция Π°Ρ‚Π°ΠΊ -```rust -// ΠŸΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ -let suspicious_patterns = [ - "/admin", "/wp-admin", "/phpmyadmin", "/.env", "/config", - "/.git", "/backup", "/db", "/sql", - "script>", " -``` - ---- - -## πŸ“€ Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² (УЛУЧШЕННАЯ Π’Π•Π Π‘Π˜Π―) - -### POST / - -Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ Ρ„Π°ΠΉΠ»(Ρ‹) Π² S3-совмСстимоС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅ (Storj) с ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½Π½ΠΎΠΉ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ ΠΊΠ²ΠΎΡ‚ ΠΈ Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠ΅ΠΉ. - -#### Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ запроса -```http -Authorization: Bearer -Content-Type: multipart/form-data -``` - -#### ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ -- **file** (required) - Ρ„Π°ΠΉΠ»(Ρ‹) для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π² multipart/form-data - -#### ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ -АвтоматичСскоС ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ MIME-Ρ‚ΠΈΠΏΠ° ΠΈΠ· содСрТимого Ρ„Π°ΠΉΠ»Π°: -- **Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΡ**: JPEG, PNG, GIF, WebP, HEIC -- **Π’ΠΈΠ΄Π΅ΠΎ**: MP4, WebM, AVI -- **Аудио**: MP3, WAV, OGG -- **Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Ρ‹**: PDF - -#### πŸ”„ Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π½Π°Ρ Π»ΠΎΠ³ΠΈΠΊΠ° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ - -1. **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ** - ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΠΈ валидация JWT Ρ‚ΠΎΠΊΠ΅Π½Π° -2. **ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ ΠΊΠ²ΠΎΡ‚Ρ‹** ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ· Redis -3. **ΠŸΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ²ΠΎΡ‚Ρ‹** - ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ достиг Π»ΠΈΠΌΠΈΡ‚Π° -4. **Streaming Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π°** с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°ΠΌΠΈ Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΌ chunk: - - ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π»ΠΈΠΌΠΈΡ‚Π° ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° (500 ΠœΠ‘) - - ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΎΠ±Ρ‰Π΅ΠΉ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ -5. **ΠŸΡ€ΠΎΠΏΡƒΡΠΊ пустых Ρ„Π°ΠΉΠ»ΠΎΠ²** -6. **ΠžΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ MIME-Ρ‚ΠΈΠΏΠ°** ΠΈΠ· содСрТимого (Π½Π΅ ΠΈΠ· Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ!) -7. **ГСнСрация UUID ΠΈΠΌΠ΅Π½ΠΈ** Ρ„Π°ΠΉΠ»Π° с ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½Ρ‹ΠΌ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ΠΌ -8. **Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Π² Storj S3** -9. **ОбновлСниС ΠΊΠ²ΠΎΡ‚Ρ‹** ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ -10. **Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ…** Π² Redis (с ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΎΠΉ ошибок) - -#### ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ - -- **Максимальная ΠΊΠ²ΠΎΡ‚Π° Π½Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ**: 5 Π“Π‘ (5,368,709,120 Π±Π°ΠΉΡ‚) -- **ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π°**: 500 ΠœΠ‘ (524,288,000 Π±Π°ΠΉΡ‚) -- **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ²ΠΎΡ‚Ρ‹ происходит Π²ΠΎ врСмя чтСния** (streaming) -- **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° мноТСствСнных Ρ„Π°ΠΉΠ»ΠΎΠ²** Π² ΠΎΠ΄Π½ΠΎΠΌ запросС - -#### Π£ΡΠΏΠ΅ΡˆΠ½Ρ‹Π΅ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ - -**Один Ρ„Π°ΠΉΠ»:** -```http -HTTP/1.1 200 OK -Content-Type: text/plain - -uuid-filename.ext -``` - -**НСсколько Ρ„Π°ΠΉΠ»ΠΎΠ²:** -```http -HTTP/1.1 200 OK -Content-Type: application/json - -{ - "uploaded_files": ["uuid1.jpg", "uuid2.png"], - "count": 2 -} -``` - -#### ΠšΠΎΠ΄Ρ‹ ошибок (Π˜Π‘ΠŸΠ ΠΠ’Π›Π•ΠΠΠ«Π•) - -| Код | УсловиС | ОписаниС | -|-----|---------|----------| -| **400 Bad Request** | НСт Ρ„Π°ΠΉΠ»ΠΎΠ² | `"No files provided or all files were empty"` | -| **401 Unauthorized** | ΠžΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΠ΅Ρ‚ Ρ‚ΠΎΠΊΠ΅Π½ | `"Authorization token required"` | -| **401 Unauthorized** | НСвСрный Ρ‚ΠΎΠΊΠ΅Π½ | `"Invalid authorization token"` | -| **413 Payload Too Large** | 🎯 ΠŸΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½Π° ΠΊΠ²ΠΎΡ‚Π° | `"Author quota limit exceeded"` | -| **413 Payload Too Large** | 🎯 Π‘ΠΎΠ»ΡŒΡˆΠΎΠΉ Ρ„Π°ΠΉΠ» | `"Single file size limit exceeded"` | -| **413 Payload Too Large** | 🎯 ΠŸΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½ΠΈΠ΅ ΠΏΡ€ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ | `"Author quota limit would be exceeded"` | -| **415 Unsupported Media Type** | НСподдСрТиваСмый MIME | `"Unsupported file format"` | -| **415 Unsupported Media Type** | НСт Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ для MIME | `"Unsupported content type"` | -| **500 Internal Server Error** | Ошибка S3 | `"File upload failed"` | -| **500 Internal Server Error** | Ошибка ΠΊΠ²ΠΎΡ‚Ρ‹ | `"Failed to update user quota"` | - -#### βœ… Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π½Ρ‹Π΅ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ - -1. **ΠŸΡ€Π°Π²ΠΈΠ»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠ΄ ошибки для ΠΊΠ²ΠΎΡ‚Ρ‹**: 413 Payload Too Large -2. **Efficient memory usage**: streaming с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°ΠΌΠΈ Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΌ chunk -3. **ΠŸΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ²ΠΎΡ‚Ρ‹** ΠΏΠ΅Ρ€Π΅Π΄ Π½Π°Ρ‡Π°Π»ΠΎΠΌ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ -4. **Π›ΠΈΠΌΠΈΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π°**: 500 ΠœΠ‘ -5. **Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π½Π°Ρ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок** с Π΄Π΅Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ сообщСниями -6. **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° мноТСствСнных Ρ„Π°ΠΉΠ»ΠΎΠ²** Π² ΠΎΠ΄Π½ΠΎΠΌ запросС -7. **Π”Π΅Ρ‚Π°Π»ΡŒΠ½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅** с ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ΠΎΠΌ использования ΠΊΠ²ΠΎΡ‚Ρ‹ - ---- - -#### πŸ”„ Как это Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ - -1. **JWT Π΄Π΅ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅** - извлСкаСтся `user_id` ΠΈΠ· Ρ‚ΠΎΠΊΠ΅Π½Π° -2. **Redis lookup** - ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ поиск сСссии ΠΏΠΎ ΠΊΠ»ΡŽΡ‡Ρƒ `session:{user_id}:{token}` -3. **Quota lookup** - ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎ ΠΊΠ»ΡŽΡ‡Ρƒ `quota:{user_id}` ΠΈΠ· Redis -4. **Activity update** - ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ `last_activity` timestamp (Ссли сСссия Π½Π°ΠΉΠ΄Π΅Π½Π°) -5. **Response building** - объСдинСниС Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈ ΠΊΠ²ΠΎΡ‚Ρ‹ - -#### Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ запроса -```http -Authorization: Bearer -``` - -#### Π£ΡΠΏΠ΅ΡˆΠ½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚ -```http -HTTP/1.1 200 OK -Content-Type: application/json - -{ - "user_id": "user_12345", - "username": "john_doe", - "token_type": "session", - "created_at": "1642248600", - "last_activity": "1642335000", - "auth_data": "{\"roles\": [\"user\"], \"permissions\": [...]}", - "device_info": "{\"platform\": \"web\", \"browser\": \"Chrome\"}", - "quota": { - "current_quota": 1073741824, - "max_quota": 5368709120, - "usage_percentage": 20.0 - } -} -``` - -#### Поля ΠΎΡ‚Π²Π΅Ρ‚Π° - -| ПолС | Π’ΠΈΠΏ | ОписаниС | -|------|-----|----------| -| `user_id` | string | Π£Π½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ | -| `username` | string \| null | Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ | -| `token_type` | string \| null | Π’ΠΈΠΏ Ρ‚ΠΎΠΊΠ΅Π½Π° (ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ "session") | -| `created_at` | string \| null | Unix timestamp создания сСссии | -| `last_activity` | string \| null | Unix timestamp послСднСй активности | -| `auth_data` | string \| null | JSON-строка с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ | -| `device_info` | string \| null | JSON-строка с ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠ΅ΠΉ ΠΎΠ± устройствС | -| `quota.current_quota` | number | Π’Π΅ΠΊΡƒΡ‰Π΅Π΅ использованиС ΠΊΠ²ΠΎΡ‚Ρ‹ Π² Π±Π°ΠΉΡ‚Π°Ρ… | -| `quota.max_quota` | number | Максимальная ΠΊΠ²ΠΎΡ‚Π° Π² Π±Π°ΠΉΡ‚Π°Ρ… | -| `quota.usage_percentage` | number | ΠŸΡ€ΠΎΡ†Π΅Π½Ρ‚ использования ΠΊΠ²ΠΎΡ‚Ρ‹ | - -#### ΠšΠΎΠ΄Ρ‹ ошибок - -| Код | УсловиС | ОписаниС | -|-----|---------|----------| -| **401 Unauthorized** | ΠžΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΠ΅Ρ‚ Ρ‚ΠΎΠΊΠ΅Π½ | `"Authorization token required"` | -| **401 Unauthorized** | НСвСрный JWT | `"Invalid or expired session token"` | -| **401 Unauthorized** | БСссия Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π° | `"Session not found or expired"` | - -#### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования - -```bash -# ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ -curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..." \ - http://localhost:8080/ -``` - ---- - -## πŸ“Š Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ - -### GET /quota - -ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΊΠ²ΠΎΡ‚Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. - -#### ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ запроса -``` -GET /quota?user_id= -``` - -#### ΠžΡ‚Π²Π΅Ρ‚ -```json -{ - "user_id": "user123", - "current_quota": 1073741824, - "max_quota": 5368709120 -} -``` - -### POST /quota/increase - -Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ ΠΊΠ²ΠΎΡ‚Ρƒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ (admin-only). - -#### Π’Π΅Π»ΠΎ запроса -```json -{ - "user_id": "user123", - "additional_bytes": 1073741824 -} -``` - -#### Валидация -- `additional_bytes` Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ > 0 -- ВрСбуСтся админский Ρ‚ΠΎΠΊΠ΅Π½ - -### POST /quota/set - -УстанавливаСт Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ (admin-only). - -#### Π’Π΅Π»ΠΎ запроса -```json -{ - "user_id": "user123", - "new_quota_bytes": 2147483648 -} -``` - ---- - -## πŸ” ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»ΠΎΠ² - -### GET /{filename} - -Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Ρ„Π°ΠΉΠ» ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ с Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹ΠΌΠΈ трансформациями. - -#### ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ URL -- `filename` - имя Ρ„Π°ΠΉΠ»Π° ΠΈΠ»ΠΈ имя_Ρ€Π°Π·ΠΌΠ΅Ρ€.Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ для ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ - -#### Query ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ -- `s=` - добавляСт ΠΎΠ²Π΅Ρ€Π»Π΅ΠΉ с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ shout (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ изобраТСния) - -#### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ -```bash -GET /uuid-file.jpg # ΠžΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» -GET /uuid-file_300.jpg # ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° 300px -GET /uuid-file_300.jpg/webp # ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° Π² WebP -GET /uuid-file.jpg?s=123 # Π‘ ΠΎΠ²Π΅Ρ€Π»Π΅Π΅ΠΌ shout -``` - ---- - -## πŸ§ͺ ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования - -### Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»Π° -```bash -curl -X POST http://localhost:8080/ \ - -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..." \ - -F "file=@photo.jpg" -``` - -**ΠžΡ‚Π²Π΅Ρ‚ ΠΏΡ€ΠΈ успСхС:** -``` -c4ca4238-a0b9-23f1-8429-81dc9bdb9a1f.jpg -``` - -**ΠžΡ‚Π²Π΅Ρ‚ ΠΏΡ€ΠΈ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½ΠΈΠΈ ΠΊΠ²ΠΎΡ‚Ρ‹:** -``` -HTTP/1.1 401 Unauthorized -Quota exceeded -``` - -### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ²ΠΎΡ‚Ρ‹ -```bash -curl "http://localhost:8080/quota?user_id=user123" \ - -H "Authorization: Bearer admin-token" -``` - -### Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ (admin) -```bash -curl -X POST http://localhost:8080/quota/increase \ - -H "Authorization: Bearer admin-token" \ - -H "Content-Type: application/json" \ - -d '{"user_id": "user123", "additional_bytes": 1073741824}' -``` - ---- - -## πŸ”§ Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ ΠΏΠΎ ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΡŽ - -1. **Π˜ΡΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΠΊΠΎΠ΄ ошибки ΠΊΠ²ΠΎΡ‚Ρ‹**: 401 β†’ 413 -2. **Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΏΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ Ρ€Π°Π·ΠΌΠ΅Ρ€Π°** ΠΈΠ· Content-Length -3. **Streaming Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°** вмСсто ΠΏΠΎΠ»Π½ΠΎΠ³ΠΎ чтСния Π² ΠΏΠ°ΠΌΡΡ‚ΡŒ -4. **Π›ΠΈΠΌΠΈΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π°** -5. **Π”Π΅Ρ‚Π°Π»ΡŒΠ½Π°Ρ валидация MIME-Ρ‚ΠΈΠΏΠΎΠ²** -6. **ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ использования ΠΊΠ²ΠΎΡ‚** - ---- - -*ДокумСнтация Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Π° для вСрсии ΠΊΠΎΠ΄Π° Π½Π° ΠΌΠΎΠΌΠ΅Π½Ρ‚ создания. Для ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ см. CHANGELOG.md.* diff --git a/docs/url-format.md b/docs/url-format.md deleted file mode 100644 index 92d26ee..0000000 --- a/docs/url-format.md +++ /dev/null @@ -1,204 +0,0 @@ -# πŸ“ Π€ΠΎΡ€ΠΌΠ°Ρ‚ URL для рСсайзСра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ - -## ΠžΠ±Π·ΠΎΡ€ - -Quoter ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ автоматичСскоС ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ Ρ‡Π΅Ρ€Π΅Π· URL ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹. БистСма автоматичСски Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ Π² ΠΏΡ€Π΅Π΄ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹Ρ… Ρ€Π°Π·ΠΌΠ΅Ρ€Π°Ρ… ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ блиТайший подходящий Ρ€Π°Π·ΠΌΠ΅Ρ€. - -## 🎯 ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρ‹ - -### ΠŸΡ€Π΅Π΄ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹Π΅ ΡˆΠΈΡ€ΠΈΠ½Ρ‹ -```rust -[10, 40, 110, 300, 600, 800, 1400] // пиксСлСй ΠΏΠΎ ΡˆΠΈΡ€ΠΈΠ½Π΅ -``` - -- **10px** - ΠΌΠΈΠΊΡ€ΠΎ-ΠΏΡ€Π΅Π²ΡŒΡŽ -- **40px** - Π°Π²Π°Ρ‚Π°Ρ€Ρ‹, ΠΈΠΊΠΎΠ½ΠΊΠΈ -- **110px** - малСнькиС ΠΏΡ€Π΅Π²ΡŒΡŽ -- **300px** - срСдниС ΠΏΡ€Π΅Π²ΡŒΡŽ -- **600px** - стандартныС изобраТСния -- **800px** - большиС изобраТСния -- **1400px** - ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ - -## πŸ“ Бинтаксис URL - -### 1. Π‘ΠΎΠ²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹ΠΉ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ (рСкомСндуСтся) -``` -GET /{filename}_{width}.{extension} -``` - -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹:** -```bash -# Запрос изобраТСния ΡˆΠΈΡ€ΠΈΠ½ΠΎΠΉ 300px -GET /439efaa0-816f-11ef-b201-439da98539bc_300.jpg - -# Запрос изобраТСния ΡˆΠΈΡ€ΠΈΠ½ΠΎΠΉ 600px -GET /5627e002-0c53-11ee-9565-0242ac110006_600.png - -# Запрос ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° (Π±Π΅Π· рСсайза) -GET /439efaa0-816f-11ef-b201-439da98539bc.jpg -``` - -### 2. Legacy Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ (поддСрТиваСтся) -``` -GET /unsafe/{width}x/production/image/{filename}.{extension} -``` - -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹:** -```bash -# Legacy Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ с ΡƒΠΊΠ°Π·Π°Π½ΠΈΠ΅ΠΌ ΡˆΠΈΡ€ΠΈΠ½Ρ‹ -GET /unsafe/1440x/production/image/439efaa0-816f-11ef-b201-439da98539bc.jpg - -# Legacy Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ Π±Π΅Π· рСсайза -GET /unsafe/production/image/5627e002-0c53-11ee-9565-0242ac110006.png -``` - -### 3. ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΡ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° -``` -GET /{filename}.{extension}/webp -``` - -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹:** -```bash -# ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΡ Π² WebP -GET /439efaa0-816f-11ef-b201-439da98539bc.jpg/webp - -# ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΡ с рСсайзом -GET /439efaa0-816f-11ef-b201-439da98539bc_600.jpg/webp -``` - -## πŸ”§ Π›ΠΎΠ³ΠΈΠΊΠ° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ - -### Алгоритм Π²Ρ‹Π±ΠΎΡ€Π° Ρ€Π°Π·ΠΌΠ΅Ρ€Π° -1. **Π’ΠΎΡ‡Π½ΠΎΠ΅ совпадСниС**: Если Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½Π°Ρ ΡˆΠΈΡ€ΠΈΠ½Π° Π΅ΡΡ‚ΡŒ Π² ΠΏΡ€Π΅Π΄ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹Ρ… Ρ€Π°Π·ΠΌΠ΅Ρ€Π°Ρ… -2. **Π‘Π»ΠΈΠΆΠ°ΠΉΡˆΠΈΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€**: ВыбираСтся Ρ€Π°Π·ΠΌΠ΅Ρ€ с минимальной Ρ€Π°Π·Π½ΠΎΡΡ‚ΡŒΡŽ -3. **ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Π»ΠΈΠΌΠΈΡ‚**: Если Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½Π°Ρ ΡˆΠΈΡ€ΠΈΠ½Π° > 1400px, возвращаСтся 1400px -4. **ΠžΡ€ΠΈΠ³ΠΈΠ½Π°Π»**: Если ΡˆΠΈΡ€ΠΈΠ½Π° Π½Π΅ ΡƒΠΊΠ°Π·Π°Π½Π° (0), возвращаСтся ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½ΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ - -### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ Π²Ρ‹Π±ΠΎΡ€Π° Ρ€Π°Π·ΠΌΠ΅Ρ€Π° -```bash -# Запрос 150px β†’ Π²Π΅Ρ€Π½Π΅Ρ‚ 110px (блиТайший мСньший) -# Запрос 250px β†’ Π²Π΅Ρ€Π½Π΅Ρ‚ 300px (блиТайший больший) -# Запрос 2000px β†’ Π²Π΅Ρ€Π½Π΅Ρ‚ 1400px (ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ) -# Запрос 299px β†’ Π²Π΅Ρ€Π½Π΅Ρ‚ 300px (блиТайший) -# Запрос 301px β†’ Π²Π΅Ρ€Π½Π΅Ρ‚ 300px (блиТайший) -``` - -### ГСнСрация ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ -- **Lazy generation**: ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ ΡΠΎΠ·Π΄Π°ΡŽΡ‚ΡΡ ΠΏΠΎ ΠΏΠ΅Ρ€Π²ΠΎΠΌΡƒ запросу -- **Асинхронная ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°**: ГСнСрация происходит Π² Ρ„ΠΎΠ½Π΅ -- **ΠšΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅**: Π‘ΠΎΠ·Π΄Π°Π½Π½Ρ‹Π΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ ΡΠΎΡ…Ρ€Π°Π½ΡΡŽΡ‚ΡΡ Π² S3 -- **Fallback**: ΠŸΡ€ΠΈ отсутствии ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ возвращаСтся ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π» - -## 🎨 ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ - -### Π’Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ -- **JPEG** (`.jpg`, `.jpeg`) -- **PNG** (`.png`) -- **GIF** (`.gif`) -- **WebP** (`.webp`) -- **HEIC** (`.heic`, `.heif`) - конвСртируСтся Π² JPEG -- **TIFF** (`.tiff`, `.tif`) - конвСртируСтся Π² JPEG - -### Π’Ρ‹Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ -- **БохраняСтся исходный Ρ„ΠΎΡ€ΠΌΠ°Ρ‚** (ΠΊΡ€ΠΎΠΌΠ΅ HEIC/TIFF β†’ JPEG) -- **WebP конвСрсия** Ρ‡Π΅Ρ€Π΅Π· `/webp` суффикс -- **АвтоматичСская оптимизация** для web - -## πŸš€ HTTP Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ - -### ΠšΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ -```http -ETag: "filename.ext" -Cache-Control: public, max-age=31536000, immutable -Access-Control-Allow-Origin: * -``` - -### УсловныС запросы -```http -# ΠšΠ»ΠΈΠ΅Π½Ρ‚ отправляСт -If-None-Match: "filename.ext" - -# Π‘Π΅Ρ€Π²Π΅Ρ€ ΠΎΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ (Ссли Π½Π΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ) -HTTP/1.1 304 Not Modified -``` - -## πŸ’‘ ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ - -### ΠšΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΠ°Ρ оптимизация -```html - -ОписаниС - - - - - ОписаниС - -``` - -### API использованиС -```javascript -// Ѐункция для получСния ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ URL -function getImageUrl(filename, maxWidth) { - const sizes = [10, 40, 110, 300, 600, 800, 1400]; - const optimalSize = sizes.find(size => size >= maxWidth) || 1400; - - const [name, ext] = filename.split('.'); - return `https://files.dscrs.site/${name}_${optimalSize}.${ext}`; -} - -// ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования -const thumbUrl = getImageUrl('image.jpg', 300); // image_300.jpg -const fullUrl = getImageUrl('image.jpg', 1200); // image_1400.jpg -``` - -## πŸ” ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ ΠΎΡ‚Π»Π°Π΄ΠΊΠ° - -### Π›ΠΎΠ³ΠΈ сСрвСра -```log -# УспСшная ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° -INFO GET image_300.jpg [START] -INFO Parsed request - base: image, width: 300, ext: jpg -INFO Cache hit for image.jpg, returning 304 - -# ГСнСрация ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ -WARN Thumbnail not found, generating: image_300.jpg -WARN generate new thumb files: image.jpg -INFO Generated thumbnail: image_300.jpg -``` - -### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‡Π΅Ρ€Π΅Π· API -```bash -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сущСствования Ρ„Π°ΠΉΠ»Π° -curl -I https://files.dscrs.site/image_300.jpg - -# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° с условным запросом -curl -H "If-None-Match: \"image.jpg\"" https://files.dscrs.site/image_300.jpg -``` - -## ⚠️ ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ ΠΈ Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ - -### Π›ΠΈΠΌΠΈΡ‚Ρ‹ -- **Максимальная ΡˆΠΈΡ€ΠΈΠ½Π°**: 1400px -- **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹**: см. список Π²Ρ‹ΡˆΠ΅ -- **Π Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π°**: Π΄ΠΎ 500MB для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ - -### Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ -1. **Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ WebP** для Π»ΡƒΡ‡ΡˆΠ΅Π³ΠΎ сТатия -2. **ΠšΡΡˆΠΈΡ€ΡƒΠΉΡ‚Π΅ Π½Π° CDN** для Π»ΡƒΡ‡ΡˆΠ΅ΠΉ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ -3. **Π£ΠΊΠ°Π·Ρ‹Π²Π°ΠΉΡ‚Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρ‹ Π·Π°Ρ€Π°Π½Π΅Π΅** для избСТания layout shift -4. **Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ lazy loading** для ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ Π²Π½Π΅ viewport - -### Troubleshooting -```bash -# Если ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π½Π΅ отобраТаСтся -1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ Ρ„Π°ΠΉΠ»Π° (поддСрТиваСтся Π»ΠΈ) -2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ запроса (Π½Π΅ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Π΅Ρ‚ Π»ΠΈ Π»ΠΈΠΌΠΈΡ‚Ρ‹) -3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ Π»ΠΎΠ³ΠΈ сСрвСра Π½Π° ошибки Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ -4. Π£Π±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ Π² коррСктности URL Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° -``` diff --git a/docs/vercel-og-quickstart.md b/docs/vercel-og-quickstart.md deleted file mode 100644 index e3e400c..0000000 --- a/docs/vercel-og-quickstart.md +++ /dev/null @@ -1,233 +0,0 @@ -# πŸš€ Quick Start: @vercel/og + Quoter - -## ⚑ 5-минутная настройка - -### 1. Установка зависимостСй - -```bash -npm install @vercel/og -``` - -### 2. Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ API endpoint - -```typescript -// pages/api/og.tsx (Next.js) -import { ImageResponse } from '@vercel/og' - -export const config = { runtime: 'edge' } - -export default function handler(req) { - const { searchParams } = new URL(req.url) - const title = searchParams.get('title') ?? 'Hello World' - - return new ImageResponse( - ( -
- {title} -
- ), - { width: 1200, height: 630 } - ) -} -``` - -### 3. Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Quoter - -```typescript -// utils/quoter.ts -export async function uploadToQuoter(imageBuffer: Buffer, filename: string, token: string) { - const formData = new FormData() - const blob = new Blob([imageBuffer], { type: 'image/png' }) - formData.append('file', blob, filename) - - const response = await fetch('https://quoter.staging.discours.io/', { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` }, - body: formData, - }) - - return response.text() // URL Ρ„Π°ΠΉΠ»Π° -} -``` - -### 4. ИспользованиС - -```typescript -// ГСнСрация OG изобраТСния -const ogResponse = await fetch('/api/og?title=My%20Amazing%20Post') -const imageBuffer = Buffer.from(await ogResponse.arrayBuffer()) - -// Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Π² Quoter -const quoterUrl = await uploadToQuoter(imageBuffer, 'og-image.png', userToken) - -// ИспользованиС Π² meta tags - -``` - -## 🎨 Π Π°ΡΡˆΠΈΡ€Π΅Π½Π½Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ с ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ΠΌ Ρ„ΠΎΠ½Π° - -```typescript -// pages/api/og-advanced.tsx -import { ImageResponse } from '@vercel/og' - -export default async function handler(req) { - const { searchParams } = new URL(req.url) - const title = searchParams.get('title') - const imageUrl = searchParams.get('image') // Quoter URL - - // Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΈΠ· Quoter - let backgroundImage = null - if (imageUrl) { - const imageResponse = await fetch(imageUrl) - const buffer = await imageResponse.arrayBuffer() - backgroundImage = `data:image/jpeg;base64,${Buffer.from(buffer).toString('base64')}` - } - - return new ImageResponse( - ( -
- {/* Π—Π°Ρ‚Π΅ΠΌΠ½Π΅Π½ΠΈΠ΅ для читаСмости */} -
- - {/* Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ */} -

- {title} -

-
- ), - { width: 1200, height: 630 } - ) -} -``` - -## πŸ“± React Hook для удобства - -```typescript -// hooks/useOgImage.ts -import { useState } from 'react' - -export function useOgImage() { - const [loading, setLoading] = useState(false) - - const generateAndUpload = async (title: string, backgroundImage?: string) => { - setLoading(true) - try { - // Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ OG ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ - const ogUrl = `/api/og?title=${encodeURIComponent(title)}${ - backgroundImage ? `&image=${encodeURIComponent(backgroundImage)}` : '' - }` - - const response = await fetch(ogUrl) - const buffer = await response.arrayBuffer() - - // Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ Π² Quoter - const formData = new FormData() - const blob = new Blob([buffer], { type: 'image/png' }) - formData.append('file', blob, `og-${Date.now()}.png`) - - const uploadResponse = await fetch('/api/upload-to-quoter', { - method: 'POST', - body: formData, - }) - - return await uploadResponse.text() - } finally { - setLoading(false) - } - } - - return { generateAndUpload, loading } -} - -// ИспользованиС Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ -function MyComponent() { - const { generateAndUpload, loading } = useOgImage() - - const handleCreateOg = async () => { - const quoterUrl = await generateAndUpload('My Post Title', '/existing-image.jpg') - console.log('OG image uploaded to:', quoterUrl) - } - - return ( - - ) -} -``` - -## ⚑ Production Tips - -### ΠšΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ -```typescript -// Cache OG images for 24 hours -export default function handler(req) { - const response = new ImageResponse(/* ... */) - - response.headers.set('Cache-Control', 'public, max-age=86400') - response.headers.set('CDN-Cache-Control', 'public, max-age=86400') - - return response -} -``` - -### Error Handling -```typescript -export default async function handler(req) { - try { - return new ImageResponse(/* ... */) - } catch (error) { - console.error('OG generation failed:', error) - - // Fallback ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ - return new Response('Failed to generate image', { status: 500 }) - } -} -``` - -### Environment Variables -```bash -# .env.local -QUOTER_API_URL=https://quoter.staging.discours.io -QUOTER_AUTH_TOKEN=your_jwt_token -NEXT_PUBLIC_OG_BASE_URL=https://yoursite.com/api/og -``` - -## πŸ”— ΠŸΠΎΠ»Π΅Π·Π½Ρ‹Π΅ ссылки - -- [Полная докумСнтация ΠΏΠΎ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ](./vercel-og-integration.md) -- [Quoter API Reference](./api-reference.md) -- [Vercel OG Official Docs](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) - ---- - -πŸ’‹ **Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅**: Один endpoint для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ, ΠΎΠ΄ΠΈΠ½ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ - ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ ΠΊΠΎΠ΄Π°, максимум Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°! diff --git a/docs/vercel-thumbnails.md b/docs/vercel-thumbnails.md new file mode 100644 index 0000000..a3a155d --- /dev/null +++ b/docs/vercel-thumbnails.md @@ -0,0 +1,363 @@ +# Vercel Thumbnail Generation Integration + +## 🎯 Overview + +**Quoter**: Dead simple file upload/download service. Just raw files. +**Vercel**: Smart thumbnail generation and optimization. + +Perfect separation of concerns! πŸ’‹ + +## πŸ”— URL Patterns for Vercel + +### Quoter File URLs +``` +https://quoter.discours.io/image.jpg β†’ Original file +https://quoter.discours.io/document.pdf β†’ Original file +``` + +### Vercel Thumbnail URLs (SolidJS) +``` +https://new.discours.io/api/thumb/300/image.jpg β†’ 300px width +https://new.discours.io/api/thumb/600/image.jpg β†’ 600px width +https://new.discours.io/api/thumb/1200/image.jpg β†’ 1200px width +``` + +## πŸ› οΈ Vercel Configuration + +### 1. SolidJS Start Config (app.config.ts) +```typescript +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig({ + server: { + preset: 'vercel', + }, + vite: { + define: { + 'process.env.QUOTER_URL': JSON.stringify('https://quoter.discours.io'), + }, + }, +}); +``` + +### 2. Thumbnail API Route (/api/thumb/[width]/[...path].ts) +```typescript +import { ImageResponse } from '@vercel/og'; +import type { APIRoute } from '@solidjs/start'; + +export const GET: APIRoute = async ({ params, request }) => { + const width = parseInt(params.width); + const imagePath = params.path.split('/').join('/'); + const quoterUrl = `https://quoter.discours.io/${imagePath}`; + + // Fetch original from Quoter + const response = await fetch(quoterUrl); + if (!response.ok) { + return new Response('Image not found', { status: 404 }); + } + + // Generate optimized thumbnail using @vercel/og + return new ImageResponse( + ( + + ), + { + width: width, + height: Math.round(width * 0.75), // 4:3 aspect ratio + } + ); +}; +``` + +## πŸ“‹ File Naming Conventions + +### Quoter Storage (No Width Patterns) +``` +βœ… image.jpg β†’ Clean filename +βœ… photo-2024.png β†’ kebab-case +βœ… user-avatar.webp β†’ descriptive names +βœ… document.pdf β†’ any file type + +❌ image_300.jpg β†’ No width patterns needed +❌ photo-thumbnail.jpg β†’ No thumbnail suffix +❌ userAvatar.png β†’ No camelCase +``` + +### URL Routing Examples +```bash +# Client requests thumbnail +GET /api/thumb/600/image.jpg + +# Vercel fetches original from Quoter +GET https://quoter.discours.io/image.jpg + +# Vercel generates and caches 600px thumbnail +β†’ Returns optimized image +``` + +## πŸš€ Benefits of This Architecture + +### For Quoter +- **Simple storage**: Just store original files +- **No processing**: Zero thumbnail generation load +- **Fast uploads**: Direct S3 storage without resizing +- **Predictable URLs**: Clean file paths + +### For Vercel +- **Edge optimization**: Global CDN caching +- **Dynamic sizing**: Any width on-demand +- **Smart caching**: Automatic cache invalidation +- **Format optimization**: WebP/AVIF when supported + +## πŸ”§ SolidJS Frontend Integration + +### 1. Install Dependencies +```bash +npm install @tanstack/solid-query @solidjs/start +``` + +### 2. Query Client Setup (app.tsx) +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'; +import { Router } from '@solidjs/router'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + }, + }, +}); + +export default function App() { + return ( + + + {/* Your app components */} + + + ); +} +``` + +### 3. File Upload Hook (hooks/useFileUpload.ts) +```tsx +import { createMutation, useQueryClient } from '@tanstack/solid-query'; + +interface UploadResponse { + url: string; + filename: string; +} + +export function useFileUpload() { + const queryClient = useQueryClient(); + + return createMutation(() => ({ + mutationFn: async (file: File): Promise => { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('https://quoter.discours.io/', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${getAuthToken()}`, + }, + body: formData, + }); + + if (!response.ok) { + throw new Error('Upload failed'); + } + + return response.json(); + }, + onSuccess: () => { + // Invalidate user quota query + queryClient.invalidateQueries({ queryKey: ['user'] }); + }, + })); +} +``` + +### 4. Image Component with Thumbnails (components/Image.tsx) +```tsx +import { createSignal, Show, Switch, Match } from 'solid-js'; + +interface ImageProps { + filename: string; + width?: number; + alt: string; + fallback?: boolean; +} + +export function Image(props: ImageProps) { + const [loadError, setLoadError] = createSignal(false); + const [loading, setLoading] = createSignal(true); + + const thumbnailUrl = () => + props.width + ? `https://new.discours.io/api/thumb/${props.width}/${props.filename}` + : `https://quoter.discours.io/${props.filename}`; + + const fallbackUrl = () => `https://quoter.discours.io/${props.filename}`; + + return ( + + +
+ + + {props.alt} setLoading(false)} + onError={() => { + setLoading(false); + setLoadError(true); + }} + /> + + + {props.alt} setLoading(false)} + /> + + + ); +} +``` + +### 5. User Quota Component (components/UserQuota.tsx) +```tsx +import { createQuery } from '@tanstack/solid-query'; +import { Show, Switch, Match } from 'solid-js'; + +export function UserQuota() { + const query = createQuery(() => ({ + queryKey: ['user'], + queryFn: async () => { + const response = await fetch('https://quoter.discours.io/', { + headers: { + 'Authorization': `Bearer ${getAuthToken()}`, + }, + }); + return response.json(); + }, + })); + + return ( + + +
Loading quota...
+
+ +
Error loading quota
+
+ + + {(data) => ( +
+

Storage: {data().storage_used_mb}MB / {data().storage_limit_mb}MB

+
+
+
+
+ )} + + + + ); +} +``` + +## πŸ”§ Implementation Steps + +1. **Quoter**: Serve raw files only (no patterns) +2. **Vercel**: Create SolidJS API routes for thumbnails +3. **Frontend**: Use TanStack Query for data fetching +4. **CORS**: Configure Quoter to allow Vercel domain + +## πŸ“Š Request Flow + +```mermaid +sequenceDiagram + participant Client + participant Vercel + participant Quoter + participant S3 + + Client->>Vercel: GET /api/thumb/600/image.jpg + Vercel->>Quoter: GET /image.jpg (original) + Quoter->>S3: Fetch image.jpg + S3->>Quoter: Return file data + Quoter->>Vercel: Return original image + Vercel->>Vercel: Generate 600px thumbnail + Vercel->>Client: Return optimized thumbnail + + Note over Vercel: Cache thumbnail at edge +``` + +## 🎨 Advanced Vercel Features + +### Smart Format Detection +```javascript +// Auto-serve WebP/AVIF when supported +export async function GET(request) { + const accept = request.headers.get('accept'); + const supportsWebP = accept?.includes('image/webp'); + const supportsAVIF = accept?.includes('image/avif'); + + return new ImageResponse( + // ... image component + { + format: supportsAVIF ? 'avif' : supportsWebP ? 'webp' : 'jpeg', + } + ); +} +``` + +### Quality Optimization +```javascript +// Different quality for different sizes +const quality = width <= 400 ? 75 : width <= 800 ? 85 : 95; + +return new ImageResponse(component, { + width, + height, + quality, +}); +``` + +## πŸ”— Integration with CORS + +Update Quoter CORS whitelist: +```bash +CORS_DOWNLOAD_ORIGINS=https://discours.io,https://*.discours.io,https://*.vercel.app +``` + +This allows Vercel Edge Functions to fetch originals from Quoter. + +## πŸ“ˆ Performance Benefits + +- **Faster uploads**: No server-side resizing in Quoter +- **Global CDN**: Vercel Edge caches thumbnails worldwide +- **On-demand sizing**: Generate any size when needed +- **Smart caching**: Automatic cache headers and invalidation +- **Format optimization**: Serve modern formats automatically + +**Result**: Clean separation of concerns - Quoter handles storage, Vercel handles optimization! πŸš€ diff --git a/src/app_state.rs b/src/app_state.rs index c666485..877b274 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,10 +1,11 @@ use crate::s3_utils::get_s3_filelist; +use crate::security::SecurityConfig; use actix_web::error::ErrorInternalServerError; use aws_config::BehaviorVersion; use aws_sdk_s3::{Client as S3Client, config::Credentials}; use log::warn; use redis::{AsyncCommands, Client as RedisClient, aio::MultiplexedConnection}; -use std::env; +use std::{env, time::Duration}; #[derive(Clone)] pub struct AppState { @@ -12,6 +13,7 @@ pub struct AppState { pub storj_client: S3Client, pub aws_client: S3Client, pub bucket: String, + pub request_timeout: Duration, } const PATH_MAPPING_KEY: &str = "filepath_mapping"; // ΠšΠ»ΡŽΡ‡ для хранСния ΠΌΠ°ΠΏΠΏΠΈΠ½Π³Π° ΠΏΡƒΡ‚Π΅ΠΉ @@ -20,13 +22,25 @@ const PATH_MAPPING_KEY: &str = "filepath_mapping"; // ΠšΠ»ΡŽΡ‡ для Ρ…Ρ€Π°Π½Π΅ impl AppState { /// Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ Π½ΠΎΠ²ΠΎΠ³ΠΎ состояния прилоТСния. pub async fn new() -> Self { - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ для Redis + let security_config = SecurityConfig::default(); + Self::new_with_config(security_config).await + } + + /// Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ с кастомной ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠ΅ΠΉ бСзопасности. + pub async fn new_with_config(security_config: SecurityConfig) -> Self { + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ для Redis с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set"); let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL"); - let redis_connection = redis_client - .get_multiplexed_async_connection() - .await - .unwrap(); + + // УстанавливаСм Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ для Redis ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ + let redis_connection = tokio::time::timeout( + Duration::from_secs(security_config.request_timeout_seconds), + redis_client.get_multiplexed_async_connection(), + ) + .await + .map_err(|_| "Redis connection timeout") + .expect("Failed to connect to Redis within timeout") + .expect("Redis connection failed"); // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ для S3 (Storj) let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set"); @@ -41,7 +55,7 @@ impl AppState { let aws_endpoint = env::var("AWS_END_POINT").unwrap_or_else(|_| "https://s3.amazonaws.com".to_string()); - // ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ S3 для Storj + // ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ S3 для Storj с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ let storj_config = aws_config::defaults(BehaviorVersion::latest()) .region("eu-west-1") .endpoint_url(s3_endpoint) @@ -52,12 +66,20 @@ impl AppState { None, "rust-storj-client", )) + .timeout_config( + aws_config::timeout::TimeoutConfig::builder() + .operation_timeout(Duration::from_secs(security_config.request_timeout_seconds)) + .operation_attempt_timeout(Duration::from_secs( + security_config.request_timeout_seconds / 2, + )) + .build(), + ) .load() .await; let storj_client = S3Client::new(&storj_config); - // ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ S3 для AWS + // ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ S3 для AWS с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ let aws_config = aws_config::defaults(BehaviorVersion::latest()) .region("eu-west-1") .endpoint_url(aws_endpoint) @@ -68,6 +90,14 @@ impl AppState { None, "rust-aws-client", )) + .timeout_config( + aws_config::timeout::TimeoutConfig::builder() + .operation_timeout(Duration::from_secs(security_config.request_timeout_seconds)) + .operation_attempt_timeout(Duration::from_secs( + security_config.request_timeout_seconds / 2, + )) + .build(), + ) .load() .await; @@ -78,6 +108,7 @@ impl AppState { storj_client, aws_client, bucket, + request_timeout: Duration::from_secs(security_config.request_timeout_seconds), }; // ΠšΡΡˆΠΈΡ€ΡƒΠ΅ΠΌ список Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΈΠ· AWS ΠΏΡ€ΠΈ стартС прилоТСния @@ -105,40 +136,51 @@ impl AppState { warn!("cached {} files", filelist.len()); } - /// ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΏΡƒΡ‚ΡŒ ΠΈΠ· ΠΊΠ»ΡŽΡ‡Π° (ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π°) Π² Redis. + /// ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΏΡƒΡ‚ΡŒ ΠΈΠ· ΠΊΠ»ΡŽΡ‡Π° (ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π°) Π² Redis с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ. pub async fn get_path(&self, filename: &str) -> Result, actix_web::Error> { let mut redis = self.redis.clone(); - let new_path: Option = redis - .hget(PATH_MAPPING_KEY, filename) - .await - .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; + + let new_path: Option = + tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename)) + .await + .map_err(|_| ErrorInternalServerError("Redis operation timeout"))? + .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; + Ok(new_path) } pub async fn set_path(&self, filename: &str, filepath: &str) { let mut redis = self.redis.clone(); - let _: () = redis - .hset(PATH_MAPPING_KEY, filename, filepath) - .await - .unwrap_or_else(|_| panic!("Failed to cache file {} in Redis", filename)); + + let _: () = tokio::time::timeout( + self.request_timeout, + redis.hset(PATH_MAPPING_KEY, filename, filepath), + ) + .await + .unwrap_or_else(|_| panic!("Redis timeout when caching file {} in Redis", filename)) + .unwrap_or_else(|_| panic!("Failed to cache file {} in Redis", filename)); } - /// создаСт ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + /// создаСт ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ pub async fn get_or_create_quota(&self, user_id: &str) -> Result { let mut redis = self.redis.clone(); let quota_key = format!("quota:{}", user_id); - // ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΊΠ²ΠΎΡ‚Ρƒ ΠΈΠ· Redis - let quota: u64 = redis.get("a_key).await.unwrap_or(0); + // ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΊΠ²ΠΎΡ‚Ρƒ ΠΈΠ· Redis с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ + let quota: u64 = tokio::time::timeout(self.request_timeout, redis.get("a_key)) + .await + .map_err(|_| ErrorInternalServerError("Redis timeout getting user quota"))? + .unwrap_or(0); if quota == 0 { - // Если ΠΊΠ²ΠΎΡ‚Π° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°, устанавливаСм Π΅Ρ‘ Π² 0 Π±Π°ΠΉΡ‚ Π±Π΅Π· TTL (постоянная ΠΊΠ²ΠΎΡ‚Π°) - redis - .set::<&str, u64, ()>("a_key, 0) - .await - .map_err(|_| { - ErrorInternalServerError("Failed to set initial user quota in Redis") - })?; + // Если ΠΊΠ²ΠΎΡ‚Π° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°, устанавливаСм Π΅Ρ‘ Π² 0 Π±Π°ΠΉΡ‚ Π±Π΅Π· TTL с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ + tokio::time::timeout( + self.request_timeout, + redis.set::<&str, u64, ()>("a_key, 0), + ) + .await + .map_err(|_| ErrorInternalServerError("Redis timeout setting user quota"))? + .map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?; Ok(0) // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ 0 ΠΊΠ°ΠΊ Π½Π°Ρ‡Π°Π»ΡŒΠ½ΡƒΡŽ ΠΊΠ²ΠΎΡ‚Ρƒ } else { @@ -146,7 +188,7 @@ impl AppState { } } - /// ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² Π±Π°ΠΉΡ‚Π°Ρ… + /// ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠ²ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² Π±Π°ΠΉΡ‚Π°Ρ… с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ pub async fn increment_uploaded_bytes( &self, user_id: &str, @@ -155,27 +197,37 @@ impl AppState { let mut redis = self.redis.clone(); let quota_key = format!("quota:{}", user_id); - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, сущСствуСт Π»ΠΈ ΠΊΠ»ΡŽΡ‡ Π² Redis - let exists: bool = redis.exists::<_, bool>("a_key).await.map_err(|_| { - ErrorInternalServerError("Failed to check if user quota exists in Redis") - })?; + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, сущСствуСт Π»ΠΈ ΠΊΠ»ΡŽΡ‡ Π² Redis с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ + let exists: bool = + tokio::time::timeout(self.request_timeout, redis.exists::<_, bool>("a_key)) + .await + .map_err(|_| { + ErrorInternalServerError("Redis timeout checking user quota existence") + })? + .map_err(|_| { + ErrorInternalServerError("Failed to check if user quota exists in Redis") + })?; // Если ΠΊΠ»ΡŽΡ‡ Π½Π΅ сущСствуСт, создаСм Π΅Π³ΠΎ с Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΌ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ΠΌ Π±Π΅Π· TTL if !exists { - redis - .set::<_, u64, ()>("a_key, bytes) - .await - .map_err(|_| { - ErrorInternalServerError("Failed to set initial user quota in Redis") - })?; + tokio::time::timeout( + self.request_timeout, + redis.set::<_, u64, ()>("a_key, bytes), + ) + .await + .map_err(|_| ErrorInternalServerError("Redis timeout setting initial user quota"))? + .map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?; return Ok(bytes); } // Если ΠΊΠ»ΡŽΡ‡ сущСствуСт, ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π΅Π³ΠΎ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π½Π° Π·Π°Π΄Π°Π½Π½ΠΎΠ΅ количСство Π±Π°ΠΉΡ‚ - let new_quota: u64 = redis - .incr::<_, u64, u64>("a_key, bytes) - .await - .map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?; + let new_quota: u64 = tokio::time::timeout( + self.request_timeout, + redis.incr::<_, u64, u64>("a_key, bytes), + ) + .await + .map_err(|_| ErrorInternalServerError("Redis timeout incrementing user quota"))? + .map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?; Ok(new_quota) } diff --git a/src/auth.rs b/src/auth.rs index c33976e..a78dedf 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,11 +2,8 @@ use actix_web::error::ErrorInternalServerError; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use log::{info, warn}; use redis::{AsyncCommands, aio::MultiplexedConnection}; -use reqwest::Client as HTTPClient; -use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::{collections::HashMap, env, error::Error}; +use std::{error::Error, time::Duration}; // Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Ρ‹ для JWT Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² #[derive(Debug, Deserialize)] @@ -87,10 +84,11 @@ pub fn validate_token(token: &str) -> Result> { } } -/// ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ user_id ΠΈΠ· JWT Ρ‚ΠΎΠΊΠ΅Π½Π° ΠΈ Π±Π°Π·ΠΎΠ²Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ +/// ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ user_id ΠΈΠ· JWT Ρ‚ΠΎΠΊΠ΅Π½Π° ΠΈ Π±Π°Π·ΠΎΠ²Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ pub async fn get_user_by_token( token: &str, redis: &mut MultiplexedConnection, + timeout: Duration, ) -> Result> { // Π”Π΅ΠΊΠΎΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ JWT Ρ‚ΠΎΠΊΠ΅Π½ для получСния user_id let claims = decode_jwt_token(token)?; @@ -98,11 +96,15 @@ pub async fn get_user_by_token( info!("Extracted user_id from JWT token: {}", user_id); - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π²Π°Π»ΠΈΠ΄Π½ΠΎΡΡ‚ΡŒ Ρ‚ΠΎΠΊΠ΅Π½Π° Ρ‡Π΅Ρ€Π΅Π· сСссию Π² Redis (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π²Π°Π»ΠΈΠ΄Π½ΠΎΡΡ‚ΡŒ Ρ‚ΠΎΠΊΠ΅Π½Π° Ρ‡Π΅Ρ€Π΅Π· сСссию Π² Redis (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) с Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠΌ let token_key = format!("session:{}:{}", user_id, token); - let session_exists: bool = redis - .exists(&token_key) + let session_exists: bool = tokio::time::timeout(timeout, redis.exists(&token_key)) .await + .map_err(|_| { + warn!("Redis timeout checking session existence"); + // НС критичная ошибка, ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ с Π±Π°Π·ΠΎΠ²Ρ‹ΠΌΠΈ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ + }) + .unwrap_or(Ok(false)) .map_err(|e| { warn!("Failed to check session existence in Redis: {}", e); // НС критичная ошибка, ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ с Π±Π°Π·ΠΎΠ²Ρ‹ΠΌΠΈ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ @@ -116,13 +118,19 @@ pub async fn get_user_by_token( .unwrap() .as_secs(); - let _: () = redis - .hset(&token_key, "last_activity", current_time.to_string()) - .await - .map_err(|e| { - warn!("Failed to update last_activity: {}", e); - }) - .unwrap_or(()); + let _: () = tokio::time::timeout( + timeout, + redis.hset(&token_key, "last_activity", current_time.to_string()), + ) + .await + .map_err(|_| { + warn!("Redis timeout updating last_activity"); + }) + .unwrap_or(Ok(())) + .map_err(|e| { + warn!("Failed to update last_activity: {}", e); + }) + .unwrap_or(()); info!("Updated last_activity for session: {}", token_key); } else { diff --git a/src/handlers/common.rs b/src/handlers/common.rs index ab050d9..7271fab 100644 --- a/src/handlers/common.rs +++ b/src/handlers/common.rs @@ -1,11 +1,76 @@ use actix_web::{HttpRequest, HttpResponse, Result, error::ErrorUnauthorized}; -use log::warn; +use log::{debug, info, warn}; +use std::env; use crate::auth::validate_token; -/// ΠžΠ±Ρ‰ΠΈΠ΅ константы -pub const CACHE_CONTROL_IMMUTABLE: &str = "public, max-age=31536000, immutable"; // 1 Π³ΠΎΠ΄ -pub const CORS_ALLOW_ORIGIN: &str = "*"; +/// ΠžΠ±Ρ‰ΠΈΠ΅ константы - optimized for Vercel Edge caching +pub const CACHE_CONTROL_VERCEL: &str = "public, max-age=86400, s-maxage=31536000"; // 1 day browser, 1 year CDN + +/// Log request source and check CORS origin +pub fn get_cors_origin(req: &HttpRequest) -> String { + let allowed_origins = env::var("CORS_DOWNLOAD_ORIGINS") + .unwrap_or_else(|_| "https://discours.io,https://*.discours.io,https://testing.discours.io,https://testing3.discours.io".to_string()); + + // Extract request source info for logging + let origin = req.headers().get("origin").and_then(|h| h.to_str().ok()); + let referer = req.headers().get("referer").and_then(|h| h.to_str().ok()); + let user_agent = req + .headers() + .get("user-agent") + .and_then(|h| h.to_str().ok()) + .unwrap_or("unknown"); + let remote_addr = req + .peer_addr() + .map(|addr| addr.ip().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Log request source for CORS whitelist analysis + match (origin, referer) { + (Some(orig), Some(ref_)) => { + info!( + "πŸ“₯ Request source: origin={}, referer={}, ip={}, ua={}", + orig, ref_, remote_addr, user_agent + ); + } + (Some(orig), None) => { + info!( + "πŸ“₯ Request source: origin={}, ip={}, ua={}", + orig, remote_addr, user_agent + ); + } + (None, Some(ref_)) => { + info!( + "πŸ“₯ Request source: referer={}, ip={}, ua={}", + ref_, remote_addr, user_agent + ); + } + (None, None) => { + debug!("πŸ“₯ Direct request: ip={}, ua={}", remote_addr, user_agent); + } + } + + if let Some(origin) = origin { + // Simple check - if origin contains any allowed domain, allow it + for allowed in allowed_origins.split(',') { + let allowed = allowed.trim(); + if allowed.contains('*') { + let base = allowed.replace("*.", ""); + if origin.contains(&base) { + debug!("βœ… CORS allowed: {} matches {}", origin, allowed); + return origin.to_string(); + } + } else if origin == allowed { + debug!("βœ… CORS allowed: {} exact match", origin); + return origin.to_string(); + } + } + warn!("⚠️ CORS not whitelisted: {}", origin); + } + + // Default permissive for file downloads + "*".to_string() +} /// Π˜Π·Π²Π»Π΅ΠΊΠ°Π΅Ρ‚ ΠΈ Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅Ρ‚ Ρ‚ΠΎΠΊΠ΅Π½ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΈΠ· Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² запроса pub fn extract_and_validate_token(req: &HttpRequest) -> Result<&str, actix_web::Error> { @@ -45,49 +110,57 @@ pub fn extract_and_validate_token(req: &HttpRequest) -> Result<&str, actix_web:: Ok(token) } -/// Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ HTTP ΠΎΡ‚Π²Π΅Ρ‚ с ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΌΠΈ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°ΠΌΠΈ ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ -pub fn create_cached_response(content_type: &str, data: Vec, etag: &str) -> HttpResponse { +// Removed unused create_file_response - using create_file_response_with_analytics instead + +/// File response with analytics logging +pub fn create_file_response_with_analytics( + content_type: &str, + data: Vec, + req: &HttpRequest, + path: &str, +) -> HttpResponse { + let cors_origin = get_cors_origin(req); + + // Log analytics for CORS whitelist analysis + log_request_analytics(req, path, data.len()); + HttpResponse::Ok() .content_type(content_type) - .insert_header(("etag", etag)) - .insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE)) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) + .insert_header(("cache-control", CACHE_CONTROL_VERCEL)) + .insert_header(("access-control-allow-origin", cors_origin)) .body(data) } -/// Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ стандартный HTTP ΠΎΡ‚Π²Π΅Ρ‚ с Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°ΠΌΠΈ CORS -pub fn create_response_with_cors(content_type: &str, data: Vec) -> HttpResponse { - HttpResponse::Ok() - .content_type(content_type) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) - .body(data) -} +// Removed complex ETag caching - Vercel handles caching on their edge -/// Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ HTTP ΠΎΡ‚Π²Π΅Ρ‚ с ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ Π½Π° основС ETag -pub fn create_etag_response(content_type: &str, data: Vec, etag: &str) -> HttpResponse { - HttpResponse::Ok() - .content_type(content_type) - .insert_header(("etag", etag)) - .insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE)) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) - .body(data) -} - -/// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ETag для ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ 304 Ссли совпадаСт -pub fn check_etag_cache(req: &HttpRequest, etag: &str) -> Option { - let client_etag = req +/// Log request analytics for CORS whitelist tuning +pub fn log_request_analytics(req: &HttpRequest, path: &str, response_size: usize) { + let origin = req.headers().get("origin").and_then(|h| h.to_str().ok()); + let referer = req.headers().get("referer").and_then(|h| h.to_str().ok()); + let user_agent = req .headers() - .get("if-none-match") - .and_then(|h| h.to_str().ok()); + .get("user-agent") + .and_then(|h| h.to_str().ok()) + .unwrap_or("unknown"); + let remote_addr = req + .peer_addr() + .map(|addr| addr.ip().to_string()) + .unwrap_or_else(|| "unknown".to_string()); - if let Some(client_etag) = client_etag { - if client_etag == etag { - return Some(HttpResponse::NotModified().finish()); - } - } - None + // Analytics log for future CORS configuration + info!( + "πŸ“Š ANALYTICS: path={}, size={}b, origin={}, referer={}, ip={}, ua={}", + path, + response_size, + origin.unwrap_or("none"), + referer.unwrap_or("none"), + remote_addr, + user_agent + ); } +// ETag caching removed - handled by Vercel Edge + /// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΏΡƒΡ‚ΡŒ Π½Π° ACME challenge ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ 404 Ссли Π½ΡƒΠΆΠ½ΠΎ pub fn check_acme_path(path: &str) -> Option { if path.starts_with(".well-known/") || path.starts_with("/.well-known/") { @@ -107,31 +180,15 @@ pub fn validate_token_format(token: &str) -> bool { } // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ Ρ‚ΠΎΠΊΠ΅Π½ содСрТит Ρ‚ΠΎΠ»ΡŒΠΊΠΎ допустимыС символы для JWT - token.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') -} - -/// Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‚ с Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ для прСдотвращСния брутфорса -pub async fn create_delayed_error_response( - status: actix_web::http::StatusCode, - message: &str, - delay_ms: u64, -) -> HttpResponse { - if delay_ms > 0 { - tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; - } - - HttpResponse::build(status) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) - .json(serde_json::json!({ - "error": message, - "retry_after": delay_ms / 1000 - })) + token + .chars() + .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') } /// Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ JSON ΠΎΡ‚Π²Π΅Ρ‚ с ошибкой pub fn create_error_response(status: actix_web::http::StatusCode, message: &str) -> HttpResponse { HttpResponse::build(status) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) + .insert_header(("access-control-allow-origin", "*")) .json(serde_json::json!({ "error": message })) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4d2ad12..ad0c2db 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,9 +1,9 @@ mod common; mod proxy; mod serve_file; +mod universal; mod upload; mod user; -mod universal; pub use universal::universal_handler; diff --git a/src/handlers/proxy.rs b/src/handlers/proxy.rs index d965874..218265e 100644 --- a/src/handlers/proxy.rs +++ b/src/handlers/proxy.rs @@ -2,16 +2,17 @@ use actix_web::error::ErrorNotFound; use actix_web::{HttpRequest, HttpResponse, Result, error::ErrorInternalServerError, web}; use log::{error, info, warn}; +use super::common::create_file_response_with_analytics; use crate::app_state::AppState; use crate::handlers::serve_file::serve_file; use crate::lookup::{find_file_by_pattern, get_mime_type}; use crate::s3_utils::{check_file_exists, load_file_from_s3, upload_to_s3}; -use crate::thumbnail::{find_closest_width, parse_file_path, thumbdata_save}; -use super::common::{check_etag_cache, create_cached_response}; +use crate::thumbnail::parse_file_path; // Π£Π΄Π°Π»Π΅Π½Π° Π΄ΡƒΠ±Π»ΠΈΡ€ΡƒΡŽΡ‰Π°Ρ функция, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΈΠ· common модуля -/// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ для скачивания Ρ„Π°ΠΉΠ»Π° ΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹, Ссли ΠΎΠ½Π° нСдоступна. +/// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ для скачивания Ρ„Π°ΠΉΠ»Π° +/// Π±Π΅Π· Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ - это Π΄Π΅Π»Π°Π΅Ρ‚ Vercel Edge API #[allow(clippy::collapsible_if)] pub async fn proxy_handler( req: HttpRequest, @@ -38,12 +39,7 @@ pub async fn proxy_handler( base_filename, requested_width, ext ); - // Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ ETag для ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ ΠΈ провСряСм кэш - let file_etag = format!("\"{}\"", &filekey); - if let Some(response) = check_etag_cache(&req, &file_etag) { - info!("Cache hit for {}, returning 304", filekey); - return Ok(response); - } + // Caching handled by Vercel Edge - focus on fast file serving let content_type = match get_mime_type(&ext) { Some(mime) => mime.to_string(), None => { @@ -77,72 +73,8 @@ pub async fn proxy_handler( ); if check_file_exists(&state.storj_client, &state.bucket, &stored_path).await? { warn!("File exists in Storj: {}", stored_path); - if content_type.starts_with("image") { - warn!("Processing image file with width: {}", requested_width); - if requested_width == 0 { - warn!("Serving original file without resizing"); - serve_file(&stored_path, &state).await - } else { - let closest: u32 = find_closest_width(requested_width); - warn!( - "Calculated closest width: {} for requested: {}", - closest, requested_width - ); - let thumb_filename = &format!("{}_{}.{}", base_filename, closest, ext); - warn!("Generated thumbnail filename: {}", thumb_filename); - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, сущСствуСт Π»ΠΈ ΡƒΠΆΠ΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° Π² Storj - match check_file_exists(&state.storj_client, &state.bucket, thumb_filename) - .await - { - Ok(true) => { - warn!("serve existed thumb file: {}", thumb_filename); - serve_file(thumb_filename, &state).await - } - Ok(false) => { - // ΠœΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Π° Π½Π΅ сущСствуСт, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π» ΠΈ запускаСм Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΡŽ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ - let original_file = serve_file(&stored_path, &state).await?; - - // ЗапускаСм Π°ΡΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½ΡƒΡŽ Π·Π°Π΄Π°Ρ‡Ρƒ для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ - let state_clone = state.clone(); - let stored_path_clone = stored_path.clone(); - let filekey_clone = filekey.clone(); - let content_type_clone = content_type.to_string(); - - actix_web::rt::spawn(async move { - if let Ok(filedata) = load_file_from_s3( - &state_clone.storj_client, - &state_clone.bucket, - &stored_path_clone, - ) - .await - { - warn!("generate new thumb files: {}", stored_path_clone); - if let Err(e) = thumbdata_save( - filedata, - &state_clone, - &filekey_clone, - content_type_clone, - ) - .await - { - error!("Failed to generate thumbnail: {}", e); - } - } - }); - - Ok(original_file) - } - Err(e) => { - error!("ошибка ΠΏΡ€ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ΅ сущСствования ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹: {}", e); - Err(ErrorInternalServerError("failed to load thumbnail")) - } - } - } - } else { - warn!("File is not an image, proceeding with normal serving"); - serve_file(&stored_path, &state).await - } + // ΠŸΡ€ΠΎΡΡ‚ΠΎ ΠΎΡ‚Π΄Π°Π΅ΠΌ Ρ„Π°ΠΉΠ», ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ Vercel Edge API + serve_file(&stored_path, &state, &req).await } else { warn!( "Attempting to load from AWS - bucket: {}, path: {}", @@ -197,7 +129,12 @@ pub async fn proxy_handler( let elapsed = start_time.elapsed(); info!("File served from AWS in {:?}: {}", elapsed, path); - return Ok(create_cached_response(&content_type, filedata, &file_etag)); + return Ok(create_file_response_with_analytics( + &content_type, + filedata, + &req, + &path, + )); } Err(err) => { warn!("Failed to load from AWS path {}: {:?}", path, err); @@ -244,26 +181,9 @@ pub async fn proxy_handler( warn!("Checking existence in Storj: {}", exists_in_storj); if exists_in_storj { - warn!( - "file {} exists in storj, try to generate thumbnails", - filepath - ); - - match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await { - Ok(filedata) => { - let _ = thumbdata_save( - filedata.clone(), - &state, - &filekey, - content_type.to_string(), - ) - .await; - } - Err(e) => { - error!("cannot download {} from storj: {}", filekey, e); - return Err(ErrorInternalServerError(e)); - } - } + warn!("file {} exists in storj, serving directly", filepath); + // Π€Π°ΠΉΠ» сущСствуСт Π² Storj, ΠΎΡ‚Π΄Π°Π΅ΠΌ Π΅Π³ΠΎ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ + return serve_file(&filepath, &state, &req).await; } else { warn!("file {} does not exist in storj", filepath); } @@ -280,13 +200,6 @@ pub async fn proxy_handler( "Successfully downloaded file from AWS, size: {} bytes", filedata.len() ); - let _ = thumbdata_save( - filedata.clone(), - &state, - &filekey, - content_type.to_string(), - ) - .await; if let Err(e) = upload_to_s3( &state.storj_client, &state.bucket, @@ -303,7 +216,12 @@ pub async fn proxy_handler( } let elapsed = start_time.elapsed(); info!("File served from AWS in {:?}: {}", elapsed, filepath); - Ok(create_cached_response(&content_type, filedata, &file_etag)) + Ok(create_file_response_with_analytics( + &content_type, + filedata, + &req, + &filepath, + )) } Err(e) => { error!("Failed to download from AWS: {} - Error: {}", filepath, e); diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs index 106ecce..35137d1 100644 --- a/src/handlers/serve_file.rs +++ b/src/handlers/serve_file.rs @@ -1,14 +1,16 @@ -use actix_web::{HttpResponse, Result, error::ErrorInternalServerError}; +use actix_web::{HttpRequest, HttpResponse, Result, error::ErrorInternalServerError}; use mime_guess::MimeGuess; +use super::common::create_file_response_with_analytics; use crate::app_state::AppState; -use crate::s3_utils::check_file_exists; -use super::common::{CACHE_CONTROL_IMMUTABLE, CORS_ALLOW_ORIGIN}; +use crate::s3_utils::{check_file_exists, load_file_from_s3}; /// Ѐункция для обслуТивания Ρ„Π°ΠΉΠ»Π° ΠΏΠΎ Π·Π°Π΄Π°Π½Π½ΠΎΠΌΡƒ ΠΏΡƒΡ‚ΠΈ. +/// Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π° для Vercel Edge caching. pub async fn serve_file( filepath: &str, state: &AppState, + req: &HttpRequest, ) -> Result { if filepath.is_empty() { return Err(ErrorInternalServerError("Filename is empty".to_string())); @@ -23,35 +25,21 @@ pub async fn serve_file( ))); } - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ ΠΈΠ· Storj S3 - let get_object_output = state - .storj_client - .get_object() - .bucket(&state.bucket) - .key(filepath) - .send() + // Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ Ρ„Π°ΠΉΠ» ΠΈΠ· S3 + let filedata = load_file_from_s3(&state.storj_client, &state.bucket, filepath) .await - .map_err(|_| { - ErrorInternalServerError(format!("Failed to get {} object from Storj", filepath)) + .map_err(|e| { + ErrorInternalServerError(format!("Failed to load {} from Storj: {}", filepath, e)) })?; - let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output - .body - .collect() - .await - .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; - - let data_bytes = data.into_bytes(); - + // ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ MIME Ρ‚ΠΈΠΏ let mime_type = MimeGuess::from_path(filepath).first_or_octet_stream(); - // Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ ETag для ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ Π½Π° основС ΠΏΡƒΡ‚ΠΈ Ρ„Π°ΠΉΠ»Π° - let etag = format!("\"{}\"", filepath); - - Ok(HttpResponse::Ok() - .content_type(mime_type.as_ref()) - .insert_header(("etag", etag.as_str())) - .insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE)) - .insert_header(("access-control-allow-origin", CORS_ALLOW_ORIGIN)) - .body(data_bytes)) + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΎΡ‚Π²Π΅Ρ‚ с Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΠΎΠΉ + Ok(create_file_response_with_analytics( + mime_type.as_ref(), + filedata, + req, + filepath, + )) } diff --git a/src/handlers/universal.rs b/src/handlers/universal.rs index 5e8c007..cebad80 100644 --- a/src/handlers/universal.rs +++ b/src/handlers/universal.rs @@ -1,10 +1,10 @@ -use actix_web::{HttpRequest, HttpResponse, Result, web}; use actix_multipart::Multipart; +use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{info, warn}; +use super::common::{check_acme_path, create_error_response}; use crate::app_state::AppState; -use crate::security::{SecurityManager, SecurityConfig}; -use super::common::{create_error_response, check_acme_path}; +use crate::security::SecurityConfig; /// Π£Π½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ опрСдСляСт HTTP ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΈ ΠΏΡƒΡ‚ΡŒ pub async fn universal_handler( @@ -14,7 +14,7 @@ pub async fn universal_handler( ) -> Result { let method = req.method().clone(); let path = req.path().to_string(); - + info!("Universal handler: {} {}", method, path); // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ACME challenge ΠΏΡƒΡ‚Π΅ΠΉ @@ -22,46 +22,31 @@ pub async fn universal_handler( return Ok(response); } - // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ SecurityManager для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ + // Базовая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° бСзопасности let security_config = SecurityConfig::default(); - let client_ip = SecurityManager::extract_client_ip(&req); - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π±Π°Π·ΠΎΠ²Ρ‹Ρ… ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ бСзопасности - if let Err(error) = SecurityManager::new(security_config.clone(), state.redis.clone()) - .validate_request_security(&req) { - warn!("Security validation failed for IP {}: {}", client_ip, error); + if let Err(error) = security_config.validate_request(&req) { + warn!("Security validation failed: {}", error); return Err(error); } - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½ΠΎΠ² - let mut security_manager = SecurityManager::new(security_config.clone(), state.redis.clone()); - if security_manager.check_suspicious_patterns(&path) { - warn!("Suspicious pattern detected from IP {}: {}", client_ip, path); - return Ok(create_error_response( - actix_web::http::StatusCode::NOT_FOUND, - "Not found" - )); + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° upload Π»ΠΈΠΌΠΈΡ‚ΠΎΠ² Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для POST запросов + if method == "POST" { + let client_ip = SecurityConfig::extract_client_ip(&req); + if let Err(error) = security_config.check_upload_limit(&client_ip).await { + warn!("Upload limit exceeded for IP {}: {}", client_ip, error); + return Err(error); + } } - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° rate limits Π² зависимости ΠΎΡ‚ endpoint - let endpoint_type = match method.as_str() { - "POST" if path == "/" => "upload", - "GET" if path == "/" => "auth", - _ => "general" - }; - if let Err(error) = security_manager.check_rate_limit(&client_ip, endpoint_type).await { - warn!("Rate limit exceeded for IP {} on {}: {}", client_ip, endpoint_type, error); - return Err(error); - } match method.as_str() { "GET" => handle_get(req, state, &path).await, "POST" => handle_post(req, payload, state, &path).await, _ => Ok(create_error_response( actix_web::http::StatusCode::METHOD_NOT_ALLOWED, - "Method not allowed" - )) + "Method not allowed", + )), } } @@ -70,7 +55,7 @@ async fn handle_get( state: web::Data, path: &str, ) -> Result { - if path == "/" || path == "" { + if path == "/" || path.is_empty() { // GET / - ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ crate::handlers::user::get_current_user_handler(req, state).await } else { @@ -88,6 +73,6 @@ async fn handle_post( _path: &str, ) -> Result { // POST / - Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ„Π°ΠΉΠ»Π° (multipart) - let multipart = Multipart::new(&req.headers(), payload); + let multipart = Multipart::new(req.headers(), payload); crate::handlers::upload::upload_handler(req, multipart, state).await } diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index 2834e87..a06ad61 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -2,12 +2,12 @@ use actix_multipart::Multipart; use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; +use super::common::extract_and_validate_token; use crate::app_state::AppState; use crate::auth::{extract_user_id_from_token, user_added_file}; use crate::handlers::MAX_USER_QUOTA_BYTES; use crate::lookup::store_file_info; use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3}; -use super::common::extract_and_validate_token; use futures::TryStreamExt; // use crate::thumbnail::convert_heic_to_jpeg; diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 23a758e..1e04c64 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -2,9 +2,9 @@ use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; use serde::Serialize; +use super::common::extract_and_validate_token; use crate::app_state::AppState; use crate::auth::{Author, get_user_by_token}; -use super::common::extract_and_validate_token; #[derive(Serialize)] pub struct UserWithQuotaResponse { @@ -32,7 +32,7 @@ pub async fn get_current_user_handler( // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ ΠΈΠ· Redis сСссии let mut redis = state.redis.clone(); - let user = match get_user_by_token(token, &mut redis).await { + let user = match get_user_by_token(token, &mut redis, state.request_timeout).await { Ok(user) => { info!( "Successfully retrieved user info: user_id={}, username={:?}", diff --git a/src/main.rs b/src/main.rs index 1d05079..5976fa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,14 +10,14 @@ use actix_cors::Cors; use actix_web::{ App, HttpServer, http::header, - middleware::{Logger, DefaultHeaders}, + middleware::{DefaultHeaders, Logger}, web, }; use app_state::AppState; -use security::{SecurityConfig, security_middleware}; +use security::SecurityConfig; use handlers::universal_handler; -use log::{warn, info}; +use log::{info, warn}; use std::env; use tokio::task::spawn_blocking; @@ -41,10 +41,11 @@ async fn main() -> std::io::Result<()> { // ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ бСзопасности let security_config = SecurityConfig::default(); - info!("Security config: max_payload={} MB, upload_rate_limit={}/{}s", - security_config.max_payload_size / (1024 * 1024), - security_config.upload_rate_limit.max_requests, - security_config.upload_rate_limit.window_seconds); + info!( + "Security config: max_payload={} MB, timeout={}s", + security_config.max_payload_size / (1024 * 1024), + security_config.request_timeout_seconds + ); HttpServer::new(move || { // Настройка CORS middleware - ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ Π² ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Π΅ @@ -71,14 +72,20 @@ async fn main() -> std::io::Result<()> { .add(("X-Frame-Options", "DENY")) .add(("X-XSS-Protection", "1; mode=block")) .add(("Referrer-Policy", "strict-origin-when-cross-origin")) - .add(("Content-Security-Policy", "default-src 'self'; img-src 'self' data: https:; object-src 'none';")) - .add(("Strict-Transport-Security", "max-age=31536000; includeSubDomains")); + .add(( + "Content-Security-Policy", + "default-src 'self'; img-src 'self' data: https:; object-src 'none';", + )) + .add(( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains", + )); App::new() .app_data(web::Data::new(app_state.clone())) .app_data(web::PayloadConfig::new(security_config.max_payload_size)) .app_data(web::JsonConfig::default().limit(1024 * 1024)) // 1MB для JSON - .wrap(actix_web::middleware::from_fn(security_middleware)) + .wrap(security_headers) .wrap(cors) .wrap(Logger::default()) diff --git a/src/security.rs b/src/security.rs index 3296c9d..4248fb5 100644 --- a/src/security.rs +++ b/src/security.rs @@ -1,42 +1,22 @@ -use actix_web::{HttpRequest, dev::ServiceRequest, middleware::Next, dev::ServiceResponse, error::ErrorTooManyRequests}; -use log::{warn, error, info}; -use redis::{AsyncCommands, aio::MultiplexedConnection}; -use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::HttpRequest; +use log::warn; use std::collections::HashMap; -use tokio::sync::RwLock; use std::sync::Arc; -use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::RwLock; -/// ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Π»ΠΈΠΌΠΈΡ‚ΠΎΠ² запросов +/// ΠŸΡ€ΠΎΡΡ‚Π°Ρ Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ Π·Π»ΠΎΡƒΠΏΠΎΡ‚Ρ€Π΅Π±Π»Π΅Π½ΠΈΠΉ для upload endpoint #[derive(Debug, Clone)] -pub struct RateLimitConfig { - /// МаксимальноС количСство запросов Π² ΠΎΠΊΠ½Π΅ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ - pub max_requests: u32, - /// Окно Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π² сСкундах - pub window_seconds: u64, - /// Π‘Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π½Π° количСство сСкунд ΠΏΡ€ΠΈ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½ΠΈΠΈ Π»ΠΈΠΌΠΈΡ‚Π° - pub block_duration_seconds: u64, +pub struct UploadProtection { + /// МаксимальноС количСство Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ Π² ΠΌΠΈΠ½ΡƒΡ‚Ρƒ с ΠΎΠ΄Π½ΠΎΠ³ΠΎ IP + pub max_uploads_per_minute: u32, + /// Π›ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш для подсчСта Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ + pub upload_counts: Arc>>, } -impl Default for RateLimitConfig { - fn default() -> Self { - Self { - max_requests: 100, // 100 запросов - window_seconds: 60, // Π² ΠΌΠΈΠ½ΡƒΡ‚Ρƒ - block_duration_seconds: 300, // Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π½Π° 5 ΠΌΠΈΠ½ΡƒΡ‚ - } - } -} - -/// ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ для Ρ€Π°Π·Π½Ρ‹Ρ… Ρ‚ΠΈΠΏΠΎΠ² запросов +/// ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ бСзопасности для простого storage proxy #[derive(Debug, Clone)] pub struct SecurityConfig { - /// ΠžΠ±Ρ‰ΠΈΠΉ Π»ΠΈΠΌΠΈΡ‚ ΠΏΠΎ IP - pub general_rate_limit: RateLimitConfig, - /// Π›ΠΈΠΌΠΈΡ‚ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Ρ„Π°ΠΉΠ»ΠΎΠ² - pub upload_rate_limit: RateLimitConfig, - /// Π›ΠΈΠΌΠΈΡ‚ для Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ - pub auth_rate_limit: RateLimitConfig, /// ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ‚Π΅Π»Π° запроса (Π±Π°ΠΉΡ‚Ρ‹) pub max_payload_size: usize, /// Π’Π°ΠΉΠΌΠ°ΡƒΡ‚ запроса (сСкунды) @@ -47,217 +27,52 @@ pub struct SecurityConfig { pub max_headers_count: usize, /// Максимальная Π΄Π»ΠΈΠ½Π° значСния Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ° pub max_header_value_length: usize, + /// Π—Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ Π·Π»ΠΎΡƒΠΏΠΎΡ‚Ρ€Π΅Π±Π»Π΅Π½ΠΈΠΉ upload + pub upload_protection: UploadProtection, } impl Default for SecurityConfig { fn default() -> Self { Self { - general_rate_limit: RateLimitConfig::default(), - upload_rate_limit: RateLimitConfig { - max_requests: 10, // 10 Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ - window_seconds: 300, // Π² 5 ΠΌΠΈΠ½ΡƒΡ‚ - block_duration_seconds: 600, // Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π½Π° 10 ΠΌΠΈΠ½ΡƒΡ‚ - }, - auth_rate_limit: RateLimitConfig { - max_requests: 20, // 20 ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ - window_seconds: 900, // Π² 15 ΠΌΠΈΠ½ΡƒΡ‚ - block_duration_seconds: 1800, // Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π½Π° 30 ΠΌΠΈΠ½ΡƒΡ‚ - }, - max_payload_size: 4000 * 1024 * 1024, // 4000 ΠœΠ‘ + max_payload_size: 500 * 1024 * 1024, // 500MB request_timeout_seconds: 300, // 5 ΠΌΠΈΠ½ΡƒΡ‚ max_path_length: 1000, max_headers_count: 50, max_header_value_length: 8192, + upload_protection: UploadProtection { + max_uploads_per_minute: 10, // 10 Π·Π°Π³Ρ€ΡƒΠ·ΠΎΠΊ Π² ΠΌΠΈΠ½ΡƒΡ‚Ρƒ + upload_counts: Arc::new(RwLock::new(HashMap::new())), + }, } } } -/// Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° для хранСния ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ запросах -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RequestInfo { - pub count: u32, - pub first_request_time: u64, - pub blocked_until: Option, -} - -/// ΠœΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ бСзопасности -pub struct SecurityManager { - pub config: SecurityConfig, - redis: MultiplexedConnection, - // Π›ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш для быстрых ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ - local_cache: Arc>>, -} - -impl SecurityManager { - pub fn new(config: SecurityConfig, redis: MultiplexedConnection) -> Self { - Self { - config, - redis, - local_cache: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ IP адрСс ΠΈΠ· запроса, учитывая прокси - pub fn extract_client_ip(req: &HttpRequest) -> String { - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ прокси - if let Some(forwarded_for) = req.headers().get("x-forwarded-for") { - if let Ok(forwarded_str) = forwarded_for.to_str() { - if let Some(first_ip) = forwarded_str.split(',').next() { - return first_ip.trim().to_string(); - } - } - } - - if let Some(real_ip) = req.headers().get("x-real-ip") { - if let Ok(ip_str) = real_ip.to_str() { - return ip_str.to_string(); - } - } - - // Fallback ΠΊ connection info - req.connection_info() - .realip_remote_addr() - .unwrap_or("unknown") - .to_string() - } - - /// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ Π»ΠΈΠΌΠΈΡ‚Ρ‹ запросов для IP - pub async fn check_rate_limit(&mut self, ip: &str, endpoint_type: &str) -> Result<(), actix_web::Error> { - let config = match endpoint_type { - "upload" => &self.config.upload_rate_limit, - "auth" => &self.config.auth_rate_limit, - _ => &self.config.general_rate_limit, - }; - - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let redis_key = format!("rate_limit:{}:{}", endpoint_type, ip); - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш - { - let cache = self.local_cache.read().await; - if let Some(info) = cache.get(&redis_key) { - if let Some(blocked_until) = info.blocked_until { - if current_time < blocked_until { - warn!("IP {} blocked until {}", ip, blocked_until); - return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked")); - } - } - } - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π² Redis - let info_str: Option = self.redis.get(&redis_key).await - .map_err(|e| { - error!("Redis error in rate limit check: {}", e); - actix_web::error::ErrorInternalServerError("Service temporarily unavailable") - })?; - - let mut request_info = if let Some(info_str) = info_str { - serde_json::from_str::(&info_str) - .unwrap_or_else(|_| RequestInfo { - count: 0, - first_request_time: current_time, - blocked_until: None, - }) - } else { - RequestInfo { - count: 0, - first_request_time: current_time, - blocked_until: None, - } - }; - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΡƒ - if let Some(blocked_until) = request_info.blocked_until { - if current_time < blocked_until { - warn!("IP {} is blocked until {}", ip, blocked_until); - return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked")); - } else { - // Π‘Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° истСкла, сбрасываСм - request_info.blocked_until = None; - request_info.count = 0; - request_info.first_request_time = current_time; - } - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΠΎΠΊΠ½ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ - if current_time - request_info.first_request_time > config.window_seconds { - // НовоС ΠΎΠΊΠ½ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ, сбрасываСм счСтчик - request_info.count = 0; - request_info.first_request_time = current_time; - } - - // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ счСтчик - request_info.count += 1; - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π»ΠΈΠΌΠΈΡ‚ - if request_info.count > config.max_requests { - warn!("Rate limit exceeded for IP {}: {} requests in window", ip, request_info.count); - - // УстанавливаСм Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΡƒ - request_info.blocked_until = Some(current_time + config.block_duration_seconds); - - // БохраняСм Π² Redis - let info_str = serde_json::to_string(&request_info).unwrap(); - let _: () = self.redis.set_ex(&redis_key, info_str, config.block_duration_seconds).await - .map_err(|e| { - error!("Redis error saving rate limit: {}", e); - actix_web::error::ErrorInternalServerError("Service temporarily unavailable") - })?; - - // ОбновляСм Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш - { - let mut cache = self.local_cache.write().await; - cache.insert(redis_key, request_info); - } - - return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked")); - } - - // БохраняСм ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½Π½ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ - let info_str = serde_json::to_string(&request_info).unwrap(); - let _: () = self.redis.set_ex(&redis_key, info_str, config.window_seconds * 2).await - .map_err(|e| { - error!("Redis error updating rate limit: {}", e); - actix_web::error::ErrorInternalServerError("Service temporarily unavailable") - })?; - - let count = request_info.count; - - // ОбновляСм Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ кэш - { - let mut cache = self.local_cache.write().await; - cache.insert(redis_key, request_info); - } - - info!("Rate limit check passed for IP {}: {}/{} requests", ip, count, config.max_requests); - Ok(()) - } - - /// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ запроса (Ρ€Π°Π·ΠΌΠ΅Ρ€, Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ, ΠΏΡƒΡ‚ΡŒ) - pub fn validate_request_security(&self, req: &HttpRequest) -> Result<(), actix_web::Error> { - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π΄Π»ΠΈΠ½Ρ‹ ΠΏΡƒΡ‚ΠΈ +impl SecurityConfig { + /// Π’Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅Ρ‚ запрос Π½Π° Π±Π°Π·ΠΎΠ²Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ бСзопасности + pub fn validate_request(&self, req: &HttpRequest) -> Result<(), actix_web::Error> { let path = req.path(); - if path.len() > self.config.max_path_length { - warn!("Request path too long: {} chars", path.len()); - return Err(actix_web::error::ErrorBadRequest("Request path too long")); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π΄Π»ΠΈΠ½Ρ‹ ΠΏΡƒΡ‚ΠΈ + if path.len() > self.max_path_length { + warn!("Path too long: {} chars", path.len()); + return Err(actix_web::error::ErrorBadRequest("Path too long")); } // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° количСства Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² - if req.headers().len() > self.config.max_headers_count { + if req.headers().len() > self.max_headers_count { warn!("Too many headers: {}", req.headers().len()); return Err(actix_web::error::ErrorBadRequest("Too many headers")); } // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π΄Π»ΠΈΠ½Ρ‹ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² - for (name, value) in req.headers().iter() { + for (name, value) in req.headers() { if let Ok(value_str) = value.to_str() { - if value_str.len() > self.config.max_header_value_length { - warn!("Header value too long: {} = {} chars", name, value_str.len()); + if value_str.len() > self.max_header_value_length { + warn!( + "Header value too long: {} = {} chars", + name, + value_str.len() + ); return Err(actix_web::error::ErrorBadRequest("Header value too long")); } } @@ -266,81 +81,104 @@ impl SecurityManager { // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ символы Π² ΠΏΡƒΡ‚ΠΈ if path.contains("..") || path.contains('\0') || path.contains('\r') || path.contains('\n') { warn!("Suspicious characters in path: {}", path); - return Err(actix_web::error::ErrorBadRequest("Invalid characters in path")); + return Err(actix_web::error::ErrorBadRequest( + "Invalid characters in path", + )); + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ + if self.check_suspicious_patterns(path) { + return Err(actix_web::error::ErrorBadRequest("Suspicious path pattern")); } Ok(()) } - /// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ Π² ΠΏΡƒΡ‚ΠΈ + /// ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΏΡƒΡ‚ΡŒ Π½Π° ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ pub fn check_suspicious_patterns(&self, path: &str) -> bool { let suspicious_patterns = [ - "/admin", "/wp-admin", "/phpmyadmin", "/.env", "/config", - "/.git", "/backup", "/db", "/sql", "/.well-known/acme-challenge", - "/xmlrpc.php", "/wp-login.php", "/wp-config.php", - "script>", "", + " Result<(), actix_web::Error> { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - let mut cache = self.local_cache.write().await; - let mut to_remove = Vec::new(); + let mut counts = self.upload_protection.upload_counts.write().await; + + // ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ старыС записи (ΡΡ‚Π°Ρ€ΡˆΠ΅ ΠΌΠΈΠ½ΡƒΡ‚Ρ‹) + counts.retain(|_, (_, timestamp)| current_time - *timestamp < 60); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ IP + let current_count = counts.get(ip).map(|(count, _)| *count).unwrap_or(0); + let first_upload_time = counts.get(ip).map(|(_, time)| *time).unwrap_or(current_time); + + if current_time - first_upload_time < 60 { + // Π’ ΠΏΡ€Π΅Π΄Π΅Π»Π°Ρ… ΠΌΠΈΠ½ΡƒΡ‚Ρ‹ + if current_count >= self.upload_protection.max_uploads_per_minute { + warn!("Upload limit exceeded for IP {}: {} uploads in minute", ip, current_count); + return Err(actix_web::error::ErrorTooManyRequests("Upload limit exceeded")); + } + counts.insert(ip.to_string(), (current_count + 1, first_upload_time)); + } else { + // Новая ΠΌΠΈΠ½ΡƒΡ‚Π°, сбрасываСм счСтчик + counts.insert(ip.to_string(), (1, current_time)); + } + + Ok(()) + } - for (key, info) in cache.iter() { - // УдаляСм записи ΡΡ‚Π°Ρ€ΡˆΠ΅ 1 часа - if current_time - info.first_request_time > 3600 { - to_remove.push(key.clone()); + /// Π˜Π·Π²Π»Π΅ΠΊΠ°Π΅Ρ‚ IP адрСс ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° + pub fn extract_client_ip(req: &HttpRequest) -> String { + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ X-Forwarded-For (для прокси) + if let Some(forwarded) = req.headers().get("x-forwarded-for") { + if let Ok(forwarded_str) = forwarded.to_str() { + if let Some(first_ip) = forwarded_str.split(',').next() { + return first_ip.trim().to_string(); + } } } - - for key in to_remove { - cache.remove(&key); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ X-Real-IP + if let Some(real_ip) = req.headers().get("x-real-ip") { + if let Ok(real_ip_str) = real_ip.to_str() { + return real_ip_str.to_string(); + } } - - info!("Cleaned {} old entries from security cache", cache.len()); + + // Fallback Π½Π° connection info + req.connection_info().peer_addr().unwrap_or("unknown").to_string() } } -/// Middleware для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ бСзопасности -pub async fn security_middleware( - req: ServiceRequest, - next: Next, -) -> Result, actix_web::Error> { - let path = req.path().to_string(); - let method = req.method().to_string(); - - // Быстрая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° извСстныС Π°Ρ‚Π°ΠΊΠΈ - if path.contains("..") || path.contains('\0') || path.len() > 1000 { - warn!("Blocked suspicious request: {} {}", method, path); - return Err(actix_web::error::ErrorBadRequest("Invalid request")); - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° bot patterns - if let Some(user_agent) = req.headers().get("user-agent") { - if let Ok(ua_str) = user_agent.to_str() { - let ua_lower = ua_str.to_lowercase(); - if ua_lower.contains("bot") || ua_lower.contains("crawler") || ua_lower.contains("spider") { - // Для Π±ΠΎΡ‚ΠΎΠ² примСняСм Π±ΠΎΠ»Π΅Π΅ строгиС Π»ΠΈΠΌΠΈΡ‚Ρ‹ - info!("Bot detected: {}", ua_str); - } - } - } - - let res = next.call(req).await?; - Ok(res) -} diff --git a/src/thumbnail.rs b/src/thumbnail.rs index 888c19a..555a927 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -1,210 +1,74 @@ -use actix_web::error::ErrorInternalServerError; -use image::{DynamicImage, ImageFormat, imageops::FilterType}; -use log::warn; -use std::{collections::HashMap, io::Cursor}; +// ΠœΠΎΠ΄ΡƒΠ»ΡŒ для парсинга ΠΏΡƒΡ‚Π΅ΠΉ ΠΊ Ρ„Π°ΠΉΠ»Π°ΠΌ (Π±Π΅Π· Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€) -use crate::{app_state::AppState, s3_utils::upload_to_s3}; - -pub const THUMB_WIDTHS: [u32; 7] = [10, 40, 110, 300, 600, 800, 1400]; - -/// ΠŸΠ°Ρ€ΡΠΈΡ‚ ΠΏΡƒΡ‚ΡŒ ΠΊ Ρ„Π°ΠΉΠ»Ρƒ, извлСкая ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½ΠΎΠ΅ имя, Ρ‚Ρ€Π΅Π±ΡƒΠ΅ΠΌΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ ΠΈ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚. -/// ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹: -/// - "filename_150.ext" -> ("filename", 150, "ext") -/// - "unsafe/1440x/production/image/439efaa0-816f-11ef-b201-439da98539bc.jpg" -> ("439efaa0-816f-11ef-b201-439da98539bc", 1440, "jpg") -/// - "unsafe/production/image/5627e002-0c53-11ee-9565-0242ac110006.png" -> ("5627e002-0c53-11ee-9565-0242ac110006", 0, "png") -/// - "unsafe/development/image/439efaa0-816f-11ef-b201-439da98539bc.jpg/webp" -> ("439efaa0-816f-11ef-b201-439da98539bc", 0, "webp") -#[allow(clippy::collapsible_if)] -pub fn parse_file_path(requested_path: &str) -> (String, u32, String) { - let mut path = requested_path.to_string(); - if requested_path.ends_with("/webp") { - path = path.replace("/webp", ""); - } - let mut path_parts: Vec<&str> = path.split('/').collect(); - let mut extension = String::new(); - let mut width = 0; - let mut base_filename = String::new(); - - if path_parts.is_empty() { - return (path.to_string(), width, extension); - } - - // пытаСмся ΠΈΠ·Π²Π»Π΅Ρ‡ΡŒ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ ΠΈΠ· ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° - if let Some(filename_part) = path_parts.pop() { - if let Some((base, ext_part)) = filename_part.rsplit_once('.') { - extension = ext_part.to_string(); - base_filename = base.to_string(); // УстанавливаСм base_filename Π±Π΅Π· Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ - } else { - base_filename = filename_part.to_string(); - } - } - - // Если base_filename Π΅Ρ‰Ρ‘ Π½Π΅ установлСно, ΠΈΠ·Π²Π»Π΅ΠΊΠ°Π΅ΠΌ Π΅Π³ΠΎ - if base_filename.is_empty() { - if let Some(filename_part) = path_parts.pop() { - if let Some((base, ext_part)) = filename_part.rsplit_once('.') { - extension = ext_part.to_string(); - base_filename = base.to_string(); - } else { - base_filename = filename_part.to_string(); - } - } - } - - // Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΡˆΠΈΡ€ΠΈΠ½Ρ‹ ΠΈΠ· base_filename, Ссли ΠΎΠ½Π° Π΅ΡΡ‚ΡŒ - if let Some((name_part, width_str)) = base_filename.rsplit_once('_') { - if let Ok(w) = width_str.parse::() { - width = w; - base_filename = name_part.to_string(); - } - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° ΡΡ‚Π°Ρ€ΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ Π² путях, Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‰ΠΈΡ…ΡΡ с "unsafe" - if path.starts_with("unsafe") && width == 0 && path_parts.len() >= 2 { - if let Some(old_width_str) = path_parts.get(1) { - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π²Ρ‚ΠΎΡ€ΠΎΠΉ элСмСнт - let old_width_str = old_width_str.trim_end_matches('x'); - if let Ok(w) = old_width_str.parse::() { - width = w; - } - } - } - - (base_filename, width, extension) -} - -/// Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹ изобраТСния. +/// ΠŸΠ°Ρ€ΡΠΈΡ‚ ΠΏΡƒΡ‚ΡŒ ΠΊ Ρ„Π°ΠΉΠ»Ρƒ, извлСкая Π±Π°Π·ΠΎΠ²ΠΎΠ΅ имя, ΡˆΠΈΡ€ΠΈΠ½Ρƒ ΠΈ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅. /// -/// Π’Π΅ΠΏΠ΅Ρ€ΡŒ функция ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ `format`, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ опрСдСляСт Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ сохранСния ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€. -/// Π­Ρ‚ΠΎ позволяСт ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Ρ‚ΡŒ Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ Π±Π΅Π· нСобходимости Π·Π°Ρ€Π°Π½Π΅Π΅ ΠΏΡ€Π΅Π΄ΡƒΠ³Π°Π΄Ρ‹Π²Π°Ρ‚ΡŒ ΠΈΡ…. -pub async fn generate_thumbnails( - image: &DynamicImage, - format: ImageFormat, -) -> Result>, actix_web::Error> { - let mut thumbnails = HashMap::new(); +/// ΠŸΡ€ΠΈΠΌΠ΅Ρ€: +/// - "image.jpg" -> ("image", 0, "jpg") +/// - "image_300.jpg" -> ("image", 300, "jpg") +/// - "image_large.jpg" -> ("image", 0, "jpg") - нСкоррСктная ΡˆΠΈΡ€ΠΈΠ½Π° игнорируСтся +pub fn parse_file_path(path: &str) -> (String, u32, String) { + let path = path.trim_start_matches('/'); - for &width in THUMB_WIDTHS.iter().filter(|&&w| w < image.width()) { - let thumbnail = image.resize(width, u32::MAX, FilterType::Lanczos3); // РСсайз изобраТСния ΠΏΠΎ ΡˆΠΈΡ€ΠΈΠ½Π΅ - let mut buffer = Vec::new(); - thumbnail - .write_to(&mut Cursor::new(&mut buffer), format) - .map_err(|e| { - log::error!("Ошибка ΠΏΡ€ΠΈ сохранСнии ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹: {}", e); - ErrorInternalServerError("НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρƒ") - })?; // Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ изобраТСния Π² ΡƒΠΊΠ°Π·Π°Π½Π½ΠΎΠΌ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ - thumbnails.insert(width, buffer); - } + // Находим послСднюю Ρ‚ΠΎΡ‡ΠΊΡƒ для раздСлСния ΠΈΠΌΠ΅Π½ΠΈ ΠΈ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ + let (name_part, extension) = match path.rfind('.') { + Some(dot_pos) => (&path[..dot_pos], path[dot_pos + 1..].to_string()), + None => (path, String::new()), + }; - Ok(thumbnails) -} + // Π˜Ρ‰Π΅ΠΌ послСднСС ΠΏΠΎΠ΄Ρ‡Π΅Ρ€ΠΊΠΈΠ²Π°Π½ΠΈΠ΅ Π² ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° + if let Some(underscore_pos) = name_part.rfind('_') { + let base_filename = name_part[..underscore_pos].to_string(); + let width_str = &name_part[underscore_pos + 1..]; -/// ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅Ρ‚ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ изобраТСния Π½Π° основС Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Ρ„Π°ΠΉΠ»Π°. -fn determine_image_format(extension: &str) -> Result { - match extension.to_lowercase().as_str() { - "jpg" | "jpeg" => Ok(ImageFormat::Jpeg), - "png" => Ok(ImageFormat::Png), - "gif" => Ok(ImageFormat::Gif), - "webp" => Ok(ImageFormat::WebP), - "heic" | "heif" | "tiff" | "tif" => { - // ΠšΠΎΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ HEIC ΠΈ TIFF Π² JPEG ΠΏΡ€ΠΈ сохранСнии - Ok(ImageFormat::Jpeg) - } - _ => { - log::error!("НСподдСрТиваСмый Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ изобраТСния: {}", extension); - Err(ErrorInternalServerError( - "НСподдСрТиваСмый Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ изобраТСния", - )) - } - } -} - -/// БохраняСт Π΄Π°Π½Π½Ρ‹Π΅ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρ‹. -/// -/// ОбновлСна для ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΠ³ΠΎ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° изобраТСния. -pub async fn thumbdata_save( - original_data: Vec, - state: &AppState, - original_filename: &str, - content_type: String, -) -> Result<(), actix_web::Error> { - if content_type.starts_with("image") { - warn!("original file name: {}", original_filename); - let (base_filename, _, extension) = parse_file_path(original_filename); - warn!("detected file extension: {}", extension); - - // Для HEIC Ρ„Π°ΠΉΠ»ΠΎΠ² просто сохраняСм ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π» ΠΊΠ°ΠΊ ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€Ρƒ - if content_type == "image/heic" { - warn!("HEIC file detected, using original as thumbnail"); - let thumb_filename = format!("{}_{}.heic", base_filename, THUMB_WIDTHS[0]); - - if let Err(e) = upload_to_s3( - &state.storj_client, - &state.bucket, - &thumb_filename, - original_data, - &content_type, - ) - .await - { - warn!("cannot save HEIC thumb {}: {}", thumb_filename, e); - return Err(ErrorInternalServerError("cant save HEIC thumbnail")); + // ΠŸΡ‹Ρ‚Π°Π΅ΠΌΡΡ ΠΏΠ°Ρ€ΡΠΈΡ‚ΡŒ ΡˆΠΈΡ€ΠΈΠ½Ρƒ + match width_str.parse::() { + Ok(width) => { + return (base_filename, width, extension); } - return Ok(()); - } - - // Для ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ ΠΊΠ°ΠΊ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ - let img = match image::load_from_memory(&original_data) { - Ok(img) => img, - Err(e) => { - warn!("cannot load image from memory: {}", e); - return Err(ErrorInternalServerError("cant load image")); - } - }; - - warn!("generate thumbnails for {}", original_filename); - let format = determine_image_format(&extension.to_lowercase())?; - - match generate_thumbnails(&img, format).await { - Ok(thumbnails_bytes) => { - for (thumb_width, thumbnail) in thumbnails_bytes { - let thumb_filename = format!("{}_{}.{}", base_filename, thumb_width, extension); - if let Err(e) = upload_to_s3( - &state.storj_client, - &state.bucket, - &thumb_filename, - thumbnail, - &content_type, - ) - .await - { - warn!("cannot load thumb {}: {}", thumb_filename, e); - } - } - } - Err(e) => { - warn!( - "cannot generate thumbnails for {}: {}", - original_filename, e - ); - return Err(e); + Err(_) => { + // Если Π½Π΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΎΡΡŒ ΠΏΠ°Ρ€ΡΠΈΡ‚ΡŒ ΠΊΠ°ΠΊ число, считаСм всС имя Ρ„Π°ΠΉΠ»Π° Π±Π°Π·ΠΎΠ²Ρ‹ΠΌ } } } - Ok(()) + // Если подчСркивания Π½Π΅Ρ‚ ΠΈΠ»ΠΈ ΡˆΠΈΡ€ΠΈΠ½Π° Π½Π΅ парсится, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ всС ΠΊΠ°ΠΊ Π±Π°Π·ΠΎΠ²ΠΎΠ΅ имя + (name_part.to_string(), 0, extension) } -/// Π’Ρ‹Π±ΠΈΡ€Π°Π΅Ρ‚ блиТайший подходящий Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΈΠ· ΠΏΡ€Π΅Π΄ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Ρ…. -/// Если `requested_width` большС максимальной ΡˆΠΈΡ€ΠΈΠ½Ρ‹ Π² `THUMB_WIDTHS`, -/// Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½ΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ. -pub fn find_closest_width(requested_width: u32) -> u32 { - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Π΅Ρ‚ Π»ΠΈ Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½Π°Ρ ΡˆΠΈΡ€ΠΈΠ½Π° ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½ΡƒΡŽ Π΄ΠΎΡΡ‚ΡƒΠΏΠ½ΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ - if requested_width > *THUMB_WIDTHS.last().unwrap() { - return *THUMB_WIDTHS.last().unwrap(); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_file_path() { + // ΠžΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» Π±Π΅Π· ΡˆΠΈΡ€ΠΈΠ½Ρ‹ + let (base, width, ext) = parse_file_path("image.jpg"); + assert_eq!(base, "image"); + assert_eq!(width, 0); + assert_eq!(ext, "jpg"); + + // Π€Π°ΠΉΠ» с ΡˆΠΈΡ€ΠΈΠ½ΠΎΠΉ + let (base, width, ext) = parse_file_path("photo_300.png"); + assert_eq!(base, "photo"); + assert_eq!(width, 300); + assert_eq!(ext, "png"); + + // Π€Π°ΠΉΠ» с нСчисловым суффиксом + let (base, width, ext) = parse_file_path("document_large.pdf"); + assert_eq!(base, "document_large"); + assert_eq!(width, 0); + assert_eq!(ext, "pdf"); + + // Π€Π°ΠΉΠ» Π±Π΅Π· Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ + let (base, width, ext) = parse_file_path("file_100"); + assert_eq!(base, "file"); + assert_eq!(width, 100); + assert_eq!(ext, ""); + + // ΠŸΡƒΡ‚ΡŒ с прСфиксом + let (base, width, ext) = parse_file_path("/uploads/image_800.jpg"); + assert_eq!(base, "uploads/image"); + assert_eq!(width, 800); + assert_eq!(ext, "jpg"); } - - // Находим ΡˆΠΈΡ€ΠΈΠ½Ρƒ с минимальной Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎΠΉ Ρ€Π°Π·Π½ΠΈΡ†Π΅ΠΉ с Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½ΠΎΠΉ - *THUMB_WIDTHS - .iter() - .min_by_key(|&&width| (width as i32 - requested_width as i32).abs()) - .unwrap_or(&THUMB_WIDTHS[0]) // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ самый малСнький Ρ€Π°Π·ΠΌΠ΅Ρ€, Ссли Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ подошло }