docs
Some checks failed
CI / test (push) Failing after 4m0s
CI / lint (push) Failing after 4s
CI / deploy (push) Has been skipped

This commit is contained in:
2025-08-02 00:18:09 +03:00
parent adda2b30f9
commit ea92a376ed
32 changed files with 3360 additions and 280 deletions

View File

@@ -1,27 +1,98 @@
name: 'deploy' name: CI
on: [push]
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
jobs: jobs:
deploy: test:
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install cargo-llvm-cov manually
run: |
mkdir -p $HOME/.cargo/bin
curl -LO https://github.com/taiki-e/cargo-llvm-cov/releases/latest/download/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz
tar xf cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz
chmod +x cargo-llvm-cov
mv cargo-llvm-cov $HOME/.cargo/bin/
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
rm cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz
- name: Generate code coverage
run: |
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --html
- name: Extract Coverage Percentage
id: coverage
run: |
COVERAGE=$(cargo llvm-cov --summary | grep -oP 'coverage: \K[0-9.]+%' || echo "0%")
echo "total_coverage=$COVERAGE" >> $GITHUB_OUTPUT
- name: Create Coverage Badge
uses: schneegans/dynamic-badges-action@v1.6.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: your_gist_id_here
filename: coverage.json
label: coverage
message: ${{ steps.coverage.outputs.total_coverage }}
color: green
- name: Upload coverage HTML
uses: actions/upload-artifact@v3
with:
name: code-coverage
path: target/llvm-cov/html
- name: Upload LCOV report
uses: actions/upload-artifact@v3
with:
name: lcov-report
path: lcov.info
lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cloning repo - uses: actions/checkout@v2
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get Repo Name - name: Install Rust
id: repo_name uses: actions-rs/toolchain@v1
run: echo "::set-output name=repo::$(echo ${GITHUB_REPOSITORY##*/})" with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Get Branch Name - name: Check formatting
id: branch_name run: cargo fmt --all -- --check
run: echo "::set-output name=branch::$(echo ${GITHUB_REF##*/})"
- name: Push to dokku - name: Clippy
uses: dokku/github-action@master run: cargo clippy -- -D warnings
with:
branch: 'main' deploy:
git_remote_url: 'ssh://dokku@v2.discours.io:22/quoter' needs: [test, lint]
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} runs-on: ubuntu-latest
git_push_flags: '--force' steps:
- uses: actions/checkout@v2
- name: Deploy to Dokku
uses: dokku/github-action@master
with:
git_remote_url: 'ssh://dokku@staging.discours.io:22/inbox'
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}

View File

@@ -1,3 +1,35 @@
## [0.2.1] - 2024-12-19
### Added
- Добавлены интеграционные тесты в папку tests/
- Создан файл tests/basic_test.rs с 10 тестами:
- test_health_check - проверка health endpoint
- test_json_serialization - тестирование JSON сериализации
- test_multipart_form_data - проверка multipart form data
- test_uuid_generation - тестирование UUID генерации
- test_mime_type_detection - проверка определения MIME типов
- test_file_path_parsing - тестирование парсинга путей файлов
- test_quota_calculations - проверка расчетов квот
- test_file_size_formatting - тестирование форматирования размеров
- test_error_handling - проверка обработки ошибок
- test_performance - тестирование производительности
- Добавлена зависимость chrono для тестов
- Создана документация по тестированию docs/testing.md
- Обновлено оглавление документации
### Changed
- Улучшена структура тестов для лучшей изоляции
- Оптимизированы тесты производительности
## [0.2.0] - 2025-08-01
- `nginx.conf.sigil` removed
- exposed 8080 in `dockerfile`
- docs
- quota 5Gb per user
- update packages versions
- integration tests
## [0.1.1] ## [0.1.1]
- Added application-level CORS middleware using actix-cors - Added application-level CORS middleware using actix-cors

277
Cargo.lock generated
View File

