Compare commits
157 Commits
cross
...
515b36176d
Author | SHA1 | Date | |
---|---|---|---|
515b36176d | |||
46c2f90f92 | |||
64a85dd5a4 | |||
a1b3cecbea | |||
c9484397a1 | |||
00aa21cb79 | |||
b8dde2c801 | |||
d905af58f5 | |||
7ffc681825 | |||
e110af6c3d | |||
b0ce533bde | |||
1e17d517bf | |||
9c0120a51e | |||
f73fc1db67 | |||
0c925f289b | |||
6bb543a9bd | |||
61934b6c0d | |||
88098dca5c | |||
16bb3d33c8 | |||
b805cddf91 | |||
b4ac8c83c8 | |||
a6ddba3a2b | |||
4d795d1a62 | |||
879e51c493 | |||
70c4b29e7f | |||
10160ab5e5 | |||
f4c679fd10 | |||
ec68d20aed | |||
2bc6adce84 | |||
1b3fcf7443 | |||
136345e872 | |||
bcf088c347 | |||
8d35c26d90 | |||
6a47df5ba3 | |||
3d8c9d44e1 | |||
17ef670de5 | |||
d5751f1e14 | |||
f86bb996b1 | |||
02d02b68ee | |||
23523c6a37 | |||
b5656ff75a | |||
e870704cb6 | |||
fdbecfbbe9 | |||
87e34d655d | |||
3d0e8ba591 | |||
67df0d7db5 | |||
e0b5e1a280 | |||
c860ac1a55 | |||
ae612fc20d | |||
b37ff4607b | |||
07094124d0 | |||
60a12113a9 | |||
f9e0c7839f | |||
b313f870df | |||
a4187b831e | |||
58e7fdc255 | |||
718d13a13a | |||
c6ff8af73e | |||
f100fbd0ee | |||
6c0e6dd341 | |||
5be7cf0cf4 | |||
6756daf8ef | |||
a39d5a8a24 | |||
93629c75ae | |||
251f47f760 | |||
14cdf3b35d | |||
1fe812ef4c | |||
32e068c1a4 | |||
c879e07a63 | |||
45d8c36136 | |||
e8c425d5fe | |||
c66a3f1322 | |||
654a51227b | |||
d862e2eece | |||
e169478bbf | |||
0879dac0ca | |||
c0a4fbfc19 | |||
b38de3a325 | |||
01bd1128c4 | |||
722bcb1c15 | |||
e9fafb3b60 | |||
5f23d5e23f | |||
53139b7ee7 | |||
254948e496 | |||
675e347c1d | |||
0671c84ae4 | |||
ba2b9539ba | |||
8288d16699 | |||
050a88b0bd | |||
0f8e34b39f | |||
96c0a3e07b | |||
0bd354983a | |||
12a4b046cf | |||
269bec83f7 | |||
8a27df2e86 | |||
38d991c393 | |||
b80bb741bc | |||
0626a4114a | |||
d3892fe3b3 | |||
cade8d3162 | |||
f98e48c3e8 | |||
7a673f9d63 | |||
46094d8e9c | |||
120657f915 | |||
0e94da7b31 | |||
487ada0e4f | |||
3f1adcc6f7 | |||
1798d1c710 | |||
3d1d86fd2e | |||
b072a63d52 | |||
ef33783dd7 | |||
3df4b6a087 | |||
57f83a4a74 | |||
d9a3562b74 | |||
9bcdb43dd5 | |||
9906bec04a | |||
d2babab848 | |||
b77645e1d3 | |||
2f1704d1d8 | |||
68ffc0337e | |||
2dafe040ae | |||
ec9381fa8d | |||
4a85866f48 | |||
872b1464e7 | |||
778a3431c0 | |||
d65b974f0c | |||
c63efca314 | |||
27cab34fce | |||
424c3ca996 | |||
5354429771 | |||
80c18df856 | |||
c4c598bb82 | |||
ae2e533b6e | |||
e232053cbb | |||
7dcc187a8a | |||
a3be2233fc | |||
0fd601f18e | |||
68bc0273ef | |||
d1bbb5c855 | |||
32bfa8bce7 | |||
fd00cba57a | |||
bb0565181a | |||
cddd7cfee0 | |||
72dd5c6533 | |||
5c6ee0f1b8 | |||
505d4db1c6 | |||
f2ec74576a | |||
6dac505504 | |||
1690592373 | |||
55e8200967 | |||
d76a58d264 | |||
94c4cc7799 | |||
31d89f14a4 | |||
12c3694774 | |||
8451432a40 | |||
ed10fe7f31 | |||
58e9d57f27 |
@ -2,7 +2,14 @@ root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml,md}]
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
59
.github/workflows/macos-arm.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: Candy macOS arm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [""]
|
||||
pull_request:
|
||||
branches: [""]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ["self-hosted", "macos-arm"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clippy check
|
||||
run: make lint
|
||||
|
||||
test:
|
||||
runs-on: ["self-hosted", "macos-arm"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
build:
|
||||
runs-on: ["self-hosted", "macos-arm"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build release
|
||||
run: make release
|
59
.github/workflows/macos-x86.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: Candy macOS x86
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [""]
|
||||
pull_request:
|
||||
branches: [""]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ["self-hosted", "macos-x86"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clippy check
|
||||
run: make lint
|
||||
|
||||
test:
|
||||
runs-on: ["self-hosted", "macos-x86"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
build:
|
||||
runs-on: ["self-hosted", "macos-x86"]
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build release
|
||||
run: make release
|
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
logs
|
||||
.DS_Store
|
||||
config.toml
|
||||
!/.cargo/config.toml
|
||||
/html
|
||||
heaptrack.*.zst
|
||||
|
||||
|
@ -4,12 +4,17 @@ stages:
|
||||
|
||||
variables:
|
||||
APP_NAME: "candy"
|
||||
GIT_CLONE_PATH: "$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_PIPELINE_ID"
|
||||
# GIT_CLONE_PATH: "$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_PIPELINE_ID"
|
||||
# use for much faster builds
|
||||
DOCKER_DRIVER: overlay2
|
||||
# ensure cross knows it's running remotely
|
||||
CROSS_REMOTE: 1
|
||||
|
||||
image: "rust:latest"
|
||||
|
||||
before_script:
|
||||
- apt update -y
|
||||
- apt-get update -y
|
||||
- apt-get install -y docker.io
|
||||
- mkdir $HOME/.cargo
|
||||
- echo "[source.crates-io]" >> $HOME/.cargo/config
|
||||
- echo "replace-with = 'ustc'" >> $HOME/.cargo/config
|
||||
@ -26,13 +31,15 @@ test:
|
||||
|
||||
build:freebsd:
|
||||
stage: build
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get update -y
|
||||
- apt-get install -y musl-tools libssl-dev
|
||||
- rustup target add x86_64-unknown-freebsd
|
||||
- cross build --release --target x86_64-unknown-freebsd
|
||||
- "mv target/release/$APP_NAME target/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
- "mv target/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG public/"
|
||||
- make build-freebsd
|
||||
- "mv target/x86_64-unknown-freebsd/release/$APP_NAME target/x86_64-unknown-freebsd/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
- "mv target/x86_64-unknown-freebsd/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- "public/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
|
||||
build:linux-gnu-amd64:
|
||||
stage: build
|
||||
@ -51,10 +58,7 @@ build:linux-musl-amd64:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get update -y
|
||||
- apt-get install -y musl-tools libssl-dev
|
||||
- rustup target add x86_64-unknown-linux-musl
|
||||
- cargo build --release --target x86_64-unknown-linux-musl
|
||||
- make build-linux-musl
|
||||
- "mv target/x86_64-unknown-linux-musl/release/$APP_NAME target/x86_64-unknown-linux-musl/release/$APP_NAME-x86_64-unknown-linux-musl-$CI_COMMIT_TAG"
|
||||
- "mv target/x86_64-unknown-linux-musl/release/$APP_NAME-x86_64-unknown-linux-musl-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
@ -66,16 +70,25 @@ build:windows-amd64:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get install -y g++-mingw-w64-x86-64
|
||||
- rustup target add x86_64-pc-windows-gnu
|
||||
- rustup toolchain install stable-x86_64-pc-windows-gnu
|
||||
- cargo build --release --target x86_64-pc-windows-gnu
|
||||
- make build-windows-gnu
|
||||
- "mv target/x86_64-pc-windows-gnu/release/$APP_NAME.exe target/x86_64-pc-windows-gnu/release/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe"
|
||||
- "mv target/x86_64-pc-windows-gnu/release/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- public/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe
|
||||
|
||||
build:linux-loongarch:
|
||||
stage: build
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- make build-loongarch
|
||||
- "mv target/loongarch64-unknown-linux-gnu/release/$APP_NAME target/loongarch64-unknown-linux-gnu/release/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG"
|
||||
- "mv target/loongarch64-unknown-linux-gnu/release/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- public/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG
|
||||
|
||||
rustdoc:
|
||||
stage: build
|
||||
rules:
|
||||
|
47
CHANGELOG.md
@ -1,14 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.1
|
||||
|
||||
Features:
|
||||
|
||||
- `auto-index` support
|
||||
- Stable rust version
|
||||
|
||||
## 0.2.0
|
||||
|
||||
Features:
|
||||
|
||||
- Reverse proxy
|
||||
- Refactor with axum
|
||||
- SSL support
|
||||
|
||||
## 0.1.1
|
||||
|
||||
Features:
|
||||
|
||||
- GitLab CI integration
|
||||
- FreeBSD support
|
||||
- Reverse proxy
|
||||
- Connection timeout
|
||||
|
||||
Break changes:
|
||||
|
||||
- Remove `keep-alive` setting
|
||||
- Add `timeout` setting
|
||||
|
||||
Fix:
|
||||
|
||||
- Internal server errror handler
|
||||
- Custom error page
|
||||
- Config tests
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Features:
|
||||
|
||||
- Graceful shutdown
|
||||
- `keep-alive` timeout setting
|
||||
- HTTP Etag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value
|
||||
- HTTP Etag: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value>
|
||||
- Content compress
|
||||
- zstd
|
||||
- gzip
|
||||
- deflate
|
||||
- br
|
||||
- zstd
|
||||
- gzip
|
||||
- deflate
|
||||
- br
|
||||
- Stream file
|
||||
- Stream content compress
|
||||
|
2483
Cargo.lock
generated
91
Cargo.toml
@ -1,34 +1,77 @@
|
||||
# cargo-features = ["profile-rustflags", "trim-paths"]
|
||||
|
||||
[package]
|
||||
name = "candy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version = "0.2.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
hyper = { version = "1.3.1", features = ["full"] }
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
http-body-util = "0.1.1"
|
||||
hyper-util = { version = "0.1.3", features = ["full"] }
|
||||
# tools
|
||||
thiserror = "1.0.60"
|
||||
anyhow = "1.0.83"
|
||||
config = { version = "0.14.0", default-features = false, features = [
|
||||
"async",
|
||||
"toml",
|
||||
# core
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
tokio-util = "0.7.15"
|
||||
tokio-rustls = "0.26.2"
|
||||
hyper = { version = "1.6.0", features = ["full"] }
|
||||
hyper-util = { version = "0.1.14", features = ["full"] }
|
||||
http = "1.3.1"
|
||||
http-body-util = "0.1.3"
|
||||
futures-util = "0.3.31"
|
||||
mimalloc = "0.1.47"
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
tower = { version = "0.5.2", features = ["full"] }
|
||||
tower-http = { version = "0.6.6", features = ["full"] }
|
||||
reqwest = { version = "0.12.20", features = [
|
||||
# "rustls-tls",
|
||||
"native-tls-vendored",
|
||||
"zstd",
|
||||
'gzip',
|
||||
'brotli',
|
||||
'deflate',
|
||||
"stream",
|
||||
] }
|
||||
serde = { version = "1.0.201", features = ["derive"] }
|
||||
toml = "0.8.12"
|
||||
# lua
|
||||
mlua = { version = "0.10.5", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"send",
|
||||
"async",
|
||||
"macros",
|
||||
"serialize",
|
||||
"anyhow",
|
||||
] }
|
||||
# tools
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.98"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.23"
|
||||
bytes = "1.10.1"
|
||||
const_format = "0.2.34"
|
||||
md5 = "0.8.0"
|
||||
dashmap = { version = "6.1.0", features = ["serde"] }
|
||||
# logging
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
futures-util = "0.3.30"
|
||||
tokio-util = "0.7.11"
|
||||
async-compression = { version = "0.4.10", features = ["tokio", "zstd", "gzip", "deflate", "brotli"] }
|
||||
http = "1.1.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
clap = { version = "4.5.40", features = ["derive"] }
|
||||
serde_repr = "0.1.20"
|
||||
serde_json = "1.0.140"
|
||||
mime_guess = "2.0.5"
|
||||
chrono = "0.4.41"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
# rustflags = ["-Zthreads=8"] # Better compile performance.
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
strip = true # Remove debug symbols
|
||||
codegen-units = 1 # Allows LLVM to perform better optimization.
|
||||
lto = true # Enables link-time-optimizations.
|
||||
opt-level = 3 # Prioritizes small binary size. Use `3` if you prefer speed.
|
||||
panic = "abort" # Higher performance by disabling panic handlers.
|
||||
strip = true # Ensures debug symbols are removed.
|
||||
# trim-paths = "all" # Removes potentially privileged information from your binaries.
|
||||
# rustflags = ["-Cdebuginfo=0", "-C", "target-cpu=native", "-Z", "threads=8"]
|
||||
|
65
Makefile
Normal file
@ -0,0 +1,65 @@
|
||||
CARGO = cargo
|
||||
RUSTC = rustc
|
||||
CROSS = CROSS_REMOTE=1 cross
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
$(CARGO) build
|
||||
|
||||
release:
|
||||
$(CARGO) build --release
|
||||
|
||||
dev:
|
||||
CANDY_LOG=debug $(CARGO) watch -x run
|
||||
|
||||
run:
|
||||
CANDY_LOG=debug $(CARGO) run
|
||||
|
||||
test:
|
||||
$(CARGO) test
|
||||
|
||||
clean:
|
||||
$(CARGO) clean
|
||||
|
||||
clean-release:
|
||||
rm -rf ./target/release/
|
||||
rm -rf ./target/debug/
|
||||
|
||||
check:
|
||||
$(CARGO) check
|
||||
|
||||
format:
|
||||
$(CARGO) fmt
|
||||
|
||||
lint:
|
||||
$(CARGO) clippy
|
||||
|
||||
fix:
|
||||
$(CARGO) fix --allow-dirty --all-features && $(CARGO) fmt
|
||||
|
||||
linux-musl: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
aarch64-linux-musl: clean-release
|
||||
$(CROSS) build --release --target aarch64-unknown-linux-musl
|
||||
|
||||
aarch64-android: clean-release
|
||||
$(CROSS) build --release --target aarch64-linux-android
|
||||
|
||||
linux-gnu: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
aarch64-linux-gnu: clean-release
|
||||
$(CROSS) build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
windows-gnu: clean-release
|
||||
$(CROSS) build --release --target x86_64-pc-windows-gnu
|
||||
|
||||
freebsd: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-freebsd
|
||||
|
||||
loongarch: clean-release
|
||||
$(CROSS) build --release --target loongarch64-unknown-linux-gnu
|
||||
|
||||
.PHONY: all
|
@ -1,27 +0,0 @@
|
||||
[tasks.format]
|
||||
install_crate = "rustfmt"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--", "--emit=files"]
|
||||
dependencies = ["fix"]
|
||||
|
||||
[tasks.fix]
|
||||
command = "cargo"
|
||||
args = ["fix", "--allow-dirty", "--all-features"]
|
||||
|
||||
[tasks.clean]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["build", "--release"]
|
||||
dependencies = ["clean"]
|
||||
|
||||
[tasks.test]
|
||||
command = "cargo"
|
||||
args = ["test"]
|
||||
dependencies = ["clean"]
|
||||
|
||||
[tasks.dev]
|
||||
command = "cargo"
|
||||
args = ["watch", "-x", "'run'"]
|
@ -2,6 +2,7 @@
|
||||
|
||||
A tiny web server built with rust.
|
||||
|
||||
[](https://deps.rs/repo/github/DefectingCat/candy)
|
||||

|
||||

|
||||
|
||||
|
73
TODO.md
@ -1,30 +1,51 @@
|
||||
## TODO
|
||||
# TODO
|
||||
|
||||
- [x] Graceful shutdown
|
||||
- [x] `keep-alive` timeout setting
|
||||
- [x] HTTP Etag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value
|
||||
- [x] Content compress
|
||||
- [x] zstd
|
||||
- [x] gzip
|
||||
- [x] deflate
|
||||
- [x] br
|
||||
## Configuration
|
||||
|
||||
### Configuration
|
||||
- [x] Overwrite headers in config
|
||||
- [x] Config init tests
|
||||
- [x] Error page
|
||||
- [x] Custom error page with stats code
|
||||
- [ ] Logging to file
|
||||
- [ ] Benches
|
||||
- [ ] Max body size
|
||||
- [x] HTTP 2
|
||||
|
||||
- [ ] File MIME type
|
||||
- [ ] Add headers in config
|
||||
- [ ] Logging to file
|
||||
- [x] Config init tests
|
||||
- [ ] Docker build file
|
||||
- [ ] Benchs
|
||||
- [ ] Max body size
|
||||
- [ ] Error page
|
||||
## Features
|
||||
|
||||
### Features
|
||||
|
||||
- [ ] Proxy
|
||||
- [ ] FastCGI
|
||||
- [ ] SSL
|
||||
- [ ] Cli
|
||||
- [ ] HTTP 2
|
||||
- [ ] HTTP 3
|
||||
- [x] Cross platform compile
|
||||
- [x] x86_64-unknown-linux-gnu
|
||||
- [x] x86_64-unknown-linux-musl
|
||||
- [x] aarch64-unknown-linux-gnu
|
||||
- [x] aarch64-unknown-linux-musl
|
||||
- [x] x86_64-pc-windows-gnu
|
||||
- [x] x86_64-unknown-freebsd
|
||||
- [x] loongarch64-unknown-linux-gnu
|
||||
- [ ] HTTP redirect
|
||||
- [ ] Load balance
|
||||
- [ ] Proxy
|
||||
- [x] Reverse Proxy
|
||||
- [x] Connect to upstream timeout setting
|
||||
- [x] Follow http 301
|
||||
- [x] Custom headers
|
||||
- [x] SSL
|
||||
- [x] Cli
|
||||
- [x] Specific custom config location
|
||||
- [x] HTTP 2
|
||||
- [ ] HTTP 3
|
||||
- [ ] Specify thread numbers
|
||||
- [ ] Dockerization
|
||||
- [ ] Docs
|
||||
- [ ] Multiple virtual hosts
|
||||
- [x] Build with compile info
|
||||
- [x] Refactor HTTP 1
|
||||
- [x] Graceful shutdown
|
||||
- [x] `keep-alive` timeout setting
|
||||
- [x] HTTP Etag: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value>
|
||||
- [x] 304 Not Modified
|
||||
- [ ] List directory
|
||||
- [x] Content compress
|
||||
- [x] zstd
|
||||
- [x] gzip
|
||||
- [x] deflate
|
||||
- [x] br
|
||||
|
65
build.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[allow(unused)]
|
||||
macro_rules! warn {
|
||||
($($tokens: tt)*) => {
|
||||
println!("cargo:warning={}", format!($($tokens)*))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set environment varibles for build.rs
|
||||
/// set_env!(NAME=xfy);
|
||||
macro_rules! set_env {
|
||||
($($tokens: tt)*) => {
|
||||
println!("cargo:rustc-env={}", format!($($tokens)*))
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
rustc_info();
|
||||
commit_info();
|
||||
}
|
||||
|
||||
/// Get rustc version info
|
||||
fn rustc_info() {
|
||||
let rustc_output = Command::new("rustc")
|
||||
.args(["-vV"])
|
||||
.output()
|
||||
.expect("detect rustc info failed")
|
||||
.stdout;
|
||||
let info_str = String::from_utf8_lossy(&rustc_output);
|
||||
let info_arr = info_str
|
||||
.split('\n')
|
||||
.filter(|info| !info.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
set_env!("RUA_COMPILER={}", info_arr[0]);
|
||||
}
|
||||
|
||||
/// Get commit info
|
||||
/// If failed, set RUA_COMMIT=unknown
|
||||
fn commit_info() {
|
||||
let output = Command::new("git").args(["rev-parse", "HEAD"]).output();
|
||||
// get commit info
|
||||
let Ok(output) = output else {
|
||||
warn!("get commit info failed");
|
||||
set_env!("RUA_COMMIT={}", "unknown");
|
||||
return;
|
||||
};
|
||||
// check is dirty
|
||||
let dirty = Command::new("git")
|
||||
.args(["diff", "--quiet"])
|
||||
.output()
|
||||
.is_ok();
|
||||
let commit = String::from_utf8_lossy(&output.stdout)
|
||||
.chars()
|
||||
.take(7)
|
||||
.collect::<String>();
|
||||
let commit = if commit.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else if dirty {
|
||||
format!("{commit}-dirty")
|
||||
} else {
|
||||
commit
|
||||
};
|
||||
set_env!("RUA_COMMIT={}", commit);
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 4000
|
||||
port = 80
|
||||
[[host.route]]
|
||||
# route path: GET /
|
||||
location = "/"
|
||||
# static file path
|
||||
root = "./html"
|
||||
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
proxy_pass = "https://www.google.com/"
|
||||
|
@ -1,17 +1,56 @@
|
||||
# mime
|
||||
default_type = "application/octet-stream"
|
||||
# add mime types
|
||||
[types]
|
||||
wasm = "application/wasm"
|
||||
# Virtual host
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 4000
|
||||
# index file name and format
|
||||
index = ["index.html"]
|
||||
# http keep alive timeout
|
||||
keep_alive = 15
|
||||
port = 80
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# SSL certificate
|
||||
# ssl enable
|
||||
# ssl = true
|
||||
# Self sign a certificate
|
||||
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./html/selfsigned.key -out ./html/selfsigned.crt
|
||||
certificate = "./html/selfsigned.crt"
|
||||
certificate_key = "./html/selfsigned.key"
|
||||
# Add custom headers to response
|
||||
[host.headers]
|
||||
X-Powered-By = "candy"
|
||||
# Routes for virtual host
|
||||
[[host.route]]
|
||||
# route path: GET /
|
||||
# Route location
|
||||
location = "/"
|
||||
# static file path
|
||||
root = "./html"
|
||||
# Static file root
|
||||
# or proxy_pass
|
||||
# or redirect
|
||||
root = "html"
|
||||
# Only use for root field
|
||||
index = ["index.html"]
|
||||
# List directory
|
||||
auto_index = true
|
||||
# Custom 500 page
|
||||
[host.route.error_page]
|
||||
status = 500
|
||||
page = "500.html"
|
||||
# Custom 404 page
|
||||
[host.route.not_found_page]
|
||||
status = 404
|
||||
page = "404.html"
|
||||
|
||||
# Reverse proxy
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
# Client request max body size (bytes)
|
||||
max_body_size = 2048
|
||||
|
||||
# Lua script
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8081
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
lua_script = "html/index.lua"
|
||||
|
12
docs/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
20
docs/.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
1
docs/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
lts/*
|
3
docs/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
.docusaurus/
|
||||
build/
|
||||
node_modules/
|
5
docs/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
PACKAGE_MANAGER = pnpm
|
||||
|
||||
all: pack
|
||||
|
||||
nvm:
|
||||
fnm use && $(CMD)
|
||||
|
||||
deps:
|
||||
make nvm CMD="$(PACKAGE_MANAGER) i"
|
||||
|
||||
fix:
|
||||
make nvm CMD="$(PACKAGE_MANAGER) i && $(PACKAGE_MANAGER) pretty"
|
||||
|
||||
pack:
|
||||
cp -r docs/* i18n/zh-Hans/docusaurus-plugin-content-docs/current \
|
||||
&& make nvm CMD="$(PACKAGE_MANAGER) i && $(PACKAGE_MANAGER) build"
|
||||
|
||||
# build:
|
||||
|
||||
.PHONY: all
|
41
docs/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
10
docs/blog/2024-05-24-hello-world.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
slug: hello-world
|
||||
title: Hello World
|
||||
authors: xfy
|
||||
tags: [hello]
|
||||
---
|
||||
|
||||
## Hello World
|
||||
|
||||
<!-- truncate -->
|
5
docs/blog/authors.yml
Normal file
@ -0,0 +1,5 @@
|
||||
xfy:
|
||||
name: xfy
|
||||
title: xfy
|
||||
url: https://github.com/DefectingCat
|
||||
image_url: https://avatars.githubusercontent.com/u/25033493?v=4
|
4
docs/docs/config/_category_.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
9
docs/docs/config/config-file.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: 配置文件
|
||||
sidebar_position: 1
|
||||
title: 配置文件
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
25
docs/docs/intro.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
||||
|
||||
## 安装
|
||||
|
||||
TODO
|
||||
|
||||
## 使用
|
||||
|
||||
Candy 支持单个可执行文件运行:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
143
docs/docusaurus.config.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import { themes as prismThemes } from 'prism-react-renderer';
|
||||
|
||||
const config: Config = {
|
||||
title: 'Candy',
|
||||
tagline: 'Tiny http web server',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
future: {
|
||||
experimental_faster: true,
|
||||
},
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://your-docusaurus-site.example.com',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'xfy', // Usually your GitHub org/user name.
|
||||
projectName: 'candy', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'zh-Hans',
|
||||
locales: ['en', 'zh-Hans'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
/* routeBasePath: '/', */
|
||||
sidebarPath: './sidebars.ts',
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl: 'https://github.com/DefectingCat/candy/tree/main/docs',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl: 'https://github.com/DefectingCat/candy/tree/main/docs',
|
||||
},
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
colorMode: {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
// Replace with your project's social card
|
||||
image: 'img/docusaurus-social-card.jpg',
|
||||
navbar: {
|
||||
title: 'RUA',
|
||||
logo: {
|
||||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'tutorialSidebar',
|
||||
position: 'left',
|
||||
label: 'Tutorial',
|
||||
},
|
||||
{ to: '/blog', label: 'Blog', position: 'left' },
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/DefectingCat',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
/* links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Tutorial',
|
||||
to: '/docs/intro',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discordapp.com/invite/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/DefectingCat/candy',
|
||||
},
|
||||
],
|
||||
},
|
||||
], */
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Candy, Inc. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
313
docs/i18n/en/code.json
Normal file
@ -0,0 +1,313 @@
|
||||
{
|
||||
"theme.ErrorPageContent.title": {
|
||||
"message": "This page crashed.",
|
||||
"description": "The title of the fallback page when the page crashed"
|
||||
},
|
||||
"theme.BackToTopButton.buttonAriaLabel": {
|
||||
"message": "Scroll back to top",
|
||||
"description": "The ARIA label for the back to top button"
|
||||
},
|
||||
"theme.blog.archive.title": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero title of the blog archive page"
|
||||
},
|
||||
"theme.blog.archive.description": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero description of the blog archive page"
|
||||
},
|
||||
"theme.blog.paginator.navAriaLabel": {
|
||||
"message": "Blog list page navigation",
|
||||
"description": "The ARIA label for the blog pagination"
|
||||
},
|
||||
"theme.blog.paginator.newerEntries": {
|
||||
"message": "Newer entries",
|
||||
"description": "The label used to navigate to the newer blog posts page (previous page)"
|
||||
},
|
||||
"theme.blog.paginator.olderEntries": {
|
||||
"message": "Older entries",
|
||||
"description": "The label used to navigate to the older blog posts page (next page)"
|
||||
},
|
||||
"theme.blog.post.paginator.navAriaLabel": {
|
||||
"message": "Blog post page navigation",
|
||||
"description": "The ARIA label for the blog posts pagination"
|
||||
},
|
||||
"theme.blog.post.paginator.newerPost": {
|
||||
"message": "Newer post",
|
||||
"description": "The blog post button label to navigate to the newer/previous post"
|
||||
},
|
||||
"theme.blog.post.paginator.olderPost": {
|
||||
"message": "Older post",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "View all tags",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "Switch between dark and light mode (currently {mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "dark mode",
|
||||
"description": "The name for the dark color mode"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.light": {
|
||||
"message": "light mode",
|
||||
"description": "The name for the light color mode"
|
||||
},
|
||||
"theme.docs.breadcrumbs.navAriaLabel": {
|
||||
"message": "Breadcrumbs",
|
||||
"description": "The ARIA label for the breadcrumbs"
|
||||
},
|
||||
"theme.docs.DocCard.categoryDescription.plurals": {
|
||||
"message": "1 item|{count} items",
|
||||
"description": "The default description for a category card in the generated index about how many items this category includes"
|
||||
},
|
||||
"theme.docs.paginator.navAriaLabel": {
|
||||
"message": "Docs pages",
|
||||
"description": "The ARIA label for the docs pagination"
|
||||
},
|
||||
"theme.docs.paginator.previous": {
|
||||
"message": "Previous",
|
||||
"description": "The label used to navigate to the previous doc"
|
||||
},
|
||||
"theme.docs.paginator.next": {
|
||||
"message": "Next",
|
||||
"description": "The label used to navigate to the next doc"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle.nDocsTagged": {
|
||||
"message": "One doc tagged|{count} docs tagged",
|
||||
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle": {
|
||||
"message": "{nDocsTagged} with \"{tagName}\"",
|
||||
"description": "The title of the page for a docs tag"
|
||||
},
|
||||
"theme.docs.versions.unreleasedVersionLabel": {
|
||||
"message": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"description": "The label used to tell the user that he's browsing an unreleased doc version"
|
||||
},
|
||||
"theme.docs.versions.unmaintainedVersionLabel": {
|
||||
"message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": {
|
||||
"message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"description": "The label used to tell the user to check the latest version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionLinkLabel": {
|
||||
"message": "latest version",
|
||||
"description": "The label used for the latest version suggestion link label"
|
||||
},
|
||||
"theme.docs.versionBadge.label": {
|
||||
"message": "Version: {versionLabel}"
|
||||
},
|
||||
"theme.common.editThisPage": {
|
||||
"message": "Edit this page",
|
||||
"description": "The link label to edit the current page"
|
||||
},
|
||||
"theme.common.headingLinkTitle": {
|
||||
"message": "Direct link to {heading}",
|
||||
"description": "Title for link to heading"
|
||||
},
|
||||
"theme.NotFound.title": {
|
||||
"message": "Page Not Found",
|
||||
"description": "The title of the 404 page"
|
||||
},
|
||||
"theme.navbar.mobileVersionsDropdown.label": {
|
||||
"message": "Versions",
|
||||
"description": "The label for the navbar versions dropdown on mobile view"
|
||||
},
|
||||
"theme.lastUpdated.atDate": {
|
||||
"message": " on {date}",
|
||||
"description": "The words used to describe on which date a page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.byUser": {
|
||||
"message": " by {user}",
|
||||
"description": "The words used to describe by who the page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.lastUpdatedAtBy": {
|
||||
"message": "Last updated{atDate}{byUser}",
|
||||
"description": "The sentence used to display when a page has been last updated, and by who"
|
||||
},
|
||||
"theme.tags.tagsListLabel": {
|
||||
"message": "Tags:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "Close",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "caution",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
},
|
||||
"theme.admonition.danger": {
|
||||
"message": "danger",
|
||||
"description": "The default label used for the Danger admonition (:::danger)"
|
||||
},
|
||||
"theme.admonition.info": {
|
||||
"message": "info",
|
||||
"description": "The default label used for the Info admonition (:::info)"
|
||||
},
|
||||
"theme.admonition.note": {
|
||||
"message": "note",
|
||||
"description": "The default label used for the Note admonition (:::note)"
|
||||
},
|
||||
"theme.admonition.tip": {
|
||||
"message": "tip",
|
||||
"description": "The default label used for the Tip admonition (:::tip)"
|
||||
},
|
||||
"theme.admonition.warning": {
|
||||
"message": "warning",
|
||||
"description": "The default label used for the Warning admonition (:::warning)"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "Blog recent posts navigation",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "Copied",
|
||||
"description": "The copied button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.copyButtonAriaLabel": {
|
||||
"message": "Copy code to clipboard",
|
||||
"description": "The ARIA label for copy code blocks button"
|
||||
},
|
||||
"theme.CodeBlock.copy": {
|
||||
"message": "Copy",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "Toggle word wrap",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.DocSidebarItem.expandCategoryAriaLabel": {
|
||||
"message": "Expand sidebar category '{label}'",
|
||||
"description": "The ARIA label to expand the sidebar category"
|
||||
},
|
||||
"theme.DocSidebarItem.collapseCategoryAriaLabel": {
|
||||
"message": "Collapse sidebar category '{label}'",
|
||||
"description": "The ARIA label to collapse the sidebar category"
|
||||
},
|
||||
"theme.NavBar.navAriaLabel": {
|
||||
"message": "Main",
|
||||
"description": "The ARIA label for the main navigation"
|
||||
},
|
||||
"theme.NotFound.p1": {
|
||||
"message": "We could not find what you were looking for.",
|
||||
"description": "The first paragraph of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p2": {
|
||||
"message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
|
||||
"description": "The 2nd paragraph of the 404 page"
|
||||
},
|
||||
"theme.navbar.mobileLanguageDropdown.label": {
|
||||
"message": "Languages",
|
||||
"description": "The label for the mobile language switcher dropdown"
|
||||
},
|
||||
"theme.TOCCollapsible.toggleButtonLabel": {
|
||||
"message": "On this page",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "Read more",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
},
|
||||
"theme.blog.post.readMoreLabel": {
|
||||
"message": "Read more about {title}",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "One min read|{readingTime} min read",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "Home page",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonTitle": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.navAriaLabel": {
|
||||
"message": "Docs sidebar",
|
||||
"description": "The ARIA label for the sidebar navigation"
|
||||
},
|
||||
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
|
||||
"message": "Close navigation bar",
|
||||
"description": "The ARIA label for close button of mobile sidebar"
|
||||
},
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
|
||||
"message": "← Back to main menu",
|
||||
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
|
||||
},
|
||||
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
|
||||
"message": "Toggle navigation bar",
|
||||
"description": "The ARIA label for hamburger menu button of mobile navigation"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonTitle": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "One post|{count} posts",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} tagged with \"{tagName}\"",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.blog.author.pageTitle": {
|
||||
"message": "{authorName} - {nPosts}",
|
||||
"description": "The title of the page for a blog author"
|
||||
},
|
||||
"theme.blog.authorsList.pageTitle": {
|
||||
"message": "Authors",
|
||||
"description": "The title of the authors page"
|
||||
},
|
||||
"theme.blog.authorsList.viewAll": {
|
||||
"message": "View all authors",
|
||||
"description": "The label of the link targeting the blog authors page"
|
||||
},
|
||||
"theme.blog.author.noPosts": {
|
||||
"message": "This author has not written any posts yet.",
|
||||
"description": "The text for authors with 0 blog post"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.title": {
|
||||
"message": "Unlisted page",
|
||||
"description": "The unlisted content banner title"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.message": {
|
||||
"message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.title": {
|
||||
"message": "Draft page",
|
||||
"description": "The draft content banner title"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.message": {
|
||||
"message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||
"description": "The draft content banner message"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "Try again",
|
||||
"description": "The label of the button to try again rendering when the React error boundary captures an error"
|
||||
},
|
||||
"theme.common.skipToMainContent": {
|
||||
"message": "Skip to main content",
|
||||
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
|
||||
},
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "Tags",
|
||||
"description": "The title of the tag list page"
|
||||
}
|
||||
}
|
14
docs/i18n/en/docusaurus-plugin-content-blog/options.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "Blog",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "Blog",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "Recent posts",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
10
docs/i18n/en/docusaurus-plugin-content-docs/current.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "Next",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.tutorialSidebar.category.配置": {
|
||||
"message": "配置",
|
||||
"description": "The label for category 配置 in sidebar tutorialSidebar"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: Config File
|
||||
sidebar_position: 1
|
||||
title: Config File
|
||||
---
|
||||
|
||||
## Config File
|
||||
|
||||
Candy follows the config file to configure.
|
25
docs/i18n/en/docusaurus-plugin-content-docs/current/intro.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Candy is a lightweight HTTP server, aiming to quickly deploy a high-performance HTTP server.
|
||||
|
||||
## Installation
|
||||
|
||||
TODO
|
||||
|
||||
## Usage
|
||||
|
||||
Candy supports single executable file running:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
6
docs/i18n/en/docusaurus-theme-classic/footer.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "Copyright © 2024 Candy, Inc. Built with Docusaurus.",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
22
docs/i18n/en/docusaurus-theme-classic/navbar.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "RUA",
|
||||
"description": "The title in the navbar"
|
||||
},
|
||||
"logo.alt": {
|
||||
"message": "My Site Logo",
|
||||
"description": "The alt text of navbar logo"
|
||||
},
|
||||
"item.label.Tutorial": {
|
||||
"message": "Tutorial",
|
||||
"description": "Navbar item with label Tutorial"
|
||||
},
|
||||
"item.label.Blog": {
|
||||
"message": "Blog",
|
||||
"description": "Navbar item with label Blog"
|
||||
},
|
||||
"item.label.GitHub": {
|
||||
"message": "GitHub",
|
||||
"description": "Navbar item with label GitHub"
|
||||
}
|
||||
}
|
313
docs/i18n/zh-Hans/code.json
Normal file
@ -0,0 +1,313 @@
|
||||
{
|
||||
"theme.ErrorPageContent.title": {
|
||||
"message": "页面已崩溃。",
|
||||
"description": "The title of the fallback page when the page crashed"
|
||||
},
|
||||
"theme.BackToTopButton.buttonAriaLabel": {
|
||||
"message": "回到顶部",
|
||||
"description": "The ARIA label for the back to top button"
|
||||
},
|
||||
"theme.blog.archive.title": {
|
||||
"message": "历史博文",
|
||||
"description": "The page & hero title of the blog archive page"
|
||||
},
|
||||
"theme.blog.archive.description": {
|
||||
"message": "历史博文",
|
||||
"description": "The page & hero description of the blog archive page"
|
||||
},
|
||||
"theme.blog.paginator.navAriaLabel": {
|
||||
"message": "博文列表分页导航",
|
||||
"description": "The ARIA label for the blog pagination"
|
||||
},
|
||||
"theme.blog.paginator.newerEntries": {
|
||||
"message": "较新的博文",
|
||||
"description": "The label used to navigate to the newer blog posts page (previous page)"
|
||||
},
|
||||
"theme.blog.paginator.olderEntries": {
|
||||
"message": "较旧的博文",
|
||||
"description": "The label used to navigate to the older blog posts page (next page)"
|
||||
},
|
||||
"theme.blog.post.paginator.navAriaLabel": {
|
||||
"message": "博文分页导航",
|
||||
"description": "The ARIA label for the blog posts pagination"
|
||||
},
|
||||
"theme.blog.post.paginator.newerPost": {
|
||||
"message": "较新一篇",
|
||||
"description": "The blog post button label to navigate to the newer/previous post"
|
||||
},
|
||||
"theme.blog.post.paginator.olderPost": {
|
||||
"message": "较旧一篇",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "查看所有标签",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "切换浅色/暗黑模式(当前为{mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "暗黑模式",
|
||||
"description": "The name for the dark color mode"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.light": {
|
||||
"message": "浅色模式",
|
||||
"description": "The name for the light color mode"
|
||||
},
|
||||
"theme.docs.breadcrumbs.navAriaLabel": {
|
||||
"message": "页面路径",
|
||||
"description": "The ARIA label for the breadcrumbs"
|
||||
},
|
||||
"theme.docs.DocCard.categoryDescription.plurals": {
|
||||
"message": "{count} 个项目",
|
||||
"description": "The default description for a category card in the generated index about how many items this category includes"
|
||||
},
|
||||
"theme.docs.paginator.navAriaLabel": {
|
||||
"message": "文件选项卡",
|
||||
"description": "The ARIA label for the docs pagination"
|
||||
},
|
||||
"theme.docs.paginator.previous": {
|
||||
"message": "上一页",
|
||||
"description": "The label used to navigate to the previous doc"
|
||||
},
|
||||
"theme.docs.paginator.next": {
|
||||
"message": "下一页",
|
||||
"description": "The label used to navigate to the next doc"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle.nDocsTagged": {
|
||||
"message": "{count} 篇文档带有标签",
|
||||
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle": {
|
||||
"message": "{nDocsTagged}「{tagName}」",
|
||||
"description": "The title of the page for a docs tag"
|
||||
},
|
||||
"theme.docs.versionBadge.label": {
|
||||
"message": "版本:{versionLabel}"
|
||||
},
|
||||
"theme.docs.versions.unreleasedVersionLabel": {
|
||||
"message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。",
|
||||
"description": "The label used to tell the user that he's browsing an unreleased doc version"
|
||||
},
|
||||
"theme.docs.versions.unmaintainedVersionLabel": {
|
||||
"message": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。",
|
||||
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": {
|
||||
"message": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。",
|
||||
"description": "The label used to tell the user to check the latest version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionLinkLabel": {
|
||||
"message": "最新版本",
|
||||
"description": "The label used for the latest version suggestion link label"
|
||||
},
|
||||
"theme.common.editThisPage": {
|
||||
"message": "编辑此页",
|
||||
"description": "The link label to edit the current page"
|
||||
},
|
||||
"theme.common.headingLinkTitle": {
|
||||
"message": "{heading}的直接链接",
|
||||
"description": "Title for link to heading"
|
||||
},
|
||||
"theme.lastUpdated.atDate": {
|
||||
"message": "于 {date} ",
|
||||
"description": "The words used to describe on which date a page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.byUser": {
|
||||
"message": "由 {user} ",
|
||||
"description": "The words used to describe by who the page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.lastUpdatedAtBy": {
|
||||
"message": "最后{byUser}{atDate}更新",
|
||||
"description": "The sentence used to display when a page has been last updated, and by who"
|
||||
},
|
||||
"theme.navbar.mobileVersionsDropdown.label": {
|
||||
"message": "选择版本",
|
||||
"description": "The label for the navbar versions dropdown on mobile view"
|
||||
},
|
||||
"theme.NotFound.title": {
|
||||
"message": "找不到页面",
|
||||
"description": "The title of the 404 page"
|
||||
},
|
||||
"theme.tags.tagsListLabel": {
|
||||
"message": "标签:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "关闭",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "警告",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
},
|
||||
"theme.admonition.danger": {
|
||||
"message": "危险",
|
||||
"description": "The default label used for the Danger admonition (:::danger)"
|
||||
},
|
||||
"theme.admonition.info": {
|
||||
"message": "信息",
|
||||
"description": "The default label used for the Info admonition (:::info)"
|
||||
},
|
||||
"theme.admonition.note": {
|
||||
"message": "备注",
|
||||
"description": "The default label used for the Note admonition (:::note)"
|
||||
},
|
||||
"theme.admonition.tip": {
|
||||
"message": "提示",
|
||||
"description": "The default label used for the Tip admonition (:::tip)"
|
||||
},
|
||||
"theme.admonition.warning": {
|
||||
"message": "注意",
|
||||
"description": "The default label used for the Warning admonition (:::warning)"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "最近博文导航",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "切换自动换行",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "复制成功",
|
||||
"description": "The copied button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.copyButtonAriaLabel": {
|
||||
"message": "复制代码到剪贴板",
|
||||
"description": "The ARIA label for copy code blocks button"
|
||||
},
|
||||
"theme.CodeBlock.copy": {
|
||||
"message": "复制",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.DocSidebarItem.expandCategoryAriaLabel": {
|
||||
"message": "展开侧边栏分类 '{label}'",
|
||||
"description": "The ARIA label to expand the sidebar category"
|
||||
},
|
||||
"theme.DocSidebarItem.collapseCategoryAriaLabel": {
|
||||
"message": "折叠侧边栏分类 '{label}'",
|
||||
"description": "The ARIA label to collapse the sidebar category"
|
||||
},
|
||||
"theme.NavBar.navAriaLabel": {
|
||||
"message": "主导航",
|
||||
"description": "The ARIA label for the main navigation"
|
||||
},
|
||||
"theme.navbar.mobileLanguageDropdown.label": {
|
||||
"message": "选择语言",
|
||||
"description": "The label for the mobile language switcher dropdown"
|
||||
},
|
||||
"theme.NotFound.p1": {
|
||||
"message": "我们找不到您要找的页面。",
|
||||
"description": "The first paragraph of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p2": {
|
||||
"message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。",
|
||||
"description": "The 2nd paragraph of the 404 page"
|
||||
},
|
||||
"theme.TOCCollapsible.toggleButtonLabel": {
|
||||
"message": "本页总览",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "阅读需 {readingTime} 分钟",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "阅读更多",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
},
|
||||
"theme.blog.post.readMoreLabel": {
|
||||
"message": "阅读 {title} 的全文",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "主页面",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonTitle": {
|
||||
"message": "收起侧边栏",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": {
|
||||
"message": "收起侧边栏",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.navAriaLabel": {
|
||||
"message": "文档侧边栏",
|
||||
"description": "The ARIA label for the sidebar navigation"
|
||||
},
|
||||
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
|
||||
"message": "关闭导航栏",
|
||||
"description": "The ARIA label for close button of mobile sidebar"
|
||||
},
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
|
||||
"message": "← 回到主菜单",
|
||||
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
|
||||
},
|
||||
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
|
||||
"message": "切换导航栏",
|
||||
"description": "The ARIA label for hamburger menu button of mobile navigation"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonTitle": {
|
||||
"message": "展开侧边栏",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": {
|
||||
"message": "展开侧边栏",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "{count} 篇博文",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} 含有标签「{tagName}」",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.blog.author.pageTitle": {
|
||||
"message": "{authorName} - {nPosts}",
|
||||
"description": "The title of the page for a blog author"
|
||||
},
|
||||
"theme.blog.authorsList.pageTitle": {
|
||||
"message": "Authors",
|
||||
"description": "The title of the authors page"
|
||||
},
|
||||
"theme.blog.authorsList.viewAll": {
|
||||
"message": "View All Authors",
|
||||
"description": "The label of the link targeting the blog authors page"
|
||||
},
|
||||
"theme.blog.author.noPosts": {
|
||||
"message": "This author has not written any posts yet.",
|
||||
"description": "The text for authors with 0 blog post"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.title": {
|
||||
"message": "未列出页",
|
||||
"description": "The unlisted content banner title"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.message": {
|
||||
"message": "此页面未列出。搜索引擎不会对其索引,只有拥有直接链接的用户才能访问。",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.title": {
|
||||
"message": "Draft page",
|
||||
"description": "The draft content banner title"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.message": {
|
||||
"message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||
"description": "The draft content banner message"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "重试",
|
||||
"description": "The label of the button to try again rendering when the React error boundary captures an error"
|
||||
},
|
||||
"theme.common.skipToMainContent": {
|
||||
"message": "跳到主要内容",
|
||||
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
|
||||
},
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "标签",
|
||||
"description": "The title of the tag list page"
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "Blog",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "Blog",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "Recent posts",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
## 配置文件
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "Next",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.tutorialSidebar.category.配置": {
|
||||
"message": "配置",
|
||||
"description": "The label for category 配置 in sidebar tutorialSidebar"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: 配置文件
|
||||
sidebar_position: 1
|
||||
title: 配置文件
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
@ -0,0 +1,25 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
||||
|
||||
## 安装
|
||||
|
||||
TODO
|
||||
|
||||
## 使用
|
||||
|
||||
Candy 支持单个可执行文件运行:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
6
docs/i18n/zh-Hans/docusaurus-theme-classic/footer.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "Copyright © 2024 Candy, Inc. Built with Docusaurus.",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
22
docs/i18n/zh-Hans/docusaurus-theme-classic/navbar.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "RUA",
|
||||
"description": "The title in the navbar"
|
||||
},
|
||||
"logo.alt": {
|
||||
"message": "Candy logo",
|
||||
"description": "The alt text of navbar logo"
|
||||
},
|
||||
"item.label.Tutorial": {
|
||||
"message": "文档",
|
||||
"description": "Navbar item with label Tutorial"
|
||||
},
|
||||
"item.label.Blog": {
|
||||
"message": "Blog",
|
||||
"description": "Navbar item with label Blog"
|
||||
},
|
||||
"item.label.GitHub": {
|
||||
"message": "GitHub",
|
||||
"description": "Navbar item with label GitHub"
|
||||
}
|
||||
}
|
55
docs/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start-zh": "docusaurus start --locale zh-Hans",
|
||||
"build": "docusaurus write-translations --locale en && docusaurus write-translations --locale zh-Hans && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations --locale zh-Hans",
|
||||
"write-translations-en": "docusaurus write-translations --locale en",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"pretty": "prettier --cache --write \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown --plugin=prettier-plugin-organize-imports",
|
||||
"pretty-check": "prettier --cache \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown --check --plugin=prettier-plugin-organize-imports",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.6.3",
|
||||
"@docusaurus/faster": "^3.6.3",
|
||||
"@docusaurus/preset-classic": "3.6.3",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.6.3",
|
||||
"@docusaurus/tsconfig": "3.6.3",
|
||||
"@docusaurus/types": "3.6.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.2"
|
||||
}
|
11992
docs/pnpm-lock.yaml
generated
Normal file
31
docs/sidebars.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
const sidebars: SidebarsConfig = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
export default sidebars;
|
70
docs/src/components/HomepageFeatures/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import Heading from '@theme/Heading';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||
description: JSX.Element;
|
||||
};
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: 'Easy to Use',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Focus on What Matters',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Powered by React',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<Heading as="h3">{title}</Heading>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures(): JSX.Element {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
11
docs/src/components/HomepageFeatures/styles.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
30
docs/src/css/custom.css
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
23
docs/src/pages/index.module.css
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
*/
|
||||
|
||||
.heroBanner {
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
45
docs/src/pages/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
import Heading from '@theme/Heading';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<Heading as="h1" className="hero__title">
|
||||
{siteConfig.title}
|
||||
</Heading>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro"
|
||||
>
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
7
docs/src/pages/markdown-page.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
0
docs/static/.nojekyll
vendored
Normal file
BIN
docs/static/img/docusaurus-social-card.jpg
vendored
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/static/img/docusaurus.png
vendored
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 3.5 KiB |
1
docs/static/img/logo.svg
vendored
Normal file
After Width: | Height: | Size: 6.3 KiB |
171
docs/static/img/undraw_docusaurus_mountain.svg
vendored
Normal file
@ -0,0 +1,171 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
|
||||
<title>Easy to Use</title>
|
||||
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
|
||||
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
|
||||
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
|
||||
<path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
|
||||
<path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
|
||||
<path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
|
||||
<path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
|
||||
<circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
|
||||
<circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
|
||||
<path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
|
||||
<path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
|
||||
<path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
|
||||
<ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
|
||||
<path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
|
||||
<path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
|
||||
<rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
|
||||
<rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
|
||||
<path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
|
||||
<path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
|
||||
<path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
|
||||
<path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
|
||||
<path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
|
||||
<path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
|
||||
<path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
<path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
|
||||
<path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
|
||||
<path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
|
||||
<path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
|
||||
<path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
|
||||
<path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
|
||||
<path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
|
||||
<path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
|
||||
<path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
|
||||
<path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
</g>
|
||||
<g id="docusaurus_keytar" transform="translate(312.271 493.733)">
|
||||
<path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
|
||||
<path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
|
||||
<path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
|
||||
<path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
|
||||
<path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
|
||||
<rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
|
||||
<g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
|
||||
<rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
|
||||
<path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
|
||||
<path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
|
||||
<path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
|
||||
<rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
|
||||
<rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
|
||||
<path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
|
||||
</g>
|
||||
<path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 31 KiB |
170
docs/static/img/undraw_docusaurus_react.svg
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
|
||||
<title>Powered by React</title>
|
||||
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
|
||||
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
|
||||
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
|
||||
<path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
|
||||
<path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
|
||||
<path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
|
||||
<path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
|
||||
<rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
|
||||
<rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
|
||||
<rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
|
||||
<path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
|
||||
<path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
|
||||
<path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
|
||||
<path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
|
||||
<path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
|
||||
<path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
|
||||
<path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
|
||||
<path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
|
||||
<path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
|
||||
<circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
|
||||
<path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
|
||||
<path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
|
||||
<path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
|
||||
<path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
|
||||
<path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
|
||||
<path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
|
||||
<path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
|
||||
<path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
|
||||
<path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
|
||||
<path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
|
||||
</g>
|
||||
<g id="docusaurus_keytar" transform="translate(670.271 615.768)">
|
||||
<path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
|
||||
<path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
|
||||
<path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
|
||||
<path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
|
||||
<path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
|
||||
<rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
|
||||
<g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
|
||||
<rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
|
||||
<path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
|
||||
<path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
|
||||
<path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
|
||||
<rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
|
||||
<rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
|
||||
<rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
|
||||
<path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
|
||||
<path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
|
||||
</g>
|
||||
<rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
|
||||
<rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
<rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
|
||||
</g>
|
||||
<path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
|
||||
<path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
|
||||
<path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g id="React-icon" transform="translate(906.3 541.56)">
|
||||
<path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
|
||||
<path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
|
||||
<circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
|
||||
<path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 35 KiB |
40
docs/static/img/undraw_docusaurus_tree.svg
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
|
||||
<title>Focus on What Matters</title>
|
||||
<circle cx="321" cy="321" r="321" fill="#f2f2f2" />
|
||||
<ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
|
||||
<ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
|
||||
<rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
|
||||
<path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
|
||||
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
|
||||
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
|
||||
<circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
|
||||
<circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
|
||||
<circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
|
||||
<rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
|
||||
<rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
|
||||
<rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
|
||||
<rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
|
||||
<rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
|
||||
<rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
|
||||
<rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
|
||||
<path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
|
||||
<ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
|
||||
<ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
|
||||
<path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
|
||||
<path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
|
||||
<path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
|
||||
<path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
|
||||
<path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
|
||||
<path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
|
||||
<path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
|
||||
<path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
|
||||
<path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
|
||||
<path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
|
||||
<path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
7
docs/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
10
src/cli.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
/// A tiny HTTP server.
|
||||
pub struct Cli {
|
||||
/// Set a custom config file location.
|
||||
#[arg(short, long, value_name = "FILE", default_value = "./config.toml")]
|
||||
pub config: String,
|
||||
}
|
194
src/config.rs
@ -1,68 +1,170 @@
|
||||
use crate::{
|
||||
consts::{
|
||||
host_index, insert_default_mimes, keep_alive_timeout_default, mime_default, types_default,
|
||||
},
|
||||
consts::{default_disabled, host_index, timeout_default, upstream_timeout_default},
|
||||
error::Result,
|
||||
};
|
||||
use std::{borrow::Cow, collections::BTreeMap, fs};
|
||||
use std::fs;
|
||||
|
||||
use anyhow::Context;
|
||||
use dashmap::DashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct SettingRoute {
|
||||
/// The register route
|
||||
pub location: String,
|
||||
/// The static assets root folder
|
||||
pub root: String,
|
||||
pub struct ErrorRoute {
|
||||
pub status: u16,
|
||||
pub page: String,
|
||||
}
|
||||
|
||||
pub type HostRouteMap = BTreeMap<String, SettingRoute>;
|
||||
|
||||
/// Route in virtual host
|
||||
/// Can be a static file or a reverse proxy
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct SettingHost {
|
||||
pub ip: String,
|
||||
pub port: u32,
|
||||
route: Vec<Option<SettingRoute>>,
|
||||
#[serde(skip_deserializing, skip_serializing)]
|
||||
pub route_map: HostRouteMap,
|
||||
pub struct SettingRoute {
|
||||
/// The register route
|
||||
/// for axum route
|
||||
pub location: String,
|
||||
/// The static assets root folder
|
||||
pub root: Option<String>,
|
||||
/// List directory
|
||||
#[serde(default = "default_disabled")]
|
||||
pub auto_index: bool,
|
||||
|
||||
/// Index files format
|
||||
#[serde(default = "host_index")]
|
||||
pub index: Vec<String>,
|
||||
/// HTTP keep-alive timeout
|
||||
#[serde(default = "keep_alive_timeout_default")]
|
||||
pub keep_alive: u16,
|
||||
/// Custom error page
|
||||
pub error_page: Option<ErrorRoute>,
|
||||
/// Custom 404 page
|
||||
pub not_found_page: Option<ErrorRoute>,
|
||||
|
||||
/// Reverse proxy url
|
||||
pub proxy_pass: Option<String>,
|
||||
/// Timeout for connect to upstream
|
||||
#[serde(default = "upstream_timeout_default")]
|
||||
pub proxy_timeout: u16,
|
||||
/// Request max body size (bytes)
|
||||
pub max_body_size: Option<u64>,
|
||||
|
||||
/// Lua script
|
||||
pub lua_script: Option<String>,
|
||||
}
|
||||
|
||||
pub type MIMEType = BTreeMap<Cow<'static, str>, Cow<'static, str>>;
|
||||
/// Host routes
|
||||
/// Each host can have multiple routes
|
||||
pub type HostRouteMap = DashMap<String, SettingRoute>;
|
||||
/// headers
|
||||
pub type HeaderMap = DashMap<String, String>;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
/// Virtual host
|
||||
/// Each host can listen on one port and one ip
|
||||
#[derive(Deserialize, Clone, Debug, Default)]
|
||||
pub struct SettingHost {
|
||||
/// Host ip
|
||||
pub ip: String,
|
||||
/// Host port
|
||||
pub port: u16,
|
||||
/// SSL enable
|
||||
#[serde(default = "default_disabled")]
|
||||
pub ssl: bool,
|
||||
/// SSL certificate location
|
||||
pub certificate: Option<String>,
|
||||
/// ssl key location
|
||||
pub certificate_key: Option<String>,
|
||||
/// Routes in config file
|
||||
pub route: Vec<SettingRoute>,
|
||||
/// Host routes convert from Vec<SettingRoute> to DashMap<String, SettingRoute>
|
||||
/// {
|
||||
/// "/doc": <SettingRoute>
|
||||
/// }
|
||||
#[serde(skip)]
|
||||
pub route_map: HostRouteMap,
|
||||
/// HTTP keep-alive timeout
|
||||
#[serde(default = "timeout_default")]
|
||||
pub timeout: u16,
|
||||
/// HTTP headers
|
||||
/// Used to overwrite headers in config
|
||||
pub headers: Option<HeaderMap>,
|
||||
}
|
||||
|
||||
/// Whole config settings
|
||||
#[derive(Deserialize, Clone, Debug, Default)]
|
||||
pub struct Settings {
|
||||
#[serde(default = "mime_default")]
|
||||
pub default_type: Cow<'static, str>,
|
||||
#[serde(default = "types_default")]
|
||||
pub types: MIMEType,
|
||||
/// Virtual host
|
||||
pub host: Vec<SettingHost>,
|
||||
}
|
||||
|
||||
pub fn init_config() -> Result<Settings> {
|
||||
let file = fs::read_to_string("./config.toml").with_context(|| "read ./config.toml failed")?;
|
||||
let mut settings: Settings = toml::from_str(&file)?;
|
||||
|
||||
// convert route map
|
||||
settings.host.iter_mut().for_each(|host| {
|
||||
let routes = &mut host.route;
|
||||
for route in routes.iter_mut() {
|
||||
if route.is_none() {
|
||||
continue;
|
||||
}
|
||||
let route = route.take().unwrap();
|
||||
host.route_map.insert(route.location.to_string(), route);
|
||||
}
|
||||
});
|
||||
|
||||
// combine mime types
|
||||
insert_default_mimes(&mut settings.types);
|
||||
|
||||
Ok(settings)
|
||||
impl Settings {
|
||||
pub fn new(path: &str) -> Result<Self> {
|
||||
let file = fs::read_to_string(path).with_context(|| format!("read {path} failed"))?;
|
||||
let settings: Settings = toml::from_str(&file)?;
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_settings_new() {
|
||||
// Create a temporary TOML config file
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
writeln!(
|
||||
file,
|
||||
r#"
|
||||
default_type = "text/plain"
|
||||
types = {{ "txt" = "text/plain", "html" = "text/html" }}
|
||||
|
||||
[[host]]
|
||||
ip = "127.0.0.1"
|
||||
port = 8080
|
||||
ssl = false
|
||||
timeout = 30
|
||||
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
root = "/var/www"
|
||||
index = ["index.html", "index.txt"]
|
||||
proxy_timeout = 10
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path = file.path().to_str().unwrap();
|
||||
let settings = Settings::new(path).unwrap();
|
||||
|
||||
// Verify host settings
|
||||
let host = &settings.host[0];
|
||||
assert_eq!(host.ip, "127.0.0.1");
|
||||
assert_eq!(host.port, 8080);
|
||||
assert_eq!(host.timeout, 30);
|
||||
|
||||
// Verify route settings
|
||||
let route = &host.route[0];
|
||||
assert_eq!(route.location, "/");
|
||||
assert_eq!(route.root, Some("/var/www".to_string()));
|
||||
assert_eq!(route.proxy_timeout, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_settings_missing_file() {
|
||||
let result = Settings::new("nonexistent.toml");
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("read nonexistent.toml failed")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_settings_invalid_toml() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
writeln!(file, "invalid toml content").unwrap();
|
||||
|
||||
let path = file.path().to_str().unwrap();
|
||||
let result = Settings::new(path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
112
src/consts.rs
@ -1,27 +1,12 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, env, process::exit, sync::OnceLock};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::config::{init_config, MIMEType, Settings};
|
||||
|
||||
// global settings
|
||||
static SETTINGS: OnceLock<Settings> = OnceLock::new();
|
||||
pub fn get_settings() -> &'static Settings {
|
||||
SETTINGS.get_or_init(|| {
|
||||
init_config()
|
||||
.map_err(|err| {
|
||||
error!("get_or_init config failed: {err}");
|
||||
exit(1);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
use std::env;
|
||||
|
||||
// pre defined
|
||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const OS: &str = env::consts::OS;
|
||||
pub const ARCH: &str = env::consts::ARCH;
|
||||
pub const COMPILER: &str = env!("RUA_COMPILER");
|
||||
pub const COMMIT: &str = env!("RUA_COMMIT");
|
||||
|
||||
// config defaults
|
||||
pub const HOST_INDEX: [&str; 1] = ["index.html"];
|
||||
@ -29,88 +14,19 @@ pub fn host_index() -> Vec<String> {
|
||||
HOST_INDEX.map(|h| h.to_string()).to_vec()
|
||||
}
|
||||
|
||||
// default http keep alive timeout
|
||||
pub const KEEP_ALIVE_TIMEOUTD_EFAULT: u16 = 75;
|
||||
pub fn keep_alive_timeout_default() -> u16 {
|
||||
KEEP_ALIVE_TIMEOUTD_EFAULT
|
||||
// default http connection timeout
|
||||
pub const TIMEOUT_EFAULT: u16 = 75;
|
||||
pub fn timeout_default() -> u16 {
|
||||
TIMEOUT_EFAULT
|
||||
}
|
||||
|
||||
// default mime type for unknow file
|
||||
pub const MIME_DEFAULT: &str = "application/octet-stream";
|
||||
pub fn mime_default() -> Cow<'static, str> {
|
||||
MIME_DEFAULT.into()
|
||||
// default reverse proxy upstream timeout
|
||||
pub const UPSTREAM_TIMEOUT: u16 = 5;
|
||||
pub fn upstream_timeout_default() -> u16 {
|
||||
UPSTREAM_TIMEOUT
|
||||
}
|
||||
|
||||
// default mime types
|
||||
pub fn types_default() -> MIMEType {
|
||||
BTreeMap::new()
|
||||
}
|
||||
macro_rules! insert_mime {
|
||||
($name:literal, $mime:ident, $map:ident) => {
|
||||
$map.entry($name.into()).or_insert($mime.into());
|
||||
};
|
||||
}
|
||||
pub fn insert_default_mimes(map: &mut MIMEType) {
|
||||
use crate::http::mime::*;
|
||||
|
||||
insert_mime!("html", TEXT_HTML, map);
|
||||
insert_mime!("htm", TEXT_HTML, map);
|
||||
insert_mime!("shtml", TEXT_HTML, map);
|
||||
insert_mime!("css", TEXT_CSS, map);
|
||||
insert_mime!("xml", TEXT_XML, map);
|
||||
insert_mime!("rss", TEXT_XML, map);
|
||||
insert_mime!("txt", TEXT_PLAIN, map);
|
||||
|
||||
insert_mime!("gif", IMAGE_GIF, map);
|
||||
insert_mime!("jpg", IMAGE_JPEG, map);
|
||||
insert_mime!("jpeg", IMAGE_JPEG, map);
|
||||
insert_mime!("png", IMAGE_PNG, map);
|
||||
insert_mime!("ico", IMAGE_ICON, map);
|
||||
insert_mime!("jng", IMAGE_JNG, map);
|
||||
insert_mime!("wbmp", IMAGE_WBMP, map);
|
||||
insert_mime!("webp", IMAGE_WEBP, map);
|
||||
insert_mime!("avif", IMAGE_AVIF, map);
|
||||
insert_mime!("svg", IMAGE_SVG, map);
|
||||
insert_mime!("svgz", IMAGE_SVG, map);
|
||||
insert_mime!("tif", IMAGE_TIFF, map);
|
||||
insert_mime!("tiff", IMAGE_TIFF, map);
|
||||
insert_mime!("bmp", IMAGE_BMP, map);
|
||||
|
||||
insert_mime!("js", APPLICATION_JAVASCRIPT, map);
|
||||
insert_mime!("wasm", APPLICATION_WASM, map);
|
||||
insert_mime!("json", APPLICATION_JSON, map);
|
||||
insert_mime!("jar", APPLICATION_JAVA_ARCHIVE, map);
|
||||
insert_mime!("war", APPLICATION_JAVA_ARCHIVE, map);
|
||||
insert_mime!("ear", APPLICATION_JAVA_ARCHIVE, map);
|
||||
insert_mime!("m3u8", APPLICATION_APPLE_MPEGURL, map);
|
||||
insert_mime!("bin", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("exe", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("dll", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("deb", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("dmg", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("iso", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("img", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("msi", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("msp", APPLICATION_OCTET_STREAM, map);
|
||||
insert_mime!("msm", APPLICATION_OCTET_STREAM, map);
|
||||
|
||||
insert_mime!("woff", FONT_WOFF, map);
|
||||
insert_mime!("woff2", FONT_WOFF2, map);
|
||||
|
||||
insert_mime!("ts", VIDEO_MP2T, map);
|
||||
insert_mime!("3gpp", VIDEO_3GPP, map);
|
||||
insert_mime!("3gp", VIDEO_3GPP, map);
|
||||
insert_mime!("mp4", VIDEO_MP4, map);
|
||||
insert_mime!("mpeg", VIDEO_MPEG, map);
|
||||
insert_mime!("mpg", VIDEO_MPEG, map);
|
||||
insert_mime!("mov", VIDEO_QUICKTIME, map);
|
||||
insert_mime!("webm", VIDEO_WEBM, map);
|
||||
|
||||
insert_mime!("flv", VIDEO_X_FLV, map);
|
||||
insert_mime!("m4v", VIDEO_X_M4V, map);
|
||||
insert_mime!("mng", VIDEO_X_MNG, map);
|
||||
insert_mime!("asx", VIDEO_X_MS_ASF, map);
|
||||
insert_mime!("asf", VIDEO_X_MS_ASF, map);
|
||||
insert_mime!("wmv", VIDEO_X_MS_WMV, map);
|
||||
insert_mime!("avi", VIDEO_X_MSVIDEO, map);
|
||||
// default boolean false
|
||||
pub fn default_disabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
26
src/error.rs
@ -1,7 +1,7 @@
|
||||
use std::{borrow::Cow, io, num::TryFromIntError, sync::PoisonError, time::SystemTimeError};
|
||||
use std::{io, num::TryFromIntError, time::SystemTimeError};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use hyper::header::{InvalidHeaderValue, ToStrError};
|
||||
use http::uri::InvalidUri;
|
||||
use hyper::header::ToStrError;
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@ -19,22 +19,12 @@ pub enum Error {
|
||||
TryFromInt(#[from] TryFromIntError),
|
||||
#[error("failed to convert str {0}")]
|
||||
ToStr(#[from] ToStrError),
|
||||
|
||||
// http
|
||||
#[error("route not found {0}")]
|
||||
NotFound(Cow<'static, str>),
|
||||
#[error("failed to convert url {0}")]
|
||||
InvalidUri(#[from] InvalidUri),
|
||||
#[error("hyper {0}")]
|
||||
HyperError(#[from] hyper::Error),
|
||||
#[error("internal server error {0}")]
|
||||
InternalServerError(#[from] anyhow::Error),
|
||||
#[error("invalide header value {0}")]
|
||||
InvalidHeader(#[from] InvalidHeaderValue),
|
||||
#[error("")]
|
||||
Empty,
|
||||
Any(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = anyhow::Result<T, E>;
|
||||
|
||||
impl<T> From<PoisonError<T>> for Error {
|
||||
fn from(err: PoisonError<T>) -> Self {
|
||||
Self::InternalServerError(anyhow!("global cache poisoned {err}"))
|
||||
}
|
||||
}
|
||||
|
100
src/http/error.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::consts::{NAME, VERSION};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use const_format::{concatcp, formatcp};
|
||||
use serde_repr::*;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RouteError {
|
||||
// Common errors
|
||||
#[error("{0}")]
|
||||
Any(#[from] anyhow::Error),
|
||||
#[error("{0}")]
|
||||
Infallible(#[from] std::convert::Infallible),
|
||||
|
||||
// Route errors
|
||||
#[error("route not found")]
|
||||
RouteNotFound(),
|
||||
#[error("internal error")]
|
||||
InternalError(),
|
||||
#[error("bad request")]
|
||||
BadRequest(),
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
|
||||
#[repr(u16)]
|
||||
pub enum ErrorCode {
|
||||
Normal = 200,
|
||||
InternalError = 500,
|
||||
NotFound = 404,
|
||||
BadRequest = 400,
|
||||
}
|
||||
|
||||
/// Normal error message
|
||||
const SERVER_ERROR_STR: &str = concatcp!(
|
||||
r#"Internal Server Error
|
||||
{NAME} v{VERSION}
|
||||
Powered by RUA
|
||||
"#
|
||||
);
|
||||
|
||||
/// Not found error message
|
||||
const NOT_FOUND_STR: &str = formatcp!(
|
||||
r#"Resource Not Found
|
||||
{NAME} v{VERSION}
|
||||
Powered by RUA
|
||||
"#
|
||||
);
|
||||
|
||||
const BAD_REQUEST_STR: &str = formatcp!(
|
||||
r#"Bad Request
|
||||
{NAME} v{VERSION}
|
||||
Powered by RUA
|
||||
"#
|
||||
);
|
||||
|
||||
impl Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use ErrorCode::*;
|
||||
|
||||
let res = match self {
|
||||
Normal => "",
|
||||
InternalError => SERVER_ERROR_STR,
|
||||
NotFound => NOT_FOUND_STR,
|
||||
BadRequest => BAD_REQUEST_STR,
|
||||
};
|
||||
f.write_str(res)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Log and return INTERNAL_SERVER_ERROR
|
||||
fn log_internal_error<T: Display>(err: T) -> (StatusCode, String) {
|
||||
use ErrorCode::*;
|
||||
|
||||
error!("{err}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, InternalError.to_string())
|
||||
}
|
||||
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> Response {
|
||||
use RouteError::*;
|
||||
|
||||
let (status_code, err_message) = match self {
|
||||
Any(err) => log_internal_error(err),
|
||||
RouteNotFound() => (StatusCode::NOT_FOUND, ErrorCode::NotFound.to_string()),
|
||||
InternalError() => (StatusCode::NOT_FOUND, ErrorCode::InternalError.to_string()),
|
||||
// Infallible(infallible) => todo!(),
|
||||
BadRequest() => (StatusCode::NOT_FOUND, ErrorCode::BadRequest.to_string()),
|
||||
};
|
||||
(status_code, err_message).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type RouteResult<T, E = RouteError> = Result<T, E>;
|
@ -1,78 +0,0 @@
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
#![allow(dead_code)]
|
||||
|
||||
macro_rules! mime {
|
||||
($a:ident, $b:literal) => {
|
||||
pub const $a: &str = $b;
|
||||
};
|
||||
}
|
||||
|
||||
mime!(TEXT_PLAIN, "text/plain");
|
||||
mime!(TEXT_PLAIN_UTF_8, "text/plain; charset=utf-8");
|
||||
mime!(TEXT_HTML, "text/html");
|
||||
mime!(TEXT_HTML_UTF_8, "text/html; charset=utf-8");
|
||||
mime!(TEXT_CSS, "text/css");
|
||||
mime!(TEXT_CSS_UTF_8, "text/css; charset=utf-8");
|
||||
mime!(TEXT_JAVASCRIPT, "text/javascript");
|
||||
mime!(TEXT_XML, "text/xml");
|
||||
mime!(TEXT_EVENT_STREAM, "text/event-stream");
|
||||
mime!(TEXT_CSV, "text/csv");
|
||||
mime!(TEXT_CSV_UTF_8, "text/csv; charset=utf-8");
|
||||
mime!(TEXT_TAB_SEPARATED_VALUES, "text/tab-separated-values");
|
||||
mime!(
|
||||
TEXT_TAB_SEPARATED_VALUES_UTF_8,
|
||||
"text/tab-separated-values; charset=utf-8"
|
||||
);
|
||||
mime!(TEXT_VCARD, "text/vcard");
|
||||
|
||||
mime!(IMAGE_JPEG, "image/jpeg");
|
||||
mime!(IMAGE_GIF, "image/gif");
|
||||
mime!(IMAGE_PNG, "image/png");
|
||||
mime!(IMAGE_ICON, "image/x-icon");
|
||||
mime!(IMAGE_JNG, "image/x-jng");
|
||||
mime!(IMAGE_WBMP, "image/vnd.wap.wbmp ");
|
||||
mime!(IMAGE_BMP, "image/bmp");
|
||||
mime!(IMAGE_SVG, "image/svg+xml");
|
||||
mime!(IMAGE_AVIF, "image/avif");
|
||||
mime!(IMAGE_TIFF, "image/tiff");
|
||||
mime!(IMAGE_WEBP, "image/webp");
|
||||
|
||||
mime!(VIDEO_3GPP, "video/3gpp");
|
||||
mime!(VIDEO_MP2T, "video/mp2t");
|
||||
mime!(VIDEO_MP4, "video/mp4");
|
||||
mime!(VIDEO_MPEG, "video/mpeg");
|
||||
mime!(VIDEO_QUICKTIME, "video/quicktime");
|
||||
mime!(VIDEO_WEBM, "video/webm");
|
||||
mime!(VIDEO_X_FLV, "video/x-flv");
|
||||
mime!(VIDEO_X_M4V, "video/x-m4v");
|
||||
mime!(VIDEO_X_MNG, "video/x-mng");
|
||||
mime!(VIDEO_X_MS_ASF, "video/x-ms-asf");
|
||||
mime!(VIDEO_X_MS_WMV, "video/x-ms-wmv");
|
||||
mime!(VIDEO_X_MSVIDEO, "video/x-msvideo");
|
||||
|
||||
mime!(FONT_WOFF, "font/woff");
|
||||
mime!(FONT_WOFF2, "font/woff2");
|
||||
|
||||
mime!(APPLICATION_JSON, "application/json");
|
||||
mime!(APPLICATION_JAVASCRIPT, "application/javascript");
|
||||
mime!(APPLICATION_WASM, "application/wasm");
|
||||
mime!(
|
||||
APPLICATION_JAVASCRIPT_UTF_8,
|
||||
"application/javascript; charset=utf-8"
|
||||
);
|
||||
mime!(
|
||||
APPLICATION_WWW_FORM_URLENCODED,
|
||||
"application/x-www-form-urlencoded"
|
||||
);
|
||||
mime!(APPLICATION_OCTET_STREAM, "application/octet-stream");
|
||||
mime!(APPLICATION_MSGPACK, "application/msgpack");
|
||||
mime!(APPLICATION_PDF, "application/pdf");
|
||||
mime!(APPLICATION_DNS, "application/dns-message");
|
||||
mime!(APPLICATION_JAVA_ARCHIVE, "application/java-archive");
|
||||
mime!(APPLICATION_APPLE_MPEGURL, "application/vnd.apple.mpegurl");
|
||||
|
||||
mime!(STAR_STAR, "*/*");
|
||||
mime!(TEXT_STAR, "text/*");
|
||||
mime!(IMAGE_STAR, "image/*");
|
||||
mime!(VIDEO_STAR, "video/*");
|
||||
mime!(AUDIO_STAR, "audio/*");
|
176
src/http/mod.rs
@ -1,4 +1,174 @@
|
||||
pub mod mime;
|
||||
pub mod response;
|
||||
use std::{net::SocketAddr, sync::LazyLock, time::Duration};
|
||||
|
||||
pub use response::*;
|
||||
use anyhow::anyhow;
|
||||
use axum::{Router, extract::DefaultBodyLimit, middleware, routing::get};
|
||||
use axum_server::{Handle, tls_rustls::RustlsConfig};
|
||||
use dashmap::DashMap;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{compression::CompressionLayer, timeout::TimeoutLayer};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
config::SettingHost,
|
||||
middlewares::{add_headers, add_version, logging_route},
|
||||
utils::graceful_shutdown,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
// handle static file
|
||||
pub mod serve;
|
||||
// handle reverse proxy
|
||||
pub mod reverse_proxy;
|
||||
|
||||
/// Host configuration
|
||||
/// use virtual host port as key
|
||||
/// use SettingHost as value
|
||||
/// Use port as parent part
|
||||
/// Use host.route.location as key
|
||||
/// Use host.route struct as value
|
||||
/// {
|
||||
/// 80: {
|
||||
/// "/doc": <SettingRoute>
|
||||
/// }
|
||||
/// }
|
||||
pub static HOSTS: LazyLock<DashMap<u16, SettingHost>> = LazyLock::new(DashMap::new);
|
||||
|
||||
pub async fn make_server(host: SettingHost) -> anyhow::Result<()> {
|
||||
let mut router = Router::new();
|
||||
let host_to_save = host.clone();
|
||||
// find routes in config
|
||||
// convert to axum routes
|
||||
// register routes
|
||||
for host_route in &host.route {
|
||||
// lua script
|
||||
if let Some(lua_path) = &host_route.lua_script {
|
||||
continue;
|
||||
}
|
||||
|
||||
// reverse proxy
|
||||
if host_route.proxy_pass.is_some() {
|
||||
router = router.route(host_route.location.as_ref(), get(reverse_proxy::serve));
|
||||
// register wildcard path /doc/*
|
||||
let route_path = format!("{}{{*path}}", host_route.location);
|
||||
router = router.route(route_path.as_ref(), get(reverse_proxy::serve));
|
||||
// Set request max body size
|
||||
if let Some(max_body_size) = host_route.max_body_size {
|
||||
router = router.layer(DefaultBodyLimit::max(max_body_size as usize));
|
||||
}
|
||||
// save route path to map
|
||||
{
|
||||
host_to_save
|
||||
.route_map
|
||||
.insert(host_route.location.clone(), host_route.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// static file
|
||||
if host_route.root.is_none() {
|
||||
warn!("root field not found for route: {:?}", host_route.location);
|
||||
continue;
|
||||
}
|
||||
// Set request max body size
|
||||
if let Some(max_body_size) = host_route.max_body_size {
|
||||
router = router.layer(DefaultBodyLimit::max(max_body_size as usize));
|
||||
}
|
||||
// resister with location
|
||||
// location = "/doc"
|
||||
// route: GET /doc/*
|
||||
// resister with file path
|
||||
// index = ["index.html", "index.txt"]
|
||||
// route: GET /doc/index.html
|
||||
// route: GET /doc/index.txt
|
||||
// register parent path /doc
|
||||
let path_morethan_one = host_route.location.len() > 1;
|
||||
let route_path = if path_morethan_one && host_route.location.ends_with('/') {
|
||||
// first register path with slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
let len = host_route.location.len();
|
||||
let path_without_slash = host_route.location.chars().collect::<Vec<_>>()[0..len - 1]
|
||||
.iter()
|
||||
.collect::<String>();
|
||||
// then register path without slash /doc/
|
||||
router = router.route(&path_without_slash, get(serve::serve));
|
||||
debug!("registed route {}", path_without_slash);
|
||||
host_route.location.clone()
|
||||
} else if path_morethan_one {
|
||||
// first register path without slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
// then register path with slash /doc/
|
||||
let path = format!("{}/", host_route.location);
|
||||
router = router.route(&path, get(serve::serve));
|
||||
debug!("registed route {}", path);
|
||||
path
|
||||
} else {
|
||||
// register path /doc/
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
host_route.location.clone()
|
||||
};
|
||||
// save route path to map
|
||||
{
|
||||
host_to_save
|
||||
.route_map
|
||||
.insert(route_path.clone(), host_route.clone());
|
||||
}
|
||||
let route_path = format!("{route_path}{{*path}}");
|
||||
// register wildcard path /doc/*
|
||||
router = router.route(route_path.as_ref(), get(serve::serve));
|
||||
debug!("registed route: {}", route_path);
|
||||
}
|
||||
|
||||
// save host to map
|
||||
HOSTS.insert(host.port, host_to_save);
|
||||
|
||||
router = router.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(middleware::from_fn(add_version))
|
||||
.layer(middleware::from_fn(add_headers))
|
||||
.layer(TimeoutLayer::new(Duration::from_secs(host.timeout.into())))
|
||||
.layer(CompressionLayer::new()),
|
||||
);
|
||||
|
||||
router = logging_route(router);
|
||||
|
||||
let addr = format!("{}:{}", host.ip, host.port);
|
||||
let addr: SocketAddr = addr.parse()?;
|
||||
|
||||
let handle = Handle::new();
|
||||
// Spawn a task to gracefully shutdown server.
|
||||
tokio::spawn(graceful_shutdown(handle.clone()));
|
||||
|
||||
// check ssl eanbled or not
|
||||
// if ssl enabled
|
||||
// then create ssl listener
|
||||
// else create tcp listener
|
||||
if host.ssl && host.certificate.is_some() && host.certificate_key.is_some() {
|
||||
let cert = host
|
||||
.certificate
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("certificate not found"))?;
|
||||
let key = host
|
||||
.certificate_key
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("certificate_key not found"))?;
|
||||
debug!("certificate {} certificate_key {}", cert, key);
|
||||
|
||||
let rustls_config = RustlsConfig::from_pem_file(cert, key).await?;
|
||||
info!("listening on https://{}", addr);
|
||||
axum_server::bind_rustls(addr, rustls_config)
|
||||
.handle(handle)
|
||||
.serve(router.into_make_service())
|
||||
.await?;
|
||||
} else {
|
||||
info!("listening on http://{}", addr);
|
||||
axum_server::bind(addr)
|
||||
.handle(handle)
|
||||
.serve(router.into_make_service())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,175 +0,0 @@
|
||||
use std::{path::PathBuf, str::FromStr, time::UNIX_EPOCH};
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
get_settings,
|
||||
utils::compress::{stream_compress, CompressType},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures_util::TryStreamExt;
|
||||
use http::response::Builder;
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
|
||||
use hyper::{
|
||||
body::{Bytes, Frame, Incoming},
|
||||
Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncBufRead, BufReader},
|
||||
};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub type CandyBody<T, E = Error> = BoxBody<T, E>;
|
||||
|
||||
/// Open local file and check last modified time,
|
||||
/// Then determine stream file or use cache file
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `path`: local file path
|
||||
pub async fn open_file(path: &str) -> Result<File> {
|
||||
// Open file for reading
|
||||
let file = File::open(path).await;
|
||||
let file = match file {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
error!("Unable to open file {err}");
|
||||
return Err(Error::NotFound(format!("path not found {}", path).into()));
|
||||
}
|
||||
};
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Open then use `ReaderStream` to stream to client.
|
||||
/// Stream a file more suitable for large file, but its slower than read file to memory.
|
||||
pub async fn stream_file<R>(file: R) -> CandyBody<Bytes>
|
||||
where
|
||||
R: AsyncBufRead + Sync + Send + 'static,
|
||||
{
|
||||
// Wrap to a tokio_util::io::ReaderStream
|
||||
let reader_stream = ReaderStream::new(file);
|
||||
// Convert to http_body_util::BoxBody
|
||||
let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data));
|
||||
// let boxed_body = stream_body.map_err(|e| Error::IoError(e)).boxed();
|
||||
BodyExt::map_err(stream_body, Error::Io).boxed()
|
||||
}
|
||||
|
||||
// pub async fn read_file_bytes(file: &mut File, size: u64) -> Result<Vec<u8>> {
|
||||
// let mut buffer = vec![0u8; size.try_into()?];
|
||||
// file.read_exact(&mut buffer[..]).await?;
|
||||
// Ok(buffer)
|
||||
// }
|
||||
|
||||
// Open local file to memory
|
||||
// pub async fn read_file(file: &mut File, size: u64) -> Result<CandyBody<Bytes>> {
|
||||
// let bytes = read_file_bytes(file, size).await?;
|
||||
// let body = Full::new(bytes.into()).map_err(|e| match e {}).boxed();
|
||||
// Ok(body)
|
||||
// }
|
||||
|
||||
// HTTP status code 404
|
||||
static NOT_FOUND: &[u8] = b"Not Found";
|
||||
pub fn not_found() -> Response<CandyBody<Bytes>> {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Full::new(NOT_FOUND.into()).map_err(|e| match e {}).boxed())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
static INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error";
|
||||
pub fn internal_server_error() -> Response<CandyBody<Bytes>> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(
|
||||
Full::new(INTERNAL_SERVER_ERROR.into())
|
||||
.map_err(|e| match e {})
|
||||
.boxed(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// HTTP methods
|
||||
/// handle http get method
|
||||
/// read static file and check If-None-Match cache
|
||||
pub async fn handle_get(
|
||||
req: &Request<Incoming>,
|
||||
mut res: Builder,
|
||||
path: &str,
|
||||
) -> Result<Response<CandyBody<Bytes>>> {
|
||||
use CompressType::*;
|
||||
use Error::*;
|
||||
|
||||
let headers = res
|
||||
.headers_mut()
|
||||
.ok_or(InternalServerError(anyhow!("build response failed")))?;
|
||||
|
||||
// file bytes
|
||||
let file = open_file(path).await?;
|
||||
// file info
|
||||
let metadata = file.metadata().await?;
|
||||
let size = metadata.len();
|
||||
let last_modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let etag = format!("{last_modified}-{size}");
|
||||
let extension = PathBuf::from_str(path).map_err(|err| InternalServerError(anyhow!(err)))?;
|
||||
let extension = extension
|
||||
.extension()
|
||||
.ok_or(InternalServerError(anyhow!("read file extension failed")))?;
|
||||
|
||||
let settings = get_settings();
|
||||
let content_type = settings.types.get(
|
||||
extension
|
||||
.to_str()
|
||||
.ok_or(InternalServerError(anyhow!("read file extension failed")))?,
|
||||
);
|
||||
headers.insert(
|
||||
"Content-Type",
|
||||
content_type.unwrap_or(&settings.default_type).parse()?,
|
||||
);
|
||||
headers.insert("Etag", etag.parse()?);
|
||||
|
||||
// check cache
|
||||
let if_none_match = req.headers().get("If-None-Match");
|
||||
match if_none_match {
|
||||
Some(inm) if *inm == *etag => {
|
||||
let res = res.status(304);
|
||||
let body = Full::new(vec![].into()).map_err(|e| match e {}).boxed();
|
||||
return Ok(res.body(body)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let file_reader = BufReader::new(file);
|
||||
// prepare compress
|
||||
let accept_encoding = req.headers().get("Accept-Encoding");
|
||||
let boxed_body = match accept_encoding {
|
||||
Some(accept) => {
|
||||
let accept = accept.to_str()?;
|
||||
debug!("Accept-Encoding {}", accept);
|
||||
match accept {
|
||||
str if str.contains("zstd") => {
|
||||
headers.insert("Content-Encoding", "zstd".parse()?);
|
||||
stream_compress(Zstd, file_reader)
|
||||
}
|
||||
str if str.contains("gzip") => {
|
||||
headers.insert("Content-Encoding", "gzip".parse()?);
|
||||
stream_compress(Gzip, file_reader)
|
||||
}
|
||||
str if str.contains("deflate") => {
|
||||
headers.insert("Content-Encoding", "deflate".parse()?);
|
||||
stream_compress(Deflate, file_reader)
|
||||
}
|
||||
str if str.contains("br") => {
|
||||
headers.insert("Content-Encoding", "br".parse()?);
|
||||
stream_compress(Brotli, file_reader)
|
||||
}
|
||||
_ => stream_file(file_reader).await,
|
||||
}
|
||||
}
|
||||
None => stream_file(file_reader).await,
|
||||
};
|
||||
|
||||
Ok(res.body(boxed_body)?)
|
||||
}
|
249
src/http/reverse_proxy.rs
Normal file
@ -0,0 +1,249 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Request},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use dashmap::mapref::one::Ref;
|
||||
use http::{
|
||||
HeaderName, HeaderValue, StatusCode, Uri,
|
||||
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||
};
|
||||
use mime_guess::from_path;
|
||||
use reqwest::Client;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::{
|
||||
config::SettingRoute,
|
||||
http::serve::{calculate_etag, resolve_parent_path},
|
||||
utils::parse_port_from_host,
|
||||
};
|
||||
|
||||
use super::{
|
||||
HOSTS,
|
||||
error::{RouteError, RouteResult},
|
||||
};
|
||||
|
||||
/// 处理自定义错误页面(如404、500等)的请求
|
||||
///
|
||||
/// 该函数根据配置信息加载自定义错误页面文件,并根据HTTP缓存机制
|
||||
/// 决定是返回完整内容还是304 Not Modified状态码。
|
||||
///
|
||||
/// # 参数
|
||||
/// - `host_config`: 主机路由配置,包含错误页面路径和根目录信息
|
||||
/// - `request`: 原始HTTP请求
|
||||
/// - `is_error_page`: 是否为错误页面(true: 错误页,false: 404页)
|
||||
///
|
||||
/// # 返回
|
||||
/// - `Ok(Response)`: 成功时返回HTTP响应
|
||||
/// - `Err(RouteError)`: 失败时返回路由错误
|
||||
pub async fn handle_custom_page(
|
||||
host_config: Ref<'_, String, SettingRoute>,
|
||||
request: Request<Body>,
|
||||
is_error_page: bool,
|
||||
) -> RouteResult<Response<Body>> {
|
||||
// 根据请求类型选择相应的页面配置
|
||||
let page = if is_error_page {
|
||||
host_config
|
||||
.error_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
} else {
|
||||
host_config
|
||||
.not_found_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
};
|
||||
|
||||
// 获取站点根目录配置
|
||||
let root = host_config
|
||||
.root
|
||||
.as_ref()
|
||||
.ok_or(RouteError::InternalError())?;
|
||||
|
||||
// 构建完整文件路径
|
||||
let path = format!("{}/{}", root, page.page);
|
||||
tracing::debug!("custom not found path: {:?}", path);
|
||||
|
||||
// 打开文件并计算ETag用于缓存验证
|
||||
let file = File::open(path.clone())
|
||||
.await
|
||||
.with_context(|| "open file failed")?;
|
||||
|
||||
let etag = calculate_etag(&file, path.as_str()).await?;
|
||||
let mut response = Response::builder();
|
||||
let mut not_modified = false;
|
||||
|
||||
// 检查客户端缓存验证头(If-None-Match)
|
||||
if let Some(if_none_match) = request.headers().get(IF_NONE_MATCH) {
|
||||
if let Ok(if_none_match_str) = if_none_match.to_str() {
|
||||
if if_none_match_str == etag {
|
||||
// 资源未修改,返回304状态码
|
||||
response = response.status(StatusCode::NOT_MODIFIED);
|
||||
not_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 准备响应主体
|
||||
let stream = if not_modified {
|
||||
// 304响应返回空内容
|
||||
let empty = File::open(PathBuf::from("/dev/null"))
|
||||
.await
|
||||
.with_context(|| "open /dev/null failed")?;
|
||||
ReaderStream::new(empty)
|
||||
} else {
|
||||
// 正常响应返回文件内容
|
||||
ReaderStream::new(file)
|
||||
};
|
||||
let body = Body::from_stream(stream);
|
||||
|
||||
// 设置响应头:内容类型和ETag
|
||||
let mime = from_path(path).first_or_octet_stream();
|
||||
response
|
||||
.headers_mut()
|
||||
.with_context(|| "insert header failed")?
|
||||
.insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime.as_ref()).with_context(|| "insert header failed")?,
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.with_context(|| "insert header failed")?
|
||||
.insert(
|
||||
ETAG,
|
||||
HeaderValue::from_str(&etag).with_context(|| "insert header failed")?,
|
||||
);
|
||||
|
||||
// 构建最终响应
|
||||
let response = response
|
||||
.body(body)
|
||||
.with_context(|| "Failed to build HTTP response with body")?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Handles the reverse proxy logic for incoming requests.
|
||||
/// This function:
|
||||
/// 1. Extracts the request path, host, and other details.
|
||||
/// 2. Resolves the parent path and proxy configuration.
|
||||
/// 3. Forwards the request to the configured proxy server.
|
||||
/// 4. Returns the response from the proxy server to the client.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `req_uri` - The URI of the incoming request.
|
||||
/// * `path` - Optional path parameter extracted from the request.
|
||||
/// * `host` - The host header from the request.
|
||||
/// * `req` - The incoming HTTP request.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `RouteResult` containing the response from the proxy server or an error.
|
||||
#[axum::debug_handler]
|
||||
pub async fn serve(
|
||||
req_uri: Uri,
|
||||
path: Option<Path<String>>,
|
||||
Host(host): Host,
|
||||
mut req: Request<Body>,
|
||||
) -> RouteResult<impl IntoResponse> {
|
||||
let req_path = req.uri().path();
|
||||
let path_query = req
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(req_path);
|
||||
|
||||
let scheme = req.uri().scheme_str().unwrap_or("http");
|
||||
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
|
||||
let route_map = &HOSTS.get(&port).ok_or(RouteError::BadRequest())?.route_map;
|
||||
tracing::debug!("Route map entries: {:?}", route_map);
|
||||
|
||||
let parent_path = resolve_parent_path(&req_uri, path.as_ref());
|
||||
tracing::debug!("parent path: {:?}", parent_path);
|
||||
let proxy_config = route_map
|
||||
.get(&parent_path)
|
||||
.ok_or(RouteError::RouteNotFound())?;
|
||||
tracing::debug!("proxy pass: {:?}", proxy_config);
|
||||
let Some(ref proxy_pass) = proxy_config.proxy_pass else {
|
||||
return handle_custom_page(proxy_config, req, true).await;
|
||||
};
|
||||
let uri = format!("{proxy_pass}{path_query}");
|
||||
tracing::debug!("reverse proxy uri: {:?}", &uri);
|
||||
*req.uri_mut() = Uri::try_from(uri.clone()).map_err(|_| RouteError::InternalError())?;
|
||||
|
||||
let timeout = proxy_config.proxy_timeout;
|
||||
|
||||
// forward request headers
|
||||
let client = Client::new();
|
||||
let mut forward_req = client
|
||||
.request(req.method().clone(), uri)
|
||||
.timeout(Duration::from_secs(timeout.into()));
|
||||
for (name, value) in req.headers().iter() {
|
||||
if !is_exclude_header(name) {
|
||||
forward_req = forward_req.header(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// forward request body
|
||||
let body = req.into_body();
|
||||
let bytes = axum::body::to_bytes(body, 2048).await.map_err(|err| {
|
||||
tracing::error!("Failed to proxy request: {}", err);
|
||||
RouteError::InternalError()
|
||||
})?;
|
||||
let body_str = String::from_utf8(bytes.to_vec()).map_err(|err| {
|
||||
tracing::error!("Failed to proxy request: {}", err);
|
||||
RouteError::InternalError()
|
||||
})?;
|
||||
forward_req = forward_req.body(body_str);
|
||||
|
||||
// send reverse proxy request
|
||||
let reqwest_response = forward_req.send().await.map_err(|e| {
|
||||
tracing::error!("Failed to proxy request: {}", e);
|
||||
RouteError::BadRequest()
|
||||
})?;
|
||||
|
||||
// response from reverse proxy server
|
||||
let mut response_builder = Response::builder().status(reqwest_response.status());
|
||||
copy_headers(
|
||||
reqwest_response.headers(),
|
||||
response_builder
|
||||
.headers_mut()
|
||||
.ok_or(RouteError::InternalError())?,
|
||||
);
|
||||
let res = response_builder
|
||||
.body(Body::from_stream(reqwest_response.bytes_stream()))
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to proxy request: {}", e);
|
||||
RouteError::BadRequest()
|
||||
})?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Checks if a given header should be excluded from being forwarded in the reverse proxy.
|
||||
/// Headers like "host", "connection", etc., are typically excluded to avoid conflicts or security issues.
|
||||
fn is_exclude_header(name: &HeaderName) -> bool {
|
||||
matches!(
|
||||
name.as_str(),
|
||||
"host"
|
||||
| "connection"
|
||||
| "proxy-authenticate"
|
||||
| "upgrade"
|
||||
| "proxy-authorization"
|
||||
| "keep-alive"
|
||||
| "transfer-encoding"
|
||||
| "te"
|
||||
)
|
||||
}
|
||||
|
||||
/// Copies headers from one `HeaderMap` to another, excluding headers specified in `is_exclude_header`.
|
||||
/// This ensures only relevant headers are forwarded, avoiding conflicts or security issues.
|
||||
fn copy_headers(from: &http::HeaderMap, to: &mut http::HeaderMap) {
|
||||
for (name, value) in from.iter() {
|
||||
if !is_exclude_header(name) {
|
||||
to.append(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
621
src/http/serve.rs
Normal file
@ -0,0 +1,621 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
time::UNIX_EPOCH,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Request},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use dashmap::mapref::one::Ref;
|
||||
use http::{
|
||||
HeaderMap, HeaderValue, StatusCode, Uri,
|
||||
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||
};
|
||||
use mime_guess::from_path;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::{
|
||||
config::SettingRoute,
|
||||
consts::HOST_INDEX,
|
||||
http::{HOSTS, error::RouteError},
|
||||
utils::parse_port_from_host,
|
||||
};
|
||||
|
||||
use super::error::RouteResult;
|
||||
|
||||
/// 处理自定义页面请求(如404错误页或自定义错误页面)
|
||||
///
|
||||
/// 此函数根据请求类型(错误页或404页)加载相应的自定义页面,
|
||||
/// 构建完整文件路径并尝试流式传输文件内容作为HTTP响应。
|
||||
///
|
||||
/// # 参数
|
||||
/// - `host_route`: 主机路由配置引用,包含页面位置和根目录信息
|
||||
/// - `request`: 原始HTTP请求
|
||||
/// - `is_error_page`: 是否为错误页面(true: 错误页,false: 404页)
|
||||
///
|
||||
/// # 返回
|
||||
/// - `RouteResult<Response>`: 成功时返回HTTP响应,失败时返回路由错误
|
||||
async fn custom_page(
|
||||
host_route: Ref<'_, String, SettingRoute>,
|
||||
request: Request<Body>,
|
||||
is_error_page: bool,
|
||||
) -> RouteResult<Response<Body>> {
|
||||
let page = if is_error_page {
|
||||
host_route
|
||||
.error_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
} else {
|
||||
host_route
|
||||
.not_found_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
};
|
||||
|
||||
let root = host_route
|
||||
.root
|
||||
.as_ref()
|
||||
.ok_or(RouteError::InternalError())?;
|
||||
|
||||
let path = format!("{}/{}", root, page.page);
|
||||
|
||||
let status = StatusCode::from_str(page.status.to_string().as_ref())
|
||||
.map_err(|_| RouteError::BadRequest())?;
|
||||
|
||||
tracing::debug!("custom not found path: {:?}", path);
|
||||
|
||||
match stream_file(path.into(), request, Some(status)).await {
|
||||
Ok(res) => RouteResult::Ok(res),
|
||||
Err(e) => {
|
||||
println!("Failed to stream file: {:?}", e);
|
||||
RouteResult::Err(RouteError::InternalError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve static files.
|
||||
///
|
||||
/// This function handles requests for static files by:
|
||||
/// 1. Resolving the parent path from the URI or provided path.
|
||||
/// 2. Looking up the route in `ROUTE_MAP` to find the root directory.
|
||||
/// 3. Attempting to serve the requested file or a default index file.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `uri`: The request URI, used to extract the full path.
|
||||
/// - `path`: Optional path segment provided by the router.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Response)`: If the file is found and successfully streamed.
|
||||
/// - `Err(RouteError)`: If the route or file is not found.
|
||||
#[axum::debug_handler]
|
||||
pub async fn serve(
|
||||
uri: Uri,
|
||||
path: Option<Path<String>>,
|
||||
Host(host): Host,
|
||||
request: Request,
|
||||
) -> RouteResult<impl IntoResponse> {
|
||||
// find parent path
|
||||
// if requested path is /doc
|
||||
// then params path is None
|
||||
// when Path is None, then use uri.path() as path
|
||||
|
||||
// if request path is /doc/index.html
|
||||
// uri path is /doc/index.html
|
||||
// path is index.html
|
||||
// find parent path by path length
|
||||
// /doc/index.html
|
||||
// /doc/
|
||||
// index.html
|
||||
|
||||
debug!(
|
||||
"Request - uri: {:?}, path: {:?}, request: {:?}",
|
||||
uri, path, request
|
||||
);
|
||||
|
||||
let parent_path = resolve_parent_path(&uri, path.as_ref());
|
||||
// parent_path is key in route map
|
||||
// which is `host_route.location`
|
||||
let scheme = request.uri().scheme_str().unwrap_or("http");
|
||||
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
|
||||
let route_map = &HOSTS.get(&port).ok_or(RouteError::BadRequest())?.route_map;
|
||||
debug!("Route map entries: {:?}", route_map);
|
||||
let host_route = route_map
|
||||
.get(&parent_path)
|
||||
.ok_or(RouteError::RouteNotFound())?;
|
||||
debug!("route: {:?}", host_route);
|
||||
// after route found
|
||||
// check static file root configuration
|
||||
// if root is None, then return InternalError
|
||||
let Some(ref root) = host_route.root else {
|
||||
return custom_page(host_route, request, true).await;
|
||||
};
|
||||
// try find index file first
|
||||
// build index filename as vec
|
||||
// ["./html/index.html", "./html/index.txt"]
|
||||
// Build the list of candidate file paths to try:
|
||||
// - If `path` is provided, use it and check is file or not.
|
||||
// - If `path` is None, use the default index files (either from `host_route.index` or `HOST_INDEX`).
|
||||
// path_arr 是包含默认索引文件的数组
|
||||
// req_path 是请求的路径
|
||||
let (req_path, path_arr) = if let Some(path) = path {
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
let path = path.to_string();
|
||||
if path.contains('.') {
|
||||
(root.into(), vec![format!("{}/{}", root, path)])
|
||||
} else {
|
||||
generate_default_index(&host_route, &format!("{root}/{path}"))
|
||||
}
|
||||
} else {
|
||||
generate_default_index(&host_route, root)
|
||||
};
|
||||
debug!("request index file {:?}", path_arr);
|
||||
// Try each candidate path in order:
|
||||
// - Return the first successfully streamed file.
|
||||
// - If all fail, return a `RouteNotFound` error.
|
||||
let mut path_exists = None;
|
||||
for path in path_arr {
|
||||
if fs::metadata(path.clone()).await.is_ok() {
|
||||
path_exists = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 检查路径是否存在
|
||||
// 不存时,检查是否开启自动生成目录索引
|
||||
let path_exists = match path_exists {
|
||||
Some(path_exists) => path_exists,
|
||||
None => {
|
||||
// 生成自动目录索引
|
||||
if host_route.auto_index {
|
||||
// HTML 中的标题路径,需要移除掉配置文件中的 root = "./html" 字段
|
||||
let host_root = if let Some(root) = &host_route.root {
|
||||
root
|
||||
} else {
|
||||
return custom_page(host_route, request, false).await;
|
||||
};
|
||||
let req_path_str = req_path.to_string_lossy();
|
||||
debug!("req_path_str: {:?}", req_path_str);
|
||||
let host_root = &req_path_str.strip_prefix(host_root).unwrap_or(host_root);
|
||||
let list = list_dir(&req_path_str, &req_path).await?;
|
||||
let list_html = render_list_html(host_root, list);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/html"));
|
||||
return Ok((headers, list_html).into_response());
|
||||
} else {
|
||||
debug!("No valid file found in path candidates");
|
||||
return custom_page(host_route, request, false).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
match stream_file(path_exists.into(), request, None).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => {
|
||||
error!("Failed to stream file: {}", e);
|
||||
Err(RouteError::InternalError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate default index files
|
||||
/// if request path is not a file
|
||||
/// this read config index field
|
||||
/// and build with root: ["./html/index.html", "./html/index.txt"]
|
||||
///
|
||||
/// ## Arguments
|
||||
/// - `host_route`: the host route config
|
||||
/// - `root`: the root path
|
||||
///
|
||||
/// ## Returns
|
||||
/// - PathBuf: 客户端访问的路径
|
||||
/// - Vec<String>: 包含默认索引文件名的数组
|
||||
fn generate_default_index(
|
||||
host_route: &Ref<'_, String, SettingRoute>,
|
||||
root: &str,
|
||||
) -> (PathBuf, Vec<String>) {
|
||||
let indices = if host_route.index.is_empty() {
|
||||
let host_iter = HOST_INDEX
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
host_iter.into_iter()
|
||||
} else {
|
||||
host_route.index.clone().into_iter()
|
||||
};
|
||||
// indices 就是 host_route.index 的中配置的 index 文件名
|
||||
(
|
||||
root.into(),
|
||||
indices.map(|s| format!("{root}/{s}")).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Stream a file as an HTTP response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `path`: The filesystem path to the file.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Response)`: If the file is successfully opened and streamed.
|
||||
/// - `Err(anyhow::Error)`: If the file cannot be opened or read.
|
||||
async fn stream_file(
|
||||
path: PathBuf,
|
||||
request: Request,
|
||||
status: Option<StatusCode>,
|
||||
) -> RouteResult<Response<Body>> {
|
||||
let file = File::open(path.clone())
|
||||
.await
|
||||
.with_context(|| "open file failed")?;
|
||||
|
||||
let path_str = path.to_str().ok_or(anyhow!("convert path to str failed"))?;
|
||||
let etag = calculate_etag(&file, path_str).await?;
|
||||
|
||||
let mut response = Response::builder();
|
||||
let mut not_modified = false;
|
||||
// check request if-none-match
|
||||
if let Some(if_none_match) = request.headers().get(IF_NONE_MATCH) {
|
||||
if let Ok(if_none_match_str) = if_none_match.to_str() {
|
||||
if if_none_match_str == etag {
|
||||
response = response.status(StatusCode::NOT_MODIFIED);
|
||||
not_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
let null = PathBuf::from("NUL");
|
||||
#[cfg(not(windows))]
|
||||
let null = PathBuf::from("/dev/null");
|
||||
|
||||
let stream = if not_modified {
|
||||
let empty = File::open(null)
|
||||
.await
|
||||
.with_context(|| "open /dev/null failed")?;
|
||||
ReaderStream::new(empty)
|
||||
} else {
|
||||
ReaderStream::new(file)
|
||||
};
|
||||
let body = Body::from_stream(stream);
|
||||
|
||||
let mime = from_path(path).first_or_octet_stream();
|
||||
response
|
||||
.headers_mut()
|
||||
.with_context(|| "insert header failed")?
|
||||
.insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime.as_ref()).with_context(|| "insert header failed")?,
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.with_context(|| "insert header failed")?
|
||||
.insert(
|
||||
ETAG,
|
||||
HeaderValue::from_str(&etag).with_context(|| "insert header failed")?,
|
||||
);
|
||||
if let Some(status) = status {
|
||||
response = response.status(status);
|
||||
}
|
||||
let response = response
|
||||
.body(body)
|
||||
.with_context(|| "Failed to build HTTP response with body")?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn calculate_etag(file: &File, path: &str) -> anyhow::Result<String> {
|
||||
// calculate file metadata as etag
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.await
|
||||
.with_context(|| "get file metadata failed")?;
|
||||
let created_timestamp = metadata
|
||||
.created()
|
||||
.with_context(|| "get file created failed")?
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.with_context(|| "calculate unix timestamp failed")?
|
||||
.as_secs();
|
||||
let modified_timestamp = metadata
|
||||
.modified()
|
||||
.with_context(|| "get file created failed")?
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.with_context(|| "calculate unix timestamp failed")?
|
||||
.as_secs();
|
||||
// file path - created - modified - len
|
||||
let etag = format!(
|
||||
"{}-{}-{}-{}",
|
||||
path,
|
||||
created_timestamp,
|
||||
modified_timestamp,
|
||||
metadata.len()
|
||||
);
|
||||
let etag = format!("W/\"{:?}\"", md5::compute(etag));
|
||||
debug!("file {:?} etag: {:?}", path, etag);
|
||||
Ok(etag)
|
||||
}
|
||||
|
||||
// Resolve the parent path:
|
||||
// - If `path` is provided, extract the parent segment from the URI.
|
||||
// - If `path` is None, use the URI path directly (ensuring it ends with '/').
|
||||
/// Resolves the parent path from the URI and optional path segment.
|
||||
pub fn resolve_parent_path(uri: &Uri, path: Option<&Path<String>>) -> String {
|
||||
match path {
|
||||
Some(path) => {
|
||||
let uri_path = uri.path();
|
||||
// use path sub to this uri path
|
||||
// to find parent path that store in ROUTE_MAP
|
||||
// uri: /assets/css/styles.07713cb6.css, path: Some(Path("assets/css/styles.07713cb6.css")
|
||||
let parent_path = uri_path.get(0..uri_path.len() - path.len());
|
||||
parent_path.unwrap_or("/").to_string()
|
||||
}
|
||||
None => {
|
||||
// uri needs end with /
|
||||
// because global ROUTE_MAP key is end with /
|
||||
// so we need add / to uri path to get correct Route
|
||||
let uri_path = uri.path().to_string();
|
||||
if uri_path.ends_with('/') {
|
||||
uri_path
|
||||
} else {
|
||||
format!("{uri_path}/")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成一个 HTML 目录列表页面,展示指定目录中的文件和子目录。
|
||||
///
|
||||
/// 该函数将一个 `DirList` 结构体的向量转换为 HTML 表格格式,
|
||||
/// 每个条目包含名称(带链接)、最后修改时间和大小信息。
|
||||
///
|
||||
/// # 参数
|
||||
/// * `root_path` - 目录路径 显示在 HTML 中的根目录
|
||||
/// * `list` - 包含目录项信息的 `DirList` 结构体向量
|
||||
///
|
||||
/// # 返回值
|
||||
/// 格式化后的 HTML 字符串,可直接作为 HTTP 响应返回
|
||||
///
|
||||
/// # 示例
|
||||
/// ```rust
|
||||
/// let dir_entries = vec![
|
||||
/// DirList {
|
||||
/// path: PathBuf::from("/home/user/docs"),
|
||||
/// name: "documents".to_string(),
|
||||
/// last_modified: "2023-05-15 14:30".to_string(),
|
||||
/// size: "4.2K".to_string(),
|
||||
/// is_dir: true
|
||||
/// },
|
||||
/// // 更多条目...
|
||||
/// ];
|
||||
///
|
||||
/// let html_output = render_list_html(dir_entries);
|
||||
/// println!("{}", html_output);
|
||||
/// ```
|
||||
fn render_list_html(root_path: &str, list: Vec<DirList>) -> String {
|
||||
debug!(
|
||||
"render list html list: {:?} root_path: {:?}",
|
||||
list, root_path
|
||||
);
|
||||
// 先生成目标目录下所有文件的行
|
||||
let body_rows = list
|
||||
.iter()
|
||||
.map(|dist| {
|
||||
if dist.is_dir {
|
||||
format!(
|
||||
r#"<tr><td><a href="{}">{}/</a></td><td>{}</td><td>{}</td></tr>"#,
|
||||
dist.path, dist.name, dist.last_modified, dist.size,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"<tr><td><a href="{}">{}</a></td><td>{}</td><td>{}</td></tr>"#,
|
||||
dist.path, dist.name, dist.last_modified, dist.size,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let list_html = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index of {root_path}</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #dddddd;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}}
|
||||
th {{
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
}}
|
||||
tr:nth-child(even) {{
|
||||
background-color: #f9f9f9;
|
||||
}}
|
||||
tr:hover {{
|
||||
background-color: #f0f0f0;
|
||||
}}
|
||||
.dir {{
|
||||
color: #0066cc;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.file {{
|
||||
color: #000000;
|
||||
}}
|
||||
a {{
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}}
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {root_path}</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Last Modified</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
<tbody id="directory-content">
|
||||
{body_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
);
|
||||
list_html
|
||||
}
|
||||
|
||||
const KB: u64 = 1024;
|
||||
const KB1: u64 = KB + 1;
|
||||
const MB: u64 = 1024 * 1024;
|
||||
const MB1: u64 = MB + 1;
|
||||
const GB: u64 = 1024 * 1024 * 1024;
|
||||
const GB1: u64 = GB + 1;
|
||||
const TB: u64 = 1024 * 1024 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ByteUnit(u64);
|
||||
|
||||
impl Display for ByteUnit {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
0..=KB => write!(f, "{} B", self.0),
|
||||
KB1..=MB => write!(f, "{:.2} KB", self.0 as f64 / 1024.0),
|
||||
MB1..=GB => write!(f, "{:.2} MB", self.0 as f64 / 1024.0 / 1024.0),
|
||||
GB1..=TB => write!(f, "{:.2} TB", self.0 as f64 / 1024.0 / 1024.0 / 1024.0),
|
||||
_ => write!(f, "{} B", self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirList {
|
||||
pub name: String, // 文件或目录名称
|
||||
pub path: String, // 文件或目录的完整路径
|
||||
pub is_dir: bool, // 是否为目录
|
||||
pub size: ByteUnit, // 文件大小(字节)
|
||||
pub last_modified: String, // 最后修改时间的字符串表示
|
||||
}
|
||||
|
||||
/// 异步列出指定目录下的所有文件和子目录信息
|
||||
///
|
||||
/// # 参数
|
||||
/// * `path` - 要列出内容的目录路径
|
||||
///
|
||||
/// # 返回
|
||||
/// 成功时返回包含 `DirList` 结构的向量,失败时返回错误
|
||||
///
|
||||
/// # 错误
|
||||
/// 可能返回与文件系统操作相关的错误,如目录不存在、权限不足等
|
||||
async fn list_dir(host_root_str: &str, path: &PathBuf) -> anyhow::Result<Vec<DirList>> {
|
||||
use chrono::{Local, TimeZone};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
let mut list = vec![];
|
||||
// 异步读取目录条目
|
||||
let mut entries = fs::read_dir(path)
|
||||
.await
|
||||
.with_context(|| format!("无法读取目录: {}", path.display()))?;
|
||||
|
||||
debug!("list dir path: {:?}", path);
|
||||
|
||||
let mut tasks = vec![];
|
||||
// 遍历目录中的每个条目
|
||||
while let Some(entry) = entries
|
||||
.next_entry()
|
||||
.await
|
||||
.with_context(|| format!("读取目录条目失败: {}", path.display()))?
|
||||
{
|
||||
let host_root_str = host_root_str.to_string();
|
||||
// 为每个条目创建异步任务,并行获取元数据
|
||||
let task = tokio::task::spawn(async move {
|
||||
// 获取文件元数据
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.await
|
||||
.with_context(|| "获取文件元数据失败")?;
|
||||
|
||||
// 获取并格式化最后修改时间
|
||||
let last_modified = metadata
|
||||
.modified()
|
||||
.with_context(|| "获取文件修改时间失败")?;
|
||||
let last_modified = last_modified
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.with_context(|| "计算 Unix 时间戳失败")?;
|
||||
|
||||
// 转换为本地时间,处理可能的歧义情况
|
||||
let datetime = match Local
|
||||
.timestamp_opt(last_modified.as_secs() as i64, last_modified.subsec_nanos())
|
||||
{
|
||||
chrono::LocalResult::Ambiguous(earlier, later) => {
|
||||
warn!("发现歧义时间: {} 和 {}", earlier, later);
|
||||
earlier
|
||||
}
|
||||
chrono::offset::LocalResult::Single(single) => {
|
||||
// warn!("发现歧义时间: {}", single);
|
||||
single
|
||||
}
|
||||
chrono::offset::LocalResult::None => {
|
||||
error!("无法解析时间时使用当前时间");
|
||||
Local::now()
|
||||
}
|
||||
};
|
||||
let last_modified = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
// 收集其他元数据
|
||||
let size = ByteUnit(metadata.len());
|
||||
let is_dir = metadata.is_dir();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let path = entry
|
||||
.path()
|
||||
.to_string_lossy()
|
||||
.strip_prefix(&host_root_str)
|
||||
.ok_or(anyhow!("strip prefix failed"))?
|
||||
.to_string();
|
||||
let path = format!("./{path}");
|
||||
// 创建并返回目录条目信息
|
||||
let dir = DirList {
|
||||
name,
|
||||
path,
|
||||
is_dir,
|
||||
size,
|
||||
last_modified,
|
||||
};
|
||||
anyhow::Ok(dir)
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
// 等待所有异步任务完成并收集结果
|
||||
for task in tasks {
|
||||
list.push(task.await??);
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
44
src/main.rs
@ -1,42 +1,58 @@
|
||||
use anyhow::{Context, Result};
|
||||
// #![feature(iterator_try_collect)]
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::error;
|
||||
|
||||
use clap::Parser;
|
||||
use config::Settings;
|
||||
use consts::{COMMIT, COMPILER};
|
||||
use http::make_server;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
config::init_config,
|
||||
consts::{get_settings, ARCH, NAME, OS, VERSION},
|
||||
consts::{ARCH, NAME, OS, VERSION},
|
||||
utils::init_logger,
|
||||
};
|
||||
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: MiMalloc = MiMalloc;
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod consts;
|
||||
mod error;
|
||||
mod http;
|
||||
mod service;
|
||||
mod middlewares;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = cli::Cli::parse();
|
||||
init_logger();
|
||||
init_config().with_context(|| "init config failed")?;
|
||||
|
||||
// global config
|
||||
let settings = get_settings();
|
||||
let settings = Settings::new(&args.config).with_context(|| "init config failed")?;
|
||||
debug!("settings {:?}", settings);
|
||||
info!("{}/{}", NAME, VERSION);
|
||||
info!("{}/{} {}", NAME, VERSION, COMMIT);
|
||||
info!("{}", COMPILER);
|
||||
info!("OS: {} {}", OS, ARCH);
|
||||
|
||||
let mut servers = settings
|
||||
.host
|
||||
.iter()
|
||||
.map(|host| host.mk_server())
|
||||
.collect::<JoinSet<_>>();
|
||||
let hosts = settings.host;
|
||||
let mut servers = hosts.into_iter().map(make_server).collect::<JoinSet<_>>();
|
||||
|
||||
info!("server started");
|
||||
|
||||
while let Some(res) = servers.join_next().await {
|
||||
res??;
|
||||
match res {
|
||||
Ok(err) => {
|
||||
err.map_err(|err| error!("server error: {}", err)).ok();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("server error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
141
src/middlewares/mod.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use std::{fmt::Display, time::Duration};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
body::{Body, Bytes},
|
||||
extract::Request,
|
||||
http::{HeaderMap, HeaderValue},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use http::HeaderName;
|
||||
use tower_http::classify::ServerErrorsFailureClass;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{Span, debug, error, info, info_span};
|
||||
|
||||
use crate::{
|
||||
consts::{NAME, VERSION},
|
||||
http::HOSTS,
|
||||
utils::parse_port_from_host,
|
||||
};
|
||||
|
||||
/// Middleware for adding version information to each response's headers.
|
||||
///
|
||||
/// This middleware takes an incoming `Request` and a `Next` handler, which represents the
|
||||
/// subsequent middleware or route in the chain. It then asynchronously runs the next handler,
|
||||
/// obtaining the response. After receiving the response, it appends two headers:
|
||||
/// - "Server": The name of the server extracted from the Cargo package name.
|
||||
/// - "S-Version": The version of the server extracted from the Cargo package version.
|
||||
pub async fn add_version(req: Request<Body>, next: Next) -> impl IntoResponse {
|
||||
let mut res = next.run(req).await;
|
||||
let headers = res.headers_mut();
|
||||
headers.append("Server", HeaderValue::from_static(NAME));
|
||||
headers.append("RUA-Version", HeaderValue::from_static(VERSION));
|
||||
res
|
||||
}
|
||||
|
||||
/// Middleware for dynamically adding headers to responses based on the requested host and port.
|
||||
///
|
||||
/// This middleware:
|
||||
/// 1. Extracts the `Host` header from the incoming request.
|
||||
/// 2. Parses the host string to determine the port (defaulting to `80` if unspecified).
|
||||
/// 3. Looks up the host configuration in the global `HOST` map (shared state) for the resolved port.
|
||||
/// 4. Appends any configured headers from the host's `SettingHost` to the response.
|
||||
///
|
||||
/// # Behavior
|
||||
/// - If the `Host` header is missing or malformed, the request proceeds unchanged.
|
||||
/// - If the port is invalid or the host configuration is not found, the request proceeds unchanged.
|
||||
/// - Headers are appended to the response only if they are explicitly configured for the host.
|
||||
///
|
||||
/// # Error Handling
|
||||
/// - Silently skips header addition for:
|
||||
/// - Missing or unparseable `Host` headers.
|
||||
/// - Invalid ports (non-numeric or out-of-range).
|
||||
/// - Missing host configurations in `HOST`.
|
||||
/// - Uses `debug!` for logging the resolved port.
|
||||
///
|
||||
/// # Example
|
||||
/// Given a request to `example.com:8080` and a `HOST` entry for port `8080` with headers:
|
||||
/// ```toml
|
||||
/// [hosts."8080"]
|
||||
/// headers = { "X-Custom" = "value" }
|
||||
pub async fn add_headers(Host(host): Host, req: Request, next: Next) -> impl IntoResponse {
|
||||
let Some(scheme) = req.uri().scheme_str() else {
|
||||
return next.run(req).await;
|
||||
};
|
||||
debug!("scheme {:?}", scheme);
|
||||
let Some(port) = parse_port_from_host(&host, scheme) else {
|
||||
return next.run(req).await;
|
||||
};
|
||||
debug!("port {:?}", port);
|
||||
let mut res = next.run(req).await;
|
||||
let req_headers = res.headers_mut();
|
||||
// let host = HOSTS.read().await;
|
||||
let Some(host) = HOSTS.get(&port) else {
|
||||
return res;
|
||||
};
|
||||
let Some(headers) = host.headers.as_ref() else {
|
||||
return res;
|
||||
};
|
||||
headers.iter().for_each(|entery| {
|
||||
let (key, value) = (entery.key(), entery.value());
|
||||
let Ok(header_name) = HeaderName::from_bytes(key.as_bytes()) else {
|
||||
error!("Invalid header name: {key}");
|
||||
return;
|
||||
};
|
||||
let Ok(header_value) = HeaderValue::from_bytes(value.as_bytes()) else {
|
||||
error!("Invalid header value: {value}");
|
||||
return;
|
||||
};
|
||||
req_headers.append(header_name, header_value);
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
/// Middleware for logging each request.
|
||||
///
|
||||
/// This middleware will calculate each request latency
|
||||
/// and add request's information to each info_span.
|
||||
pub fn logging_route(router: Router) -> Router {
|
||||
let make_span = |req: &Request<_>| {
|
||||
let unknown = &HeaderValue::from_static("Unknown");
|
||||
let empty = &HeaderValue::from_static("");
|
||||
let headers = req.headers();
|
||||
let ua = headers
|
||||
.get("User-Agent")
|
||||
.unwrap_or(unknown)
|
||||
.to_str()
|
||||
.unwrap_or("Unknown");
|
||||
let host = headers.get("Host").unwrap_or(empty).to_str().unwrap_or("");
|
||||
info_span!("HTTP", method = ?req.method(), host, uri = ?req.uri(), ua)
|
||||
};
|
||||
|
||||
let trace_layer = TraceLayer::new_for_http()
|
||||
.make_span_with(make_span)
|
||||
.on_request(|_req: &Request<_>, _span: &Span| {})
|
||||
.on_response(|res: &Response, latency: Duration, _span: &Span| {
|
||||
info!("{}", format_latency(latency, res.status()));
|
||||
})
|
||||
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {})
|
||||
.on_eos(|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {})
|
||||
.on_failure(
|
||||
|error: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
|
||||
error!("{}", format_latency(latency, error));
|
||||
},
|
||||
);
|
||||
|
||||
router.layer(trace_layer)
|
||||
}
|
||||
|
||||
/// Format request latency and status message
|
||||
/// return a string
|
||||
fn format_latency(latency: Duration, status: impl Display) -> String {
|
||||
let micros = latency.as_micros();
|
||||
let millis = latency.as_millis();
|
||||
if micros >= 1000 {
|
||||
format!("{status} {millis}ms")
|
||||
} else {
|
||||
format!("{status} {micros}μs")
|
||||
}
|
||||
}
|
175
src/service.rs
@ -1,175 +0,0 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
io::ErrorKind::{NotConnected, NotFound},
|
||||
net::SocketAddr,
|
||||
path::Path,
|
||||
pin::pin,
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::SettingHost,
|
||||
consts::{NAME, VERSION},
|
||||
error::{Error, Result},
|
||||
http::{handle_get, internal_server_error, not_found, CandyBody},
|
||||
utils::{find_route, parse_assets_path},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures_util::Future;
|
||||
use http::{Method, Request, Response};
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
server::conn::http1,
|
||||
service::service_fn,
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
select,
|
||||
};
|
||||
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
impl SettingHost {
|
||||
pub fn mk_server(&'static self) -> impl Future<Output = anyhow::Result<()>> + 'static {
|
||||
let addr = format!("{}:{}", self.ip, self.port);
|
||||
async move {
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
info!("host bind on {}", addr);
|
||||
loop {
|
||||
let socket = listener.accept().await?;
|
||||
tokio::spawn(async move {
|
||||
graceful_shutdown(self, socket).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle tokio TcpListener socket,
|
||||
/// then use hyper and service_fn to handle connection
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `host`: host configuration from config file
|
||||
/// `socket`: the socket what tokio TcpListener accepted
|
||||
pub async fn graceful_shutdown(host: &SettingHost, socket: (TcpStream, SocketAddr)) {
|
||||
let (stream, addr) = socket;
|
||||
let io = TokioIo::new(stream);
|
||||
|
||||
// Use keep_alive in config for incoming connections to the server.
|
||||
// use process_timeout in config for processing the final request and graceful shutdown.
|
||||
let connection_timeout = Duration::from_secs(host.keep_alive.into());
|
||||
|
||||
// service_fn
|
||||
let service = move |req: Request<Incoming>| async move {
|
||||
let start_time = time::Instant::now();
|
||||
let res = handle_connection(&req, host).await;
|
||||
let response = match res {
|
||||
Ok(res) => res,
|
||||
Err(Error::NotFound(err)) => {
|
||||
warn!("{err}");
|
||||
not_found()
|
||||
}
|
||||
_ => internal_server_error(),
|
||||
};
|
||||
let end_time = (Instant::now() - start_time).as_micros() as f32;
|
||||
let end_time = end_time / 1000_f32;
|
||||
let method = &req.method();
|
||||
let path = &req.uri().path();
|
||||
let version = &req.version();
|
||||
let res_status = response.status();
|
||||
info!(
|
||||
"\"{}\" {} {} {:?} {} {:.3}ms",
|
||||
addr, method, path, version, res_status, end_time
|
||||
);
|
||||
anyhow::Ok(response)
|
||||
};
|
||||
|
||||
let conn = http1::Builder::new().serve_connection(io, service_fn(service));
|
||||
let mut conn = pin!(conn);
|
||||
// Iterate the timeouts. Use tokio::select! to wait on the
|
||||
// result of polling the connection itself,
|
||||
// and also on tokio::time::sleep for the current timeout duration.
|
||||
select! {
|
||||
res = conn.as_mut() => {
|
||||
match res {
|
||||
Ok(_) => {
|
||||
debug!("close connection");
|
||||
}
|
||||
Err(err)
|
||||
if err.source().is_some()
|
||||
&& err
|
||||
.source()
|
||||
.unwrap()
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.unwrap_or(&std::io::Error::new(NotFound, &Error::Empty))
|
||||
.kind()
|
||||
== NotConnected =>
|
||||
{
|
||||
// The client closed connection
|
||||
debug!("client closed connection");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("handle connection {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(connection_timeout) => {
|
||||
debug!("keep-alive timeout {}s, calling conn.graceful_shutdown", host.keep_alive);
|
||||
conn.as_mut().graceful_shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection handler in service_fn
|
||||
pub async fn handle_connection(
|
||||
req: &Request<Incoming>,
|
||||
host: &SettingHost,
|
||||
) -> Result<Response<CandyBody<Bytes>>> {
|
||||
use Error::*;
|
||||
|
||||
let req_path = req.uri().path();
|
||||
let req_method = req.method();
|
||||
|
||||
// find route path
|
||||
let not_found_err = NotFound(format!("resource {} not found", &req_path).into());
|
||||
let (router, assets_path) = find_route(req_path, &host.route_map)?;
|
||||
|
||||
// find resource local file path
|
||||
let mut path = None;
|
||||
for index in host.index.iter() {
|
||||
let p = parse_assets_path(assets_path, &router.root, index);
|
||||
if Path::new(&p).exists() {
|
||||
path = Some(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let path = match path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Err(not_found_err);
|
||||
}
|
||||
};
|
||||
|
||||
// build the response for client
|
||||
let mut res = Response::builder();
|
||||
// TODO: headers from config
|
||||
let headers = res
|
||||
.headers_mut()
|
||||
.ok_or(InternalServerError(anyhow!("build response failed")))?;
|
||||
let server = format!("{}/{}", NAME, VERSION);
|
||||
headers.insert("Server", server.parse()?);
|
||||
|
||||
// http method handle
|
||||
let res = match *req_method {
|
||||
Method::GET => handle_get(req, res, &path).await?,
|
||||
Method::POST => handle_get(req, res, &path).await?,
|
||||
// Return the 404 Not Found for other routes.
|
||||
_ => {
|
||||
return Err(not_found_err);
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
use async_compression::tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder};
|
||||
use futures_util::TryStreamExt;
|
||||
use http_body_util::{BodyExt, StreamBody};
|
||||
use hyper::body::{Bytes, Frame};
|
||||
use tokio::io::{AsyncBufRead, BufReader};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::{error::Error, http::CandyBody};
|
||||
|
||||
pub enum CompressType {
|
||||
Zstd,
|
||||
Gzip,
|
||||
Deflate,
|
||||
Brotli,
|
||||
}
|
||||
|
||||
macro_rules! encode {
|
||||
($encoder:ident, $file:ident) => {{
|
||||
let encoder_stream = $encoder::new($file);
|
||||
let reader_stream = ReaderStream::new(encoder_stream);
|
||||
let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data));
|
||||
let boxed_body = BodyExt::map_err(stream_body, Error::Io).boxed();
|
||||
boxed_body
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn stream_compress<R>(compress_type: CompressType, reader: R) -> CandyBody<Bytes>
|
||||
where
|
||||
R: AsyncBufRead + Sync + Send + 'static,
|
||||
{
|
||||
use CompressType::*;
|
||||
|
||||
let file_reader = BufReader::new(reader);
|
||||
|
||||
match compress_type {
|
||||
Zstd => {
|
||||
encode!(ZstdEncoder, file_reader)
|
||||
}
|
||||
Gzip => encode!(GzipEncoder, file_reader),
|
||||
Deflate => encode!(DeflateEncoder, file_reader),
|
||||
Brotli => encode!(BrotliEncoder, file_reader),
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
use tracing_subscriber::{
|
||||
EnvFilter,
|
||||
fmt::{self},
|
||||
prelude::*,
|
||||
registry, EnvFilter,
|
||||
registry,
|
||||
};
|
||||
|
||||
pub fn init_logger() {
|
||||
|
@ -1,4 +1,3 @@
|
||||
pub mod compress;
|
||||
pub mod logging;
|
||||
pub mod service;
|
||||
|
||||
|
@ -1,86 +1,135 @@
|
||||
use crate::error::{Error, Result};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::{HostRouteMap, SettingRoute};
|
||||
use axum_server::Handle;
|
||||
use tokio::{signal, time::sleep};
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Parse assets file path
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `assets_path`: the rest part of client request path
|
||||
/// `assets_root`: local directory path from config file
|
||||
/// `index_file`: index file format from config file
|
||||
#[inline]
|
||||
pub fn parse_assets_path(assets_path: &str, assets_root: &str, index_file: &str) -> String {
|
||||
match assets_path {
|
||||
str if str.ends_with('/') => {
|
||||
format!("{}{}{}", assets_root, assets_path, index_file)
|
||||
}
|
||||
str if str.contains('.') && !str.starts_with('/') => {
|
||||
format!("{}/{}", assets_root, assets_path)
|
||||
}
|
||||
str if !str.starts_with('/') => {
|
||||
format!("{}/{}{}", assets_root, assets_path, index_file)
|
||||
}
|
||||
_ => {
|
||||
format!("{}{}/{}", assets_root, assets_path, index_file)
|
||||
}
|
||||
// Asynchronously waits for a shutdown signal and executes a callback function when a signal is received.
|
||||
//
|
||||
// This function listens for shutdown signals in the form of `Ctrl+C` and termination signals. When one of
|
||||
// these signals is received, it invokes the provided callback function `shutdown_cb`.
|
||||
//
|
||||
// The behavior of the signal handling depends on the operating system:
|
||||
//
|
||||
// - On Unix-based systems (e.g., Linux, macOS), it listens for termination signals (such as SIGTERM).
|
||||
// - On non-Unix systems (e.g., Windows), it only listens for `Ctrl+C` and ignores termination signals.
|
||||
//
|
||||
// The `shutdown_cb` callback function is executed when either signal is received. This function should
|
||||
// contain the logic needed to gracefully shut down the application or perform any necessary cleanup tasks.
|
||||
// # Parameters
|
||||
//
|
||||
// - `shutdown_cb`: A closure or function to call when a shutdown signal is received. The function should
|
||||
// have the signature `Fn()`. This callback is executed without any parameters.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// - If setting up the signal handlers fails, the function will panic with an error message.
|
||||
//
|
||||
// # Panics
|
||||
//
|
||||
// - Panics if the setup for `Ctrl+C` or termination signal handlers fails.
|
||||
//
|
||||
// # Platform-specific behavior
|
||||
//
|
||||
// - On Unix-based systems, termination signals are handled using the `signal` crate for Unix signals.
|
||||
// - On non-Unix systems, only `Ctrl+C` signals are handled, and termination signals are not supported.
|
||||
//
|
||||
// # Future
|
||||
//
|
||||
// This function returns a future that resolves when either `Ctrl+C` or a termination signal is received
|
||||
// and the callback function has been executed.
|
||||
// pub async fn shutdown_signal<F>(shutdown_cb: F)
|
||||
// where
|
||||
// F: Fn(),
|
||||
// {
|
||||
// let ctrl_c = async {
|
||||
// signal::ctrl_c()
|
||||
// .await
|
||||
// .expect("failed to install Ctrl+C handler");
|
||||
// };
|
||||
//
|
||||
// #[cfg(unix)]
|
||||
// let terminate = async {
|
||||
// signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
// .expect("failed to install signal handler")
|
||||
// .recv()
|
||||
// .await;
|
||||
// };
|
||||
//
|
||||
// #[cfg(not(unix))]
|
||||
// let terminate = std::future::pending::<()>();
|
||||
//
|
||||
// tokio::select! {
|
||||
// _ = ctrl_c => {
|
||||
// },
|
||||
// _ = terminate => {
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// tracing::info!("Received termination signal shutting down");
|
||||
// shutdown_cb()
|
||||
// }
|
||||
|
||||
pub async fn graceful_shutdown(handle: Handle) {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {
|
||||
},
|
||||
_ = terminate => {
|
||||
},
|
||||
}
|
||||
|
||||
info!("Received termination signal shutting down");
|
||||
info!("Server shuting down");
|
||||
|
||||
// Signal the server to shutdown using Handle.
|
||||
handle.graceful_shutdown(Some(Duration::from_secs(30)));
|
||||
|
||||
// Print alive connection count every second.
|
||||
loop {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
debug!("alive connections: {}", handle.connection_count());
|
||||
}
|
||||
}
|
||||
|
||||
/// Find target route by req path
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `req_path`: client request path
|
||||
/// `route_map`: router map from config
|
||||
///
|
||||
/// ## Return
|
||||
///
|
||||
/// a result. return none when path not registried
|
||||
/// `router`: host from config file
|
||||
/// `assets_path`: the rest part of client request path
|
||||
pub fn find_route<'a>(
|
||||
req_path: &'a str,
|
||||
route_map: &'a HostRouteMap,
|
||||
) -> Result<(&'a SettingRoute, &'a str)> {
|
||||
let mut index = 1;
|
||||
let len = req_path.len();
|
||||
let not_found_err = format!("resource {} not found", &req_path).into();
|
||||
let (router, assets_path) = loop {
|
||||
if index > len {
|
||||
return Err(Error::NotFound(not_found_err));
|
||||
}
|
||||
let check_path = &req_path[..index];
|
||||
match route_map.get(check_path) {
|
||||
Some(router) => break (router, &req_path[index..]),
|
||||
None => {
|
||||
index += 1;
|
||||
/// Parse port from host
|
||||
/// if host is localhost:8080
|
||||
/// return 8080
|
||||
/// if host is localhost
|
||||
/// return 80
|
||||
pub fn parse_port_from_host(host: &str, scheme: &str) -> Option<u16> {
|
||||
// localhost:8080
|
||||
// ["localhost", "8080"]
|
||||
// localhost
|
||||
// ["localhost"]
|
||||
let host_parts = host.split(':').collect::<Vec<&str>>();
|
||||
let port = if host_parts.len() == 1 {
|
||||
match scheme {
|
||||
"http" => 80,
|
||||
"https" => 443,
|
||||
_ => {
|
||||
debug!("scheme not support");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
host_parts.get(1)?.parse::<u16>().ok()?
|
||||
};
|
||||
Ok((router, assets_path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_assets_path_works() {
|
||||
let path = parse_assets_path("/docs/", "./public", "index.html");
|
||||
assert_eq!(path, "./public/docs/index.html".to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_route_works() {
|
||||
let setting_route = SettingRoute {
|
||||
location: "/".to_string(),
|
||||
root: "./public".to_string(),
|
||||
};
|
||||
let map = BTreeMap::from([("/".to_string(), setting_route)]);
|
||||
let (_, assets_path) = find_route("/docs/home", &map).unwrap();
|
||||
assert_eq!(assets_path, "docs/home")
|
||||
}
|
||||
Some(port)
|
||||
}
|
||||
|