docs
This commit is contained in:
@@ -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 }}
|
||||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -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
277
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
19
LICENSE
Normal 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
136
README.md
@@ -1,47 +1,119 @@
|
|||||||
# Квотер
|
# Quoter 🚀
|
||||||
|
|
||||||
Управляет квотами на загрузку файлов и их размещением в S3-хранилище. Поддерживает создание миниатюр для изображений и управляет квотами на использование дискового пространства для каждого пользователя с использованием Redis.
|
[](https://www.rust-lang.org/)
|
||||||
|
[](https://actix.rs/)
|
||||||
|
[](https://redis.io/)
|
||||||
|
[](https://aws.amazon.com/s3/)
|
||||||
|
[](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
35
docs/README.md
Normal 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
169
docs/api-reference.md
Normal 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
196
docs/architecture.md
Normal 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
137
docs/configuration.md
Normal 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
292
docs/contributing.md
Normal 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
318
docs/deployment.md
Normal 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
422
docs/development.md
Normal 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
25
docs/how-it-works.md
Normal 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
341
docs/monitoring.md
Normal 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
198
docs/testing.md
Normal 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
|
||||||
@@ -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 }}
|
|
||||||
@@ -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("a_key).await.unwrap_or(0);
|
let quota: u64 = redis.get("a_key).await.unwrap_or(0);
|
||||||
|
|
||||||
if quota == 0 {
|
if quota == 0 {
|
||||||
// Если квота не найдена, устанавливаем её в 0 байт и задаем TTL на одну неделю
|
// Если квота не найдена, устанавливаем её в 0 байт без TTL (постоянная квота)
|
||||||
redis
|
redis
|
||||||
.set_ex::<&str, u64, ()>("a_key, 0, WEEK_SECONDS)
|
.set::<&str, u64, ()>("a_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, ()>("a_key, bytes, WEEK_SECONDS)
|
.set::<_, u64, ()>("a_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, ()>("a_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("a_key).await.unwrap_or(0);
|
||||||
|
|
||||||
|
// Вычисляем новую квоту
|
||||||
|
let new_quota = current_quota + additional_bytes;
|
||||||
|
|
||||||
|
// Устанавливаем новое значение
|
||||||
|
redis
|
||||||
|
.set::<_, u64, ()>("a_key, new_quota)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorInternalServerError("Failed to increase user quota in Redis"))?;
|
||||||
|
|
||||||
|
Ok(new_quota)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
12
src/core.rs
12
src/core.rs
@@ -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(),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
151
src/handlers/quota.rs
Normal 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("a_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("a_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))
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/main.rs
33
src/main.rs
@@ -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)?
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
322
tests/basic_test.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user