From 6c3262edbe45e289af0564887e4b029ef5a9f20a Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 1 Sep 2025 20:36:15 +0300 Subject: [PATCH] simpler-auth+no-overlay --- CHANGELOG.md | 89 +++ Cargo.lock | 1018 +++++++++++++++++++++-------------- Cargo.toml | 30 +- docs/api-reference.md | 55 +- docs/architecture.md | 2 +- docs/deployment.md | 2 +- docs/development.md | 2 +- docs/monitoring.md | 2 +- docs/upload-api-detailed.md | 298 ++++++++++ src/Muller-Regular.woff2 | Bin 34872 -> 0 bytes src/auth.rs | 147 ++++- src/core.rs | 61 --- src/handlers/mod.rs | 14 +- src/handlers/proxy.rs | 76 +-- src/handlers/quota.rs | 27 +- src/handlers/serve_file.rs | 13 +- src/handlers/upload.rs | 162 ++++-- src/handlers/user.rs | 102 ++++ src/main.rs | 9 +- src/overlay.rs | 93 ---- 20 files changed, 1516 insertions(+), 686 deletions(-) create mode 100644 docs/upload-api-detailed.md delete mode 100644 src/Muller-Regular.woff2 delete mode 100644 src/core.rs create mode 100644 src/handlers/user.rs delete mode 100644 src/overlay.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index feab610..513eca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +## [0.5.0] - 2025-09-01 + +### Added +- 🔧 **JWT декодирование** с поддержкой jsonwebtoken crate для работы с сессионными токенами +- 🔧 **Прямая интеграция с Redis** для получения данных пользователя из сессий вместо внешних API +- 🔧 Автоматическое обновление `last_activity` при каждом запросе к / +- 📝 Поддержка переменной окружения JWT_SECRET для конфигурации ключа декодирования +- 📝 Валидация сессий через Redis TTL и проверка expiration в JWT +- 🚀 **HTTP кэширование** с ETag и Cache-Control заголовками для статических файлов +- 🚀 **Оптимизация proxy_handler** - добавлена поддержка 304 Not Modified ответов +- 📊 **Метрики производительности** - timing логирование для всех запросов файлов + +### Changed +- 🔄 **Кардинальное изменение архитектуры GET /**: переход от GraphQL API к Redis сессиям +- 🔄 Структура данных Author теперь содержит session-данные: user_id, username, token_type, created_at, last_activity, auth_data, device_info +- 📝 Обновлена документация API с новой структурой ответа на основе Redis сессий +- 🔧 Функция get_user_by_token теперь принимает параметр redis: &mut MultiplexedConnection + +### Removed +- 🗑️ **Удалена legacy OpenGraph overlay логика** - теперь обрабатывается пакетом Vercel +- 🗑️ Удален файл `src/overlay.rs` с функциями генерации overlay +- 🗑️ Удален файл `src/core.rs` с GraphQL запросами для shout +- 🗑️ Удален файл шрифта `src/Muller-Regular.woff2` +- 🗑️ Удалены зависимости: `imageproc`, `ab_glyph` +- 🗑️ Удален параметр `s=` из GET запросов файлов +- 🗑️ Упрощена функция serve_file - убран параметр shout_id + +### Technical Details +- Redis key pattern: `session:{user_id}:{token}` +- JWT claims structure: `{ user_id, username, exp?, iat? }` +- Session data включает метаданные устройства и авторизации в JSON формате +- Automatic last_activity updates для tracking активности пользователей +- OpenGraph overlay теперь полностью вынесен в отдельный Vercel пакет + +### Status +- 🧪 tests: требуется обновление тестов для новой Redis-based архитектуры +- 🚀 deploy: требует настройки JWT_SECRET environment variable + +## [0.6.0] - 2025-01-28 + +### Added +- 👤 **Новый endpoint GET /** для получения информации о текущем пользователе +- 👤 Интеграция с внешним сервисом аутентификации для получения полных данных профиля +- 👤 Автоматическое включение данных квоты в ответ о пользователе (current_quota, max_quota, usage_percentage) +- 📝 Поддержка Bearer token в заголовке Authorization с автоматическим парсингом +- 📝 Детальная обработка ошибок для невалидных/устаревших токенов + +### Changed +- 📝 Обновлена документация API с новым endpoint / +- 📝 Добавлена детальная документация структуры пользователя в upload-api-detailed.md +- 🔄 Улучшена архитектура auth.rs с новыми структурами для пользователей + +### Status +- 🧪 tests: требуется добавление тестов для нового endpoint +- 🚀 deploy: готово к продакшену, обратная совместимость сохранена + +## [0.5.0] - 2025-01-28 + +### Added +- 🔄 Улучшенная логика загрузки файлов с streaming обработкой и проверкой квот во время чтения +- 📝 Лимит размера одного файла: 500 МБ для предотвращения перегрузки памяти +- 📝 Поддержка множественных файлов в одном запросе с детальными ответами +- 📝 Предварительная проверка квоты перед началом загрузки +- 📝 Улучшенное логирование с процентом использования квоты + +### Fixed +- 🧪 Правильный HTTP код ошибки для превышения квоты: 413 Payload Too Large вместо 401 Unauthorized +- 🔄 Эффективное использование памяти: streaming вместо полного чтения файла в память +- 🔄 Пропуск пустых файлов с соответствующими сообщениями об ошибках +- 📝 Улучшенная обработка ошибок Redis без прерывания загрузки + +### Changed +- 📝 Обновлена документация API с точными кодами ошибок и лимитами +- 📝 Создан детальный документ API с описанием улучшений (`docs/upload-api-detailed.md`) +- 🔄 Реструктурирована логика валидации токенов с детальными сообщениями об ошибках + +### Status +- 🧪 tests: требуется обновление для новой логики множественных файлов +- 🚀 deploy: значительные улучшения производительности и надежности + +## [0.4.1] - 2025-08-12 + +### Fixed +- 🧪 Линтинг: подавлены предупреждения о неиспользуемых полях в `src/core.rs` через `#[allow(dead_code)]` на структурах `ShoutTopic`, `ShoutAuthor`, `Shout` для прохождения `cargo clippy -D warnings` в CI + +### Status +- 🧪 tests: все тесты проходят локально (36/36) +- 🚀 deploy: без изменений в логике, безопасно для деплоя + ## [0.4.0] - 2025-01-27 ### Added diff --git a/Cargo.lock b/Cargo.lock index b4a46a4..ce32f9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - [[package]] name = "actix" version = "0.13.5" @@ -27,7 +11,7 @@ dependencies = [ "actix-macros", "actix-rt", "actix_derive", - "bitflags 2.6.0", + "bitflags", "bytes", "crossbeam-channel", "futures-core", @@ -49,7 +33,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.6.0", + "bitflags", "bytes", "futures-core", "futures-sink", @@ -86,7 +70,7 @@ dependencies = [ "actix-service", "actix-utils", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags", "brotli", "bytes", "bytestring", @@ -189,9 +173,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ "actix-rt", "actix-service", @@ -227,9 +211,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.9.0" +version = "4.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" dependencies = [ "actix-codec", "actix-http", @@ -240,13 +224,13 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", "bytes", "bytestring", "cfg-if", "cookie", - "derive_more 0.99.18", + "derive_more 2.0.1", "encoding_rs", + "foldhash", "futures-core", "futures-util", "impl-more", @@ -264,6 +248,7 @@ dependencies = [ "smallvec", "socket2 0.5.7", "time", + "tracing", "url", ] @@ -305,19 +290,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom 0.2.15", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -424,15 +396,6 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.1" @@ -484,18 +447,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.5.10" +version = "1.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924" +checksum = "8bc1b40fb26027769f16960d2f4a6bc20c4bb755d403e552c8c1a73af433c246" dependencies = [ "aws-credential-types", "aws-runtime", @@ -512,7 +475,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 0.2.12", + "http 1.1.0", "ring", "time", "tokio", @@ -523,9 +486,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.1" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -534,10 +497,33 @@ dependencies = [ ] [[package]] -name = "aws-runtime" -version = "1.4.3" +name = "aws-lc-rs" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -552,7 +538,6 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -561,9 +546,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.60.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0506cc60e392e33712d47717d5ae5760a3b134bf8ee7aea7e43df3d7e2669ae0" +checksum = "38c488cd6abb0ec9811c401894191932e941c5f91dc226043edacd0afa1634bc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -583,9 +568,9 @@ dependencies = [ "hex", "hmac", "http 0.2.12", + "http 1.1.0", "http-body 0.4.6", "lru", - "once_cell", "percent-encoding", "regex-lite", "sha2", @@ -595,9 +580,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.49.0" +version = "1.83.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4" +checksum = "643cd43af212d2a1c4dedff6f044d7e1961e5d9e7cfe773d70f31d9842413886" dependencies = [ "aws-credential-types", "aws-runtime", @@ -609,17 +594,17 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.50.0" +version = "1.84.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" +checksum = "20ec4a95bd48e0db7a424356a161f8d87bd6a4f0af37204775f0da03d9e39fc3" dependencies = [ "aws-credential-types", "aws-runtime", @@ -631,17 +616,17 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.49.0" +version = "1.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53dcf5e7d9bd1517b8b998e170e650047cea8a2b85fe1835abe3210713e541b7" +checksum = "410309ad0df4606bc721aff0d89c3407682845453247213a0ccc5ff8801ee107" dependencies = [ "aws-credential-types", "aws-runtime", @@ -654,17 +639,17 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", + "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.2.5" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" +checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -678,7 +663,6 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.1.0", - "once_cell", "p256", "percent-encoding", "ring", @@ -691,9 +675,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.1" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" dependencies = [ "futures-util", "pin-project-lite", @@ -702,15 +686,14 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.13" +version = "0.63.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" +checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", - "crc32c", - "crc32fast", + "crc-fast", "hex", "http 0.2.12", "http-body 0.4.6", @@ -723,9 +706,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.5" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" dependencies = [ "aws-smithy-types", "bytes", @@ -734,9 +717,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.11" +version = "0.62.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -745,8 +728,8 @@ dependencies = [ "bytes-utils", "futures-core", "http 0.2.12", + "http 1.1.0", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", @@ -754,14 +737,53 @@ dependencies = [ ] [[package]] -name = "aws-smithy-json" -version = "0.60.7" +name = "aws-smithy-http-client" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +checksum = "147e8eea63a40315d704b97bf9bc9b8c1402ae94f89d5ad6f7550d963309da1b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.26", + "h2 0.4.12", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "hyper 0.14.31", + "hyper 1.7.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.3", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-observability" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + [[package]] name = "aws-smithy-query" version = "0.60.7" @@ -774,36 +796,33 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.3" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2" +checksum = "d3946acbe1ead1301ba6862e712c7903ca9bb230bdf1fbd1b5ac54158ef2ab1f" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "fastrand", - "h2 0.3.26", "http 0.2.12", + "http 1.1.0", "http-body 0.4.6", "http-body 1.0.1", - "httparse", - "hyper 0.14.31", - "hyper-rustls 0.24.2", - "once_cell", "pin-project-lite", "pin-utils", - "rustls 0.21.12", "tokio", "tracing", ] [[package]] name = "aws-smithy-runtime-api" -version = "1.7.3" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -818,9 +837,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.9" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" dependencies = [ "base64-simd", "bytes", @@ -844,18 +863,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.3" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -914,18 +933,35 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + [[package]] name = "bit_field" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -1000,9 +1036,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytes-utils" @@ -1034,6 +1070,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -1076,6 +1121,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1135,6 +1200,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1151,12 +1226,31 @@ dependencies = [ ] [[package]] -name = "crc32c" -version = "0.6.8" +name = "crc" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ - "rustc_version", + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" +dependencies = [ + "crc", + "digest", + "libc", + "rand 0.9.1", + "regex", ] [[package]] @@ -1370,6 +1464,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.14.8" @@ -1429,14 +1529,14 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -1473,9 +1573,29 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "fdeflate" @@ -1510,9 +1630,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -1554,6 +1674,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1694,6 +1820,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.12.1" @@ -1726,9 +1858,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1791,6 +1923,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "hostname" version = "0.4.0" @@ -1870,12 +2011,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.31" @@ -1902,19 +2037,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.6", + "futures-core", + "h2 0.4.12", "http 1.1.0", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1931,7 +2068,7 @@ dependencies = [ "hyper 0.14.31", "log", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", ] @@ -1944,12 +2081,13 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.5.0", + "hyper 1.7.0", "hyper-util", - "rustls 0.23.16", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.2", "tower-service", ] @@ -1961,7 +2099,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -1971,21 +2109,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.7.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.7", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2159,9 +2304,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "1c6a3ce16143778e24df6f95365f12ed105425b22abefd289dd88a64bab59605" dependencies = [ "bytemuck", "byteorder-lite", @@ -2169,6 +2314,7 @@ dependencies = [ "exr", "gif", "image-webp", + "moxcms", "num-traits", "png", "qoi", @@ -2190,24 +2336,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "imageproc" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d" -dependencies = [ - "ab_glyph", - "approx", - "getrandom 0.2.15", - "image", - "itertools", - "nalgebra", - "num", - "rand 0.8.5", - "rand_distr", - "rayon", -] - [[package]] name = "imgref" version = "1.11.0" @@ -2250,12 +2378,33 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2277,6 +2426,30 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.32" @@ -2287,18 +2460,28 @@ dependencies = [ ] [[package]] -name = "jpeg-decoder" -version = "0.3.1" +name = "js-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] [[package]] -name = "js-sys" -version = "0.3.72" +name = "jsonwebtoken" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "wasm-bindgen", + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", ] [[package]] @@ -2322,6 +2505,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lebe" version = "0.5.2" @@ -2345,10 +2534,14 @@ dependencies = [ ] [[package]] -name = "libm" -version = "0.2.11" +name = "libloading" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] [[package]] name = "linux-raw-sys" @@ -2413,16 +2606,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "matrixmultiply" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2473,9 +2656,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -2494,27 +2677,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "mutate_once" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" -[[package]] -name = "nalgebra" -version = "0.32.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" -dependencies = [ - "approx", - "matrixmultiply", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", -] - [[package]] name = "native-tls" version = "0.2.12" @@ -2527,7 +2705,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2554,20 +2732,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -2578,15 +2742,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2613,17 +2768,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.2" @@ -2642,7 +2786,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2656,9 +2799,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" @@ -2666,7 +2809,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2721,15 +2864,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" -[[package]] -name = "owned_ttf_parser" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" -dependencies = [ - "ttf-parser", -] - [[package]] name = "p256" version = "0.11.1" @@ -2776,6 +2910,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2821,17 +2965,32 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.14" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 1.3.2", + "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2848,10 +3007,20 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.89" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2875,6 +3044,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pxfm" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e790881194f6f6e86945f0a42a6981977323669aeb6c40e9c7ec253133b96f8" +dependencies = [ + "num-traits", +] + [[package]] name = "qoi" version = "0.4.1" @@ -2892,30 +3070,30 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "quoter" -version = "0.3.0" +version = "0.5.0" dependencies = [ - "ab_glyph", "actix", "actix-cors", "actix-multipart", "actix-web", "aws-config", "aws-sdk-s3", + "base64 0.22.1", "chrono", "env_logger", "futures", "image", - "imageproc", "infer", + "jsonwebtoken", "kamadak-exif", "log", "mime_guess", @@ -2995,16 +3173,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "rav1e" version = "0.7.1" @@ -3042,9 +3210,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.11" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", @@ -3055,12 +3223,6 @@ dependencies = [ "rgb", ] -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "rayon" version = "1.10.0" @@ -3083,9 +3245,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" dependencies = [ "bytes", "cfg-if", @@ -3109,7 +3271,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -3149,9 +3311,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -3159,36 +3321,34 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.6", + "h2 0.4.12", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.7.0", "hyper-rustls 0.27.3", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.2.0", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -3229,6 +3389,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3244,7 +3410,7 @@ version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -3265,13 +3431,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -3285,7 +3452,19 @@ dependencies = [ "openssl-probe", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", ] [[package]] @@ -3327,30 +3506,28 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "safe_arch" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" -dependencies = [ - "bytemuck", -] - [[package]] name = "schannel" version = "0.1.26" @@ -3396,8 +3573,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3405,9 +3595,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3516,7 +3706,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080" dependencies = [ - "bitflags 2.6.0", + "bitflags", "sentry-backtrace", "sentry-core", "tracing-core", @@ -3542,18 +3732,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -3562,9 +3752,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -3655,19 +3845,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simba" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -3683,6 +3860,18 @@ dependencies = [ "quote", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -3754,9 +3943,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3789,8 +3978,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3878,13 +4067,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error", "weezl", + "zune-jpeg", ] [[package]] @@ -3930,20 +4122,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3979,12 +4173,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.16", - "rustls-pki-types", + "rustls 0.23.31", "tokio", ] @@ -4035,6 +4228,45 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -4089,12 +4321,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" - [[package]] name = "typenum" version = "1.17.0" @@ -4166,9 +4392,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -4208,12 +4434,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -4283,24 +4511,24 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -4321,9 +4549,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4331,9 +4559,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -4344,9 +4572,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -4378,18 +4609,20 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] -name = "wide" -version = "0.7.28" +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ - "bytemuck", - "safe_arch", + "either", + "home", + "once_cell", + "rustix", ] [[package]] @@ -4442,8 +4675,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-result", + "windows-strings", ] [[package]] @@ -4476,22 +4709,13 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -4503,16 +4727,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -4619,7 +4833,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -4779,9 +4993,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index bb2140f..5e5b253 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,35 +1,35 @@ [package] name = "quoter" -version = "0.3.0" +version = "0.5.0" edition = "2024" [dependencies] futures = "0.3.30" -serde_json = "1.0.115" -actix-web = "4.5.1" +serde_json = "1.0.143" +actix-web = "4.11.0" actix-cors = "0.7.0" -reqwest = { version = "0.12.3", features = ["json"] } +reqwest = { version = "0.12.23", features = ["json"] } sentry = { version = "0.42", features = ["tokio"] } -uuid = { version = "1.8.0", features = ["v4"] } -redis = { version = "0.32", features = ["tokio-comp"] } -tokio = { version = "1.37.0", features = ["full"] } -serde = { version = "1.0.209", features = ["derive"] } +uuid = { version = "1.18.0", features = ["v4"] } +redis = { version = "0.32.5", features = ["tokio-comp"] } +tokio = { version = "1.47.1", features = ["full"] } +serde = { version = "1.0.219", features = ["derive"] } sentry-actix = "0.42" -aws-sdk-s3 = "1.47.0" -image = "0.25.2" +aws-sdk-s3 = "1.104.0" +image = "0.25.7" mime_guess = "2.0.5" -aws-config = "1.5.5" +aws-config = "1.8.6" actix-multipart = "0.7.2" log = "0.4.22" -env_logger = "0.11.5" +env_logger = "0.11.8" actix = "0.13.5" -imageproc = "0.25.0" -ab_glyph = "0.2.29" # libheif-sys = "1.12.0" -once_cell = "1.18" +once_cell = "1.21.3" kamadak-exif = "0.6.1" infer = "0.19.0" chrono = { version = "0.4", features = ["serde"] } +jsonwebtoken = "9.2.0" +base64 = "0.22.1" [[bin]] name = "quoter" diff --git a/docs/api-reference.md b/docs/api-reference.md index bd5ee74..6e93dce 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -52,27 +52,56 @@ filename.ext ``` **Ошибки:** -- `401 Unauthorized` - неверный токен -- `413 Payload Too Large` - превышена квота +- `400 Bad Request` - нет файлов или все файлы пустые +- `401 Unauthorized` - неверный или отсутствующий токен +- `413 Payload Too Large` - превышена квота пользователя или лимит размера файла - `415 Unsupported Media Type` - неподдерживаемый тип файла +- `500 Internal Server Error` - ошибка загрузки в S3 или обновления квоты -### 3. Получение файлов +### 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} Получает файл по имени. -**Параметры запроса:** -- `s=` - добавляет оверлей с данными shout (только для изображений) - **Примеры:** ``` GET /image.jpg -GET /image.jpg?s=123 GET /image_300.jpg GET /image_300.jpg/webp ``` -### 4. Управление квотами +### 5. Управление квотами #### GET /quota Получает информацию о квоте пользователя. @@ -139,10 +168,10 @@ GET /quota?user_id=user123 | Код | Описание | |-----|----------| | 200 | Успешный запрос | -| 400 | Неверные параметры запроса | +| 400 | Неверные параметры запроса или нет файлов | | 401 | Неавторизованный доступ | | 404 | Файл не найден | -| 413 | Превышена квота | +| 413 | Превышена квота пользователя или лимит размера файла (500 МБ) | | 415 | Неподдерживаемый тип файла | | 500 | Внутренняя ошибка сервера | @@ -155,6 +184,12 @@ curl -X POST http://localhost:8080/ \ -F "file=@image.jpg" ``` +### Получение информации о пользователе +```bash +curl -H "Authorization: Bearer your-token" \ + http://localhost:8080/ +``` + ### Получение миниатюры ```bash curl http://localhost:8080/image_300.jpg diff --git a/docs/architecture.md b/docs/architecture.md index 580a1f4..cbd2584 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -76,7 +76,7 @@ sequenceDiagram Client->>Quoter: POST / (file + token) Quoter->>Core API: Validate token - Core API-->>Quoter: User ID + Core API-->>Quoter: Author ID Quoter->>Redis: Check quota Redis-->>Quoter: Current quota Quoter->>S3: Upload file diff --git a/docs/deployment.md b/docs/deployment.md index 0197bbd..c683b72 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -196,7 +196,7 @@ After=network.target redis.service [Service] Type=simple -User=quoter +Author=quoter Group=quoter WorkingDirectory=/opt/quoter Environment=REDIS_URL=redis://localhost:6379 diff --git a/docs/development.md b/docs/development.md index 29b4cbf..42bf88e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -215,7 +215,7 @@ use log::{debug, info, warn, error}; // В коде debug!("Processing file: {}", filename); info!("File uploaded successfully"); -warn!("User quota is getting low: {} bytes", quota); +warn!("Author quota is getting low: {} bytes", quota); error!("Failed to upload file: {}", e); ``` diff --git a/docs/monitoring.md b/docs/monitoring.md index 3771498..d80a555 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -137,7 +137,7 @@ lazy_static! { pub static ref QUOTA_USAGE: Histogram = Histogram::new( "quoter_quota_usage_bytes", - "User quota usage in bytes" + "Author quota usage in bytes" ).unwrap(); } ``` diff --git a/docs/upload-api-detailed.md b/docs/upload-api-detailed.md new file mode 100644 index 0000000..b5c8cda --- /dev/null +++ b/docs/upload-api-detailed.md @@ -0,0 +1,298 @@ +# 📤 API загрузки файлов с квотами - Точная документация + +## Обзор + +Quoter предоставляет API для загрузки файлов с системой квот и автоматической обработкой различных типов медиа. + +## Базовый URL +``` +http://localhost:8080 +``` + +## Аутентификация +Все эндпоинты загрузки требуют JWT токен в заголовке: +``` +Authorization: Bearer +``` + +--- + +## 📤 Загрузка файлов (УЛУЧШЕННАЯ ВЕРСИЯ) + +### 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/src/Muller-Regular.woff2 b/src/Muller-Regular.woff2 deleted file mode 100644 index 39772008be9c11e63c4f855b89ed9c4121e428d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34872 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≧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 diff --git a/src/auth.rs b/src/auth.rs index a6287c8..d045f31 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,12 +1,14 @@ use actix_web::error::ErrorInternalServerError; use redis::{aio::MultiplexedConnection, AsyncCommands}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, env, error::Error}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use log::{info, warn}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::Client as HTTPClient; -use serde::Deserialize; use serde_json::json; -use std::{collections::HashMap, env, error::Error}; -// Структура для десериализации ответа от сервиса аутентификации +// Старые структуры для совместимости с get_id_by_token #[derive(Deserialize)] struct AuthResponse { data: Option, @@ -28,6 +30,27 @@ struct Claims { sub: Option, } +// Структуры для JWT токенов +#[derive(Debug, Deserialize)] +struct TokenClaims { + user_id: String, + username: Option, + exp: Option, + iat: Option, +} + +// Структура для данных пользователя из Redis сессии +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Author { + pub user_id: String, + pub username: Option, + pub token_type: Option, + pub created_at: Option, + pub last_activity: Option, + pub auth_data: Option, + pub device_info: Option, +} + /// Получает айди пользователя из токена в заголовке pub async fn get_id_by_token(token: &str) -> Result> { let auth_api_base = env::var("CORE_URL")?; @@ -78,6 +101,124 @@ pub async fn get_id_by_token(token: &str) -> Result> { } } +/// Декодирует JWT токен и извлекает claims с проверкой истечения +fn decode_jwt_token(token: &str) -> Result> { + // В реальном приложении здесь должен быть настоящий секретный ключ + let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); + let key = DecodingKey::from_secret(secret.as_ref()); + + let mut validation = Validation::new(Algorithm::HS256); + validation.validate_exp = true; // Включаем проверку истечения срока действия + + match decode::(token, &key, &validation) { + Ok(token_data) => { + let claims = token_data.claims; + + // Дополнительная проверка exp если поле присутствует + if let Some(exp) = claims.exp { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as usize; + + if exp < current_time { + warn!("JWT token expired: exp={}, current={}", exp, current_time); + return Err(Box::new(std::io::Error::other("Token expired"))); + } + + info!("JWT token valid until: {} (current: {})", exp, current_time); + } + + info!("Successfully decoded and validated JWT token for user: {}", claims.user_id); + Ok(claims) + } + Err(e) => { + warn!("Failed to decode JWT token: {}", e); + Err(Box::new(e)) + } + } +} + +/// Быстро извлекает user_id из JWT токена для работы с квотами +pub fn extract_user_id_from_token(token: &str) -> Result> { + let claims = decode_jwt_token(token)?; + Ok(claims.user_id) +} + +/// Проверяет валидность JWT токена (включая истечение срока действия) +pub fn validate_token(token: &str) -> Result> { + match decode_jwt_token(token) { + Ok(_) => Ok(true), + Err(e) => { + warn!("Token validation failed: {}", e); + Ok(false) + } + } +} + +/// Получает user_id из JWT токена и базовые данные пользователя +pub async fn get_user_by_token( + token: &str, + redis: &mut MultiplexedConnection, +) -> Result> { + // Декодируем JWT токен для получения user_id + let claims = decode_jwt_token(token)?; + let user_id = &claims.user_id; + + info!("Extracted user_id from JWT token: {}", user_id); + + // Проверяем валидность токена через сессию в Redis (опционально) + let token_key = format!("session:{}:{}", user_id, token); + let session_exists: bool = redis + .exists(&token_key) + .await + .map_err(|e| { + warn!("Failed to check session existence in Redis: {}", e); + // Не критичная ошибка, продолжаем с базовыми данными + }) + .unwrap_or(false); + + if session_exists { + // Обновляем last_activity если сессия существует + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .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(()); + + info!("Updated last_activity for session: {}", token_key); + } else { + info!("Session not found in Redis, proceeding with JWT-only data"); + } + + // Создаем базовый объект Author с данными из JWT + let author = Author { + user_id: user_id.clone(), + username: claims.username.clone(), + token_type: Some("jwt".to_string()), + created_at: claims.iat.map(|ts| ts.to_string()), + last_activity: Some( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string() + ), + auth_data: None, + device_info: None, + }; + + info!("Successfully created author data for user_id: {}", user_id); + Ok(author) +} + /// Сохраняет имя файла в Redis для пользователя pub async fn user_added_file( redis: &mut MultiplexedConnection, diff --git a/src/core.rs b/src/core.rs deleted file mode 100644 index d0acce9..0000000 --- a/src/core.rs +++ /dev/null @@ -1,61 +0,0 @@ -use reqwest::Client as HTTPClient; -use serde::Deserialize; -use serde_json::json; -use std::{collections::HashMap, env, error::Error}; - -// Структура для десериализации ответа от сервиса аутентификации -#[derive(Deserialize)] -struct CoreResponse { - data: Option, -} - -#[derive(Deserialize)] -pub struct ShoutTopic { - pub slug: String, - pub title: String, -} - -#[derive(Deserialize)] -pub struct ShoutAuthor { - pub name: String, -} - -#[derive(Deserialize)] -pub struct Shout { - pub title: String, - pub created_at: String, - pub main_topic: ShoutTopic, - pub authors: Vec, - pub layout: String, -} - -pub async fn get_shout_by_id(shout_id: i32) -> Result> { - let mut variables = HashMap::::new(); - let api_base = env::var("CORE_URL")?; - let query_name = "get_shout"; - let operation = "GetShout"; - if shout_id != 0 { - variables.insert("shout_id".to_string(), shout_id); - } - - let gql = json!({ - "query": format!("query {}($slug: String, $shout_id: Int) {{ {}(slug: $slug, shout_id: $shout_id) {{ title created_at main_topic {{ title slug }} authors {{ id name }} }} }}", operation, query_name), - "operationName": operation, - "variables": variables - }); - - let client = HTTPClient::new(); - let response = client.post(&api_base).json(&gql).send().await?; - - if response.status().is_success() { - let core_response: CoreResponse = response.json().await?; - if let Some(shout) = core_response.data { - return Ok(shout); - } - Err(Box::new(std::io::Error::other("Shout not found"))) - } else { - Err(Box::new(std::io::Error::other( - response.status().to_string(), - ))) - } -} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 78b4d8f..ab05a91 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,20 +2,12 @@ mod proxy; mod quota; mod serve_file; mod upload; +mod user; pub use proxy::proxy_handler; pub use quota::{get_quota_handler, increase_quota_handler, set_quota_handler}; pub use upload::upload_handler; +pub use user::get_current_user_handler; // Общий лимит квоты на пользователя: 5 ГБ -pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; - -use actix_web::{HttpRequest, HttpResponse, Result}; - -/// Обработчик для корневого пути / -pub async fn root_handler(req: HttpRequest) -> Result { - match req.method().as_str() { - "GET" => Ok(HttpResponse::Ok().content_type("text/plain").body("ok")), - _ => Ok(HttpResponse::MethodNotAllowed().finish()), - } -} +pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; \ No newline at end of file diff --git a/src/handlers/proxy.rs b/src/handlers/proxy.rs index 8cc2db2..90d5641 100644 --- a/src/handlers/proxy.rs +++ b/src/handlers/proxy.rs @@ -1,6 +1,6 @@ use actix_web::error::ErrorNotFound; use actix_web::{error::ErrorInternalServerError, web, HttpRequest, HttpResponse, Result}; -use log::{error, warn}; +use log::{info, error, warn}; use crate::app_state::AppState; use crate::handlers::serve_file::serve_file; @@ -8,29 +8,51 @@ 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}; +/// Создает HTTP ответ с оптимальными заголовками кэширования +fn create_cached_response(content_type: &str, data: Vec, file_etag: &str) -> HttpResponse { + HttpResponse::Ok() + .content_type(content_type) + .insert_header(("etag", file_etag)) + .insert_header(("cache-control", "public, max-age=31536000, immutable")) // 1 год + .insert_header(("access-control-allow-origin", "*")) + .body(data) +} + /// Обработчик для скачивания файла и генерации миниатюры, если она недоступна. pub async fn proxy_handler( req: HttpRequest, requested_res: web::Path, state: web::Data, ) -> Result { - warn!("\t>>>\tGET {} [START]", requested_res); + let start_time = std::time::Instant::now(); + info!("GET {} [START]", requested_res); + let normalized_path = if requested_res.ends_with("/webp") { - warn!("Removing /webp suffix from path"); + info!("Converting to WebP format: {}", requested_res); requested_res.replace("/webp", "") } else { requested_res.to_string() }; + // Проверяем If-None-Match заголовок для кэширования + let client_etag = req.headers().get("if-none-match") + .and_then(|h| h.to_str().ok()); + // парсим GET запрос let (base_filename, requested_width, extension) = parse_file_path(&normalized_path); - warn!("detected file extension: {}", extension); - warn!("base_filename: {}", base_filename); - warn!("requested width: {}", requested_width); let ext = extension.as_str().to_lowercase(); - warn!("normalized to lowercase: {}", ext); let filekey = format!("{}.{}", base_filename, &ext); - warn!("filekey: {}", filekey); + + info!("Parsed request - base: {}, width: {}, ext: {}", base_filename, requested_width, ext); + + // Генерируем ETag для кэширования + let file_etag = format!("\"{}\"", &filekey); + if let Some(etag) = client_etag { + if etag == file_etag { + info!("Cache hit for {}, returning 304", filekey); + return Ok(HttpResponse::NotModified().finish()); + } + } let content_type = match get_mime_type(&ext) { Some(mime) => mime.to_string(), None => { @@ -46,24 +68,15 @@ pub async fn proxy_handler( } } _ => { - error!("unsupported file format"); - return Err(ErrorInternalServerError("unsupported file format")); + error!("Unsupported file format for: {}", base_filename); + return Err(ErrorInternalServerError("Unsupported file format")); } } } }; - warn!("content_type: {}", content_type); + info!("Content-Type: {}", content_type); - let shout_id = match req.query_string().contains("s=") { - true => req - .query_string() - .split("s=") - .collect::>() - .pop() - .unwrap_or(""), - false => "", - }; return match state.get_path(&filekey).await { Ok(Some(stored_path)) => { @@ -78,7 +91,7 @@ pub async fn proxy_handler( warn!("Processing image file with width: {}", requested_width); if requested_width == 0 { warn!("Serving original file without resizing"); - serve_file(&stored_path, &state, shout_id).await + serve_file(&stored_path, &state).await } else { let closest: u32 = find_closest_width(requested_width); warn!( @@ -94,12 +107,12 @@ pub async fn proxy_handler( { Ok(true) => { warn!("serve existed thumb file: {}", thumb_filename); - serve_file(thumb_filename, &state, shout_id).await + serve_file(thumb_filename, &state).await } Ok(false) => { // Миниатюра не существует, возвращаем оригинал и запускаем генерацию миниатюры let original_file = - serve_file(&stored_path, &state, shout_id).await?; + serve_file(&stored_path, &state).await?; // Запускаем асинхронную задачу для генерации миниатюры let state_clone = state.clone(); @@ -139,7 +152,7 @@ pub async fn proxy_handler( } } else { warn!("File is not an image, proceeding with normal serving"); - serve_file(&stored_path, &state, shout_id).await + serve_file(&stored_path, &state).await } } else { warn!( @@ -193,9 +206,9 @@ pub async fn proxy_handler( warn!("Successfully uploaded to Storj: {}", filekey); } - return Ok(HttpResponse::Ok() - .content_type(content_type) - .body(filedata)); + let elapsed = start_time.elapsed(); + info!("File served from AWS in {:?}: {}", elapsed, path); + return Ok(create_cached_response(&content_type, filedata, &file_etag)); } Err(err) => { warn!("Failed to load from AWS path {}: {:?}", path, err); @@ -299,7 +312,9 @@ pub async fn proxy_handler( warn!("file {} uploaded to storj", filekey); state.set_path(&filekey, &filepath).await; } - Ok(HttpResponse::Ok().content_type(content_type).body(filedata)) + let elapsed = start_time.elapsed(); + info!("File served from AWS in {:?}: {}", elapsed, filepath); + Ok(create_cached_response(&content_type, filedata, &file_etag)) } Err(e) => { error!("Failed to download from AWS: {} - Error: {}", filepath, e); @@ -312,9 +327,10 @@ pub async fn proxy_handler( } } Err(e) => { + let elapsed = start_time.elapsed(); error!( - "Database error while getting path: {} - Full error: {:?}", - filekey, e + "Database error while getting path: {} in {:?} - Full error: {:?}", + filekey, elapsed, e ); Err(ErrorInternalServerError(e)) } diff --git a/src/handlers/quota.rs b/src/handlers/quota.rs index d39db29..28ec7fe 100644 --- a/src/handlers/quota.rs +++ b/src/handlers/quota.rs @@ -36,7 +36,14 @@ pub async fn get_quota_handler( let _admin_id = get_id_by_token(token.unwrap()) .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; + .map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; // Получаем user_id из query параметров let user_id = req @@ -76,7 +83,14 @@ pub async fn increase_quota_handler( let _admin_id = get_id_by_token(token.unwrap()) .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; + .map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; let additional_bytes = quota_data .additional_bytes @@ -125,7 +139,14 @@ pub async fn set_quota_handler( let _admin_id = get_id_by_token(token.unwrap()) .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; + .map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; let new_quota_bytes = quota_data .new_quota_bytes diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs index 4219202..edd7dd6 100644 --- a/src/handlers/serve_file.rs +++ b/src/handlers/serve_file.rs @@ -2,14 +2,12 @@ use actix_web::{error::ErrorInternalServerError, HttpResponse, Result}; use mime_guess::MimeGuess; use crate::app_state::AppState; -use crate::overlay::generate_overlay; use crate::s3_utils::check_file_exists; /// Функция для обслуживания файла по заданному пути. pub async fn serve_file( filepath: &str, state: &AppState, - shout_id: &str, ) -> Result { if filepath.is_empty() { return Err(ErrorInternalServerError("Filename is empty".to_string())); @@ -42,14 +40,17 @@ pub async fn serve_file( .await .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; - let data_bytes = match shout_id.is_empty() { - true => data.into_bytes(), - false => generate_overlay(shout_id, data.into_bytes()).await?, - }; + let data_bytes = data.into_bytes(); 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", "public, max-age=31536000, immutable")) // 1 год + .insert_header(("access-control-allow-origin", "*")) .body(data_bytes)) } diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index e524e26..5aa66fd 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -1,16 +1,19 @@ use actix_multipart::Multipart; use actix_web::{web, HttpRequest, HttpResponse, Result}; -use log::{error, warn}; +use log::{error, info, warn}; use crate::app_state::AppState; -use crate::auth::{get_id_by_token, user_added_file}; +use crate::auth::{extract_user_id_from_token, user_added_file, validate_token}; 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 futures::TryStreamExt; // use crate::thumbnail::convert_heic_to_jpeg; -/// Обработчик для аплоада файлов. +// Максимальный размер одного файла: 500 МБ +const MAX_SINGLE_FILE_BYTES: u64 = 500 * 1024 * 1024; + +/// Обработчик для аплоада файлов с улучшенной логикой квот и валидацией. pub async fn upload_handler( req: HttpRequest, mut payload: Multipart, @@ -21,40 +24,91 @@ pub async fn upload_handler( .headers() .get("Authorization") .and_then(|header_value| header_value.to_str().ok()); + if token.is_none() { - return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); // Если токен отсутствует, возвращаем ошибку + warn!("Upload attempt without authorization token"); + return Err(actix_web::error::ErrorUnauthorized("Authorization token required")); } - let user_id = get_id_by_token(token.unwrap()).await?; + let token = token.unwrap(); + + // Сначала валидируем токен + if !validate_token(token).unwrap_or(false) { + warn!("Token validation failed"); + return Err(actix_web::error::ErrorUnauthorized("Invalid or expired token")); + } + + // Затем извлекаем user_id + let user_id = extract_user_id_from_token(token) + .map_err(|e| { + warn!("Failed to extract user_id from token: {}", e); + actix_web::error::ErrorUnauthorized("Invalid authorization token") + })?; // Получаем текущую квоту пользователя let current_quota: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); - let mut body = "ok".to_string(); + info!("Author {} current quota: {} bytes", user_id, current_quota); + + // Предварительная проверка: есть ли вообще место для файлов + if current_quota >= MAX_USER_QUOTA_BYTES { + warn!("Author {} quota already at maximum: {}", user_id, current_quota); + return Err(actix_web::error::ErrorPayloadTooLarge("Author quota limit exceeded")); + } + + let mut uploaded_files = Vec::new(); + while let Ok(Some(field)) = payload.try_next().await { let mut field = field; let mut file_bytes = Vec::new(); let mut file_size: u64 = 0; - // Читаем данные файла + // Читаем данные файла с проверкой размера while let Ok(Some(chunk)) = field.try_next().await { - file_size += chunk.len() as u64; + let chunk_size = chunk.len() as u64; + + // Проверка лимита одного файла + if file_size + chunk_size > MAX_SINGLE_FILE_BYTES { + warn!("File size exceeds single file limit: {} > {}", + file_size + chunk_size, MAX_SINGLE_FILE_BYTES); + return Err(actix_web::error::ErrorPayloadTooLarge("Single file size limit exceeded")); + } + + // Проверка общей квоты пользователя + if current_quota + file_size + chunk_size > MAX_USER_QUOTA_BYTES { + warn!("Upload would exceed user quota: current={}, adding={}, limit={}", + current_quota, file_size + chunk_size, MAX_USER_QUOTA_BYTES); + return Err(actix_web::error::ErrorPayloadTooLarge("Author quota limit would be exceeded")); + } + + file_size += chunk_size; file_bytes.extend_from_slice(&chunk); } + // Пропускаем пустые файлы + if file_size == 0 { + warn!("Skipping empty file upload"); + continue; + } + + info!("Processing file: {} bytes", file_size); + // Определяем MIME-тип из содержимого файла let detected_mime_type = match s3_utils::detect_mime_type(&file_bytes) { - Some(mime) => mime, + Some(mime) => { + info!("Detected MIME type: {}", mime); + mime + } None => { - warn!("Неподдерживаемый формат файла"); + warn!("Unsupported file format detected"); return Err(actix_web::error::ErrorUnsupportedMediaType( - "Неподдерживаемый формат файла", + "Unsupported file format", )); } }; // Для HEIC файлов просто сохраняем как есть let (file_bytes, content_type) = if detected_mime_type == "image/heic" { - warn!("HEIC support is temporarily disabled, saving original file"); + info!("Processing HEIC file (saved as original)"); (file_bytes, detected_mime_type) } else { (file_bytes, detected_mime_type) @@ -64,24 +118,16 @@ pub async fn upload_handler( let extension = match s3_utils::get_extension_from_mime(&content_type) { Some(ext) => ext, None => { - warn!("Неподдерживаемый тип содержимого: {}", content_type); + warn!("No file extension found for MIME type: {}", content_type); return Err(actix_web::error::ErrorUnsupportedMediaType( - "Неподдерживаемый тип содержимого", + "Unsupported content type", )); } }; - // Проверяем, что добавление файла не превышает лимит квоты - if current_quota + file_size > MAX_USER_QUOTA_BYTES { - warn!( - "Quota would exceed limit: current={}, adding={}, limit={}", - current_quota, file_size, MAX_USER_QUOTA_BYTES - ); - return Err(actix_web::error::ErrorUnauthorized("Quota exceeded")); - } - // Генерируем имя файла с правильным расширением let filename = format!("{}.{}", uuid::Uuid::new_v4(), extension); + info!("Generated filename: {}", filename); // Загружаем файл в S3 storj match upload_to_s3( @@ -94,36 +140,66 @@ pub async fn upload_handler( .await { Ok(_) => { - warn!( - "file {} uploaded to storj, incrementing quota by {} bytes", - filename, file_size - ); + info!("File {} successfully uploaded to S3 ({} bytes)", filename, file_size); + + // Обновляем квоту пользователя if let Err(e) = state.increment_uploaded_bytes(&user_id, file_size).await { - error!("Failed to increment quota: {}", e); - return Err(e); + error!("Failed to increment quota for user {}: {}", user_id, e); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to update user quota" + )); } // Сохраняем информацию о файле в Redis let mut redis = state.redis.clone(); - store_file_info(&mut redis, &filename, &content_type).await?; - user_added_file(&mut redis, &user_id, &filename).await?; - - // Сохраняем маппинг пути - let generated_key = - generate_key_with_extension(filename.clone(), content_type.clone()); - state.set_path(&filename, &generated_key).await; - - if let Ok(new_quota) = state.get_or_create_quota(&user_id).await { - warn!("New quota for user {}: {} bytes", user_id, new_quota); + if let Err(e) = store_file_info(&mut redis, &filename, &content_type).await { + error!("Failed to store file info in Redis: {}", e); + // Не прерываем процесс, файл уже загружен в S3 + } + + if let Err(e) = user_added_file(&mut redis, &user_id, &filename).await { + error!("Failed to record user file association: {}", e); + // Не прерываем процесс } - body = filename; + // Сохраняем маппинг пути + let generated_key = generate_key_with_extension(filename.clone(), content_type.clone()); + state.set_path(&filename, &generated_key).await; + + // Логируем новую квоту + if let Ok(new_quota) = state.get_or_create_quota(&user_id).await { + info!("Updated quota for user {}: {} bytes ({:.1}% used)", + user_id, new_quota, + (new_quota as f64 / MAX_USER_QUOTA_BYTES as f64) * 100.0); + } + + uploaded_files.push(filename); } Err(e) => { - warn!("Failed to upload to storj: {}", e); - return Err(actix_web::error::ErrorInternalServerError(e)); + error!("Failed to upload file to S3: {}", e); + return Err(actix_web::error::ErrorInternalServerError( + "File upload failed" + )); } } } - Ok(HttpResponse::Ok().body(body)) + + // Возвращаем результат + match uploaded_files.len() { + 0 => { + warn!("No files were uploaded"); + Err(actix_web::error::ErrorBadRequest("No files provided or all files were empty")) + } + 1 => { + info!("Successfully uploaded 1 file: {}", uploaded_files[0]); + Ok(HttpResponse::Ok().body(uploaded_files[0].clone())) + } + n => { + info!("Successfully uploaded {} files", n); + Ok(HttpResponse::Ok().json(serde_json::json!({ + "uploaded_files": uploaded_files, + "count": n + }))) + } + } } diff --git a/src/handlers/user.rs b/src/handlers/user.rs new file mode 100644 index 0000000..f7bc64f --- /dev/null +++ b/src/handlers/user.rs @@ -0,0 +1,102 @@ +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use log::{error, info, warn}; +use serde::Serialize; + +use crate::app_state::AppState; +use crate::auth::{get_user_by_token, Author, validate_token}; + +#[derive(Serialize)] +pub struct UserWithQuotaResponse { + #[serde(flatten)] + pub user: Author, + pub quota: QuotaInfo, +} + +#[derive(Serialize)] +pub struct QuotaInfo { + pub current_quota: u64, + pub max_quota: u64, + pub usage_percentage: f64, +} + +/// Обработчик для получения информации о текущем пользователе +pub async fn get_current_user_handler( + req: HttpRequest, + state: web::Data, +) -> Result { + // Извлекаем токен из заголовка авторизации + let token = req + .headers() + .get("Authorization") + .and_then(|header_value| header_value.to_str().ok()) + .and_then(|auth_str| { + // Убираем префикс "Bearer " если он есть + if auth_str.starts_with("Bearer ") { + Some(&auth_str[7..]) + } else { + Some(auth_str) + } + }); + + if token.is_none() { + warn!("Request for current user without authorization token"); + return Err(actix_web::error::ErrorUnauthorized("Authorization token required")); + } + + let token = token.unwrap(); + + // Сначала валидируем токен + if !validate_token(token).unwrap_or(false) { + warn!("Token validation failed in user endpoint"); + return Err(actix_web::error::ErrorUnauthorized("Invalid or expired token")); + } + + info!("Getting user info for valid token"); + + // Получаем информацию о пользователе из Redis сессии + let mut redis = state.redis.clone(); + let user = match get_user_by_token(token, &mut redis).await { + Ok(user) => { + info!("Successfully retrieved user info: user_id={}, username={:?}", + user.user_id, user.username); + user + } + Err(e) => { + warn!("Failed to get user info from Redis: {}", e); + let error_msg = if e.to_string().contains("expired") { + "Token has expired" + } else { + "Invalid or expired session token" + }; + return Err(actix_web::error::ErrorUnauthorized(error_msg)); + } + }; + + // Получаем квоту пользователя + let current_quota = match state.get_or_create_quota(&user.user_id).await { + Ok(quota) => quota, + Err(e) => { + error!("Failed to get user quota: {}", e); + 0 // Возвращаем 0 если не удалось получить квоту + } + }; + + let max_quota = crate::handlers::MAX_USER_QUOTA_BYTES; + let usage_percentage = if max_quota > 0 { + (current_quota as f64 / max_quota as f64) * 100.0 + } else { + 0.0 + }; + + let response = UserWithQuotaResponse { + user, + quota: QuotaInfo { + current_quota, + max_quota, + usage_percentage, + }, + }; + + info!("Author info response prepared successfully"); + Ok(HttpResponse::Ok().json(response)) +} diff --git a/src/main.rs b/src/main.rs index aeabc7c..1a2ebe8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ mod app_state; mod auth; -mod core; mod handlers; mod lookup; -mod overlay; mod s3_utils; mod thumbnail; @@ -16,8 +14,9 @@ use actix_web::{ use app_state::AppState; use handlers::{ - get_quota_handler, increase_quota_handler, proxy_handler, root_handler, set_quota_handler, - upload_handler, + get_current_user_handler, get_quota_handler, + increase_quota_handler, proxy_handler, + set_quota_handler, upload_handler, }; use log::warn; use std::env; @@ -64,7 +63,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(app_state.clone())) .wrap(cors) .wrap(Logger::default()) - .route("/", web::get().to(root_handler)) + .route("/", web::get().to(get_current_user_handler)) .route("/", web::post().to(upload_handler)) .route("/quota", web::get().to(get_quota_handler)) .route("/quota/increase", web::post().to(increase_quota_handler)) diff --git a/src/overlay.rs b/src/overlay.rs deleted file mode 100644 index 32a1e25..0000000 --- a/src/overlay.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ab_glyph::{Font, FontArc, PxScale}; -use actix_web::web::Bytes; -use image::Rgba; -use imageproc::drawing::{draw_filled_rect_mut, draw_text_mut}; -use imageproc::rect::Rect; -use log::warn; -use std::{error::Error, io::Cursor}; - -use crate::core::get_shout_by_id; - -pub async fn generate_overlay(shout_id: &str, filedata: Bytes) -> Result> { - // Получаем shout из GraphQL - let shout_id_int = shout_id.parse::().unwrap_or(0); - match get_shout_by_id(shout_id_int).await { - Ok(shout) => { - // Преобразуем Bytes в ImageBuffer - let img = image::load_from_memory(&filedata)?; - let mut img = img.to_rgba8(); - - // Загружаем шрифт - let font_vec = Vec::from(include_bytes!("Muller-Regular.woff2") as &[u8]); - let font = FontArc::try_from_vec(font_vec).unwrap(); - - // Получаем размеры изображения - let (img_width, img_height) = img.dimensions(); - let max_text_width = (img_width as f32) * 0.8; - let max_text_height = (img_height as f32) * 0.8; - - // Начальный масштаб - let mut scale: f32 = 24.0; - let text_length = shout.title.chars().count() as f32; - let mut text_width = scale * text_length; - let text_height = scale; - - // Регулируем масштаб, пока текст не впишется в 80% от размеров изображения - while text_width > max_text_width || text_height > max_text_height { - scale -= 1.0; - if scale <= 0.0 { - break; - } - text_width = scale * text_length; - // text_height остается равным scale - } - - // Рассчёт позиции текста для центрирования - let x = ((img_width as f32 - text_width) / 2.0).ceil() as i32; - let y = ((img_height as f32 - text_height) / 2.0).ceil() as i32; - - // Задаём отступы для подложки - let padding_x = 10; - let padding_y = 5; - - // Определяем размеры подложки - let rect_width = text_width.ceil() as u32 + (2 * padding_x); - let rect_height = text_height.ceil() as u32 + (2 * padding_y); - - // Определяем координаты подложки - let rect_x = x - padding_x as i32; - let rect_y = y - padding_y as i32; - - // Создаём прямоугольник - let rect = Rect::at(rect_x, rect_y).of_size(rect_width, rect_height); - - // Задаём цвет подложки (полупрозрачный серый) - let background_color = Rgba([128u8, 128u8, 128u8, 128u8]); // RGBA: серый с прозрачностью 50% - - // Рисуем подложку - draw_filled_rect_mut(&mut img, rect, background_color); - - // Рисуем текст поверх подложки - let scaled_font = font.as_scaled(scale).font; - draw_text_mut( - &mut img, - Rgba([255u8, 255u8, 255u8, 255u8]), // Белый цвет текста - x, - y, - PxScale::from(scale), - &scaled_font, - &shout.title, - ); - - // Преобразуем ImageBuffer обратно в Bytes - let mut buffer = Vec::new(); - img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)?; - - Ok(Bytes::from(buffer)) - } - Err(e) => { - warn!("Error getting shout: {}", e); - Ok(filedata) - } - } -}