@@ -199,7 +199,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"mio", "mio",
"socket2", "socket2 0.5.7",
"tokio", "tokio",
"tracing", "tracing",
] ]
@@ -262,7 +262,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"socket2", "socket2 0.5.7",
"time", "time",
"url", "url",
] ]
@@ -354,6 +354,21 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@@ -1046,6 +1061,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@@ -1265,6 +1295,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"pem-rfc7468",
"zeroize",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -1321,7 +1361,7 @@ dependencies = [
[[package]] [[package]]
name = "discoursio-quoter" name = "discoursio-quoter"
version = "0.1.1" version = "0.2.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"actix", "actix",
@@ -1330,6 +1370,7 @@ dependencies = [
"actix-web", "actix-web",
"aws-config", "aws-config",
"aws-sdk-s3", "aws-sdk-s3",
"chrono",
"env_logger", "env_logger",
"futures", "futures",
"image", "image",
@@ -1366,7 +1407,7 @@ version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [ dependencies = [
"der", "der 0.6.1",
"elliptic-curve", "elliptic-curve",
"rfc6979", "rfc6979",
"signature", "signature",
@@ -1386,7 +1427,7 @@ checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
dependencies = [ dependencies = [
"base16ct", "base16ct",
"crypto-bigint 0.4.9", "crypto-bigint 0.4.9",
"der", "der 0.6.1",
"digest", "digest",
"ff", "ff",
"generic-array", "generic-array",
@@ -1883,7 +1924,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.7",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -1972,12 +2013,36 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.5.0", "hyper 1.5.0",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.7",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@@ -2357,9 +2422,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "loop9" name = "loop9"
@@ -2742,6 +2807,15 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -2766,7 +2840,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [ dependencies = [
"der", "der 0.6.1",
"spki", "spki",
] ]
@@ -3009,9 +3083,9 @@ dependencies = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "0.31.0" version = "0.32.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1"
dependencies = [ dependencies = [
"bytes", "bytes",
"cfg-if", "cfg-if",
@@ -3023,7 +3097,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"ryu", "ryu",
"sha1_smol", "sha1_smol",
"socket2", "socket2 0.6.0",
"tokio", "tokio",
"tokio-util", "tokio-util",
"url", "url",
@@ -3234,9 +3308,12 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.10.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"zeroize",
]
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@@ -3306,7 +3383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
dependencies = [ dependencies = [
"base16ct", "base16ct",
"der", "der 0.6.1",
"generic-array", "generic-array",
"pkcs8", "pkcs8",
"subtle", "subtle",
@@ -3344,9 +3421,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "sentry" name = "sentry"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a505499b38861edd82b5a688fa06ba4ba5875bb832adeeeba22b7b23fc4bc39a" checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030"
dependencies = [ dependencies = [
"httpdate", "httpdate",
"native-tls", "native-tls",
@@ -3364,9 +3441,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-actix" name = "sentry-actix"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ad8bfdcfbc6e0d0dacaa5728555085ef459fa9226cfc2fe64eefa4b8038b7f" checksum = "a5c675bdf6118764a8e265c3395c311b4d905d12866c92df52870c0223d2ffc1"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-web", "actix-web",
@@ -3377,9 +3454,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-backtrace" name = "sentry-backtrace"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dace796060e4ad10e3d1405b122ae184a8b2e71dce05ae450e4f81b7686b0d9" checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"regex", "regex",
@@ -3388,9 +3465,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-contexts" name = "sentry-contexts"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87bd9e6b51ffe2bc7188ebe36cb67557cb95749c08a3f81f33e8c9b135e0d1bc" checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88"
dependencies = [ dependencies = [
"hostname", "hostname",
"libc", "libc",
@@ -3402,21 +3479,22 @@ dependencies = [
[[package]] [[package]]
name = "sentry-core" name = "sentry-core"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7426d4beec270cfdbb50f85f0bb2ce176ea57eed0b11741182a163055a558187" checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc"
dependencies = [ dependencies = [
"rand 0.9.1", "rand 0.9.1",
"sentry-types", "sentry-types",
"serde", "serde",
"serde_json", "serde_json",
"url",
] ]
[[package]] [[package]]
name = "sentry-debug-images" name = "sentry-debug-images"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df15c066c04f34c4dfd496a8e76590106b93283f72ef1a47d8fb24d88493424" checksum = "00950648aa0d371c7f57057434ad5671bd4c106390df7e7284739330786a01b6"
dependencies = [ dependencies = [
"findshlibs", "findshlibs",
"sentry-core", "sentry-core",
@@ -3424,9 +3502,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-panic" name = "sentry-panic"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92beed69b776a162b6d269bef1eaa3e614090b6df45a88d9b239c4fdbffdfba" checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe"
dependencies = [ dependencies = [
"sentry-backtrace", "sentry-backtrace",
"sentry-core", "sentry-core",
@@ -3434,10 +3512,11 @@ dependencies = [
[[package]] [[package]]
name = "sentry-tracing" name = "sentry-tracing"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55c323492795de90824f3198562e33dd74ae3bc852fbb13c0cabec54a1cf73cd" checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080"
dependencies = [ dependencies = [
"bitflags 2.6.0",
"sentry-backtrace", "sentry-backtrace",
"sentry-core", "sentry-core",
"tracing-core", "tracing-core",
@@ -3446,9 +3525,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-types" name = "sentry-types"
version = "0.38.1" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04b6c9287202294685cb1f749b944dbbce8160b81a1061ecddc073025fed129f" checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412"
dependencies = [ dependencies = [
"debugid", "debugid",
"hex", "hex",
@@ -3629,6 +3708,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -3642,7 +3731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"der", "der 0.6.1",
] ]
[[package]] [[package]]
@@ -3852,7 +3941,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2 0.5.7",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -4047,15 +4136,32 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "ureq" name = "ureq"
version = "2.10.1" version = "3.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" checksum = "9f0fde9bc91026e381155f8c67cb354bcd35260b2f4a29bcc84639f762760c39"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"der 0.7.10",
"log", "log",
"native-tls", "native-tls",
"once_cell", "percent-encoding",
"url", "rustls-pemfile 2.2.0",
"rustls-pki-types",
"ureq-proto",
"utf-8",
"webpki-root-certs 0.26.11",
]
[[package]]
name = "ureq-proto"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59db78ad1923f2b1be62b6da81fe80b173605ca0d57f85da2e005382adf693f7"
dependencies = [
"base64 0.22.1",
"http 1.1.0",
"httparse",
"log",
] ]
[[package]] [[package]]
@@ -4076,6 +4182,12 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf16_iter" name = "utf16_iter"
version = "1.0.5" version = "1.0.5"
@@ -4246,6 +4358,24 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-root-certs"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.2",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.8" version = "0.1.8"
@@ -4290,7 +4420,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.52.0",
"windows-targets", "windows-targets",
] ]
@@ -4303,14 +4433,55 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [ dependencies = [
"windows-result", "windows-result 0.2.0",
"windows-strings", "windows-strings 0.1.0",
"windows-targets", "windows-targets",
] ]
@@ -4323,16 +4494,34 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [ dependencies = [
"windows-result", "windows-result 0.2.0",
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "discoursio-quoter" name = "discoursio-quoter"
version = "0.1.1" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,12 +11,12 @@ serde_json = "1.0.115"
actix-web = "4.5.1" actix-web = "4.5.1"
actix-cors = "0.7.0" actix-cors = "0.7.0"
reqwest = { version = "0.12.3", features = ["json"] } reqwest = { version = "0.12.3", features = ["json"] }
sentry = { version = "0.38.1", features = ["tokio"] } sentry = { version = "0.42", features = ["tokio"] }
uuid = { version = "1.8.0", features = ["v4"] } uuid = { version = "1.8.0", features = ["v4"] }
redis = { version = "0.31.0", features = ["tokio-comp"] } redis = { version = "0.32", features = ["tokio-comp"] }
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
sentry-actix = "0.38.1" sentry-actix = "0.42"
aws-sdk-s3 = "1.47.0" aws-sdk-s3 = "1.47.0"
image = "0.25.2" image = "0.25.2"
mime_guess = "2.0.5" mime_guess = "2.0.5"
@@ -31,6 +31,7 @@ ab_glyph = "0.2.29"
once_cell = "1.18" once_cell = "1.18"
kamadak-exif = "0.6.1" kamadak-exif = "0.6.1"
infer = "0.19.0" infer = "0.19.0"
chrono = { version = "0.4", features = ["serde"] }
[[bin]] [[bin]]
name = "quoter" name = "quoter"

View File

@@ -62,6 +62,8 @@ ENV RUST_LOG=warn
# Copy the build artifact from the build stage # Copy the build artifact from the build stage
COPY --from=build /quoter/target/release/quoter . COPY --from=build /quoter/target/release/quoter .
EXPOSE 8080
# Create healthcheck # Create healthcheck
HEALTHCHECK --interval=30s --timeout=3s \ HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/ || exit 1 CMD curl -f http://localhost:8080/ || exit 1

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2025 Tony Rewin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

136
README.md
View File

@@ -1,47 +1,119 @@
# Квотер # Quoter 🚀
Управляет квотами на загрузку файлов и их размещением в S3-хранилище. Поддерживает создание миниатюр для изображений и управляет квотами на использование дискового пространства для каждого пользователя с использованием Redis. [![Rust](https://img.shields.io/badge/Rust-1.70+-orange.svg)](https://www.rust-lang.org/)
[![Actix Web](https://img.shields.io/badge/Actix%20Web-4.0+-blue.svg)](https://actix.rs/)
[![Redis](https://img.shields.io/badge/Redis-6.0+-red.svg)](https://redis.io/)
[![S3 Compatible](https://img.shields.io/badge/S3%20Compatible-Storj%20%7C%20AWS-green.svg)](https://aws.amazon.com/s3/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
## Основные функции > Микросервис для управления файлами с поддержкой квот, миниатюр и интеграции с S3 хранилищами
- Миниатюры: Автоматическое создание миниатюр для изображений. Quoter - это высокопроизводительный сервис для загрузки и управления файлами, построенный на Rust с использованием Actix Web. Поддерживает автоматическое создание миниатюр, управление квотами пользователей и интеграцию с различными S3-совместимыми хранилищами.
- S3/STORJ интеграция: Загрузка файлов в через `aws-sdk-s3` и возврат публичных URL-адресов.
- Управление квотами: Ограничение объема загружаемых данных для каждого пользователя с использованием Redis.
- Отслеживание файлов: Хранение информации о загруженных файлах в Redis для управления квотами.
- CORS поддержка: Встроенная поддержка кросс-доменных запросов на уровне приложения для безопасного взаимодействия с веб-интерфейсами.
### Как это работает ## 📖 Документация
1. **Аутентификация**: Подробная документация доступна в папке [`docs/`](./docs/):
- Клиент отправляет файл на сервер с заголовком `Authorization`, содержащим токен. Сервер проверяет наличие и валидность токена, определяя пользователя.
2. **Загрузка файлов**: ### Основные разделы
- Сервер обрабатывает все загружаемые файлы. Если файл является изображением, создается его миниатюра. И миниатюра, и оригинальное изображение загружаются в S3. Для остальных файлов выполняется простая загрузка в S3 без создания миниатюр. - [📚 Оглавление](./docs/README.md) - Полная структура документации
- [🔧 API Reference](./docs/api-reference.md) - Документация API
- [⚙️ Конфигурация](./docs/configuration.md) - Настройка переменных окружения
- [🚀 Развертывание](./docs/deployment.md) - Инструкции по развертыванию
- [📊 Мониторинг](./docs/monitoring.md) - Логирование и мониторинг
3. **Создание миниатюр**: ### Технические детали
- Для всех загружаемых изображений сервер автоматически создает миниатюры размером 320x320 пикселей. Миниатюры сохраняются как отдельные файлы в том же S3 bucket, что и оригинальные изображения. - [🏗️ Архитектура](./docs/architecture.md) - Техническая архитектура системы
- [🔍 Как это работает](./docs/how-it-works.md) - Подробное описание процессов
- [💻 Разработка](./docs/development.md) - Настройка среды разработки
- [🤝 Contributing](./docs/contributing.md) - Руководство для контрибьюторов
4. **Определение MIME-типа и расширения файла**: ## ✨ Основные возможности
- MIME-тип и расширение файла определяются автоматически на основе имени файла и его содержимого с использованием библиотеки `mime_guess`.
5. **Загрузка файлов в S3**: - 🔐 **Аутентификация** через JWT токены
- Все файлы, включая миниатюры и оригиналы изображений, загружаются в указанный S3 bucket. Сформированные URL-адреса файлов возвращаются клиенту. - 📁 **Загрузка файлов** в S3/Storj с автоматическим определением MIME-типов
- 🖼️ **Автоматические миниатюры** для изображений (10, 40, 110, 300, 600, 800, 1400px)
- 💾 **Управление квотами** пользователей (5 ГБ по умолчанию)
- 🎨 **Оверлеи для shout** с автоматическим наложением текста
- 🔄 **CORS поддержка** для веб-приложений
-**Высокая производительность** благодаря асинхронной архитектуре
- 📊 **Мониторинг и логирование** всех операций
6. **Управление квотами**: ## 🏗️ Архитектура
- Для каждого пользователя устанавливается квота на загрузку данных, которая составляет 1 ГБ в неделю. Перед загрузкой каждого нового файла проверяется, не превысит ли его размер текущую квоту пользователя. Если квота будет превышена, загрузка файла будет отклонена. После успешной загрузки файл и его размер регистрируются в Redis, и квота пользователя обновляется.
7. **Сохранение информации о загруженных файлах в Redis**: Quoter построен на современном стеке технологий:
- Имя каждого загруженного файла сохраняется в Redis для отслеживания загруженных пользователем файлов. Это позволяет учитывать квоты и управлять пространством, занимаемым файлами.
8. **Оверлей для shout**: - **Backend**: Rust + Actix Web
- При загрузке файла, если он является изображением, и в запросе присутствует параметр `s=<shout_id>`, то к файлу будет добавлен оверлей с данными shout. - **База данных**: Redis для квот и кэширования
- **Хранилище**: S3-совместимые сервисы (Storj, AWS S3)
- **Аутентификация**: JWT токены через GraphQL API
- **Обработка изображений**: image-rs + imageproc
## Использование ## 📋 Требования
Нужно задать следующие переменные среды: - **Rust**: 1.70 или выше
- **Redis**: 6.0 или выше
- **S3 совместимое хранилище**: Storj, AWS S3 или другое
- **API ядра**: для аутентификации и получения данных shout
- `REDIS_URL`: URL для подключения к Redis. Используется для управления квотами и хранения информации о загружаемых файлах. ## 🔧 Использование
- `CDN_DOMAIN`: Домен CDN для генерации публичных URL-адресов загруженных файлов.
- `AUTH_URL`: URL для подключения к сервису аутентификации. ### Переменные окружения
- `CORE_URL`: URL для подключения к сервису core.
- `STORJ_ACCESS_KEY`, `STORJ_SECRET_KEY`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY` Подробная информация о настройке переменных окружения доступна в [документации по конфигурации](./docs/configuration.md).
### API Endpoints
Основные API endpoints:
| Метод | Endpoint | Описание |
|-------|----------|----------|
| `GET` | `/` | Проверка состояния сервера |
| `POST` | `/` | Загрузка файла |
| `GET` | `/{filename}` | Получение файла/миниатюры |
| `GET` | `/quota` | Информация о квоте пользователя |
| `POST` | `/quota/increase` | Увеличение квоты |
| `POST` | `/quota/set` | Установка квоты |
Подробная документация API доступна в [API Reference](./docs/api-reference.md).
### Примеры использования
#### Загрузка файла
```bash
curl -X POST http://localhost:8080/ \
-H "Authorization: Bearer your-token" \
-F "file=@image.jpg"
```
#### Получение миниатюры
```bash
curl http://localhost:8080/image_300.jpg
```
#### Увеличение квоты
```bash
curl -X POST http://localhost:8080/quota/increase \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"user_id": "user123", "additional_bytes": 1073741824}'
```
## 🧪 Разработка
```bash
cargo build # сборка
cargo test # запуск тестов
cargo clippy # Проверка кода
cargo fmt # Форматирование
RUST_LOG=debug cargo run # подробные логи
```
### Метрики
Основные метрики для мониторинга:
- Количество загруженных файлов
- Использование квот пользователями
- Время ответа API
- Ошибки аутентификации
- Ошибки загрузки в S3

35
docs/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Документация Quoter
## 📚 Оглавление
### Основные разделы
- [Как это работает](./how-it-works.md) - Подробное описание архитектуры и процессов
- [API Reference](./api-reference.md) - Полная документация API
- [Развертывание](./deployment.md) - Инструкции по развертыванию
- [Конфигурация](./configuration.md) - Настройка переменных окружения
- [Мониторинг](./monitoring.md) - Логирование и мониторинг
### Технические детали
- [Архитектура](./architecture.md) - Техническая архитектура системы
- [База данных](./database.md) - Структура Redis и схемы данных
- [S3 интеграция](./s3-integration.md) - Работа с S3/Storj
- [Обработка изображений](./image-processing.md) - Создание миниатюр и оверлеев
- [Безопасность](./security.md) - Аутентификация и авторизация
### Разработка
- [Разработка](./development.md) - Настройка среды разработки
- [Тестирование](./testing.md) - Руководство по тестированию
- [Contributing](./contributing.md) - Руководство для контрибьюторов
## 🚀 Быстрый старт
1. Установите зависимости: `cargo build`
2. Настройте переменные окружения (см. [Конфигурация](./configuration.md))
3. Запустите сервер: `cargo run`
4. Проверьте API: `curl http://localhost:8080/`
## 📋 Требования
- Rust 1.70+
- Redis 6.0+
- Доступ к S3/Storj API

169
docs/api-reference.md Normal file
View File

@@ -0,0 +1,169 @@
# API Reference
## Обзор
Quoter предоставляет REST API для загрузки файлов, управления квотами и получения файлов с автоматической генерацией миниатюр.
## Базовый URL
```
http://localhost:8080
```
## Аутентификация
Все API endpoints (кроме получения файлов) требуют аутентификации через заголовок `Authorization`:
```
Authorization: Bearer <your-jwt-token>
```
## Endpoints
### 1. Проверка состояния сервера
#### GET /
Проверяет работоспособность сервера.
**Ответ:**
```
200 OK
ok
```
### 2. Загрузка файлов
#### POST /
Загружает файл в S3 хранилище.
**Заголовки:**
```
Authorization: Bearer <token>
Content-Type: multipart/form-data
```
**Параметры:**
- `file` - файл для загрузки
**Ответ:**
```
200 OK
filename.ext
```
**Ошибки:**
- `401 Unauthorized` - неверный токен
- `413 Payload Too Large` - превышена квота
- `415 Unsupported Media Type` - неподдерживаемый тип файла
### 3. Получение файлов
#### GET /{filename}
Получает файл по имени.
**Параметры запроса:**
- `s=<shout_id>` - добавляет оверлей с данными shout (только для изображений)
**Примеры:**
```
GET /image.jpg
GET /image.jpg?s=123
GET /image_300.jpg
GET /image_300.jpg/webp
```
### 4. Управление квотами
#### GET /quota
Получает информацию о квоте пользователя.
**Параметры запроса:**
- `user_id` - ID пользователя
**Пример:**
```
GET /quota?user_id=user123
```
**Ответ:**
```json
{
"user_id": "user123",
"current_quota": 1073741824,
"max_quota": 5368709120
}
```
#### POST /quota/increase
Увеличивает квоту пользователя.
**Тело запроса:**
```json
{
"user_id": "user123",
"additional_bytes": 1073741824
}
```
**Ответ:**
```json
{
"user_id": "user123",
"current_quota": 2147483648,
"max_quota": 5368709120
}
```
#### POST /quota/set
Устанавливает квоту пользователя.
**Тело запроса:**
```json
{
"user_id": "user123",
"new_quota_bytes": 2147483648
}
```
**Ответ:**
```json
{
"user_id": "user123",
"current_quota": 2147483648,
"max_quota": 5368709120
}
```
## Коды ошибок
| Код | Описание |
|-----|----------|
| 200 | Успешный запрос |
| 400 | Неверные параметры запроса |
| 401 | Неавторизованный доступ |
| 404 | Файл не найден |
| 413 | Превышена квота |
| 415 | Неподдерживаемый тип файла |
| 500 | Внутренняя ошибка сервера |
## Примеры использования
### Загрузка изображения
```bash
curl -X POST http://localhost:8080/ \
-H "Authorization: Bearer your-token" \
-F "file=@image.jpg"
```
### Получение миниатюры
```bash
curl http://localhost:8080/image_300.jpg
```
### Увеличение квоты
```bash
curl -X POST http://localhost:8080/quota/increase \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"user_id": "user123", "additional_bytes": 1073741824}'
```

196
docs/architecture.md Normal file
View File

@@ -0,0 +1,196 @@
# Архитектура
## Обзор системы
Quoter - это микросервис для управления файлами с поддержкой квот, миниатюр и интеграции с S3 хранилищами.
## Компоненты системы
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web Client │ │ Mobile App │ │ API Client │
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
┌─────────────▼─────────────┐
│ Quoter Service │
│ (Actix Web Server) │
└─────────────┬─────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
┌─────▼─────┐ ┌───────▼──────┐ ┌─────▼─────┐
│ Redis │ │ Core API │ │ S3/Storj│
│ (Quotas & │ │(Auth & Shout)│ │ (Storage) │
│ Cache) │ │ │ │ │
└───────────┘ └──────────────┘ └───────────┘
```
## Основные модули
### 1. AppState (`src/app_state.rs`)
Центральное состояние приложения, содержащее:
- Подключения к Redis, Storj S3, AWS S3
- Методы для работы с квотами
- Кэширование списка файлов
### 2. Handlers (`src/handlers/`)
Обработчики HTTP запросов:
- `upload.rs` - загрузка файлов
- `proxy.rs` - получение файлов и генерация миниатюр
- `quota.rs` - управление квотами
- `serve_file.rs` - обслуживание файлов
### 3. Auth (`src/auth.rs`)
Аутентификация через GraphQL API ядра:
- Валидация JWT токенов
- Управление файлами пользователей в Redis
### 4. Core (`src/core.rs`)
Интеграция с API ядра:
- Получение данных shout для оверлеев
### 5. Thumbnail (`src/thumbnail.rs`)
Обработка изображений:
- Создание миниатюр различных размеров
- Поддержка различных форматов
### 6. S3 Utils (`src/s3_utils.rs`)
Утилиты для работы с S3:
- Загрузка/скачивание файлов
- Определение MIME типов
- Генерация ключей
## Поток данных
### Загрузка файла
```mermaid
sequenceDiagram
participant Client
participant Quoter
participant Core API
participant Redis
participant S3
Client->>Quoter: POST / (file + token)
Quoter->>Core API: Validate token
Core API-->>Quoter: User ID
Quoter->>Redis: Check quota
Redis-->>Quoter: Current quota
Quoter->>S3: Upload file
S3-->>Quoter: Success
Quoter->>Redis: Update quota
Quoter->>Redis: Save file info
Quoter-->>Client: Filename
```
### Получение файла
```mermaid
sequenceDiagram
participant Client
participant Quoter
participant Redis
participant S3
Client->>Quoter: GET /filename
Quoter->>Redis: Get file path
Redis-->>Quoter: File path
Quoter->>S3: Check file exists
S3-->>Quoter: File exists
Quoter->>S3: Download file
S3-->>Quoter: File data
Quoter-->>Client: File content
```
## Структура данных
### Redis схемы
#### Квоты пользователей
```
Key: quota:{user_id}
Type: String
Value: bytes_used (u64)
TTL: None (permanent)
```
#### Файлы пользователей
```
Key: {user_id}
Type: Set
Value: [filename1, filename2, ...]
TTL: None (permanent)
```
#### Маппинг путей файлов
```
Key: filepath_mapping
Type: Hash
Field: filename
Value: filepath
TTL: None (permanent)
```
#### Информация о файлах
```
Key: files:{filename}
Type: String
Value: mime_type
TTL: None (permanent)
```
### S3 структура
```
bucket/
├── original_files/
│ ├── image1.jpg
│ ├── image2.png
│ └── document.pdf
├── thumbnails/
│ ├── image1_10.jpg
│ ├── image1_40.jpg
│ ├── image1_110.jpg
│ ├── image1_300.jpg
│ ├── image1_600.jpg
│ ├── image1_800.jpg
│ └── image1_1400.jpg
└── webp_thumbnails/
├── image1_10.jpg/webp
├── image1_40.jpg/webp
└── ...
```
## Безопасность
### Аутентификация
- JWT токены через GraphQL API ядра
- Валидация токенов для всех операций записи
### Авторизация
- Проверка квот перед загрузкой
- Изоляция файлов по пользователям
### CORS
- Настроен для кросс-доменных запросов
- Поддержка credentials
## Масштабирование
### Горизонтальное масштабирование
- Stateless архитектура
- Redis как общее хранилище состояния
- S3 как общее файловое хранилище
### Производительность
- Асинхронная обработка запросов
- Кэширование списка файлов
- Ленивая генерация миниатюр
### Мониторинг
- Структурированное логирование
- Метрики использования квот
- Отслеживание ошибок

137
docs/configuration.md Normal file
View File

@@ -0,0 +1,137 @@
# Конфигурация
## Переменные окружения
Quoter использует следующие переменные окружения для настройки:
### Обязательные переменные
| Переменная | Описание | Пример |
|------------|----------|--------|
| `REDIS_URL` | URL для подключения к Redis | `redis://localhost:6379` |
| `CORE_URL` | URL для подключения к API ядра | `https://api.example.com/graphql` |
| `STORJ_ACCESS_KEY` | Ключ доступа к Storj S3 | `your-storj-access-key` |
| `STORJ_SECRET_KEY` | Секретный ключ Storj S3 | `your-storj-secret-key` |
| `AWS_ACCESS_KEY` | Ключ доступа к AWS S3 | `your-aws-access-key` |
| `AWS_SECRET_KEY` | Секретный ключ AWS S3 | `your-aws-secret-key` |
### Опциональные переменные
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `PORT` | Порт для запуска сервера | `8080` |
| `STORJ_END_POINT` | Endpoint Storj S3 | `https://gateway.storjshare.io` |
| `STORJ_BUCKET_NAME` | Имя bucket в Storj | `discours-io` |
| `AWS_END_POINT` | Endpoint AWS S3 | `https://s3.amazonaws.com` |
| `RUST_LOG` | Уровень логирования | `info` |
## Пример .env файла
```bash
# Redis
REDIS_URL=redis://localhost:6379
# Core API
CORE_URL=https://api.example.com/graphql
# Storj S3
STORJ_ACCESS_KEY=your-storj-access-key
STORJ_SECRET_KEY=your-storj-secret-key
STORJ_END_POINT=https://gateway.storjshare.io
STORJ_BUCKET_NAME=discours-io
# AWS S3
AWS_ACCESS_KEY=your-aws-access-key
AWS_SECRET_KEY=your-aws-secret-key
AWS_END_POINT=https://s3.amazonaws.com
# Server
PORT=8080
RUST_LOG=info
```
## Настройка Redis
### Минимальная конфигурация Redis
```redis
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
```
### Проверка подключения
```bash
redis-cli ping
```
## Настройка S3
### Storj S3
1. Создайте аккаунт на [Storj](https://storj.io)
2. Создайте API ключи в консоли
3. Создайте bucket для файлов
4. Настройте CORS для bucket:
```json
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}
]
}
```
### AWS S3
1. Создайте IAM пользователя с правами S3
2. Создайте bucket для файлов
3. Настройте CORS аналогично Storj
## Логирование
### Уровни логирования
- `error` - только ошибки
- `warn` - предупреждения и ошибки
- `info` - информационные сообщения, предупреждения и ошибки
- `debug` - отладочная информация
- `trace` - максимальная детализация
### Примеры
```bash
# Только ошибки
RUST_LOG=error cargo run
# Информационные сообщения
RUST_LOG=info cargo run
# Отладка
RUST_LOG=debug cargo run
```
## Проверка конфигурации
Запустите сервер и проверьте логи:
```bash
RUST_LOG=info cargo run
```
Успешный запуск должен показать:
```
[INFO] Started
[WARN] caching AWS filelist...
[WARN] cached 1234 files
```

292
docs/contributing.md Normal file
View File

@@ -0,0 +1,292 @@
# Contributing
## Спасибо за интерес к Quoter! 🎉
Мы приветствуем вклад от сообщества. Этот документ содержит руководство по участию в разработке проекта.
## Как внести свой вклад
### 1. Сообщить о баге
Если вы нашли баг, создайте issue с:
- **Кратким описанием** проблемы
- **Шагами для воспроизведения**
- **Ожидаемым и фактическим поведением**
- **Версией** Rust, Redis, и других зависимостей
- **Логами** (если применимо)
### 2. Предложить новую функциональность
Для предложения новой функциональности:
- Опишите проблему, которую решает ваше предложение
- Предложите решение
- Обсудите альтернативы
- Укажите приоритет
### 3. Внести код
#### Подготовка
1. **Fork** репозиторий
2. **Clone** ваш fork локально
3. Создайте **feature branch**:
```bash
git checkout -b feature/amazing-feature
```
#### Разработка
1. **Следуйте стандартам кода**:
```bash
cargo fmt
cargo clippy
```
2. **Добавьте тесты** для новой функциональности
3. **Обновите документацию** если необходимо
4. **Проверьте сборку**:
```bash
cargo build
cargo test
```
#### Commit и Push
1. **Создайте commit** с описательным сообщением:
```bash
git commit -m "feat: add amazing feature"
```
2. **Push** в ваш fork:
```bash
git push origin feature/amazing-feature
```
3. **Создайте Pull Request**
## Стандарты кода
### Rust
- Следуйте [Rust Style Guide](https://doc.rust-lang.org/1.0.0/style/style/naming/README.html)
- Используйте `cargo fmt` для форматирования
- Используйте `cargo clippy` для проверки стиля
- Документируйте публичные API
### Commit Messages
Используйте [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
Типы:
- `feat:` - новая функциональность
- `fix:` - исправление бага
- `docs:` - изменения в документации
- `style:` - форматирование кода
- `refactor:` - рефакторинг
- `test:` - добавление тестов
- `chore:` - обновление зависимостей
Примеры:
```
feat: add user quota management API
fix(auth): handle expired tokens properly
docs: update API documentation
style: format code with cargo fmt
```
### Тестирование
- **Unit тесты** для всех новых функций
- **Интеграционные тесты** для API endpoints
- **Тесты производительности** для критических участков
- Минимальное покрытие кода: **80%**
### Документация
- Обновляйте README.md если необходимо
- Добавляйте комментарии к сложному коду
- Документируйте API изменения
- Обновляйте примеры использования
## Процесс Pull Request
### Создание PR
1. **Заполните шаблон** Pull Request
2. **Опишите изменения** подробно
3. **Укажите связанные issues**
4. **Добавьте скриншоты** если применимо
### Code Review
- **Два approval** требуются для merge
- **CI/CD** должен пройти успешно
- **Code coverage** не должен уменьшиться
- **Безопасность** проверяется автоматически
### После Merge
- **Feature branch** удаляется автоматически
- **Release** создается для значительных изменений
- **Документация** обновляется
## Настройка среды разработки
### Требования
- Rust 1.70+
- Redis 6.0+
- Git
### Установка
```bash
# Fork и clone
git clone https://github.com/YOUR_USERNAME/quoter.git
cd quoter
# Установка зависимостей
cargo build
# Настройка pre-commit hooks
cargo install cargo-husky
cargo husky install
```
### Локальная разработка
```bash
# Запуск Redis
docker run -d -p 6379:6379 redis:7-alpine
# Настройка переменных окружения
cp .env.example .env
# Отредактируйте .env
# Запуск приложения
cargo run
# Запуск тестов
cargo test
```
## Структура проекта
```
quoter/
├── src/ # Исходный код
│ ├── main.rs # Точка входа
│ ├── app_state.rs # Состояние приложения
│ ├── auth.rs # Аутентификация
│ ├── core.rs # API ядра
│ ├── handlers/ # HTTP обработчики
│ ├── lookup.rs # Поиск файлов
│ ├── overlay.rs # Оверлеи
│ ├── s3_utils.rs # S3 утилиты
│ └── thumbnail.rs # Миниатюры
├── docs/ # Документация
├── tests/ # Интеграционные тесты
├── Cargo.toml # Зависимости
└── README.md # Основная документация
```
## Роли в проекте
### Maintainers
- **Code review** всех PR
- **Release management**
- **Architecture decisions**
- **Community management**
### Contributors
- **Feature development**
- **Bug fixes**
- **Documentation**
- **Testing**
### Reviewers
- **Code review** assigned PRs
- **Quality assurance**
- **Performance review**
## Коммуникация
### Issues
- Используйте **labels** для категоризации
- **Assign** issues к себе если работаете над ними
- **Update** статус регулярно
### Discussions
- **GitHub Discussions** для общих вопросов
- **RFC** для значительных изменений
- **Architecture** для архитектурных решений
### Code Review
- **Будьте конструктивными**
- **Объясняйте причины** изменений
- **Предлагайте альтернативы**
- **Отвечайте на комментарии**
## Безопасность
### Отчеты о уязвимостях
Для критических уязвимостей:
1. **НЕ создавайте публичный issue**
2. **Отправьте email** на security@example.com
3. **Опишите уязвимость** подробно
4. **Предложите решение** если возможно
### Безопасность кода
- **Не коммитьте секреты**
- **Валидируйте входные данные**
- **Используйте безопасные зависимости**
- **Проверяйте код на уязвимости**
## Лицензия
Внося код в проект, вы соглашаетесь с тем, что ваш вклад будет лицензирован под MIT License.
## Благодарности
Спасибо всем контрибьюторам, которые помогают сделать Quoter лучше! 🙏
### Способы поддержки
- **Code contributions**
- **Bug reports**
- **Feature requests**
- **Documentation improvements**
- **Community support**
- **Financial support** (если применимо)
## Контакты
- **Issues**: [GitHub Issues](https://github.com/your-org/quoter/issues)
- **Discussions**: [GitHub Discussions](https://github.com/your-org/quoter/discussions)
- **Email**: maintainers@example.com
- **Chat**: [Discord/Slack] (если есть)
---
**Спасибо за ваш вклад в Quoter!** 🚀

318
docs/deployment.md Normal file
View File

@@ -0,0 +1,318 @@
# Развертывание
## Обзор
Quoter можно развернуть различными способами в зависимости от ваших потребностей и инфраструктуры.
## Способы развертывания
### 1. Docker (Рекомендуется)
#### Сборка образа
```bash
# Сборка production образа
docker build -t quoter:latest .
# Сборка с тегами
docker build -t quoter:v1.0.0 .
```
#### Запуск контейнера
```bash
docker run -d \
--name quoter \
-p 8080:8080 \
-e REDIS_URL=redis://redis:6379 \
-e CORE_URL=https://api.example.com/graphql \
-e STORJ_ACCESS_KEY=your-key \
-e STORJ_SECRET_KEY=your-secret \
-e AWS_ACCESS_KEY=your-aws-key \
-e AWS_SECRET_KEY=your-aws-secret \
quoter:latest
```
#### Docker Compose
Создайте `docker-compose.yml`:
```yaml
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
quoter:
build: .
ports:
- "8080:8080"
environment:
- REDIS_URL=redis://redis:6379
- CORE_URL=https://api.example.com/graphql
- STORJ_ACCESS_KEY=${STORJ_ACCESS_KEY}
- STORJ_SECRET_KEY=${STORJ_SECRET_KEY}
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY}
- AWS_SECRET_KEY=${AWS_SECRET_KEY}
- RUST_LOG=info
depends_on:
- redis
restart: unless-stopped
volumes:
redis_data:
```
Запуск:
```bash
docker-compose up -d
```
### 2. Kubernetes
#### Deployment
Создайте `k8s/deployment.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: quoter
labels:
app: quoter
spec:
replicas: 3
selector:
matchLabels:
app: quoter
template:
metadata:
labels:
app: quoter
spec:
containers:
- name: quoter
image: quoter:latest
ports:
- containerPort: 8080
env:
- name: REDIS_URL
value: "redis://redis-service:6379"
- name: CORE_URL
value: "https://api.example.com/graphql"
- name: STORJ_ACCESS_KEY
valueFrom:
secretKeyRef:
name: quoter-secrets
key: storj-access-key
- name: STORJ_SECRET_KEY
valueFrom:
secretKeyRef:
name: quoter-secrets
key: storj-secret-key
- name: AWS_ACCESS_KEY
valueFrom:
secretKeyRef:
name: quoter-secrets
key: aws-access-key
- name: AWS_SECRET_KEY
valueFrom:
secretKeyRef:
name: quoter-secrets
key: aws-secret-key
- name: RUST_LOG
value: "info"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
```
#### Service
```yaml
apiVersion: v1
kind: Service
metadata:
name: quoter-service
spec:
selector:
app: quoter
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
```
#### Secrets
```yaml
apiVersion: v1
kind: Secret
metadata:
name: quoter-secrets
type: Opaque
data:
storj-access-key: <base64-encoded-key>
storj-secret-key: <base64-encoded-secret>
aws-access-key: <base64-encoded-key>
aws-secret-key: <base64-encoded-secret>
```
### 3. Systemd (Linux)
#### Создание сервиса
Создайте `/etc/systemd/system/quoter.service`:
```ini
[Unit]
Description=Quoter File Service
After=network.target redis.service
[Service]
Type=simple
User=quoter
Group=quoter
WorkingDirectory=/opt/quoter
Environment=REDIS_URL=redis://localhost:6379
Environment=CORE_URL=https://api.example.com/graphql
Environment=STORJ_ACCESS_KEY=your-key
Environment=STORJ_SECRET_KEY=your-secret
Environment=AWS_ACCESS_KEY=your-aws-key
Environment=AWS_SECRET_KEY=your-aws-secret
Environment=RUST_LOG=info
ExecStart=/opt/quoter/quoter
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
#### Управление сервисом
```bash
# Создание пользователя
sudo useradd -r -s /bin/false quoter
# Копирование бинарного файла
sudo cp target/release/quoter /opt/quoter/
sudo chown quoter:quoter /opt/quoter/quoter
# Включение и запуск сервиса
sudo systemctl daemon-reload
sudo systemctl enable quoter
sudo systemctl start quoter
# Проверка статуса
sudo systemctl status quoter
```
## Мониторинг и логирование
### Prometheus метрики
Добавьте в `Cargo.toml`:
```toml
[dependencies]
prometheus = "0.13"
actix-web-prom = "0.6"
```
### Grafana дашборд
Создайте дашборд для мониторинга:
- Количество запросов в секунду
- Время ответа API
- Использование памяти и CPU
- Ошибки по типам
- Использование квот
### Логирование
#### Структурированные логи
```bash
# JSON формат для ELK stack
RUST_LOG=info cargo run | jq .
```
#### Ротация логов
Настройте logrotate:
```
/var/log/quoter/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 644 quoter quoter
postrotate
systemctl reload quoter
endscript
}
```
## Масштабирование
### Горизонтальное масштабирование
1. **Load Balancer**: Настройте nginx или HAProxy
2. **Redis Cluster**: Для высоких нагрузок
3. **S3 CDN**: Для статических файлов
### Вертикальное масштабирование
- Увеличьте ресурсы контейнера/сервера
- Настройте пул соединений Redis
- Оптимизируйте размер изображений
## Безопасность
### Сетевая безопасность
- Используйте HTTPS в продакшене
- Настройте firewall
- Ограничьте доступ к Redis
### Секреты
- Используйте Kubernetes Secrets или Docker Secrets
- Не храните секреты в коде
- Ротация ключей доступа
### Аудит
- Логируйте все операции с файлами
- Отслеживайте использование квот
- Мониторьте подозрительную активность

422
docs/development.md Normal file
View File

@@ -0,0 +1,422 @@
# Разработка
## Настройка среды разработки
### Требования
- **Rust**: 1.70 или выше
- **Redis**: 6.0 или выше (локально или Docker)
- **Git**: для работы с репозиторием
- **IDE**: VS Code, IntelliJ IDEA или другой редактор с поддержкой Rust
### Установка Rust
```bash
# Установка Rust через rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Перезагрузка shell
source ~/.bashrc
# Проверка установки
rustc --version
cargo --version
```
### Клонирование репозитория
```bash
git clone https://github.com/your-org/quoter.git
cd quoter
```
### Установка зависимостей
```bash
# Сборка проекта
cargo build
# Установка дополнительных инструментов
cargo install cargo-watch # для автоматической пересборки
cargo install cargo-audit # для проверки безопасности
cargo install cargo-tarpaulin # для покрытия кода тестами
```
## Структура проекта
```
quoter/
├── src/
│ ├── main.rs # Точка входа приложения
│ ├── app_state.rs # Состояние приложения и подключения
│ ├── auth.rs # Аутентификация и авторизация
│ ├── core.rs # Интеграция с API ядра
│ ├── lookup.rs # Поиск и определение MIME-типов
│ ├── overlay.rs # Генерация оверлеев для изображений
│ ├── s3_utils.rs # Утилиты для работы с S3
│ ├── thumbnail.rs # Создание миниатюр
│ └── handlers/ # HTTP обработчики
│ ├── mod.rs # Модуль обработчиков
│ ├── upload.rs # Загрузка файлов
│ ├── proxy.rs # Получение файлов
│ ├── quota.rs # Управление квотами
│ └── serve_file.rs # Обслуживание файлов
├── docs/ # Документация
├── tests/ # Интеграционные тесты
├── Cargo.toml # Зависимости и конфигурация
├── Cargo.lock # Фиксированные версии зависимостей
├── Dockerfile # Docker образ
└── README.md # Основная документация
```
## Локальная разработка
### Настройка переменных окружения
Создайте файл `.env` в корне проекта:
```bash
# Redis (локально или Docker)
REDIS_URL=redis://localhost:6379
# Core API (замените на ваш endpoint)
CORE_URL=https://api.example.com/graphql
# Storj S3 (тестовые ключи)
STORJ_ACCESS_KEY=your-test-key
STORJ_SECRET_KEY=your-test-secret
STORJ_BUCKET_NAME=test-bucket
# AWS S3 (тестовые ключи)
AWS_ACCESS_KEY=your-test-aws-key
AWS_SECRET_KEY=your-test-aws-secret
# Server
PORT=8080
RUST_LOG=debug
```
### Запуск Redis
#### Локально
```bash
# Ubuntu/Debian
sudo apt-get install redis-server
sudo systemctl start redis-server
# macOS
brew install redis
brew services start redis
# Проверка
redis-cli ping
```
#### Docker
```bash
docker run -d \
--name redis-dev \
-p 6379:6379 \
redis:7-alpine
```
### Запуск приложения
```bash
# Обычный запуск
cargo run
# С автоматической пересборкой
cargo watch -x run
# В режиме отладки
RUST_LOG=debug cargo run
# С профилированием
cargo run --release
```
### Проверка работоспособности
```bash
# Проверка сервера
curl http://localhost:8080/
# Проверка загрузки файла (требует токен)
curl -X POST http://localhost:8080/ \
-H "Authorization: Bearer your-token" \
-F "file=@test-image.jpg"
```
## Тестирование
### Unit тесты
```bash
# Запуск всех тестов
cargo test
# Запуск тестов с выводом
cargo test -- --nocapture
# Запуск конкретного теста
cargo test test_upload_file
# Запуск тестов в параллельном режиме
cargo test -- --test-threads=4
```
### Интеграционные тесты
Создайте файл `tests/integration_test.rs`:
```rust
use actix_web::{test, web, App};
use quoter::app_state::AppState;
#[actix_web::test]
async fn test_upload_endpoint() {
let app_state = AppState::new().await;
let app = test::init_service(
App::new()
.app_data(web::Data::new(app_state))
.route("/", web::post().to(upload_handler))
).await;
let req = test::TestRequest::post()
.uri("/")
.insert_header(("Authorization", "Bearer test-token"))
.set_form(("file", "test-data"))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
```
### Тестирование производительности
```bash
# Бенчмарки (если настроены)
cargo bench
# Профилирование с flamegraph
cargo install flamegraph
cargo flamegraph --bin quoter
```
## Отладка
### Логирование
```rust
use log::{debug, info, warn, error};
// В коде
debug!("Processing file: {}", filename);
info!("File uploaded successfully");
warn!("User quota is getting low: {} bytes", quota);
error!("Failed to upload file: {}", e);
```
### Отладка с GDB
```bash
# Компиляция с отладочной информацией
cargo build
# Запуск с GDB
gdb target/debug/quoter
# В GDB
(gdb) break main
(gdb) run
(gdb) continue
```
### Отладка с LLDB (macOS)
```bash
lldb target/debug/quoter
(lldb) breakpoint set --name main
(lldb) run
```
## Проверка кода
### Clippy
```bash
# Проверка стиля кода
cargo clippy
# Проверка с дополнительными предупреждениями
cargo clippy -- -D warnings
# Автоматическое исправление
cargo clippy --fix
```
### Форматирование
```bash
# Форматирование кода
cargo fmt
# Проверка форматирования
cargo fmt -- --check
```
### Проверка безопасности
```bash
# Аудит зависимостей
cargo audit
# Проверка уязвимостей
cargo audit --deny warnings
```
## Покрытие кода
### Tarpaulin
```bash
# Установка
cargo install cargo-tarpaulin
# Запуск
cargo tarpaulin
# С HTML отчетом
cargo tarpaulin --out Html
```
### grcov
```bash
# Установка
cargo install grcov
# Настройка переменных
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
export RUSTDOCFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
# Запуск тестов
cargo test
# Генерация отчета
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./coverage/
```
## Git workflow
### Создание feature branch
```bash
# Создание новой ветки
git checkout -b feature/new-feature
# Внесение изменений
# ...
# Коммит изменений
git add .
git commit -m "feat: add new feature"
# Push в репозиторий
git push origin feature/new-feature
```
### Commit conventions
Используйте [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` - новая функциональность
- `fix:` - исправление багов
- `docs:` - изменения в документации
- `style:` - форматирование кода
- `refactor:` - рефакторинг кода
- `test:` - добавление тестов
- `chore:` - обновление зависимостей, конфигурации
### Pull Request
1. Создайте Pull Request в GitHub/GitLab
2. Добавьте описание изменений
3. Укажите связанные issues
4. Дождитесь code review
5. Исправьте замечания если есть
6. Получите approval и merge
## Полезные команды
### Cargo
```bash
# Обновление зависимостей
cargo update
# Очистка сборки
cargo clean
# Проверка зависимостей
cargo tree
# Документация
cargo doc --open
# Проверка типов без компиляции
cargo check
```
### Отладка
```bash
# Просмотр логов в реальном времени
tail -f logs/quoter.log
# Мониторинг ресурсов
htop
iotop
# Сетевые соединения
netstat -tulpn | grep 8080
```
### Docker
```bash
# Сборка для разработки
docker build -t quoter:dev .
# Запуск с volume для hot reload
docker run -v $(pwd):/app -p 8080:8080 quoter:dev
# Просмотр логов контейнера
docker logs -f quoter-container
```
## Рекомендации
### Производительность
1. Используйте `cargo build --release` для production
2. Настройте профилирование для критических участков
3. Мониторьте использование памяти и CPU
4. Оптимизируйте размер изображений
### Безопасность
1. Регулярно обновляйте зависимости
2. Используйте `cargo audit` для проверки уязвимостей
3. Не храните секреты в коде
4. Валидируйте все входные данные
### Качество кода
1. Пишите тесты для новой функциональности
2. Используйте `cargo clippy` для проверки стиля
3. Документируйте публичные API
4. Следуйте принципам SOLID

25
docs/how-it-works.md Normal file
View File

@@ -0,0 +1,25 @@
### Как это работает
1. **Аутентификация**:
- Клиент отправляет файл на сервер с заголовком `Authorization`, содержащим токен. Сервер проверяет наличие и валидность токена через API ядра, определяя пользователя.
2. **Загрузка файлов**:
- Сервер обрабатывает все загружаемые файлы. Если файл является изображением, создается его миниатюра. И миниатюра, и оригинальное изображение загружаются в S3. Для остальных файлов выполняется простая загрузка в S3 без создания миниатюр.
3. **Создание миниатюр**:
- Для всех загружаемых изображений сервер автоматически создает миниатюры различных размеров (10, 40, 110, 300, 600, 800, 1400 пикселей по ширине). Миниатюры сохраняются как отдельные файлы в том же S3 bucket, что и оригинальные изображения.
4. **Определение MIME-типа и расширения файла**:
- MIME-тип и расширение файла определяются автоматически на основе имени файла и его содержимого с использованием библиотеки `mime_guess`.
5. **Загрузка файлов в S3**:
- Все файлы, включая миниатюры и оригиналы изображений, загружаются в указанный S3 bucket. Сформированные URL-адреса файлов возвращаются клиенту.
6. **Управление квотами**:
- Для каждого пользователя устанавливается общая квота на загрузку данных, которая составляет 5 ГБ. Перед загрузкой каждого нового файла проверяется, не превысит ли его размер текущую квоту пользователя. Если квота будет превышена, загрузка файла будет отклонена. После успешной загрузки файл и его размер регистрируются в Redis, и квота пользователя обновляется.
7. **Сохранение информации о загруженных файлах в Redis**:
- Имя каждого загруженного файла сохраняется в Redis для отслеживания загруженных пользователем файлов. Это позволяет учитывать квоты и управлять пространством, занимаемым файлами.
8. **Оверлей для shout**:
- При загрузке файла, если он является изображением, и в запросе присутствует параметр `s=<shout_id>`, то к файлу будет добавлен оверлей с данными shout.

341
docs/monitoring.md Normal file
View File

@@ -0,0 +1,341 @@
# Мониторинг
## Обзор
Мониторинг Quoter включает в себя логирование, метрики производительности и отслеживание состояния системы.
## Логирование
### Уровни логирования
Quoter использует библиотеку `log` с различными уровнями:
- **error** - Критические ошибки, требующие немедленного внимания
- **warn** - Предупреждения, которые могут указывать на проблемы
- **info** - Информационные сообщения о нормальной работе
- **debug** - Отладочная информация для разработчиков
- **trace** - Максимальная детализация для глубокой отладки
### Настройка логирования
```bash
# Только ошибки
RUST_LOG=error cargo run
# Предупреждения и ошибки
RUST_LOG=warn cargo run
# Информационные сообщения (рекомендуется для продакшена)
RUST_LOG=info cargo run
# Отладка
RUST_LOG=debug cargo run
# Максимальная детализация
RUST_LOG=trace cargo run
```
### Структура логов
#### Загрузка файла
```
[INFO] Started
[WARN] file abc123.jpg uploaded to storj, incrementing quota by 1048576 bytes
[WARN] New quota for user user123: 2097152 bytes
```
#### Получение файла
```
[WARN] >>> GET image_300.jpg [START]
[WARN] detected file extension: jpg
[WARN] base_filename: image
[WARN] requested width: 300
[WARN] Found stored path in DB: production/image/image.jpg
[WARN] File exists in Storj: production/image/image.jpg
[WARN] Processing image file with width: 300
[WARN] Calculated closest width: 300 for requested: 300
[WARN] serve existed thumb file: image_300.jpg
```
#### Ошибки
```
[ERROR] Failed to upload to Storj: image.jpg - Error: Network error
[ERROR] Database error while getting path: image.jpg - Full error: Connection timeout
[ERROR] unsupported file format
```
## Метрики
### Основные метрики для мониторинга
#### Производительность
- **Requests per second (RPS)** - количество запросов в секунду
- **Response time** - время ответа API
- **Error rate** - процент ошибок
- **Upload success rate** - процент успешных загрузок
#### Ресурсы
- **Memory usage** - использование памяти
- **CPU usage** - использование процессора
- **Disk I/O** - операции с диском
- **Network I/O** - сетевой трафик
#### Бизнес-метрики
- **Files uploaded** - количество загруженных файлов
- **Quota usage** - использование квот пользователями
- **Thumbnail generation** - количество сгенерированных миниатюр
- **Storage usage** - использование хранилища
### Prometheus метрики
Добавьте в `Cargo.toml`:
```toml
[dependencies]
prometheus = "0.13"
actix-web-prom = "0.6"
```
Настройте в `main.rs`:
```rust
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let prometheus = PrometheusMetricsBuilder::new("quoter")
.endpoint("/metrics")
.build()
.unwrap();
HttpServer::new(move || {
App::new()
.wrap(prometheus.clone())
// ... остальные настройки
})
.bind(addr)?
.run()
.await
}
```
### Кастомные метрики
```rust
use prometheus::{Counter, Histogram, Registry};
lazy_static! {
pub static ref UPLOAD_COUNTER: Counter = Counter::new(
"quoter_uploads_total",
"Total number of file uploads"
).unwrap();
pub static ref UPLOAD_SIZE: Histogram = Histogram::new(
"quoter_upload_size_bytes",
"File upload size in bytes"
).unwrap();
pub static ref QUOTA_USAGE: Histogram = Histogram::new(
"quoter_quota_usage_bytes",
"User quota usage in bytes"
).unwrap();
}
```
## Алерты
### Критические алерты
- **Service down** - сервис недоступен
- **High error rate** - высокий процент ошибок (>5%)
- **High response time** - медленные ответы (>2s)
- **Memory usage high** - высокое использование памяти (>80%)
- **Redis connection failed** - потеря соединения с Redis
### Предупреждения
- **Quota usage high** - пользователи приближаются к лимиту квоты
- **Storage usage high** - высокое использование хранилища
- **Thumbnail generation slow** - медленная генерация миниатюр
### Настройка алертов в Prometheus
```yaml
groups:
- name: quoter
rules:
- alert: QuoterServiceDown
expr: up{job="quoter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Quoter service is down"
- alert: HighErrorRate
expr: rate(http_requests_total{job="quoter",status=~"5.."}[5m]) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "High error rate detected"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="quoter"}[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
```
## Дашборды
### Grafana дашборд
Создайте дашборд с панелями:
#### Обзор системы
- Статус сервиса (up/down)
- Количество запросов в секунду
- Время ответа (p50, p95, p99)
- Процент ошибок
#### Загрузка файлов
- Количество загрузок в час/день
- Размер загружаемых файлов
- Успешность загрузок
- Использование квот
#### Ресурсы
- Использование CPU и памяти
- Сетевой трафик
- Операции с диском
- Соединения с Redis
#### Бизнес-метрики
- Топ пользователей по использованию квоты
- Популярные размеры миниатюр
- Использование хранилища по типам файлов
### Пример запроса для Grafana
```sql
-- Количество загрузок по часам
SELECT
time_bucket('1 hour', timestamp) AS time,
COUNT(*) as uploads
FROM quoter_uploads
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY time
ORDER BY time;
```
## Здоровье системы
### Health check endpoint
Добавьте endpoint для проверки здоровья:
```rust
async fn health_check() -> HttpResponse {
// Проверка Redis
let redis_ok = check_redis_connection().await;
// Проверка S3
let s3_ok = check_s3_connection().await;
if redis_ok && s3_ok {
HttpResponse::Ok().json(json!({
"status": "healthy",
"timestamp": chrono::Utc::now(),
"services": {
"redis": "ok",
"s3": "ok"
}
}))
} else {
HttpResponse::ServiceUnavailable().json(json!({
"status": "unhealthy",
"timestamp": chrono::Utc::now(),
"services": {
"redis": if redis_ok { "ok" } else { "error" },
"s3": if s3_ok { "ok" } else { "error" }
}
}))
}
}
```
### Kubernetes liveness/readiness probes
```yaml
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
```
## Трассировка
### Distributed tracing
Для отслеживания запросов через микросервисы:
```toml
[dependencies]
opentelemetry = "0.20"
tracing = "0.1"
tracing-opentelemetry = "0.20"
```
### Логирование с контекстом
```rust
use tracing::{info, warn, error, instrument};
#[instrument(skip(state))]
async fn upload_handler(
req: HttpRequest,
payload: Multipart,
state: web::Data<AppState>,
) -> Result<HttpResponse> {
let user_id = get_user_id(&req).await?;
info!(user_id = %user_id, "Starting file upload");
// ... логика загрузки
info!(user_id = %user_id, filename = %filename, "File uploaded successfully");
Ok(response)
}
```
## Рекомендации
### Продакшен
1. **Логирование**: Используйте структурированные логи в JSON формате
2. **Метрики**: Настройте Prometheus + Grafana
3. **Алерты**: Настройте уведомления для критических событий
4. **Ротация логов**: Настройте logrotate или отправку в ELK stack
5. **Мониторинг ресурсов**: Отслеживайте CPU, память, диск, сеть
### Разработка
1. **Локальное логирование**: Используйте `RUST_LOG=debug`
2. **Отладка**: Включите trace логи для детальной отладки
3. **Тестирование**: Создайте тесты для проверки метрик
4. **Документация**: Документируйте все кастомные метрики

198
docs/testing.md Normal file
View File

@@ -0,0 +1,198 @@
# Тестирование
Этот документ описывает подход к тестированию проекта Quoter.
## Обзор
Проект использует интеграционные тесты для проверки функциональности без внешних зависимостей. Тесты написаны на Rust с использованием фреймворка Actix Web для тестирования HTTP endpoints.
## Запуск тестов
### Все тесты
```bash
cargo test --tests # все
cargo test --test basic_test test_health_check # конкретный тест
cargo test --tests -- --nocapture # Тесты с выводом
```
## Описание тестов
### 1. Health Check (`test_health_check`)
Проверяет работу основного endpoint `/`:
- GET запрос возвращает статус 200 и тело "ok"
- POST запрос возвращает статус 404 (не найден)
### 2. JSON Сериализация (`test_json_serialization`)
Проверяет корректность сериализации и десериализации JSON:
- Создание структуры с данными квоты
- Сериализация в JSON строку
- Десериализация обратно в структуру
- Проверка соответствия данных
### 3. Multipart Form Data (`test_multipart_form_data`)
Проверяет создание multipart form data для загрузки файлов:
- Формирование правильного boundary
- Добавление заголовков Content-Disposition
- Добавление содержимого файла
- Проверка корректности структуры
### 4. UUID Генерация (`test_uuid_generation`)
Проверяет работу с UUID:
- Генерация уникальных UUID
- Проверка формата (36 символов с дефисами)
- Парсинг UUID обратно
### 5. MIME Типы (`test_mime_type_detection`)
Проверяет определение MIME типов по расширениям файлов:
- Поддерживаемые форматы (jpg, png, gif, webp, mp3, wav, mp4)
- Неподдерживаемые форматы (pdf, txt)
- Регистронезависимость
### 6. Парсинг путей файлов (`test_file_path_parsing`)
Проверяет парсинг путей файлов с размерами:
- Извлечение базового имени, ширины и расширения
- Обработка путей без размеров
- Обработка путей с подчеркиваниями
### 7. Расчеты квот (`test_quota_calculations`)
Проверяет логику расчета квот:
- Различные сценарии использования квоты
- Проверка превышения лимитов
- Корректность математических операций
### 8. Форматирование размеров (`test_file_size_formatting`)
Проверяет форматирование размеров файлов:
- Байты, килобайты, мегабайты, гигабайты
- Правильное округление
- Корректные единицы измерения
### 9. Обработка ошибок (`test_error_handling`)
Проверяет обработку некорректных данных:
- Неверный JSON
- Неполный JSON
- Неверные UUID
- Пустые значения
### 10. Производительность (`test_performance`)
Проверяет производительность критических операций:
- Генерация UUID (должна быть < 1μs)
- JSON сериализация (должна быть < 100μs)
- Вывод статистики производительности
## Принципы тестирования
### 1. Изоляция
- Тесты не зависят от внешних сервисов (Redis, S3)
- Каждый тест независим от других
- Используются моки и заглушки
### 2. Покрытие
- Тестируются основные функции
- Проверяются граничные случаи
- Тестируется обработка ошибок
### 3. Производительность
- Тесты должны выполняться быстро
- Проверяется производительность критических операций
- Устанавливаются временные лимиты
### 4. Читаемость
- Понятные названия тестов
- Описательные сообщения об ошибках
- Комментарии к сложной логике
## Добавление новых тестов
### 1. Создание теста
```rust
#[test]
async fn test_new_feature() {
// Подготовка
let test_data = create_test_data();
// Выполнение
let result = process_data(test_data);
// Проверка
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected_value);
}
```
### 2. Тестирование HTTP endpoints
```rust
#[actix_web::test]
async fn test_http_endpoint() {
let app = test::init_service(
App::new()
.route("/test", web::get().to(test_handler))
).await;
let req = test::TestRequest::get()
.uri("/test")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
```
### 3. Тестирование производительности
```rust
#[test]
async fn test_performance() {
use std::time::Instant;
let start = Instant::now();
let iterations = 10000;
for _ in 0..iterations {
// Тестируемая операция
}
let duration = start.elapsed();
let avg_time = duration.as_micros() as f64 / iterations as f64;
assert!(avg_time < 100.0, "Operation too slow: {:.2} μs", avg_time);
}
```
## Лучшие практики
### 1. Именование
- Используйте описательные имена тестов
- Группируйте связанные тесты
- Используйте префиксы для типов тестов
### 2. Организация
- Разделяйте тесты на логические группы
- Используйте модули для организации
- Документируйте сложные тесты
### 3. Надежность
- Избегайте хрупких тестов
- Не полагайтесь на порядок выполнения
- Очищайте состояние после тестов
### 4. Производительность
- Минимизируйте время выполнения
- Используйте параллельное выполнение
- Оптимизируйте медленные тесты
## Отладка тестов
```bash
RUST_LOG=debug cargo test --tests -- --nocapture # Вывод отладочной информации
cargo test --tests -- --nocapture --test-threads=1 # Продолжение после ошибки
```
## Покрытие кода
```bash
cargo install cargo-tarpaulin # Установка cargo-tarpaulin
cargo tarpaulin --tests # Запуск анализа покрытия
```
## CI/CD интеграция
Тесты автоматически запускаются в CI/CD pipeline

View File

@@ -1,52 +0,0 @@
{{ $proxy_settings := "proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_set_header Host $http_host; proxy_set_header X-Request-Start $msec;" }}
{{ range $port_map := .PROXY_PORT_MAP | split " " }}
{{ $port_map_list := $port_map | split ":" }}
{{ $scheme := index $port_map_list 0 }}
{{ $listen_port := index $port_map_list 1 }}
{{ $upstream_port := index $port_map_list 2 }}
server {
{{ if eq $scheme "http" }}
listen [::]:{{ $listen_port }};
listen {{ $listen_port }};
server_name {{ $.NOSSL_SERVER_NAME }};
client_max_body_size 100M;
{{ else if eq $scheme "https" }}
listen [::]:{{ $listen_port }} ssl http2;
listen {{ $listen_port }} ssl http2;
server_name {{ $.NOSSL_SERVER_NAME }};
ssl_certificate {{ $.APP_SSL_PATH }}/server.crt;
ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
client_max_body_size 100M;
{{ end }}
location / {
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
{{ $proxy_settings }}
}
# Browser caching for media files
location ~* \.(jpg|jpeg|png|gif|ico|webp|mp3|wav|ogg|flac|aac|aif|webm|pdf)$ {
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
expires 30d;
add_header Cache-Control "public, immutable";
}
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
}
{{ end }}
{{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }}
upstream {{ $.APP }}-{{ $upstream_port }} {
{{ range $listeners := $.DOKKU_APP_WEB_LISTENERS | split " " }}
{{ $listener_list := $listeners | split ":" }}
{{ $listener_ip := index $listener_list 0 }}
{{ $listener_port := index $listener_list 1 }}
server {{ $listener_ip }}:{{ $upstream_port }};
{{ end }}
}
{{ end }}

View File

@@ -1,22 +1,22 @@
use crate::s3_utils::get_s3_filelist;
use actix_web::error::ErrorInternalServerError; use actix_web::error::ErrorInternalServerError;
use aws_config::BehaviorVersion; use aws_config::BehaviorVersion;
use aws_sdk_s3::{config::Credentials, Client as S3Client}; use aws_sdk_s3::{config::Credentials, Client as S3Client};
use log::warn;
use redis::{aio::MultiplexedConnection, AsyncCommands, Client as RedisClient}; use redis::{aio::MultiplexedConnection, AsyncCommands, Client as RedisClient};
use std::env; use std::env;
use log::warn;
use crate::s3_utils::get_s3_filelist;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub redis: MultiplexedConnection, pub redis: MultiplexedConnection,
pub storj_client: S3Client, pub storj_client: S3Client,
pub aws_client: S3Client, pub aws_client: S3Client,
pub bucket: String pub bucket: String,
} }
const PATH_MAPPING_KEY: &str = "filepath_mapping"; // Ключ для хранения маппинга путей const PATH_MAPPING_KEY: &str = "filepath_mapping"; // Ключ для хранения маппинга путей
const WEEK_SECONDS: u64 = 604800; // Убираем TTL для квоты - она должна быть постоянной на пользователя
const QUOTA_TTL: u64 = 0; // 0 означает отсутствие TTL
impl AppState { impl AppState {
/// Инициализация нового состояния приложения. /// Инициализация нового состояния приложения.
@@ -40,7 +40,7 @@ impl AppState {
let aws_access_key = env::var("AWS_ACCESS_KEY").expect("AWS_ACCESS_KEY must be set"); let aws_access_key = env::var("AWS_ACCESS_KEY").expect("AWS_ACCESS_KEY must be set");
let aws_secret_key = env::var("AWS_SECRET_KEY").expect("AWS_SECRET_KEY must be set"); let aws_secret_key = env::var("AWS_SECRET_KEY").expect("AWS_SECRET_KEY must be set");
let aws_endpoint = let aws_endpoint =
env::var("AWS_END_POINT").unwrap_or_else(|_| "https://s3.amazonaws.com".to_string()); env::var("AWS_END_POINT").unwrap_or_else(|_| "https://s3.amazonaws.com".to_string());
// Конфигурируем клиент S3 для Storj // Конфигурируем клиент S3 для Storj
let storj_config = aws_config::defaults(BehaviorVersion::latest()) let storj_config = aws_config::defaults(BehaviorVersion::latest())
@@ -71,14 +71,14 @@ impl AppState {
)) ))
.load() .load()
.await; .await;
let aws_client = S3Client::new(&aws_config); let aws_client = S3Client::new(&aws_config);
let app_state = AppState { let app_state = AppState {
redis: redis_connection, redis: redis_connection,
storj_client, storj_client,
aws_client, aws_client,
bucket bucket,
}; };
// Кэшируем список файлов из AWS при старте приложения // Кэшируем список файлов из AWS при старте приложения
@@ -91,7 +91,7 @@ impl AppState {
pub async fn cache_filelist(&self) { pub async fn cache_filelist(&self) {
warn!("caching AWS filelist..."); warn!("caching AWS filelist...");
let mut redis = self.redis.clone(); let mut redis = self.redis.clone();
// Запрашиваем список файлов из Storj S3 // Запрашиваем список файлов из Storj S3
let filelist = get_s3_filelist(&self.aws_client, &self.bucket).await; let filelist = get_s3_filelist(&self.aws_client, &self.bucket).await;
@@ -105,7 +105,7 @@ impl AppState {
warn!("cached {} files", filelist.len()); warn!("cached {} files", filelist.len());
} }
/// Получает путь из ключа (имени файла) в Redis. /// Получает путь из ключа (имени файла) в Redis.
pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> { pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> {
let mut redis = self.redis.clone(); let mut redis = self.redis.clone();
@@ -133,9 +133,9 @@ impl AppState {
let quota: u64 = redis.get(&quota_key).await.unwrap_or(0); let quota: u64 = redis.get(&quota_key).await.unwrap_or(0);
if quota == 0 { if quota == 0 {
// Если квота не найдена, устанавливаем её в 0 байт и задаем TTL на одну неделю // Если квота не найдена, устанавливаем её в 0 байт без TTL (постоянная квота)
redis redis
.set_ex::<&str, u64, ()>(&quota_key, 0, WEEK_SECONDS) .set::<&str, u64, ()>(&quota_key, 0)
.await .await
.map_err(|_| { .map_err(|_| {
ErrorInternalServerError("Failed to set initial user quota in Redis") ErrorInternalServerError("Failed to set initial user quota in Redis")
@@ -161,10 +161,10 @@ impl AppState {
ErrorInternalServerError("Failed to check if user quota exists in Redis") ErrorInternalServerError("Failed to check if user quota exists in Redis")
})?; })?;
// Если ключ не существует, создаем его с начальным значением и устанавливаем TTL // Если ключ не существует, создаем его с начальным значением без TTL
if !exists { if !exists {
redis redis
.set_ex::<_, u64, ()>(&quota_key, bytes, WEEK_SECONDS) .set::<_, u64, ()>(&quota_key, bytes)
.await .await
.map_err(|_| { .map_err(|_| {
ErrorInternalServerError("Failed to set initial user quota in Redis") ErrorInternalServerError("Failed to set initial user quota in Redis")
@@ -180,4 +180,42 @@ impl AppState {
Ok(new_quota) Ok(new_quota)
} }
/// Устанавливает квоту пользователя в байтах (позволяет увеличить или уменьшить)
pub async fn set_user_quota(&self, user_id: &str, bytes: u64) -> Result<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Устанавливаем новое значение квоты
redis
.set::<_, u64, ()>(&quota_key, bytes)
.await
.map_err(|_| ErrorInternalServerError("Failed to set user quota in Redis"))?;
Ok(bytes)
}
/// Увеличивает квоту пользователя на указанное количество байт
pub async fn increase_user_quota(
&self,
user_id: &str,
additional_bytes: u64,
) -> Result<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Получаем текущую квоту
let current_quota: u64 = redis.get(&quota_key).await.unwrap_or(0);
// Вычисляем новую квоту
let new_quota = current_quota + additional_bytes;
// Устанавливаем новое значение
redis
.set::<_, u64, ()>(&quota_key, new_quota)
.await
.map_err(|_| ErrorInternalServerError("Failed to increase user quota in Redis"))?;
Ok(new_quota)
}
} }

View File

@@ -30,7 +30,7 @@ struct Claims {
/// Получает айди пользователя из токена в заголовке /// Получает айди пользователя из токена в заголовке
pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> { pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> {
let auth_api_base = env::var("AUTH_URL")?; let auth_api_base = env::var("CORE_URL")?;
let query_name = "validate_jwt_token"; let query_name = "validate_jwt_token";
let operation = "ValidateToken"; let operation = "ValidateToken";
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();

View File

@@ -1,9 +1,9 @@
use reqwest::Client as HTTPClient;
use reqwest::Client as HTTPClient;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use std::{collections::HashMap, env, error::Error}; use std::{collections::HashMap, env, error::Error};
// Структура для десериализации ответа от сервиса аутентификации // Структура для десериализации ответа от сервиса аутентификации
#[derive(Deserialize)] #[derive(Deserialize)]
struct CoreResponse { struct CoreResponse {
@@ -46,11 +46,7 @@ pub async fn get_shout_by_id(shout_id: i32) -> Result<Shout, Box<dyn Error>> {
}); });
let client = HTTPClient::new(); let client = HTTPClient::new();
let response = client let response = client.post(&api_base).json(&gql).send().await?;
.post(&api_base)
.json(&gql)
.send()
.await?;
if response.status().is_success() { if response.status().is_success() {
let core_response: CoreResponse = response.json().await?; let core_response: CoreResponse = response.json().await?;
@@ -67,4 +63,4 @@ pub async fn get_shout_by_id(shout_id: i32) -> Result<Shout, Box<dyn Error>> {
response.status().to_string(), response.status().to_string(),
))) )))
} }
} }

View File

@@ -1,21 +1,21 @@
mod proxy; mod proxy;
mod upload; mod quota;
mod serve_file; mod serve_file;
mod upload;
pub use proxy::proxy_handler; pub use proxy::proxy_handler;
pub use quota::{get_quota_handler, increase_quota_handler, set_quota_handler};
pub use upload::upload_handler; pub use upload::upload_handler;
// Лимит квоты на пользователя: 2 ГБ в неделю // Общий лимит квоты на пользователя: 5 ГБ
pub const MAX_WEEK_BYTES: u64 = 2 * 1024 * 1024 * 1024; pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024;
use actix_web::{HttpResponse, Result, HttpRequest}; use actix_web::{HttpRequest, HttpResponse, Result};
/// Обработчик для корневого пути / /// Обработчик для корневого пути /
pub async fn root_handler(req: HttpRequest) -> Result<HttpResponse> { pub async fn root_handler(req: HttpRequest) -> Result<HttpResponse> {
match req.method().as_str() { match req.method().as_str() {
"GET" => Ok(HttpResponse::Ok() "GET" => Ok(HttpResponse::Ok().content_type("text/plain").body("ok")),
.content_type("text/plain") _ => Ok(HttpResponse::MethodNotAllowed().finish()),
.body("ok")),
_ => Ok(HttpResponse::MethodNotAllowed().finish())
} }
} }

View File

@@ -4,9 +4,9 @@ use log::{error, warn};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::handlers::serve_file::serve_file; use crate::handlers::serve_file::serve_file;
use crate::lookup::{find_file_by_pattern, get_mime_type};
use crate::s3_utils::{check_file_exists, load_file_from_s3, upload_to_s3}; use crate::s3_utils::{check_file_exists, load_file_from_s3, upload_to_s3};
use crate::thumbnail::{find_closest_width, parse_file_path, thumbdata_save}; use crate::thumbnail::{find_closest_width, parse_file_path, thumbdata_save};
use crate::lookup::{find_file_by_pattern, get_mime_type};
/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна. /// Обработчик для скачивания файла и генерации миниатюры, если она недоступна.
pub async fn proxy_handler( pub async fn proxy_handler(
@@ -56,14 +56,22 @@ pub async fn proxy_handler(
warn!("content_type: {}", content_type); warn!("content_type: {}", content_type);
let shout_id = match req.query_string().contains("s=") { let shout_id = match req.query_string().contains("s=") {
true => req.query_string().split("s=").collect::<Vec<&str>>().pop().unwrap_or(""), true => req
false => "" .query_string()
.split("s=")
.collect::<Vec<&str>>()
.pop()
.unwrap_or(""),
false => "",
}; };
return match state.get_path(&filekey).await { return match state.get_path(&filekey).await {
Ok(Some(stored_path)) => { Ok(Some(stored_path)) => {
warn!("Found stored path in DB: {}", stored_path); warn!("Found stored path in DB: {}", stored_path);
warn!("Checking Storj path - bucket: {}, path: {}", state.bucket, stored_path); warn!(
"Checking Storj path - bucket: {}, path: {}",
state.bucket, stored_path
);
if check_file_exists(&state.storj_client, &state.bucket, &stored_path).await? { if check_file_exists(&state.storj_client, &state.bucket, &stored_path).await? {
warn!("File exists in Storj: {}", stored_path); warn!("File exists in Storj: {}", stored_path);
if content_type.starts_with("image") { if content_type.starts_with("image") {
@@ -73,20 +81,26 @@ pub async fn proxy_handler(
serve_file(&stored_path, &state, shout_id).await serve_file(&stored_path, &state, shout_id).await
} else { } else {
let closest: u32 = find_closest_width(requested_width as u32); let closest: u32 = find_closest_width(requested_width as u32);
warn!("Calculated closest width: {} for requested: {}", closest, requested_width); warn!(
"Calculated closest width: {} for requested: {}",
closest, requested_width
);
let thumb_filename = &format!("{}_{}.{}", base_filename, closest, ext); let thumb_filename = &format!("{}_{}.{}", base_filename, closest, ext);
warn!("Generated thumbnail filename: {}", thumb_filename); warn!("Generated thumbnail filename: {}", thumb_filename);
// Проверяем, существует ли уже миниатюра в Storj // Проверяем, существует ли уже миниатюра в Storj
match check_file_exists(&state.storj_client, &state.bucket, thumb_filename).await { match check_file_exists(&state.storj_client, &state.bucket, thumb_filename)
.await
{
Ok(true) => { Ok(true) => {
warn!("serve existed thumb file: {}", thumb_filename); warn!("serve existed thumb file: {}", thumb_filename);
serve_file(thumb_filename, &state, shout_id).await serve_file(thumb_filename, &state, shout_id).await
}, }
Ok(false) => { Ok(false) => {
// Миниатюра не существует, возвращаем оригинал и запускаем генерацию миниатюры // Миниатюра не существует, возвращаем оригинал и запускаем генерацию миниатюры
let original_file = serve_file(&stored_path, &state, shout_id).await?; let original_file =
serve_file(&stored_path, &state, shout_id).await?;
// Запускаем асинхронную задачу для генерации миниатюры // Запускаем асинхронную задачу для генерации миниатюры
let state_clone = state.clone(); let state_clone = state.clone();
let stored_path_clone = stored_path.clone(); let stored_path_clone = stored_path.clone();
@@ -94,9 +108,22 @@ pub async fn proxy_handler(
let content_type_clone = content_type.to_string(); let content_type_clone = content_type.to_string();
actix_web::rt::spawn(async move { actix_web::rt::spawn(async move {
if let Ok(filedata) = load_file_from_s3(&state_clone.storj_client, &state_clone.bucket, &stored_path_clone).await { if let Ok(filedata) = load_file_from_s3(
&state_clone.storj_client,
&state_clone.bucket,
&stored_path_clone,
)
.await
{
warn!("generate new thumb files: {}", stored_path_clone); warn!("generate new thumb files: {}", stored_path_clone);
if let Err(e) = thumbdata_save(filedata, &state_clone, &filekey_clone, content_type_clone).await { if let Err(e) = thumbdata_save(
filedata,
&state_clone,
&filekey_clone,
content_type_clone,
)
.await
{
error!("Failed to generate thumbnail: {}", e); error!("Failed to generate thumbnail: {}", e);
} }
} }
@@ -115,7 +142,10 @@ pub async fn proxy_handler(
serve_file(&stored_path, &state, shout_id).await serve_file(&stored_path, &state, shout_id).await
} }
} else { } else {
warn!("Attempting to load from AWS - bucket: {}, path: {}", state.bucket, stored_path); warn!(
"Attempting to load from AWS - bucket: {}, path: {}",
state.bucket, stored_path
);
// Определяем тип медиа из content_type // Определяем тип медиа из content_type
let media_type = content_type.split("/").next().unwrap_or("image"); let media_type = content_type.split("/").next().unwrap_or("image");
@@ -124,7 +154,7 @@ pub async fn proxy_handler(
let paths_lower = vec![ let paths_lower = vec![
stored_path.clone(), stored_path.clone(),
// format!("production/{}", stored_path), // format!("production/{}", stored_path),
format!("production/{}/{}", media_type, stored_path) format!("production/{}/{}", media_type, stored_path),
]; ];
// Создаем те же пути, но с оригинальным регистром расширения // Создаем те же пути, но с оригинальным регистром расширения
@@ -133,7 +163,7 @@ pub async fn proxy_handler(
let paths_orig = vec![ let paths_orig = vec![
orig_stored_path.clone(), orig_stored_path.clone(),
// format!("production/{}", orig_stored_path), // format!("production/{}", orig_stored_path),
format!("production/{}/{}", media_type, orig_stored_path) format!("production/{}/{}", media_type, orig_stored_path),
]; ];
// Объединяем все пути для проверки // Объединяем все пути для проверки
@@ -143,22 +173,29 @@ pub async fn proxy_handler(
warn!("Trying AWS path: {}", path); warn!("Trying AWS path: {}", path);
match load_file_from_s3(&state.aws_client, &state.bucket, &path).await { match load_file_from_s3(&state.aws_client, &state.bucket, &path).await {
Ok(filedata) => { Ok(filedata) => {
warn!("Successfully loaded file from AWS, size: {} bytes", filedata.len()); warn!(
"Successfully loaded file from AWS, size: {} bytes",
filedata.len()
);
warn!("Attempting to upload to Storj with key: {}", filekey); warn!("Attempting to upload to Storj with key: {}", filekey);
if let Err(e) = upload_to_s3( if let Err(e) = upload_to_s3(
&state.storj_client, &state.storj_client,
&state.bucket, &state.bucket,
&filekey, &filekey,
filedata.clone(), filedata.clone(),
&content_type, &content_type,
).await { )
.await
{
error!("Failed to upload to Storj: {} - Error: {}", filekey, e); error!("Failed to upload to Storj: {} - Error: {}", filekey, e);
} else { } else {
warn!("Successfully uploaded to Storj: {}", filekey); warn!("Successfully uploaded to Storj: {}", filekey);
} }
return Ok(HttpResponse::Ok().content_type(content_type).body(filedata)); return Ok(HttpResponse::Ok()
.content_type(content_type)
.body(filedata));
} }
Err(err) => { Err(err) => {
warn!("Failed to load from AWS path {}: {:?}", path, err); warn!("Failed to load from AWS path {}: {:?}", path, err);
@@ -174,18 +211,23 @@ pub async fn proxy_handler(
Ok(None) => { Ok(None) => {
warn!("No stored path found in DB for: {}", filekey); warn!("No stored path found in DB for: {}", filekey);
let ct_parts = content_type.split("/").collect::<Vec<&str>>(); let ct_parts = content_type.split("/").collect::<Vec<&str>>();
// Создаем два варианта пути - с оригинальным расширением и с нижним регистром // Создаем два варианта пути - с оригинальным расширением и с нижним регистром
let filepath_lower = format!("production/{}/{}.{}", ct_parts[0], base_filename, ext); let filepath_lower = format!("production/{}/{}.{}", ct_parts[0], base_filename, ext);
let filepath_orig = format!("production/{}/{}.{}", ct_parts[0], base_filename, extension); let filepath_orig =
format!("production/{}/{}.{}", ct_parts[0], base_filename, extension);
warn!("Looking up files with paths: {} or {} in bucket: {}",
filepath_lower, filepath_orig, state.bucket); warn!(
"Looking up files with paths: {} or {} in bucket: {}",
filepath_lower, filepath_orig, state.bucket
);
// Проверяем существование файла с обоими вариантами расширения // Проверяем существование файла с обоими вариантами расширения
let exists_in_aws_lower = check_file_exists(&state.aws_client, &state.bucket, &filepath_lower).await?; let exists_in_aws_lower =
let exists_in_aws_orig = check_file_exists(&state.aws_client, &state.bucket, &filepath_orig).await?; check_file_exists(&state.aws_client, &state.bucket, &filepath_lower).await?;
let exists_in_aws_orig =
check_file_exists(&state.aws_client, &state.bucket, &filepath_orig).await?;
let filepath = if exists_in_aws_orig { let filepath = if exists_in_aws_orig {
filepath_orig filepath_orig
} else if exists_in_aws_lower { } else if exists_in_aws_lower {
@@ -195,15 +237,25 @@ pub async fn proxy_handler(
filepath_lower filepath_lower
}; };
let exists_in_storj = check_file_exists(&state.storj_client, &state.bucket, &filepath).await?; let exists_in_storj =
check_file_exists(&state.storj_client, &state.bucket, &filepath).await?;
warn!("Checking existence in Storj: {}", exists_in_storj); warn!("Checking existence in Storj: {}", exists_in_storj);
if exists_in_storj { if exists_in_storj {
warn!("file {} exists in storj, try to generate thumbnails", filepath); warn!(
"file {} exists in storj, try to generate thumbnails",
filepath
);
match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await { match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await {
Ok(filedata) => { Ok(filedata) => {
let _ = thumbdata_save(filedata.clone(), &state, &filekey, content_type.to_string()).await; let _ = thumbdata_save(
filedata.clone(),
&state,
&filekey,
content_type.to_string(),
)
.await;
} }
Err(e) => { Err(e) => {
error!("cannot download {} from storj: {}", filekey, e); error!("cannot download {} from storj: {}", filekey, e);
@@ -214,16 +266,25 @@ pub async fn proxy_handler(
warn!("file {} does not exist in storj", filepath); warn!("file {} does not exist in storj", filepath);
} }
let exists_in_aws = check_file_exists(&state.aws_client, &state.bucket, &filepath).await?; let exists_in_aws =
check_file_exists(&state.aws_client, &state.bucket, &filepath).await?;
warn!("Checking existence in AWS: {}", exists_in_aws); warn!("Checking existence in AWS: {}", exists_in_aws);
if exists_in_aws { if exists_in_aws {
warn!("File found in AWS, attempting to download: {}", filepath); warn!("File found in AWS, attempting to download: {}", filepath);
match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await { match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await {
Ok(filedata) => { Ok(filedata) => {
warn!("Successfully downloaded file from AWS, size: {} bytes", filedata.len()); warn!(
let _ = thumbdata_save(filedata.clone(), &state, &filekey, content_type.to_string()) "Successfully downloaded file from AWS, size: {} bytes",
.await; filedata.len()
);
let _ = thumbdata_save(
filedata.clone(),
&state,
&filekey,
content_type.to_string(),
)
.await;
if let Err(e) = upload_to_s3( if let Err(e) = upload_to_s3(
&state.storj_client, &state.storj_client,
&state.bucket, &state.bucket,
@@ -231,27 +292,31 @@ pub async fn proxy_handler(
filedata.clone(), filedata.clone(),
&content_type, &content_type,
) )
.await { .await
{
warn!("cannot upload to storj: {}", e); warn!("cannot upload to storj: {}", e);
} else { } else {
warn!("file {} uploaded to storj", filekey); warn!("file {} uploaded to storj", filekey);
state.set_path(&filekey, &filepath).await; state.set_path(&filekey, &filepath).await;
} }
Ok(HttpResponse::Ok().content_type(content_type).body(filedata)) Ok(HttpResponse::Ok().content_type(content_type).body(filedata))
}, }
Err(e) => { Err(e) => {
error!("Failed to download from AWS: {} - Error: {}", filepath, e); error!("Failed to download from AWS: {} - Error: {}", filepath, e);
Err(ErrorInternalServerError(e)) Err(ErrorInternalServerError(e))
}, }
} }
} else { } else {
error!("File not found in either Storj or AWS: {}", filepath); error!("File not found in either Storj or AWS: {}", filepath);
Err(ErrorNotFound("file does not exist")) Err(ErrorNotFound("file does not exist"))
} }
}, }
Err(e) => { Err(e) => {
error!("Database error while getting path: {} - Full error: {:?}", filekey, e); error!(
"Database error while getting path: {} - Full error: {:?}",
filekey, e
);
Err(ErrorInternalServerError(e)) Err(ErrorInternalServerError(e))
} }
} };
} }

151
src/handlers/quota.rs Normal file
View File

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

View File

@@ -6,7 +6,11 @@ use crate::overlay::generate_overlay;
use crate::s3_utils::check_file_exists; use crate::s3_utils::check_file_exists;
/// Функция для обслуживания файла по заданному пути. /// Функция для обслуживания файла по заданному пути.
pub async fn serve_file(filepath: &str, state: &AppState, shout_id: &str) -> Result<HttpResponse, actix_web::Error> { pub async fn serve_file(
filepath: &str,
state: &AppState,
shout_id: &str,
) -> Result<HttpResponse, actix_web::Error> {
if filepath.is_empty() { if filepath.is_empty() {
return Err(ErrorInternalServerError("Filename is empty".to_string())); return Err(ErrorInternalServerError("Filename is empty".to_string()));
} }
@@ -14,7 +18,10 @@ pub async fn serve_file(filepath: &str, state: &AppState, shout_id: &str) -> Res
// Проверяем наличие файла в Storj S3 // Проверяем наличие файла в Storj S3
let exists = check_file_exists(&state.storj_client, &state.bucket, &filepath).await?; let exists = check_file_exists(&state.storj_client, &state.bucket, &filepath).await?;
if !exists { if !exists {
return Err(ErrorInternalServerError(format!("File {} not found in Storj", filepath))); return Err(ErrorInternalServerError(format!(
"File {} not found in Storj",
filepath
)));
} }
// Получаем объект из Storj S3 // Получаем объект из Storj S3
@@ -25,7 +32,9 @@ pub async fn serve_file(filepath: &str, state: &AppState, shout_id: &str) -> Res
.key(filepath) .key(filepath)
.send() .send()
.await .await
.map_err(|_| ErrorInternalServerError(format!("Failed to get {} object from Storj", filepath)))?; .map_err(|_| {
ErrorInternalServerError(format!("Failed to get {} object from Storj", filepath))
})?;
let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output
.body .body
@@ -35,10 +44,9 @@ pub async fn serve_file(filepath: &str, state: &AppState, shout_id: &str) -> Res
let data_bytes = match shout_id.is_empty() { let data_bytes = match shout_id.is_empty() {
true => data.into_bytes(), true => data.into_bytes(),
false => generate_overlay(shout_id, data.into_bytes()).await? false => generate_overlay(shout_id, data.into_bytes()).await?,
}; };
let mime_type = MimeGuess::from_path(&filepath).first_or_octet_stream(); let mime_type = MimeGuess::from_path(&filepath).first_or_octet_stream();
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()

View File

@@ -4,10 +4,10 @@ use log::{error, warn};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth::{get_id_by_token, user_added_file}; use crate::auth::{get_id_by_token, user_added_file};
use crate::s3_utils::{self, upload_to_s3, generate_key_with_extension}; use crate::handlers::MAX_USER_QUOTA_BYTES;
use crate::lookup::store_file_info; use crate::lookup::store_file_info;
use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3};
use futures::TryStreamExt; use futures::TryStreamExt;
use crate::handlers::MAX_WEEK_BYTES;
// use crate::thumbnail::convert_heic_to_jpeg; // use crate::thumbnail::convert_heic_to_jpeg;
/// Обработчик для аплоада файлов. /// Обработчик для аплоада файлов.
@@ -28,7 +28,7 @@ pub async fn upload_handler(
let user_id = get_id_by_token(token.unwrap()).await?; let user_id = get_id_by_token(token.unwrap()).await?;
// Получаем текущую квоту пользователя // Получаем текущую квоту пользователя
let this_week_amount: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); let current_quota: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0);
let mut body = "ok".to_string(); let mut body = "ok".to_string();
while let Ok(Some(field)) = payload.try_next().await { while let Ok(Some(field)) = payload.try_next().await {
let mut field = field; let mut field = field;
@@ -46,7 +46,9 @@ pub async fn upload_handler(
Some(mime) => mime, Some(mime) => mime,
None => { None => {
warn!("Неподдерживаемый формат файла"); warn!("Неподдерживаемый формат файла");
return Err(actix_web::error::ErrorUnsupportedMediaType("Неподдерживаемый формат файла")); return Err(actix_web::error::ErrorUnsupportedMediaType(
"Неподдерживаемый формат файла",
));
} }
}; };
@@ -63,14 +65,18 @@ pub async fn upload_handler(
Some(ext) => ext, Some(ext) => ext,
None => { None => {
warn!("Неподдерживаемый тип содержимого: {}", content_type); warn!("Неподдерживаемый тип содержимого: {}", content_type);
return Err(actix_web::error::ErrorUnsupportedMediaType("Неподдерживаемый тип содержимого")); return Err(actix_web::error::ErrorUnsupportedMediaType(
"Неподдерживаемый тип содержимого",
));
} }
}; };
// Проверяем, что добавление файла не превышает лимит квоты // Проверяем, что добавление файла не превышает лимит квоты
if this_week_amount + file_size > MAX_WEEK_BYTES { if current_quota + file_size > MAX_USER_QUOTA_BYTES {
warn!("Quota would exceed limit: current={}, adding={}, limit={}", warn!(
this_week_amount, file_size, MAX_WEEK_BYTES); "Quota would exceed limit: current={}, adding={}, limit={}",
current_quota, file_size, MAX_USER_QUOTA_BYTES
);
return Err(actix_web::error::ErrorUnauthorized("Quota exceeded")); return Err(actix_web::error::ErrorUnauthorized("Quota exceeded"));
} }
@@ -84,27 +90,33 @@ pub async fn upload_handler(
&filename, &filename,
file_bytes, file_bytes,
&content_type, &content_type,
).await { )
.await
{
Ok(_) => { Ok(_) => {
warn!("file {} uploaded to storj, incrementing quota by {} bytes", filename, file_size); warn!(
"file {} uploaded to storj, incrementing quota by {} bytes",
filename, file_size
);
if let Err(e) = state.increment_uploaded_bytes(&user_id, file_size).await { if let Err(e) = state.increment_uploaded_bytes(&user_id, file_size).await {
error!("Failed to increment quota: {}", e); error!("Failed to increment quota: {}", e);
return Err(e); return Err(e);
} }
// Сохраняем информацию о файле в Redis // Сохраняем информацию о файле в Redis
let mut redis = state.redis.clone(); let mut redis = state.redis.clone();
store_file_info(&mut redis, &filename, &content_type).await?; store_file_info(&mut redis, &filename, &content_type).await?;
user_added_file(&mut redis, &user_id, &filename).await?; user_added_file(&mut redis, &user_id, &filename).await?;
// Сохраняем маппинг пути // Сохраняем маппинг пути
let generated_key = generate_key_with_extension(filename.clone(), content_type.clone()); let generated_key =
generate_key_with_extension(filename.clone(), content_type.clone());
state.set_path(&filename, &generated_key).await; state.set_path(&filename, &generated_key).await;
if let Ok(new_quota) = state.get_or_create_quota(&user_id).await { if let Ok(new_quota) = state.get_or_create_quota(&user_id).await {
warn!("New quota for user {}: {} bytes", user_id, new_quota); warn!("New quota for user {}: {} bytes", user_id, new_quota);
} }
body = filename; body = filename;
} }
Err(e) => { Err(e) => {

View File

@@ -1,8 +1,8 @@
use std::collections::HashMap;
use actix_web::error::ErrorInternalServerError; use actix_web::error::ErrorInternalServerError;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use redis::aio::MultiplexedConnection; use redis::aio::MultiplexedConnection;
use redis::AsyncCommands; use redis::AsyncCommands;
use std::collections::HashMap;
pub static MIME_TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| { pub static MIME_TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new(); let mut m = HashMap::new();
@@ -36,7 +36,6 @@ pub fn get_mime_type(extension: &str) -> Option<&'static str> {
MIME_TYPES.get(extension).copied() MIME_TYPES.get(extension).copied()
} }
/// Ищет файл в Redis по шаблону имени /// Ищет файл в Redis по шаблону имени
pub async fn find_file_by_pattern( pub async fn find_file_by_pattern(
redis: &mut MultiplexedConnection, redis: &mut MultiplexedConnection,
@@ -67,4 +66,4 @@ pub async fn store_file_info(
.await .await
.map_err(|_| ErrorInternalServerError("Failed to store file info in Redis"))?; .map_err(|_| ErrorInternalServerError("Failed to store file info in Redis"))?;
Ok(()) Ok(())
} }

View File

@@ -1,20 +1,27 @@
mod app_state; mod app_state;
mod auth; mod auth;
mod lookup; mod core;
mod handlers; mod handlers;
mod lookup;
mod overlay;
mod s3_utils; mod s3_utils;
mod thumbnail; mod thumbnail;
mod core;
mod overlay;
use actix_web::{middleware::Logger, web, App, HttpServer, http::header::{self, HeaderName}};
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{
http::header::{self, HeaderName},
middleware::Logger,
web, App, HttpServer,
};
use app_state::AppState; use app_state::AppState;
use handlers::{proxy_handler, upload_handler, root_handler};
use log::warn;
use tokio::task::spawn_blocking;
use std::env;
use env_logger; use env_logger;
use handlers::{
get_quota_handler, increase_quota_handler, proxy_handler, root_handler, set_quota_handler,
upload_handler,
};
use log::warn;
use std::env;
use tokio::task::spawn_blocking;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@@ -38,7 +45,7 @@ async fn main() -> std::io::Result<()> {
// Настройка CORS middleware // Настройка CORS middleware
let cors = Cors::default() let cors = Cors::default()
.allow_any_origin() // TODO: ограничить конкретными доменами в продакшене .allow_any_origin() // TODO: ограничить конкретными доменами в продакшене
.allowed_methods(vec!["GET", "POST", "OPTIONS"]) .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
.allowed_headers(vec![ .allowed_headers(vec![
header::DNT, header::DNT,
header::USER_AGENT, header::USER_AGENT,
@@ -49,10 +56,7 @@ async fn main() -> std::io::Result<()> {
header::RANGE, header::RANGE,
header::AUTHORIZATION, header::AUTHORIZATION,
]) ])
.expose_headers(vec![ .expose_headers(vec![header::CONTENT_LENGTH, header::CONTENT_RANGE])
header::CONTENT_LENGTH,
header::CONTENT_RANGE,
])
.supports_credentials() .supports_credentials()
.max_age(1728000); // 20 дней .max_age(1728000); // 20 дней
@@ -62,6 +66,9 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default()) .wrap(Logger::default())
.route("/", web::get().to(root_handler)) .route("/", web::get().to(root_handler))
.route("/", web::post().to(upload_handler)) .route("/", web::post().to(upload_handler))
.route("/quota", web::get().to(get_quota_handler))
.route("/quota/increase", web::post().to(increase_quota_handler))
.route("/quota/set", web::post().to(set_quota_handler))
.route("/{path:.*}", web::get().to(proxy_handler)) .route("/{path:.*}", web::get().to(proxy_handler))
}) })
.bind(addr)? .bind(addr)?

View File

@@ -1,14 +1,17 @@
use std::{error::Error, io::Cursor};
use actix_web::web::Bytes;
use log::warn;
use image::Rgba;
use imageproc::drawing::{draw_text_mut, draw_filled_rect_mut};
use imageproc::rect::Rect;
use ab_glyph::{Font, FontArc, PxScale}; 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; use crate::core::get_shout_by_id;
pub async fn generate_overlay<'a>(shout_id: &'a str, filedata: Bytes) -> Result<Bytes, Box<dyn Error>> { pub async fn generate_overlay<'a>(
shout_id: &'a str,
filedata: Bytes,
) -> Result<Bytes, Box<dyn Error>> {
// Получаем shout из GraphQL // Получаем shout из GraphQL
let shout_id_int = shout_id.parse::<i32>().unwrap_or(0); let shout_id_int = shout_id.parse::<i32>().unwrap_or(0);
match get_shout_by_id(shout_id_int).await { match get_shout_by_id(shout_id_int).await {
@@ -84,7 +87,7 @@ pub async fn generate_overlay<'a>(shout_id: &'a str, filedata: Bytes) -> Result<
img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)?; img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)?;
Ok(Bytes::from(buffer)) Ok(Bytes::from(buffer))
}, }
Err(e) => { Err(e) => {
warn!("Error getting shout: {}", e); warn!("Error getting shout: {}", e);
Ok(filedata) Ok(filedata)

View File

@@ -1,8 +1,8 @@
use actix_web::error::ErrorInternalServerError; use actix_web::error::ErrorInternalServerError;
use aws_sdk_s3::{error::SdkError, primitives::ByteStream, Client as S3Client}; use aws_sdk_s3::{error::SdkError, primitives::ByteStream, Client as S3Client};
use infer::get;
use mime_guess::mime; use mime_guess::mime;
use std::str::FromStr; use std::str::FromStr;
use infer::get;
/// Загружает файл в S3 хранилище. /// Загружает файл в S3 хранилище.
pub async fn upload_to_s3( pub async fn upload_to_s3(
@@ -32,7 +32,13 @@ pub async fn check_file_exists(
bucket: &str, bucket: &str,
filepath: &str, filepath: &str,
) -> Result<bool, actix_web::Error> { ) -> Result<bool, actix_web::Error> {
match s3_client.head_object().bucket(bucket).key(filepath).send().await { match s3_client
.head_object()
.bucket(bucket)
.key(filepath)
.send()
.await
{
Ok(_) => Ok(true), // Файл найден Ok(_) => Ok(true), // Файл найден
Err(SdkError::ServiceError(service_error)) if service_error.err().is_not_found() => { Err(SdkError::ServiceError(service_error)) if service_error.err().is_not_found() => {
Ok(false) // Файл не найден Ok(false) // Файл не найден
@@ -117,6 +123,6 @@ pub fn get_extension_from_mime(mime_type: &str) -> Option<&str> {
"image/webp" => Some("webp"), "image/webp" => Some("webp"),
"image/heic" => Some("heic"), "image/heic" => Some("heic"),
"image/tiff" => Some("tiff"), "image/tiff" => Some("tiff"),
_ => None _ => None,
} }
} }

View File

@@ -60,7 +60,8 @@ pub fn parse_file_path(requested_path: &str) -> (String, u32, String) {
// Проверка на старую ширину в путях, начинающихся с "unsafe" // Проверка на старую ширину в путях, начинающихся с "unsafe"
if path.starts_with("unsafe") && width == 0 { if path.starts_with("unsafe") && width == 0 {
if path_parts.len() >= 2 { if path_parts.len() >= 2 {
if let Some(old_width_str) = path_parts.get(1) { // Получаем второй элемент if let Some(old_width_str) = path_parts.get(1) {
// Получаем второй элемент
let old_width_str = old_width_str.trim_end_matches('x'); let old_width_str = old_width_str.trim_end_matches('x');
if let Ok(w) = old_width_str.parse::<u32>() { if let Ok(w) = old_width_str.parse::<u32>() {
width = w; width = w;
@@ -78,7 +79,7 @@ pub fn parse_file_path(requested_path: &str) -> (String, u32, String) {
/// Это позволяет поддерживать различные форматы изображений без необходимости заранее предугадывать их. /// Это позволяет поддерживать различные форматы изображений без необходимости заранее предугадывать их.
pub async fn generate_thumbnails( pub async fn generate_thumbnails(
image: &DynamicImage, image: &DynamicImage,
format: ImageFormat format: ImageFormat,
) -> Result<HashMap<u32, Vec<u8>>, actix_web::Error> { ) -> Result<HashMap<u32, Vec<u8>>, actix_web::Error> {
let mut thumbnails = HashMap::new(); let mut thumbnails = HashMap::new();
@@ -107,16 +108,18 @@ fn determine_image_format(extension: &str) -> Result<ImageFormat, actix_web::Err
"heic" | "heif" | "tiff" | "tif" => { "heic" | "heif" | "tiff" | "tif" => {
// Конвертируем HEIC и TIFF в JPEG при сохранении // Конвертируем HEIC и TIFF в JPEG при сохранении
Ok(ImageFormat::Jpeg) Ok(ImageFormat::Jpeg)
}, }
_ => { _ => {
log::error!("Неподдерживаемый формат изображения: {}", extension); log::error!("Неподдерживаемый формат изображения: {}", extension);
Err(ErrorInternalServerError("Неподдерживаемый формат изображения")) Err(ErrorInternalServerError(
}, "Неподдерживаемый формат изображения",
))
}
} }
} }
/// Сохраняет данные миниатюры. /// Сохраняет данные миниатюры.
/// ///
/// Обновлена для передачи корректного формата изображения. /// Обновлена для передачи корректного формата изображения.
pub async fn thumbdata_save( pub async fn thumbdata_save(
original_data: Vec<u8>, original_data: Vec<u8>,
@@ -128,12 +131,12 @@ pub async fn thumbdata_save(
warn!("original file name: {}", original_filename); warn!("original file name: {}", original_filename);
let (base_filename, _, extension) = parse_file_path(&original_filename); let (base_filename, _, extension) = parse_file_path(&original_filename);
warn!("detected file extension: {}", extension); warn!("detected file extension: {}", extension);
// Для HEIC файлов просто сохраняем оригинал как миниатюру // Для HEIC файлов просто сохраняем оригинал как миниатюру
if content_type == "image/heic" { if content_type == "image/heic" {
warn!("HEIC file detected, using original as thumbnail"); warn!("HEIC file detected, using original as thumbnail");
let thumb_filename = format!("{}_{}.heic", base_filename, THUMB_WIDTHS[0]); let thumb_filename = format!("{}_{}.heic", base_filename, THUMB_WIDTHS[0]);
if let Err(e) = upload_to_s3( if let Err(e) = upload_to_s3(
&state.storj_client, &state.storj_client,
&state.bucket, &state.bucket,
@@ -179,7 +182,10 @@ pub async fn thumbdata_save(
} }
} }
Err(e) => { Err(e) => {
warn!("cannot generate thumbnails for {}: {}", original_filename, e); warn!(
"cannot generate thumbnails for {}: {}",
original_filename, e
);
return Err(e); return Err(e);
} }
} }

322
tests/basic_test.rs Normal file
View File

@@ -0,0 +1,322 @@
use actix_web::{test, web, App, HttpResponse};
/// Простой тест health check
#[actix_web::test]
async fn test_health_check() {
let app = test::init_service(App::new().route(
"/",
web::get().to(|req: actix_web::HttpRequest| async move {
match req.method().as_str() {
"GET" => Ok::<HttpResponse, actix_web::Error>(
HttpResponse::Ok().content_type("text/plain").body("ok"),
),
_ => {
Ok::<HttpResponse, actix_web::Error>(HttpResponse::MethodNotAllowed().finish())
}
}
}),
))
.await;
// Тестируем GET запрос
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = test::read_body(resp).await;
assert_eq!(body, "ok");
// Тестируем POST запрос (должен вернуть 405)
let req = test::TestRequest::post().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);
}
/// Тест для проверки JSON сериализации/десериализации
#[test]
async fn test_json_serialization() {
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct TestStruct {
user_id: String,
current_quota: u64,
max_quota: u64,
}
let test_data = TestStruct {
user_id: "test-user-123".to_string(),
current_quota: 1024,
max_quota: 5368709120, // 5 GB
};
// Сериализация
let json_string = serde_json::to_string(&test_data).unwrap();
assert!(json_string.contains("test-user-123"));
assert!(json_string.contains("1024"));
assert!(json_string.contains("5368709120"));
// Десериализация
let deserialized: TestStruct = serde_json::from_str(&json_string).unwrap();
assert_eq!(deserialized, test_data);
}
/// Тест для проверки multipart form data
#[test]
async fn test_multipart_form_data() {
let boundary = "test-boundary";
let filename = "test.png";
let content = b"fake image data";
let mut form_data = Vec::new();
// Начало multipart
form_data.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
form_data.extend_from_slice(
format!(
"Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n",
filename
)
.as_bytes(),
);
form_data.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
// Содержимое файла
form_data.extend_from_slice(content);
form_data.extend_from_slice(b"\r\n");
// Конец multipart
form_data.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
// Проверяем, что form_data содержит ожидаемые части
let form_data_str = String::from_utf8_lossy(&form_data);
assert!(form_data_str.contains(filename));
assert!(form_data_str.contains("image/png"));
assert!(form_data_str.contains("fake image data"));
assert!(form_data_str.contains(&format!("--{}", boundary)));
}
/// Тест для проверки UUID генерации
#[test]
async fn test_uuid_generation() {
use uuid::Uuid;
let uuid1 = Uuid::new_v4();
let uuid2 = Uuid::new_v4();
// UUID должны быть разными
assert_ne!(uuid1, uuid2);
// UUID должны быть в правильном формате
let uuid_str = uuid1.to_string();
assert_eq!(uuid_str.len(), 36); // 32 символа + 4 дефиса
assert!(uuid_str.contains('-'));
// Проверяем, что UUID можно парсить обратно
let parsed_uuid = Uuid::parse_str(&uuid_str).unwrap();
assert_eq!(parsed_uuid, uuid1);
}
/// Тест для проверки MIME типов
#[test]
async fn test_mime_type_detection() {
// Тестируем определение MIME типа по расширению
let get_mime_type = |ext: &str| -> Option<&'static str> {
match ext.to_lowercase().as_str() {
"jpg" | "jpeg" => Some("image/jpeg"),
"png" => Some("image/png"),
"gif" => Some("image/gif"),
"webp" => Some("image/webp"),
"mp3" => Some("audio/mpeg"),
"wav" => Some("audio/wav"),
"mp4" => Some("video/mp4"),
_ => None,
}
};
assert_eq!(get_mime_type("jpg"), Some("image/jpeg"));
assert_eq!(get_mime_type("jpeg"), Some("image/jpeg"));
assert_eq!(get_mime_type("png"), Some("image/png"));
assert_eq!(get_mime_type("gif"), Some("image/gif"));
assert_eq!(get_mime_type("webp"), Some("image/webp"));
assert_eq!(get_mime_type("mp3"), Some("audio/mpeg"));
assert_eq!(get_mime_type("wav"), Some("audio/wav"));
assert_eq!(get_mime_type("mp4"), Some("video/mp4"));
assert_eq!(get_mime_type("pdf"), None);
assert_eq!(get_mime_type(""), None);
}
/// Тест для проверки парсинга путей файлов
#[test]
async fn test_file_path_parsing() {
fn parse_file_path(path: &str) -> (String, u32, String) {
let parts: Vec<&str> = path.split('.').collect();
if parts.len() != 2 {
return (path.to_string(), 0, "".to_string());
}
let base_with_width = parts[0];
let ext = parts[1];
// Ищем ширину в формате _NUMBER
let base_parts: Vec<&str> = base_with_width.split('_').collect();
if base_parts.len() >= 2 {
if let Ok(width) = base_parts.last().unwrap().parse::<u32>() {
let base = base_parts[..base_parts.len() - 1].join("_");
return (base, width, ext.to_string());
}
}
(base_with_width.to_string(), 0, ext.to_string())
}
let (base, width, ext) = parse_file_path("image_300.jpg");
assert_eq!(base, "image");
assert_eq!(width, 300);
assert_eq!(ext, "jpg");
let (base, width, ext) = parse_file_path("document.pdf");
assert_eq!(base, "document");
assert_eq!(width, 0);
assert_eq!(ext, "pdf");
let (base, width, ext) = parse_file_path("file_with_underscore_but_no_width.jpg");
assert_eq!(base, "file_with_underscore_but_no_width");
assert_eq!(width, 0);
assert_eq!(ext, "jpg");
}
/// Тест для проверки расчетов квот
#[test]
async fn test_quota_calculations() {
const MAX_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; // 5 ГБ
const MB: u64 = 1024 * 1024;
const GB: u64 = 1024 * 1024 * 1024;
// Тестируем различные сценарии
let test_cases = vec![
(0, 1 * MB, true), // Пустая квота + 1MB = OK
(1 * GB, 1 * MB, true), // 1GB + 1MB = OK
(4 * GB, 1 * GB, true), // 4GB + 1GB = OK
(4 * GB, 2 * GB, false), // 4GB + 2GB = превышение
(5 * GB, 1 * MB, false), // 5GB + 1MB = превышение
];
for (current_quota, file_size, should_allow) in test_cases {
let would_exceed = current_quota + file_size > MAX_QUOTA_BYTES;
assert_eq!(
would_exceed,
!should_allow,
"Квота: {} + файл: {} = {}, должно быть разрешено: {}",
current_quota,
file_size,
current_quota + file_size,
should_allow
);
}
}
/// Тест для проверки форматирования размеров файлов
#[test]
async fn test_file_size_formatting() {
fn format_file_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * 1024;
const GB: u64 = 1024 * 1024 * 1024;
match bytes {
0..KB => format!("{} B", bytes),
KB..MB => format!("{:.1} KB", bytes as f64 / KB as f64),
MB..GB => format!("{:.1} MB", bytes as f64 / MB as f64),
_ => format!("{:.1} GB", bytes as f64 / GB as f64),
}
}
assert_eq!(format_file_size(512), "512 B");
assert_eq!(format_file_size(1024), "1.0 KB");
assert_eq!(format_file_size(1536), "1.5 KB");
assert_eq!(format_file_size(1024 * 1024), "1.0 MB");
assert_eq!(format_file_size(1024 * 1024 * 1024), "1.0 GB");
assert_eq!(format_file_size(5 * 1024 * 1024 * 1024), "5.0 GB");
}
/// Тест для проверки обработки ошибок
#[test]
async fn test_error_handling() {
// Тестируем парсинг неверного JSON
let invalid_json = "{ invalid json }";
let result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
assert!(result.is_err());
// Тестируем парсинг неполного JSON
let incomplete_json = r#"{"user_id": "test"#;
let result: Result<serde_json::Value, _> = serde_json::from_str(incomplete_json);
assert!(result.is_err());
// Тестируем неверный UUID
use uuid::Uuid;
let invalid_uuid = "not-a-uuid";
let result = Uuid::parse_str(invalid_uuid);
assert!(result.is_err());
// Тестируем пустой UUID
let empty_uuid = "";
let result = Uuid::parse_str(empty_uuid);
assert!(result.is_err());
}
/// Тест для проверки производительности
#[test]
async fn test_performance() {
use std::time::Instant;
// Тест UUID генерации
let start = Instant::now();
let iterations = 10000;
for _ in 0..iterations {
let _uuid = uuid::Uuid::new_v4();
}
let duration = start.elapsed();
let avg_time = duration.as_nanos() as f64 / iterations as f64;
println!(
"UUID generation: {} UUIDs in {:?}, avg: {:.2} ns per UUID",
iterations, duration, avg_time
);
// Проверяем, что среднее время меньше 1μs
assert!(
avg_time < 1000.0,
"UUID generation too slow: {:.2} ns",
avg_time
);
// Тест JSON сериализации
let start = Instant::now();
for _ in 0..iterations {
let data = serde_json::json!({
"user_id": "test-user-123",
"current_quota": 1024,
"max_quota": 5368709120u64
});
let _json_string = serde_json::to_string(&data).unwrap();
}
let duration = start.elapsed();
let avg_time = duration.as_micros() as f64 / iterations as f64;
println!(
"JSON serialization: {} operations in {:?}, avg: {:.2} μs per operation",
iterations, duration, avg_time
);
// Проверяем, что среднее время меньше 100μs
assert!(
avg_time < 100.0,
"JSON serialization too slow: {:.2} μs",
avg_time
);
}