├── .cargo └── config.toml ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── LICENSE ├── README.md ├── build-docker.sh ├── resources └── screenshots │ ├── 开启页面认证.png │ ├── 浅色主题.png │ ├── 深色主题.png │ ├── 添加新设备.png │ └── 页面认证.png ├── rustfmt.toml ├── src ├── api │ ├── auth.rs │ ├── device.rs │ └── mod.rs ├── args.rs ├── asset.rs ├── errors.rs ├── main.rs ├── middleware.rs ├── settings.rs └── wol.rs ├── web ├── .gitignore ├── README.md ├── eslint.config.mjs ├── index.html ├── package.json ├── public │ ├── favicon.ico │ ├── logo.png │ └── manifest.json ├── rsbuild.config.ts ├── src │ ├── Wol.tsx │ ├── components │ │ ├── AuthEdit │ │ │ └── index.tsx │ │ ├── DeviceCard │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── DeviceEdit │ │ │ └── index.tsx │ │ ├── ErrorBoundary │ │ │ └── index.tsx │ │ ├── ErrorElement │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── NProgress │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── Root │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── env.d.ts │ ├── hooks │ │ ├── useAuth.ts │ │ ├── useBoolean.ts │ │ ├── useDevices.ts │ │ ├── useLocalStorage.ts │ │ ├── useMessage.ts │ │ ├── useModal.ts │ │ └── useTheme.ts │ ├── index.tsx │ ├── styles │ │ └── index.less │ ├── types │ │ ├── auth.ts │ │ └── device.ts │ └── utils │ │ └── fetcher.ts ├── tsconfig.json └── yarn.lock ├── wol.code-workspace ├── wol.example.yaml └── www └── .gitkeep /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | 4 | [target.aarch64-pc-windows-msvc] 5 | rustflags = ["-C", "target-feature=+crt-static"] 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: # 手动触发 5 | push: 6 | branches: ["master"] 7 | # Publish semver tags as releases. 8 | tags: ["v*.*.*"] 9 | pull_request: 10 | branches: ["master"] 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | # Use docker.io for Docker Hub if empty 18 | REGISTRY: ghcr.io 19 | 20 | jobs: 21 | build: 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | settings: 26 | - target: x86_64-apple-darwin 27 | host: macos-latest 28 | build: | 29 | cargo build --release --target x86_64-apple-darwin && \ 30 | mkdir build && \ 31 | cp target/x86_64-apple-darwin/release/wol build/wol && \ 32 | strip -x build/wol 33 | - target: aarch64-apple-darwin 34 | host: macos-latest 35 | build: | 36 | cargo build --release --target aarch64-apple-darwin && \ 37 | mkdir build && \ 38 | cp target/aarch64-apple-darwin/release/wol build/wol && \ 39 | strip -x build/wol 40 | 41 | - target: x86_64-pc-windows-msvc 42 | host: windows-latest 43 | build: | 44 | cargo build --release --target x86_64-pc-windows-msvc 45 | md build 46 | copy target/x86_64-pc-windows-msvc/release/wol.exe build/wol.exe 47 | - target: aarch64-pc-windows-msvc 48 | host: windows-latest 49 | build: | 50 | cargo build --release --target aarch64-pc-windows-msvc 51 | md build 52 | copy target/aarch64-pc-windows-msvc/release/wol.exe build/wol.exe 53 | 54 | - target: x86_64-unknown-linux-gnu 55 | host: ubuntu-latest 56 | build: | 57 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 58 | cross build --release --target x86_64-unknown-linux-gnu && \ 59 | mkdir build && \ 60 | cp target/x86_64-unknown-linux-gnu/release/wol build/wol && \ 61 | strip -x build/wol 62 | - target: x86_64-unknown-linux-musl 63 | host: ubuntu-latest 64 | build: | 65 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 66 | cross build --release --target x86_64-unknown-linux-musl && \ 67 | mkdir build && \ 68 | cp target/x86_64-unknown-linux-musl/release/wol build/wol && \ 69 | strip -x build/wol 70 | 71 | - target: aarch64-unknown-linux-gnu 72 | host: ubuntu-latest 73 | build: | 74 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 75 | cross build --release --target aarch64-unknown-linux-gnu && \ 76 | mkdir build && \ 77 | cp target/aarch64-unknown-linux-gnu/release/wol build/wol 78 | - target: aarch64-unknown-linux-musl 79 | host: ubuntu-latest 80 | build: | 81 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 82 | cross build --release --target aarch64-unknown-linux-musl && \ 83 | mkdir build && \ 84 | cp target/aarch64-unknown-linux-musl/release/wol build/wol 85 | - target: armv7-unknown-linux-gnueabi 86 | host: ubuntu-latest 87 | build: | 88 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 89 | cross build --release --target armv7-unknown-linux-gnueabi && \ 90 | mkdir build && \ 91 | cp target/armv7-unknown-linux-gnueabi/release/wol build/wol 92 | - target: armv7-unknown-linux-gnueabihf 93 | host: ubuntu-latest 94 | build: | 95 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 96 | cross build --release --target armv7-unknown-linux-gnueabihf && \ 97 | mkdir build && \ 98 | cp target/armv7-unknown-linux-gnueabihf/release/wol build/wol 99 | - target: armv7-unknown-linux-musleabi 100 | host: ubuntu-latest 101 | build: | 102 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 103 | cross build --release --target armv7-unknown-linux-musleabi && \ 104 | mkdir build && \ 105 | cp target/armv7-unknown-linux-musleabi/release/wol build/wol 106 | - target: armv7-unknown-linux-musleabihf 107 | host: ubuntu-latest 108 | build: | 109 | cargo install cross --force --git https://github.com/cross-rs/cross && \ 110 | cross build --release --target armv7-unknown-linux-musleabihf && \ 111 | mkdir build && \ 112 | cp target/armv7-unknown-linux-musleabihf/release/wol build/wol 113 | 114 | name: build ${{ matrix.settings.target }} 115 | runs-on: ${{ matrix.settings.host }} 116 | steps: 117 | - name: Checkout repository 118 | uses: actions/checkout@v4 119 | - name: Cache cargo 120 | uses: actions/cache@v4 121 | with: 122 | path: | 123 | ~/.cargo/bin/ 124 | ~/.cargo/registry/index/ 125 | ~/.cargo/registry/cache/ 126 | ~/.cargo/git/db/ 127 | target/ 128 | key: ${{ runner.os }}-${{ matrix.settings.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} 129 | - name: Install rust toolchain 130 | uses: dtolnay/rust-toolchain@stable 131 | with: 132 | toolchain: stable 133 | target: ${{ matrix.settings.target }} 134 | 135 | - name: Setup Node.js 136 | uses: actions/setup-node@v4 137 | with: 138 | node-version: latest 139 | cache: "yarn" 140 | cache-dependency-path: web/yarn.lock 141 | - name: Build web 142 | run: yarn && yarn build 143 | working-directory: web 144 | 145 | - name: Move web files to www 146 | run: mv web/dist/* www 147 | 148 | - name: Build server 149 | run: ${{ matrix.settings.build }} 150 | 151 | - name: Upload artifact 152 | uses: actions/upload-artifact@v4 153 | with: 154 | name: wol-${{ matrix.settings.target }} 155 | path: build/ 156 | if-no-files-found: error 157 | 158 | publish: 159 | runs-on: ubuntu-latest 160 | needs: 161 | - build 162 | if: startsWith(github.ref, 'refs/tags/') 163 | steps: 164 | - name: Checkout repository 165 | uses: actions/checkout@v4 166 | - name: Download all artifact 167 | uses: actions/download-artifact@v4 168 | with: 169 | path: artifacts 170 | - name: Display structure of downloaded files 171 | run: ls -R 172 | working-directory: artifacts 173 | - name: Pack as zip file 174 | run: ls | xargs -I filename zip -r -m filename.zip filename 175 | working-directory: artifacts 176 | - name: Display structure of zip files 177 | run: ls -R 178 | working-directory: artifacts 179 | - name: Release 180 | uses: softprops/action-gh-release@v2 181 | with: 182 | files: artifacts/*.zip 183 | 184 | docker: 185 | runs-on: ubuntu-latest 186 | needs: 187 | - publish 188 | if: startsWith(github.ref, 'refs/tags/') 189 | steps: 190 | - name: Checkout repository 191 | uses: actions/checkout@v4 192 | - name: Setup QEMU 193 | uses: docker/setup-qemu-action@v3 194 | 195 | - name: Setup docker buildx 196 | uses: docker/setup-buildx-action@v3 197 | 198 | # https://github.com/docker/login-action 199 | - name: Login to ${{ env.REGISTRY }} 200 | uses: docker/login-action@v3 201 | with: 202 | registry: ${{ env.REGISTRY }} 203 | username: ${{ github.actor }} 204 | password: ${{ secrets.GITHUB_TOKEN }} 205 | 206 | - name: Build and push Docker images 207 | uses: docker/build-push-action@v6 208 | with: 209 | context: . 210 | platforms: linux/amd64,linux/arm64 211 | push: true 212 | tags: ${{ env.REGISTRY }}/nashaofu/wol:latest,${{ env.REGISTRY }}/nashaofu/wol:${{ github.ref_name }} 213 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | wol.yaml 13 | wol.yml 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "actix-codec" 7 | version = "0.5.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 | dependencies = [ 11 | "bitflags", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "memchr", 16 | "pin-project-lite", 17 | "tokio", 18 | "tokio-util", 19 | "tracing", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-http" 24 | version = "3.9.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" 27 | dependencies = [ 28 | "actix-codec", 29 | "actix-rt", 30 | "actix-service", 31 | "actix-utils", 32 | "ahash", 33 | "base64 0.22.1", 34 | "bitflags", 35 | "bytes", 36 | "bytestring", 37 | "derive_more", 38 | "encoding_rs", 39 | "futures-core", 40 | "http", 41 | "httparse", 42 | "httpdate", 43 | "itoa", 44 | "language-tags", 45 | "local-channel", 46 | "mime", 47 | "percent-encoding", 48 | "pin-project-lite", 49 | "rand", 50 | "sha1", 51 | "smallvec", 52 | "tokio", 53 | "tokio-util", 54 | "tracing", 55 | ] 56 | 57 | [[package]] 58 | name = "actix-macros" 59 | version = "0.2.4" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 62 | dependencies = [ 63 | "quote", 64 | "syn", 65 | ] 66 | 67 | [[package]] 68 | name = "actix-router" 69 | version = "0.5.3" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" 72 | dependencies = [ 73 | "bytestring", 74 | "cfg-if", 75 | "http", 76 | "regex-lite", 77 | "serde", 78 | "tracing", 79 | ] 80 | 81 | [[package]] 82 | name = "actix-rt" 83 | version = "2.10.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" 86 | dependencies = [ 87 | "futures-core", 88 | "tokio", 89 | ] 90 | 91 | [[package]] 92 | name = "actix-server" 93 | version = "2.5.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" 96 | dependencies = [ 97 | "actix-rt", 98 | "actix-service", 99 | "actix-utils", 100 | "futures-core", 101 | "futures-util", 102 | "mio", 103 | "socket2", 104 | "tokio", 105 | "tracing", 106 | ] 107 | 108 | [[package]] 109 | name = "actix-service" 110 | version = "2.0.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 113 | dependencies = [ 114 | "futures-core", 115 | "paste", 116 | "pin-project-lite", 117 | ] 118 | 119 | [[package]] 120 | name = "actix-utils" 121 | version = "3.0.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 124 | dependencies = [ 125 | "local-waker", 126 | "pin-project-lite", 127 | ] 128 | 129 | [[package]] 130 | name = "actix-web" 131 | version = "4.9.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" 134 | dependencies = [ 135 | "actix-codec", 136 | "actix-http", 137 | "actix-macros", 138 | "actix-router", 139 | "actix-rt", 140 | "actix-server", 141 | "actix-service", 142 | "actix-utils", 143 | "actix-web-codegen", 144 | "ahash", 145 | "bytes", 146 | "bytestring", 147 | "cfg-if", 148 | "derive_more", 149 | "encoding_rs", 150 | "futures-core", 151 | "futures-util", 152 | "impl-more", 153 | "itoa", 154 | "language-tags", 155 | "log", 156 | "mime", 157 | "once_cell", 158 | "pin-project-lite", 159 | "regex-lite", 160 | "serde", 161 | "serde_json", 162 | "serde_urlencoded", 163 | "smallvec", 164 | "socket2", 165 | "time", 166 | "url", 167 | ] 168 | 169 | [[package]] 170 | name = "actix-web-codegen" 171 | version = "4.3.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" 174 | dependencies = [ 175 | "actix-router", 176 | "proc-macro2", 177 | "quote", 178 | "syn", 179 | ] 180 | 181 | [[package]] 182 | name = "addr2line" 183 | version = "0.22.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 186 | dependencies = [ 187 | "gimli", 188 | ] 189 | 190 | [[package]] 191 | name = "adler" 192 | version = "1.0.2" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 195 | 196 | [[package]] 197 | name = "ahash" 198 | version = "0.8.11" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 201 | dependencies = [ 202 | "cfg-if", 203 | "getrandom", 204 | "once_cell", 205 | "version_check", 206 | "zerocopy", 207 | ] 208 | 209 | [[package]] 210 | name = "aho-corasick" 211 | version = "1.1.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 214 | dependencies = [ 215 | "memchr", 216 | ] 217 | 218 | [[package]] 219 | name = "anstream" 220 | version = "0.6.15" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 223 | dependencies = [ 224 | "anstyle", 225 | "anstyle-parse", 226 | "anstyle-query", 227 | "anstyle-wincon", 228 | "colorchoice", 229 | "is_terminal_polyfill", 230 | "utf8parse", 231 | ] 232 | 233 | [[package]] 234 | name = "anstyle" 235 | version = "1.0.8" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 238 | 239 | [[package]] 240 | name = "anstyle-parse" 241 | version = "0.2.5" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 244 | dependencies = [ 245 | "utf8parse", 246 | ] 247 | 248 | [[package]] 249 | name = "anstyle-query" 250 | version = "1.1.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 253 | dependencies = [ 254 | "windows-sys 0.52.0", 255 | ] 256 | 257 | [[package]] 258 | name = "anstyle-wincon" 259 | version = "3.0.4" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 262 | dependencies = [ 263 | "anstyle", 264 | "windows-sys 0.52.0", 265 | ] 266 | 267 | [[package]] 268 | name = "anyhow" 269 | version = "1.0.87" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" 272 | 273 | [[package]] 274 | name = "autocfg" 275 | version = "1.3.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 278 | 279 | [[package]] 280 | name = "backtrace" 281 | version = "0.3.73" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 284 | dependencies = [ 285 | "addr2line", 286 | "cc", 287 | "cfg-if", 288 | "libc", 289 | "miniz_oxide", 290 | "object", 291 | "rustc-demangle", 292 | ] 293 | 294 | [[package]] 295 | name = "base64" 296 | version = "0.21.7" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 299 | 300 | [[package]] 301 | name = "base64" 302 | version = "0.22.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 305 | 306 | [[package]] 307 | name = "bitflags" 308 | version = "2.6.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 311 | dependencies = [ 312 | "serde", 313 | ] 314 | 315 | [[package]] 316 | name = "block-buffer" 317 | version = "0.10.4" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 320 | dependencies = [ 321 | "generic-array", 322 | ] 323 | 324 | [[package]] 325 | name = "byteorder" 326 | version = "1.5.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 329 | 330 | [[package]] 331 | name = "bytes" 332 | version = "1.7.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 335 | 336 | [[package]] 337 | name = "bytestring" 338 | version = "1.3.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" 341 | dependencies = [ 342 | "bytes", 343 | ] 344 | 345 | [[package]] 346 | name = "cc" 347 | version = "1.1.17" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "a93fe60e2fc87b6ba2c117f67ae14f66e3fc7d6a1e612a25adb238cc980eadb3" 350 | dependencies = [ 351 | "shlex", 352 | ] 353 | 354 | [[package]] 355 | name = "cfg-if" 356 | version = "1.0.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 359 | 360 | [[package]] 361 | name = "clap" 362 | version = "4.5.17" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" 365 | dependencies = [ 366 | "clap_builder", 367 | "clap_derive", 368 | ] 369 | 370 | [[package]] 371 | name = "clap_builder" 372 | version = "4.5.17" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" 375 | dependencies = [ 376 | "anstream", 377 | "anstyle", 378 | "clap_lex", 379 | "strsim", 380 | ] 381 | 382 | [[package]] 383 | name = "clap_derive" 384 | version = "4.5.13" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 387 | dependencies = [ 388 | "heck", 389 | "proc-macro2", 390 | "quote", 391 | "syn", 392 | ] 393 | 394 | [[package]] 395 | name = "clap_lex" 396 | version = "0.7.2" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 399 | 400 | [[package]] 401 | name = "colorchoice" 402 | version = "1.0.2" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 405 | 406 | [[package]] 407 | name = "config" 408 | version = "0.14.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" 411 | dependencies = [ 412 | "lazy_static", 413 | "nom", 414 | "pathdiff", 415 | "ron", 416 | "serde", 417 | "yaml-rust", 418 | ] 419 | 420 | [[package]] 421 | name = "convert_case" 422 | version = "0.4.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 425 | 426 | [[package]] 427 | name = "cpufeatures" 428 | version = "0.2.14" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 431 | dependencies = [ 432 | "libc", 433 | ] 434 | 435 | [[package]] 436 | name = "crypto-common" 437 | version = "0.1.6" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 440 | dependencies = [ 441 | "generic-array", 442 | "typenum", 443 | ] 444 | 445 | [[package]] 446 | name = "deranged" 447 | version = "0.3.11" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 450 | dependencies = [ 451 | "powerfmt", 452 | ] 453 | 454 | [[package]] 455 | name = "derive_more" 456 | version = "0.99.18" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" 459 | dependencies = [ 460 | "convert_case", 461 | "proc-macro2", 462 | "quote", 463 | "rustc_version", 464 | "syn", 465 | ] 466 | 467 | [[package]] 468 | name = "digest" 469 | version = "0.10.7" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 472 | dependencies = [ 473 | "block-buffer", 474 | "crypto-common", 475 | ] 476 | 477 | [[package]] 478 | name = "dotenv" 479 | version = "0.15.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 482 | 483 | [[package]] 484 | name = "encoding_rs" 485 | version = "0.8.34" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 488 | dependencies = [ 489 | "cfg-if", 490 | ] 491 | 492 | [[package]] 493 | name = "env_filter" 494 | version = "0.1.2" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" 497 | dependencies = [ 498 | "log", 499 | "regex", 500 | ] 501 | 502 | [[package]] 503 | name = "env_logger" 504 | version = "0.11.5" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" 507 | dependencies = [ 508 | "anstream", 509 | "anstyle", 510 | "env_filter", 511 | "humantime", 512 | "log", 513 | ] 514 | 515 | [[package]] 516 | name = "equivalent" 517 | version = "1.0.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 520 | 521 | [[package]] 522 | name = "fnv" 523 | version = "1.0.7" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 526 | 527 | [[package]] 528 | name = "form_urlencoded" 529 | version = "1.2.1" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 532 | dependencies = [ 533 | "percent-encoding", 534 | ] 535 | 536 | [[package]] 537 | name = "futures-core" 538 | version = "0.3.30" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 541 | 542 | [[package]] 543 | name = "futures-sink" 544 | version = "0.3.30" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 547 | 548 | [[package]] 549 | name = "futures-task" 550 | version = "0.3.30" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 553 | 554 | [[package]] 555 | name = "futures-util" 556 | version = "0.3.30" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 559 | dependencies = [ 560 | "futures-core", 561 | "futures-task", 562 | "pin-project-lite", 563 | "pin-utils", 564 | ] 565 | 566 | [[package]] 567 | name = "generic-array" 568 | version = "0.14.7" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 571 | dependencies = [ 572 | "typenum", 573 | "version_check", 574 | ] 575 | 576 | [[package]] 577 | name = "getrandom" 578 | version = "0.2.15" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 581 | dependencies = [ 582 | "cfg-if", 583 | "libc", 584 | "wasi", 585 | ] 586 | 587 | [[package]] 588 | name = "gimli" 589 | version = "0.29.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 592 | 593 | [[package]] 594 | name = "glob" 595 | version = "0.3.1" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 598 | 599 | [[package]] 600 | name = "hashbrown" 601 | version = "0.14.5" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 604 | 605 | [[package]] 606 | name = "heck" 607 | version = "0.5.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 610 | 611 | [[package]] 612 | name = "hermit-abi" 613 | version = "0.3.9" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 616 | 617 | [[package]] 618 | name = "hex" 619 | version = "0.4.3" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 622 | 623 | [[package]] 624 | name = "http" 625 | version = "0.2.12" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 628 | dependencies = [ 629 | "bytes", 630 | "fnv", 631 | "itoa", 632 | ] 633 | 634 | [[package]] 635 | name = "httparse" 636 | version = "1.9.4" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 639 | 640 | [[package]] 641 | name = "httpdate" 642 | version = "1.0.3" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 645 | 646 | [[package]] 647 | name = "humantime" 648 | version = "2.1.0" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 651 | 652 | [[package]] 653 | name = "idna" 654 | version = "0.5.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 657 | dependencies = [ 658 | "unicode-bidi", 659 | "unicode-normalization", 660 | ] 661 | 662 | [[package]] 663 | name = "impl-more" 664 | version = "0.1.6" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" 667 | 668 | [[package]] 669 | name = "indexmap" 670 | version = "2.5.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 673 | dependencies = [ 674 | "equivalent", 675 | "hashbrown", 676 | ] 677 | 678 | [[package]] 679 | name = "is_terminal_polyfill" 680 | version = "1.70.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 683 | 684 | [[package]] 685 | name = "itoa" 686 | version = "1.0.11" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 689 | 690 | [[package]] 691 | name = "language-tags" 692 | version = "0.3.2" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 695 | 696 | [[package]] 697 | name = "lazy_static" 698 | version = "1.5.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 701 | 702 | [[package]] 703 | name = "libc" 704 | version = "0.2.158" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 707 | 708 | [[package]] 709 | name = "linked-hash-map" 710 | version = "0.5.6" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 713 | 714 | [[package]] 715 | name = "local-channel" 716 | version = "0.1.5" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" 719 | dependencies = [ 720 | "futures-core", 721 | "futures-sink", 722 | "local-waker", 723 | ] 724 | 725 | [[package]] 726 | name = "local-waker" 727 | version = "0.1.4" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" 730 | 731 | [[package]] 732 | name = "lock_api" 733 | version = "0.4.12" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 736 | dependencies = [ 737 | "autocfg", 738 | "scopeguard", 739 | ] 740 | 741 | [[package]] 742 | name = "log" 743 | version = "0.4.22" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 746 | 747 | [[package]] 748 | name = "memchr" 749 | version = "2.7.4" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 752 | 753 | [[package]] 754 | name = "mime" 755 | version = "0.3.17" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 758 | 759 | [[package]] 760 | name = "mime_guess" 761 | version = "2.0.5" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 764 | dependencies = [ 765 | "mime", 766 | "unicase", 767 | ] 768 | 769 | [[package]] 770 | name = "minimal-lexical" 771 | version = "0.2.1" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 774 | 775 | [[package]] 776 | name = "miniz_oxide" 777 | version = "0.7.4" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 780 | dependencies = [ 781 | "adler", 782 | ] 783 | 784 | [[package]] 785 | name = "mio" 786 | version = "1.0.2" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 789 | dependencies = [ 790 | "hermit-abi", 791 | "libc", 792 | "log", 793 | "wasi", 794 | "windows-sys 0.52.0", 795 | ] 796 | 797 | [[package]] 798 | name = "no-std-net" 799 | version = "0.6.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" 802 | 803 | [[package]] 804 | name = "nom" 805 | version = "7.1.3" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 808 | dependencies = [ 809 | "memchr", 810 | "minimal-lexical", 811 | ] 812 | 813 | [[package]] 814 | name = "num-conv" 815 | version = "0.1.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 818 | 819 | [[package]] 820 | name = "object" 821 | version = "0.36.4" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 824 | dependencies = [ 825 | "memchr", 826 | ] 827 | 828 | [[package]] 829 | name = "once_cell" 830 | version = "1.19.0" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 833 | 834 | [[package]] 835 | name = "parking_lot" 836 | version = "0.12.3" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 839 | dependencies = [ 840 | "lock_api", 841 | "parking_lot_core", 842 | ] 843 | 844 | [[package]] 845 | name = "parking_lot_core" 846 | version = "0.9.10" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 849 | dependencies = [ 850 | "cfg-if", 851 | "libc", 852 | "redox_syscall", 853 | "smallvec", 854 | "windows-targets", 855 | ] 856 | 857 | [[package]] 858 | name = "paste" 859 | version = "1.0.15" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 862 | 863 | [[package]] 864 | name = "pathdiff" 865 | version = "0.2.1" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 868 | 869 | [[package]] 870 | name = "percent-encoding" 871 | version = "2.3.1" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 874 | 875 | [[package]] 876 | name = "pin-project-lite" 877 | version = "0.2.14" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 880 | 881 | [[package]] 882 | name = "pin-utils" 883 | version = "0.1.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 886 | 887 | [[package]] 888 | name = "pnet_base" 889 | version = "0.34.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" 892 | dependencies = [ 893 | "no-std-net", 894 | ] 895 | 896 | [[package]] 897 | name = "pnet_macros" 898 | version = "0.34.0" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" 901 | dependencies = [ 902 | "proc-macro2", 903 | "quote", 904 | "regex", 905 | "syn", 906 | ] 907 | 908 | [[package]] 909 | name = "pnet_macros_support" 910 | version = "0.34.0" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" 913 | dependencies = [ 914 | "pnet_base", 915 | ] 916 | 917 | [[package]] 918 | name = "pnet_packet" 919 | version = "0.34.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" 922 | dependencies = [ 923 | "glob", 924 | "pnet_base", 925 | "pnet_macros", 926 | "pnet_macros_support", 927 | ] 928 | 929 | [[package]] 930 | name = "powerfmt" 931 | version = "0.2.0" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 934 | 935 | [[package]] 936 | name = "ppv-lite86" 937 | version = "0.2.20" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 940 | dependencies = [ 941 | "zerocopy", 942 | ] 943 | 944 | [[package]] 945 | name = "proc-macro2" 946 | version = "1.0.86" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 949 | dependencies = [ 950 | "unicode-ident", 951 | ] 952 | 953 | [[package]] 954 | name = "quote" 955 | version = "1.0.37" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 958 | dependencies = [ 959 | "proc-macro2", 960 | ] 961 | 962 | [[package]] 963 | name = "rand" 964 | version = "0.8.5" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 967 | dependencies = [ 968 | "libc", 969 | "rand_chacha", 970 | "rand_core", 971 | ] 972 | 973 | [[package]] 974 | name = "rand_chacha" 975 | version = "0.3.1" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 978 | dependencies = [ 979 | "ppv-lite86", 980 | "rand_core", 981 | ] 982 | 983 | [[package]] 984 | name = "rand_core" 985 | version = "0.6.4" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 988 | dependencies = [ 989 | "getrandom", 990 | ] 991 | 992 | [[package]] 993 | name = "redox_syscall" 994 | version = "0.5.3" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 997 | dependencies = [ 998 | "bitflags", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "regex" 1003 | version = "1.10.6" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 1006 | dependencies = [ 1007 | "aho-corasick", 1008 | "memchr", 1009 | "regex-automata", 1010 | "regex-syntax", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "regex-automata" 1015 | version = "0.4.7" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 1018 | dependencies = [ 1019 | "aho-corasick", 1020 | "memchr", 1021 | "regex-syntax", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "regex-lite" 1026 | version = "0.1.6" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" 1029 | 1030 | [[package]] 1031 | name = "regex-syntax" 1032 | version = "0.8.4" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 1035 | 1036 | [[package]] 1037 | name = "ron" 1038 | version = "0.8.1" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" 1041 | dependencies = [ 1042 | "base64 0.21.7", 1043 | "bitflags", 1044 | "serde", 1045 | "serde_derive", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "rust-embed" 1050 | version = "8.5.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" 1053 | dependencies = [ 1054 | "rust-embed-impl", 1055 | "rust-embed-utils", 1056 | "walkdir", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "rust-embed-impl" 1061 | version = "8.5.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" 1064 | dependencies = [ 1065 | "proc-macro2", 1066 | "quote", 1067 | "rust-embed-utils", 1068 | "syn", 1069 | "walkdir", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "rust-embed-utils" 1074 | version = "8.5.0" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" 1077 | dependencies = [ 1078 | "mime_guess", 1079 | "sha2", 1080 | "walkdir", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "rustc-demangle" 1085 | version = "0.1.24" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1088 | 1089 | [[package]] 1090 | name = "rustc_version" 1091 | version = "0.4.1" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1094 | dependencies = [ 1095 | "semver", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "ryu" 1100 | version = "1.0.18" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1103 | 1104 | [[package]] 1105 | name = "same-file" 1106 | version = "1.0.6" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1109 | dependencies = [ 1110 | "winapi-util", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "scopeguard" 1115 | version = "1.2.0" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1118 | 1119 | [[package]] 1120 | name = "semver" 1121 | version = "1.0.23" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 1124 | 1125 | [[package]] 1126 | name = "serde" 1127 | version = "1.0.210" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 1130 | dependencies = [ 1131 | "serde_derive", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "serde_derive" 1136 | version = "1.0.210" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "syn", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "serde_json" 1147 | version = "1.0.128" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 1150 | dependencies = [ 1151 | "itoa", 1152 | "memchr", 1153 | "ryu", 1154 | "serde", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "serde_urlencoded" 1159 | version = "0.7.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1162 | dependencies = [ 1163 | "form_urlencoded", 1164 | "itoa", 1165 | "ryu", 1166 | "serde", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "serde_yaml" 1171 | version = "0.9.34+deprecated" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1174 | dependencies = [ 1175 | "indexmap", 1176 | "itoa", 1177 | "ryu", 1178 | "serde", 1179 | "unsafe-libyaml", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "sha1" 1184 | version = "0.10.6" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1187 | dependencies = [ 1188 | "cfg-if", 1189 | "cpufeatures", 1190 | "digest", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "sha2" 1195 | version = "0.10.8" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1198 | dependencies = [ 1199 | "cfg-if", 1200 | "cpufeatures", 1201 | "digest", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "shlex" 1206 | version = "1.3.0" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1209 | 1210 | [[package]] 1211 | name = "signal-hook-registry" 1212 | version = "1.4.2" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1215 | dependencies = [ 1216 | "libc", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "smallvec" 1221 | version = "1.13.2" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1224 | 1225 | [[package]] 1226 | name = "socket2" 1227 | version = "0.5.7" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1230 | dependencies = [ 1231 | "libc", 1232 | "windows-sys 0.52.0", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "strsim" 1237 | version = "0.11.1" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1240 | 1241 | [[package]] 1242 | name = "surge-ping" 1243 | version = "0.8.1" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "efbf95ce4c7c5b311d2ce3f088af2b93edef0f09727fa50fbe03c7a979afce77" 1246 | dependencies = [ 1247 | "hex", 1248 | "parking_lot", 1249 | "pnet_packet", 1250 | "rand", 1251 | "socket2", 1252 | "thiserror", 1253 | "tokio", 1254 | "tracing", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "syn" 1259 | version = "2.0.77" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 1262 | dependencies = [ 1263 | "proc-macro2", 1264 | "quote", 1265 | "unicode-ident", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "thiserror" 1270 | version = "1.0.63" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 1273 | dependencies = [ 1274 | "thiserror-impl", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "thiserror-impl" 1279 | version = "1.0.63" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 1282 | dependencies = [ 1283 | "proc-macro2", 1284 | "quote", 1285 | "syn", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "time" 1290 | version = "0.3.36" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1293 | dependencies = [ 1294 | "deranged", 1295 | "itoa", 1296 | "num-conv", 1297 | "powerfmt", 1298 | "serde", 1299 | "time-core", 1300 | "time-macros", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "time-core" 1305 | version = "0.1.2" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1308 | 1309 | [[package]] 1310 | name = "time-macros" 1311 | version = "0.2.18" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1314 | dependencies = [ 1315 | "num-conv", 1316 | "time-core", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "tinyvec" 1321 | version = "1.8.0" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1324 | dependencies = [ 1325 | "tinyvec_macros", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "tinyvec_macros" 1330 | version = "0.1.1" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1333 | 1334 | [[package]] 1335 | name = "tokio" 1336 | version = "1.40.0" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 1339 | dependencies = [ 1340 | "backtrace", 1341 | "libc", 1342 | "mio", 1343 | "parking_lot", 1344 | "pin-project-lite", 1345 | "signal-hook-registry", 1346 | "socket2", 1347 | "windows-sys 0.52.0", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "tokio-util" 1352 | version = "0.7.12" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 1355 | dependencies = [ 1356 | "bytes", 1357 | "futures-core", 1358 | "futures-sink", 1359 | "pin-project-lite", 1360 | "tokio", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "tracing" 1365 | version = "0.1.40" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1368 | dependencies = [ 1369 | "log", 1370 | "pin-project-lite", 1371 | "tracing-attributes", 1372 | "tracing-core", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "tracing-attributes" 1377 | version = "0.1.27" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1380 | dependencies = [ 1381 | "proc-macro2", 1382 | "quote", 1383 | "syn", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "tracing-core" 1388 | version = "0.1.32" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1391 | dependencies = [ 1392 | "once_cell", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "typenum" 1397 | version = "1.17.0" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1400 | 1401 | [[package]] 1402 | name = "unicase" 1403 | version = "2.7.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1406 | dependencies = [ 1407 | "version_check", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "unicode-bidi" 1412 | version = "0.3.15" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1415 | 1416 | [[package]] 1417 | name = "unicode-ident" 1418 | version = "1.0.12" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1421 | 1422 | [[package]] 1423 | name = "unicode-normalization" 1424 | version = "0.1.23" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1427 | dependencies = [ 1428 | "tinyvec", 1429 | ] 1430 | 1431 | [[package]] 1432 | name = "unsafe-libyaml" 1433 | version = "0.2.11" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1436 | 1437 | [[package]] 1438 | name = "url" 1439 | version = "2.5.2" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1442 | dependencies = [ 1443 | "form_urlencoded", 1444 | "idna", 1445 | "percent-encoding", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "utf8parse" 1450 | version = "0.2.2" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1453 | 1454 | [[package]] 1455 | name = "version_check" 1456 | version = "0.9.5" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1459 | 1460 | [[package]] 1461 | name = "walkdir" 1462 | version = "2.5.0" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1465 | dependencies = [ 1466 | "same-file", 1467 | "winapi-util", 1468 | ] 1469 | 1470 | [[package]] 1471 | name = "wasi" 1472 | version = "0.11.0+wasi-snapshot-preview1" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1475 | 1476 | [[package]] 1477 | name = "winapi-util" 1478 | version = "0.1.9" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1481 | dependencies = [ 1482 | "windows-sys 0.59.0", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "windows-sys" 1487 | version = "0.52.0" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1490 | dependencies = [ 1491 | "windows-targets", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "windows-sys" 1496 | version = "0.59.0" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1499 | dependencies = [ 1500 | "windows-targets", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "windows-targets" 1505 | version = "0.52.6" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1508 | dependencies = [ 1509 | "windows_aarch64_gnullvm", 1510 | "windows_aarch64_msvc", 1511 | "windows_i686_gnu", 1512 | "windows_i686_gnullvm", 1513 | "windows_i686_msvc", 1514 | "windows_x86_64_gnu", 1515 | "windows_x86_64_gnullvm", 1516 | "windows_x86_64_msvc", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "windows_aarch64_gnullvm" 1521 | version = "0.52.6" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1524 | 1525 | [[package]] 1526 | name = "windows_aarch64_msvc" 1527 | version = "0.52.6" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1530 | 1531 | [[package]] 1532 | name = "windows_i686_gnu" 1533 | version = "0.52.6" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1536 | 1537 | [[package]] 1538 | name = "windows_i686_gnullvm" 1539 | version = "0.52.6" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1542 | 1543 | [[package]] 1544 | name = "windows_i686_msvc" 1545 | version = "0.52.6" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1548 | 1549 | [[package]] 1550 | name = "windows_x86_64_gnu" 1551 | version = "0.52.6" 1552 | source = "registry+https://github.com/rust-lang/crates.io-index" 1553 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1554 | 1555 | [[package]] 1556 | name = "windows_x86_64_gnullvm" 1557 | version = "0.52.6" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1560 | 1561 | [[package]] 1562 | name = "windows_x86_64_msvc" 1563 | version = "0.52.6" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1566 | 1567 | [[package]] 1568 | name = "wol" 1569 | version = "0.1.0" 1570 | dependencies = [ 1571 | "actix-web", 1572 | "anyhow", 1573 | "base64 0.22.1", 1574 | "clap", 1575 | "config", 1576 | "dotenv", 1577 | "env_logger", 1578 | "futures-util", 1579 | "lazy_static", 1580 | "log", 1581 | "rust-embed", 1582 | "serde", 1583 | "serde_yaml", 1584 | "surge-ping", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "yaml-rust" 1589 | version = "0.4.5" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1592 | dependencies = [ 1593 | "linked-hash-map", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "zerocopy" 1598 | version = "0.7.35" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1601 | dependencies = [ 1602 | "byteorder", 1603 | "zerocopy-derive", 1604 | ] 1605 | 1606 | [[package]] 1607 | name = "zerocopy-derive" 1608 | version = "0.7.35" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1611 | dependencies = [ 1612 | "proc-macro2", 1613 | "quote", 1614 | "syn", 1615 | ] 1616 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wol" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-web = { version = "4.9", default-features = false, features = ["macros", "compat"] } 10 | anyhow = "1.0" 11 | base64 = "0.22" 12 | clap = { version = "4.5", features = ["derive"] } 13 | config = { version = "0.14", default-features = false, features = [ 14 | "ron", 15 | "yaml", 16 | "yaml-rust", 17 | ] } 18 | dotenv = "0.15" 19 | env_logger = "0.11" 20 | futures-util = { version = "0.3", default-features = false } 21 | lazy_static = "1.5" 22 | log = "0.4" 23 | rust-embed = { version = "8.5", features = ["mime-guess"] } 24 | serde = { version = "1.0", features = ["derive"] } 25 | serde_yaml = "0.9" 26 | surge-ping = "0.8" 27 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | image = "rust:latest" 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=amd64 alpine:latest as builder 2 | 3 | WORKDIR /build 4 | 5 | COPY build-docker.sh . 6 | 7 | ARG TARGETARCH 8 | 9 | RUN chmod +x ./build-docker.sh 10 | RUN ./build-docker.sh 11 | 12 | FROM alpine:latest 13 | 14 | WORKDIR /opt/wol 15 | 16 | COPY --from=builder /build/wol . 17 | 18 | EXPOSE 3300 19 | 20 | ENV RUST_LOG=info \ 21 | RUST_BACKTRACE=1 22 | 23 | CMD ["/opt/wol/wol"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wol 2 | 3 | Wol 是 wake on lan 的简写,是一个轻量、简洁的 Wol 管理服务,支持检测设备是否开机成功。 4 | 5 | ## 功能特性 6 | 7 | - 部署简单,且可私有部署。 8 | - 默认使用 yaml 作为配置文件,易于编辑与迁移。 9 | - 主题切换:支持浅色主题与暗黑主题。 10 | - 占用资源少,运行速度快。 11 | - 跨平台:可以在 Linux、macOS 和 Windows 操作系统上运行。 12 | - 支持 basic auth,保护服务配置 13 | - 支持检测设备是否启动(设备需支持 ping) 14 | 15 | ## 项目截图 16 | 17 | | 截图说明 | 截图 | 18 | | ------------ | ------------------------------------------------------- | 19 | | 浅色主题 | ![浅色主题](resources/screenshots/浅色主题.png) | 20 | | 深色主题 | ![深色主题](resources/screenshots/深色主题.png) | 21 | | 添加新设备 | ![添加新设备](resources/screenshots/添加新设备.png) | 22 | | 开启页面认证 | ![开启页面认证](resources/screenshots/开启页面认证.png) | 23 | | 页面认证 | ![页面认证](resources/screenshots/页面认证.png) | 24 | 25 | ## 安装和使用 26 | 27 | ### Docker 中使用(Linux 推荐) 28 | 29 | 推荐使用 Docker 安装方式,使用简单方便,只需运行如下命令: 30 | 31 | ```sh 32 | docker pull ghcr.io/nashaofu/wol:latest 33 | 34 | # 使用docker host模式 35 | docker run -d \ 36 | --name wol \ 37 | --net host \ 38 | -v /path/to/wol.yaml:/opt/wol/wol.yaml \ 39 | ghcr.io/nashaofu/wol:latest 40 | 41 | # 不使用docker host模式 42 | docker run -d \ 43 | --name wol \ 44 | -p 3300:3300 \ 45 | -v /path/to/wol.yaml:/opt/wol/wol.yaml \ 46 | ghcr.io/nashaofu/wol:latest 47 | ``` 48 | 49 | 然后在浏览器中访问 `http://127.0.0.1:3300` 即可使用。 50 | 51 | 如果需要自定义配置,可将项目根目录下的 `wol.example.yaml` 文件拷贝到 `/opt/wol` 目录下并重命名为 `wol.yaml`,具体配置参考配置章节,也可以修改启动命令,指定配置文件位置。 52 | 53 | ### 系统中使用(Windows/Mac 推荐) 54 | 55 | Windows/Mac 桌面版的 docker 不支持`--net=host`,所以推荐这种使用方式。 56 | 57 | 1. 前往[release](https://github.com/nashaofu/wol/releases)页面下载`wol-xxxx.zip`,`xxxx`表示系统架构,请根据自己的情况选择 58 | 2. 解压出`wol-xxxx.zip`中的可执行文件,然后在终端中运行即可启动服务。同时也支持在启动时指定服务的端口号与配置文件。 59 | 60 | ```bash 61 | Usage: wol [OPTIONS] 62 | 63 | Options: 64 | -p, --port App listen port [default: 3300] 65 | -c, --config Config file path [default: ./wol.yaml] 66 | -h, --help Print help 67 | -V, --version Print version 68 | ``` 69 | 70 | ## 配置 71 | 72 | 项目配置文件为`wol.yaml`,配置内容如下: 73 | 74 | ```yaml 75 | # basic auth 配置,auth 可为 null,表示关闭认证 76 | auth: 77 | username: "" 78 | password: "" 79 | # 设备列表 80 | devices: 81 | - name: Windows # 设备名称 82 | mac: 00:00:00:00:00:00 # 设备 mac 地址 83 | ip: 192.168.1.1 # 设备 ipv4 地址 84 | netmask: 255.255.255.0 # 子网掩码 85 | port: 9 # wake on lan 唤醒端口号,一般为 9、7 或者 0 86 | ``` 87 | 88 | ## 特别说明 89 | 90 | 在这个项目中,我们使用了[Wake-On-Lan(WOL)](https://en.wikipedia.org/wiki/Wake-on-LAN)技术来实现远程唤醒功能,WOL 是一种网络唤醒技术,可以让计算机在休眠或关机状态下通过局域网进行唤醒操作。 91 | 92 | 为了启用 WOL 功能,我们需要完成以下准备工作(以 Windows 为例): 93 | 94 | 1. 进入计算机的主板 BIOS 设置并打开 Wake On Lan 选项。需要注意的是,不同品牌的主板可能有不同的设置名称,因此请参考相应品牌的主板设置指南。 95 | 2. 在设备管理器中找到电脑网卡,右键属性,在电源管理选项中勾选`允许此设备唤醒计算机`以及`只允许幻数据包唤醒计算机` 96 | 3. 在 Wol 网页中添加设备,然后配置以下内容: 97 | 1. MAC 地址:配置为用于唤醒设备的网卡(通常为有线网卡)的 MAC 地址 98 | 2. IP 地址:目标主机的局域网 IP 地址 99 | 3. 子网掩码:当前网络的子网掩码,用于指定广播数据发送到哪个子网,通常可填写 255.255.255.0 100 | 4. 端口号:WOL 唤醒接收幻包的端口,通常为 0、7、8、9 101 | 102 | 完成这些步骤后,正常情况就可以通过局域网唤醒处于休眠状态或关机状态的计算机了。其中 1、2 步可参考:https://sspai.com/post/67003 103 | 104 | ## 贡献指南 105 | 106 | 如果您想为 Wol 做出贡献,可以按照以下步骤进行: 107 | 108 | 1. 克隆项目到本地: 109 | 110 | ```sh 111 | git clone https://github.com/nashaofu/wol.git 112 | ``` 113 | 114 | 2. 创建新分支: 115 | 116 | ```sh 117 | git checkout -b my-feature-branch 118 | ``` 119 | 120 | 3. 启动项目:你需要安装 rust、nodejs 与 yarn 121 | 122 | ```sh 123 | # 启动服务端项目 124 | cargo run 125 | # 启动前端项目 126 | cd client && yarn && yarn dev 127 | ``` 128 | 129 | 4. 修改并提交代码: 130 | 131 | ```sh 132 | git add . 133 | git commit -m "Add new feature" 134 | ``` 135 | 136 | 5. 推送代码到远程仓库: 137 | 138 | ```sh 139 | git push origin my-feature-branch 140 | ``` 141 | 142 | 6. 创建 Pull Request:在 GitHub 上创建一个新的 Pull Request 并等待审核。 143 | 144 | ## 许可证 145 | 146 | Wol 使用 Apache 许可证,详情请参阅 [LICENSE](LICENSE) 文件。 147 | -------------------------------------------------------------------------------- /build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | 3 | # https://stackoverflow.com/a/9893727 4 | set -e 5 | 6 | case $TARGETARCH in 7 | "amd64") 8 | target="x86_64-unknown-linux-musl" 9 | ;; 10 | "arm64") 11 | target="aarch64-unknown-linux-musl" 12 | ;; 13 | *) 14 | echo "unsupported architecture" 15 | exit 1 16 | ;; 17 | esac 18 | 19 | mkdir wol 20 | 21 | wget https://github.com/nashaofu/wol/releases/latest/download/wol-${target}.zip 22 | unzip wol-${target}.zip 23 | mv wol-${target}/wol wol/wol 24 | chmod +x wol/wol 25 | -------------------------------------------------------------------------------- /resources/screenshots/开启页面认证.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/resources/screenshots/开启页面认证.png -------------------------------------------------------------------------------- /resources/screenshots/浅色主题.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/resources/screenshots/浅色主题.png -------------------------------------------------------------------------------- /resources/screenshots/深色主题.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/resources/screenshots/深色主题.png -------------------------------------------------------------------------------- /resources/screenshots/添加新设备.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/resources/screenshots/添加新设备.png -------------------------------------------------------------------------------- /resources/screenshots/页面认证.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/resources/screenshots/页面认证.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /src/api/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::Result, 3 | settings::{Auth, SETTINGS}, 4 | }; 5 | 6 | use actix_web::{get, post, web, HttpResponse, Responder}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[get("/info")] 10 | async fn get() -> Result { 11 | Ok(HttpResponse::Ok().json(&SETTINGS.read()?.auth)) 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct SaveAuthData { 16 | username: String, 17 | password: Option, 18 | } 19 | 20 | #[post("/save")] 21 | async fn save(data: web::Json>) -> Result { 22 | let settings = &mut SETTINGS.write()?; 23 | if let Some(data) = data.clone() { 24 | settings.auth = Some(Auth { 25 | username: data.username, 26 | password: data.password.unwrap_or(String::default()), 27 | }); 28 | } else { 29 | settings.auth = None; 30 | } 31 | settings.save()?; 32 | 33 | Ok(HttpResponse::Ok().json(&settings.auth)) 34 | } 35 | -------------------------------------------------------------------------------- /src/api/device.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::Result, 3 | settings::{Device, SETTINGS}, 4 | wol, 5 | }; 6 | 7 | use actix_web::{get, post, web, HttpResponse, Responder}; 8 | use serde::{Deserialize, Serialize}; 9 | use surge_ping; 10 | 11 | #[get("/all")] 12 | async fn all() -> Result { 13 | Ok(HttpResponse::Ok().json(&SETTINGS.read()?.devices)) 14 | } 15 | 16 | #[post("/save")] 17 | async fn save(data: web::Json>) -> Result { 18 | let settings = &mut SETTINGS.write()?; 19 | settings.devices = data.clone(); 20 | settings.save()?; 21 | 22 | Ok(HttpResponse::Ok().json(&settings.devices)) 23 | } 24 | 25 | #[post("/wake")] 26 | async fn wake(data: web::Json) -> Result { 27 | wol::wake(&data)?; 28 | Ok(HttpResponse::Ok().json(&data)) 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | pub enum DeviceStatus { 33 | Online, 34 | Offline, 35 | } 36 | 37 | #[get("/status/{ip}")] 38 | async fn status(ip: web::Path) -> Result { 39 | let payload = [0; 8]; 40 | let device = ip.parse()?; 41 | 42 | let device_status = surge_ping::ping(device, &payload) 43 | .await 44 | .map(|_| DeviceStatus::Online) 45 | .unwrap_or(DeviceStatus::Offline); 46 | 47 | Ok(HttpResponse::Ok().json(device_status)) 48 | } 49 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod device; 3 | 4 | use actix_web::web; 5 | 6 | pub fn init(cfg: &mut web::ServiceConfig) { 7 | cfg 8 | .service( 9 | web::scope("/device") 10 | .service(device::all) 11 | .service(device::save) 12 | .service(device::wake) 13 | .service(device::status), 14 | ) 15 | .service(web::scope("/auth").service(auth::get).service(auth::save)); 16 | } 17 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use lazy_static::lazy_static; 3 | 4 | lazy_static! { 5 | pub static ref ARGS: Args = Args::parse(); 6 | } 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(author, version, about, long_about = None)] 10 | pub struct Args { 11 | /// App listen port 12 | #[arg(short, long, default_value_t = 3300)] 13 | pub port: u16, 14 | 15 | /// Config file path 16 | #[arg(short, long, default_value_t = String::from("./wol.yaml"))] 17 | pub config: String, 18 | } 19 | -------------------------------------------------------------------------------- /src/asset.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error::ErrorNotFound, HttpRequest, HttpResponse, Responder, Result}; 2 | use rust_embed::RustEmbed; 3 | 4 | #[derive(RustEmbed)] 5 | #[folder = "www"] 6 | struct Asset; 7 | 8 | pub async fn serve(req: HttpRequest) -> Result { 9 | let path = &req.path()[1..]; 10 | 11 | let file = Asset::get(path) 12 | .or(Asset::get("index.html")) 13 | .ok_or(ErrorNotFound("Not Found"))?; 14 | 15 | Ok( 16 | HttpResponse::Ok() 17 | .content_type(file.metadata.mimetype()) 18 | .body(file.data), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{body::BoxBody, http::StatusCode, HttpResponse, ResponseError}; 2 | use serde::Serialize; 3 | use serde_yaml; 4 | use std::{error::Error, fmt, io, net::AddrParseError, result, sync::PoisonError}; 5 | use surge_ping::SurgeError; 6 | 7 | pub type Result = result::Result; 8 | 9 | #[derive(Debug)] 10 | pub struct AppError { 11 | pub status_code: StatusCode, 12 | pub code: u16, 13 | pub message: String, 14 | } 15 | 16 | impl fmt::Display for AppError { 17 | // This trait requires `fmt` with this exact signature. 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | write!(f, "AppError{:?}", self) 20 | } 21 | } 22 | 23 | #[derive(Serialize, Debug)] 24 | pub struct AppErrorJson { 25 | pub code: u16, 26 | pub message: String, 27 | } 28 | 29 | impl AppError { 30 | pub fn new(status_code: StatusCode, code: u16, message: M) -> Self { 31 | AppError { 32 | status_code, 33 | code, 34 | message: message.to_string(), 35 | } 36 | } 37 | 38 | pub fn from_err(err: E) -> Self { 39 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 40 | } 41 | } 42 | 43 | impl ResponseError for AppError { 44 | fn status_code(&self) -> StatusCode { 45 | self.status_code 46 | } 47 | 48 | fn error_response(&self) -> HttpResponse { 49 | HttpResponse::build(self.status_code()).json(AppErrorJson { 50 | code: self.code, 51 | message: self.message.clone(), 52 | }) 53 | } 54 | } 55 | 56 | impl From for AppError { 57 | fn from(err: anyhow::Error) -> Self { 58 | log::error!("anyhow::Error {}", err); 59 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 60 | } 61 | } 62 | 63 | impl From for AppError { 64 | fn from(err: AddrParseError) -> Self { 65 | log::error!("AddrParseError {}", err); 66 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 67 | } 68 | } 69 | 70 | impl From for AppError { 71 | fn from(err: SurgeError) -> Self { 72 | log::error!("SurgeError {}", err); 73 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 74 | } 75 | } 76 | 77 | impl From> for AppError { 78 | fn from(err: PoisonError) -> Self { 79 | log::error!("PoisonError {}", err); 80 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 81 | } 82 | } 83 | 84 | impl From for AppError { 85 | fn from(err: io::Error) -> Self { 86 | log::error!("io::Error {}", err); 87 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 88 | } 89 | } 90 | 91 | impl From for AppError { 92 | fn from(err: serde_yaml::Error) -> Self { 93 | log::error!("serde_yaml::Error {}", err); 94 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, 500, err.to_string()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod args; 3 | pub mod asset; 4 | pub mod errors; 5 | pub mod middleware; 6 | pub mod settings; 7 | pub mod wol; 8 | 9 | use actix_web::{ 10 | middleware::{Logger, NormalizePath}, 11 | web, App, HttpServer, 12 | }; 13 | use dotenv::dotenv; 14 | use std::io; 15 | 16 | use args::ARGS; 17 | use asset::serve; 18 | use middleware::BasicAuth; 19 | 20 | #[actix_web::main] 21 | async fn main() -> Result<(), io::Error> { 22 | dotenv().ok(); 23 | 24 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 25 | 26 | log::info!("starting HTTP server at http://0.0.0.0:{}", ARGS.port); 27 | 28 | HttpServer::new(|| { 29 | App::new() 30 | .wrap(NormalizePath::trim()) 31 | .wrap(BasicAuth) 32 | .service( 33 | web::scope("/api") 34 | .wrap(Logger::default()) 35 | .configure(api::init), 36 | ) 37 | .default_service(web::to(serve)) 38 | }) 39 | .bind(("0.0.0.0", ARGS.port))? 40 | .run() 41 | .await 42 | } 43 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | future::{ready, Ready}, 4 | rc::Rc, 5 | }; 6 | 7 | use actix_web::{ 8 | body::BoxBody, 9 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, 10 | http::{ 11 | header::{AUTHORIZATION, WWW_AUTHENTICATE}, 12 | StatusCode, 13 | }, 14 | Error, HttpResponse, ResponseError, 15 | }; 16 | use anyhow::{anyhow, Result}; 17 | use base64::prelude::{Engine, BASE64_STANDARD}; 18 | use futures_util::future::LocalBoxFuture; 19 | 20 | use crate::{errors::AppError, settings::SETTINGS}; 21 | 22 | fn parse_header(req: &ServiceRequest) -> Result<(String, String)> { 23 | let header = req 24 | .headers() 25 | .get(AUTHORIZATION) 26 | .ok_or(anyhow!("failed get header"))?; 27 | 28 | // "Basic *" length 29 | if header.len() < 7 { 30 | return Err(anyhow!("failed parse header")); 31 | } 32 | 33 | let mut parts = header.to_str()?.splitn(2, ' '); 34 | let is_basic = parts.next().is_some_and(|scheme| scheme == "Basic"); 35 | 36 | if !is_basic { 37 | return Err(anyhow!("failed parse header")); 38 | } 39 | 40 | let decoded = 41 | BASE64_STANDARD.decode(parts.next().ok_or(anyhow!("failed get header content"))?)?; 42 | 43 | let credentials = String::from_utf8(decoded)?; 44 | let mut credentials = credentials.splitn(2, ':'); 45 | 46 | let user_id = credentials 47 | .next() 48 | .ok_or(anyhow!("failed get user_id")) 49 | .map(|user_id| user_id.to_string().into())?; 50 | 51 | let password = credentials 52 | .next() 53 | .ok_or(anyhow!("failed get password")) 54 | .map(|password| password.to_string().into())?; 55 | 56 | Ok((user_id, password)) 57 | } 58 | 59 | // There are two steps in middleware processing. 60 | // 1. Middleware initialization, middleware factory gets called with 61 | // next service in chain as parameter. 62 | // 2. Middleware's call method gets called with normal request. 63 | pub struct BasicAuth; 64 | 65 | // Middleware factory is `Transform` trait 66 | // `S` - type of the next service 67 | // `B` - type of response's body 68 | impl Transform for BasicAuth 69 | where 70 | S: Service, Error = Error> + 'static, 71 | S::Future: 'static, 72 | B: 'static, 73 | { 74 | type Response = ServiceResponse; 75 | type Error = Error; 76 | type InitError = (); 77 | type Transform = BasicAuthMiddleware; 78 | type Future = Ready>; 79 | 80 | fn new_transform(&self, service: S) -> Self::Future { 81 | ready(Ok(BasicAuthMiddleware { 82 | service: Rc::new(service), 83 | })) 84 | } 85 | } 86 | 87 | pub struct BasicAuthMiddleware { 88 | service: Rc, 89 | } 90 | 91 | impl Service for BasicAuthMiddleware 92 | where 93 | S: Service, Error = Error> + 'static, 94 | S::Future: 'static, 95 | B: 'static, 96 | { 97 | type Response = ServiceResponse; 98 | type Error = Error; 99 | type Future = LocalBoxFuture<'static, Result>; 100 | 101 | forward_ready!(service); 102 | 103 | fn call(&self, req: ServiceRequest) -> Self::Future { 104 | let service = Rc::clone(&self.service); 105 | 106 | Box::pin(async move { 107 | let auth = { 108 | // 确保锁被释放掉, 否则 RwLock 会死锁 109 | let auth = &SETTINGS.read().map_err(|err| AppError::from(err))?.auth; 110 | 111 | auth.clone() 112 | }; 113 | if let Some(auth) = auth { 114 | let (user_id, password) = parse_header(&req).map_err(|_| BasicAuthError)?; 115 | if auth.username == user_id && auth.password == password { 116 | let res = service.call(req).await?; 117 | Ok(res) 118 | } else { 119 | Err(BasicAuthError.into()) 120 | } 121 | } else { 122 | let res = service.call(req).await?; 123 | Ok(res) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | #[derive(Debug)] 130 | pub struct BasicAuthError; 131 | 132 | impl fmt::Display for BasicAuthError { 133 | // This trait requires `fmt` with this exact signature. 134 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 135 | write!(f, "BasicAuthError{:?}", self) 136 | } 137 | } 138 | 139 | impl ResponseError for BasicAuthError { 140 | fn status_code(&self) -> StatusCode { 141 | StatusCode::UNAUTHORIZED 142 | } 143 | 144 | fn error_response(&self) -> HttpResponse { 145 | HttpResponse::build(self.status_code()) 146 | .insert_header((WWW_AUTHENTICATE, "Basic")) 147 | .finish() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use crate::args::ARGS; 2 | 3 | use std::{env, fs, path::PathBuf, sync::RwLock}; 4 | 5 | use anyhow::{anyhow, Result}; 6 | use config::{Config, File}; 7 | use lazy_static::lazy_static; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_yaml; 10 | 11 | lazy_static! { 12 | pub static ref CONFIG_FILE: PathBuf = env::current_dir() 13 | .map(|current_dir| current_dir.join(&ARGS.config).to_path_buf()) 14 | .expect("DATA_DIR parse failed"); 15 | pub static ref SETTINGS: RwLock = 16 | RwLock::new(Settings::init().expect("Settings init failed")); 17 | } 18 | 19 | #[derive(Debug, Clone, Deserialize, Serialize)] 20 | pub struct Auth { 21 | pub username: String, 22 | pub password: String, 23 | } 24 | 25 | #[derive(Debug, Clone, Deserialize, Serialize)] 26 | pub struct Device { 27 | pub name: String, 28 | pub mac: String, 29 | pub ip: String, 30 | pub port: Option, 31 | pub netmask: String, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize)] 35 | pub struct Settings { 36 | pub auth: Option, 37 | pub devices: Vec, 38 | } 39 | 40 | impl Settings { 41 | pub fn init() -> Result { 42 | let config = Config::builder() 43 | .add_source(File::with_name(&CONFIG_FILE.display().to_string()).required(false)) 44 | .build()?; 45 | 46 | let devices = match config.get::>("devices") { 47 | Ok(devices) => devices, 48 | Err(err) => { 49 | log::error!("Failed get devices from config: {}", err); 50 | 51 | if CONFIG_FILE.exists() { 52 | let filename = CONFIG_FILE 53 | .file_name() 54 | .and_then(|name| name.to_str()) 55 | .ok_or(anyhow!("Failed get config file name"))?; 56 | 57 | let target = CONFIG_FILE 58 | .parent() 59 | .ok_or(anyhow!("Failed get config file parent dirname"))? 60 | .join(format!("{}.backup", filename)); 61 | 62 | log::info!("Backup config file {} to {}", CONFIG_FILE.display(), target.display()); 63 | 64 | fs::copy(CONFIG_FILE.as_path(), target)?; 65 | } 66 | 67 | Vec::new() 68 | } 69 | }; 70 | 71 | let auth = config.get::("auth").ok(); 72 | 73 | let settings = Settings { auth, devices }; 74 | 75 | log::debug!("Init settings: {:?}", settings); 76 | settings.save()?; 77 | Ok(settings) 78 | } 79 | 80 | pub fn save(self: &Settings) -> Result<()> { 81 | let yaml = serde_yaml::to_string(&self)?; 82 | fs::write(&CONFIG_FILE.to_path_buf(), yaml)?; 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/wol.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{net::UdpSocket, result::Result as StdResult}; 4 | 5 | /** 6 | * mac address eg: FF-FF-FF-FF-FF-FF 7 | * @wiki https://en.wikipedia.org/wiki/Wake-on-LAN 8 | * @docs http://support.amd.com/TechDocs/20213.pdf 9 | */ 10 | fn parse_mac(mac: T) -> Result<[u8; 6]> { 11 | let mac = mac.to_string(); 12 | let mac_vec: Vec<&str> = mac.split(":").collect(); 13 | 14 | if mac_vec.len() != 6 { 15 | return Err(anyhow!("mac address is invalid")); 16 | } 17 | 18 | let mut mac_bytes = [0_u8; 6]; 19 | for i in 0..mac_bytes.len() { 20 | mac_bytes[i] = u8::from_str_radix(mac_vec[i], 16)?; 21 | } 22 | 23 | Ok(mac_bytes) 24 | } 25 | 26 | /** 27 | * WOL magic packet 构成: 28 | * 前6个字节为 0xff,然后是目标计算机的 MAC 地址的16次重复,总共 102 个字节。 29 | */ 30 | fn create_magic_packet(mac: [u8; 6]) -> [u8; 102] { 31 | // 前6个字节为 0xff 32 | let mut magic_packet = [0xff_u8; 102]; 33 | 34 | // MAC 地址重复 16 次 35 | magic_packet[6..].copy_from_slice(&mac.repeat(16)); 36 | 37 | magic_packet 38 | } 39 | 40 | /** 41 | * IP 转换为 u32 数值 42 | */ 43 | fn ip_to_u32(ip: &str) -> Result { 44 | let parts = ip 45 | .split('.') 46 | .map(|src| u32::from_str_radix(src, 10)) 47 | .collect::, _>>()?; 48 | 49 | if parts.len() != 4 { 50 | return Err(anyhow!("ip address is invalid")); 51 | } 52 | 53 | Ok((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) 54 | } 55 | 56 | /** 57 | * 根据 IP 与子网掩码计算广播地址 58 | */ 59 | fn calculate_broadcast(ip: &str, netmask: &str) -> Result { 60 | // 将 IP 地址和子网掩码转换为 u32 61 | let ip_u32 = ip_to_u32(ip)?; 62 | let netmask_u32 = ip_to_u32(netmask)?; 63 | 64 | // 计算广播地址 65 | let broadcast_u32 = ip_u32 | !netmask_u32; 66 | 67 | // 将 u32 转换回点分十进制字符串 68 | let broadcast_ip = format!( 69 | "{}.{}.{}.{}", 70 | (broadcast_u32 >> 24) & 0xFF, 71 | (broadcast_u32 >> 16) & 0xFF, 72 | (broadcast_u32 >> 8) & 0xFF, 73 | broadcast_u32 & 0xFF 74 | ); 75 | 76 | Ok(broadcast_ip) 77 | } 78 | 79 | #[derive(Debug, Serialize, Deserialize)] 80 | pub struct WakeData { 81 | ip: String, 82 | port: Option, 83 | mac: String, 84 | netmask: String, 85 | } 86 | 87 | pub fn wake(data: &WakeData) -> Result<()> { 88 | let mac = parse_mac(&data.mac)?; 89 | let magic_packet = create_magic_packet(mac); 90 | 91 | log::info!("wake magic packet {:?}", magic_packet); 92 | 93 | let broadcast = calculate_broadcast(&data.ip, &data.netmask)?; 94 | log::info!("wake broadcast address {}", broadcast); 95 | 96 | let socket = UdpSocket::bind(("0.0.0.0", 0))?; 97 | socket.set_broadcast(true)?; 98 | socket.send_to(&magic_packet, (broadcast.as_str(), data.port.unwrap_or(9)))?; 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # wol-web 2 | 3 | wol-web 是 Wol 的前端项目,基于 React、Antd、Rspack 开发。 4 | 5 | ## 开发 6 | 7 | ### 安装依赖 8 | 9 | 在当前目录下执行以下命令安装依赖: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | ### 开发 16 | 17 | 在当前目录下执行以下命令启动开发模式: 18 | 19 | ```sh 20 | yarn dev 21 | ``` 22 | 23 | ### 打包编译 24 | 25 | 在当前目录下执行以下命令进行打包编译: 26 | 27 | ```sh 28 | yarn build 29 | ``` 30 | -------------------------------------------------------------------------------- /web/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; 2 | import js from "@eslint/js"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactJsx from "eslint-plugin-react/configs/jsx-runtime.js"; 5 | import react from "eslint-plugin-react/configs/recommended.js"; 6 | import globals from "globals"; 7 | import ts from "typescript-eslint"; 8 | 9 | export default [ 10 | { languageOptions: { globals: globals.browser } }, 11 | js.configs.recommended, 12 | ...ts.configs.recommended, 13 | ...fixupConfigRules([ 14 | { 15 | ...react, 16 | settings: { 17 | react: { version: "detect" }, 18 | }, 19 | }, 20 | reactJsx, 21 | ]), 22 | { 23 | plugins: { 24 | "react-hooks": fixupPluginRules(reactHooks), 25 | }, 26 | rules: { 27 | ...reactHooks.configs.recommended.rules, 28 | }, 29 | }, 30 | { ignores: ["dist/"] }, 31 | ]; 32 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Wol 10 | 11 | 12 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wol-web", 3 | "private": true, 4 | "version": "0.0.3", 5 | "scripts": { 6 | "dev": "rsbuild dev", 7 | "build": "npm run lint && tsc && rsbuild build", 8 | "preview": "rsbuild preview", 9 | "lint": "eslint . --fix" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.4.0", 13 | "antd": "^5.20.5", 14 | "axios": "^1.7.7", 15 | "classnames": "^2.5.1", 16 | "lodash-es": "^4.17.21", 17 | "nanoid": "^5.0.7", 18 | "nprogress": "^0.2.0", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "swr": "^2.2.5" 22 | }, 23 | "devDependencies": { 24 | "@eslint/compat": "^1.1.1", 25 | "@eslint/js": "^9.9.1", 26 | "@rsbuild/core": "^1.0.1-rc.5", 27 | "@rsbuild/plugin-less": "^1.0.1-rc.5", 28 | "@rsbuild/plugin-react": "^1.0.1-rc.5", 29 | "@types/lodash-es": "^4.17.12", 30 | "@types/nprogress": "^0.2.3", 31 | "@types/react": "^18.3.5", 32 | "@types/react-dom": "^18.3.0", 33 | "eslint": "^9.9.1", 34 | "eslint-plugin-react": "^7.35.2", 35 | "eslint-plugin-react-hooks": "^4.6.2", 36 | "globals": "^15.9.0", 37 | "typescript": "^5.5.4", 38 | "typescript-eslint": "^8.4.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/web/public/logo.png -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wol", 3 | "short_name": "Wol", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#001529", 7 | "description": "Wol 是 wake on lan 的简写,是一个轻量、简洁的 Wol 管理服务,支持检测设备是否开机成功。", 8 | "icons": [ 9 | { 10 | "src": "logo.png", 11 | "sizes": "any" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /web/rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rsbuild/core"; 2 | import { pluginReact } from "@rsbuild/plugin-react"; 3 | import { pluginLess } from "@rsbuild/plugin-less"; 4 | 5 | export default defineConfig({ 6 | plugins: [pluginReact(), pluginLess()], 7 | html: { 8 | template: "./index.html", 9 | }, 10 | server: { 11 | proxy: { 12 | "/api": "http://127.0.0.1:3300", 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /web/src/Wol.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider, App, theme } from 'antd'; 2 | import zhCN from 'antd/locale/zh_CN'; 3 | import useTheme, { ITheme } from './hooks/useTheme'; 4 | import Root from './components/Root'; 5 | 6 | export default function Wol() { 7 | const [themeValue] = useTheme(); 8 | const algorithm = themeValue === ITheme.Dark ? theme.darkAlgorithm : theme.defaultAlgorithm; 9 | 10 | return ( 11 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /web/src/components/AuthEdit/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { 3 | Form, Input, Modal, Switch, 4 | } from 'antd'; 5 | import { get } from 'lodash-es'; 6 | import useMessage from '@/hooks/useMessage'; 7 | import { useAuth, useSaveAuth } from '@/hooks/useAuth'; 8 | import { Auth } from '@/types/auth'; 9 | 10 | export interface AuthEditModel extends Auth { 11 | enable: boolean; 12 | } 13 | 14 | export interface AuthEditProps { 15 | open: boolean; 16 | onOk: () => unknown; 17 | onCancel: () => unknown; 18 | } 19 | 20 | export default function AuthEdit({ open, onOk, onCancel }: AuthEditProps) { 21 | const [form] = Form.useForm(); 22 | const message = useMessage(); 23 | const enableValue = Form.useWatch('enable', form); 24 | 25 | const { data: auth } = useAuth(); 26 | const { isMutating: saveAuthLoading, trigger: saveAuth } = useSaveAuth({ 27 | onSuccess: () => { 28 | onOk(); 29 | message.success('保存成功'); 30 | }, 31 | onError: (err) => { 32 | message.error(get(err, 'response.data.message', '保存失败')); 33 | }, 34 | }); 35 | 36 | const loading = saveAuthLoading; 37 | 38 | const onFinish = useCallback(() => { 39 | const authModel = form.getFieldsValue(); 40 | saveAuth( 41 | authModel.enable 42 | ? { 43 | username: authModel.username, 44 | password: authModel.password, 45 | } 46 | : null, 47 | ); 48 | }, [form, saveAuth]); 49 | 50 | useEffect(() => { 51 | if (!open) { 52 | return; 53 | } 54 | 55 | form.setFieldsValue({ 56 | enable: !!auth, 57 | username: auth?.username, 58 | password: auth?.password, 59 | }); 60 | 61 | // eslint-disable-next-line react-hooks/exhaustive-deps 62 | }, [open]); 63 | 64 | return ( 65 | 80 |
87 | 94 | 95 | 96 | {enableValue && ( 97 | <> 98 | 116 | 117 | 118 | 129 | 130 | 131 | 132 | )} 133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /web/src/components/DeviceCard/index.module.less: -------------------------------------------------------------------------------- 1 | .device { 2 | padding: 24px; 3 | transition: all 0.3s ease; 4 | display: flex; 5 | align-items: center; 6 | border-radius: 8px; 7 | position: relative; 8 | overflow: hidden; 9 | } 10 | 11 | .switch { 12 | width: 48px; 13 | height: 48px; 14 | line-height: 48px; 15 | text-align: center; 16 | font-size: 48px; 17 | cursor: pointer; 18 | border-radius: 50%; 19 | } 20 | 21 | .info { 22 | flex-grow: 1; 23 | padding-left: 24px; 24 | } 25 | 26 | .name { 27 | line-height: 24px; 28 | padding-bottom: 10px; 29 | font-size: 16px; 30 | font-weight: 600; 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | } 35 | 36 | .mac { 37 | line-height: 18px; 38 | font-size: 14px; 39 | white-space: nowrap; 40 | overflow: hidden; 41 | text-overflow: ellipsis; 42 | } 43 | 44 | @position-right: 24px + 20px; 45 | 46 | .online { 47 | position: absolute; 48 | top: 50%; 49 | right: @position-right; 50 | transform: translate(50%, -50%); 51 | width: 20px; 52 | height: 20px; 53 | border-radius: 50%; 54 | } 55 | 56 | .loading { 57 | position: absolute; 58 | top: 50%; 59 | right: @position-right; 60 | transform: translate(50%, -50%); 61 | font-size: 40px; 62 | } 63 | 64 | .edit { 65 | position: absolute; 66 | top: 10px; 67 | right: 10px; 68 | } 69 | 70 | .editBtn { 71 | width: 24px; 72 | height: 24px; 73 | line-height: 24px; 74 | font-size: 18px; 75 | border-radius: 50%; 76 | cursor: pointer; 77 | text-align: center; 78 | } 79 | -------------------------------------------------------------------------------- /web/src/components/DeviceCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Dropdown, MenuProps, theme } from 'antd'; 3 | import { 4 | DeleteOutlined, 5 | EditOutlined, 6 | LoadingOutlined, 7 | MoreOutlined, 8 | PoweroffOutlined, 9 | } from '@ant-design/icons'; 10 | import useSWR from 'swr'; 11 | import { get } from 'lodash-es'; 12 | import { Device, DeviceStatus } from '@/types/device'; 13 | import useMessage from '@/hooks/useMessage'; 14 | import useModal from '@/hooks/useModal'; 15 | import DeviceEdit from '../DeviceEdit'; 16 | import useBoolean from '@/hooks/useBoolean'; 17 | import { useDeleteDevice, useWakeDevice } from '@/hooks/useDevices'; 18 | import styles from './index.module.less'; 19 | import fetcher from '@/utils/fetcher'; 20 | 21 | export interface DeviceCardProps { 22 | device: Device; 23 | } 24 | 25 | export default function DeviceCard({ device }: DeviceCardProps) { 26 | const { token } = theme.useToken(); 27 | const message = useMessage(); 28 | const modal = useModal(); 29 | const [open, actions] = useBoolean(false); 30 | 31 | const { 32 | data: status, 33 | isLoading, 34 | mutate: fetchDeviceStatus, 35 | } = useSWR( 36 | `/device/status/${device.ip}`, 37 | (url) => fetcher.get(url), 38 | { 39 | refreshInterval: 7000, 40 | }, 41 | ); 42 | 43 | const { isMutating: isWaking, trigger: wakeDevice } = useWakeDevice({ 44 | onSuccess: () => { 45 | fetchDeviceStatus(); 46 | }, 47 | onError: (err) => { 48 | message.error(get(err, 'response.data.message', '开机失败')); 49 | }, 50 | }); 51 | 52 | const { trigger: deleteDevice } = useDeleteDevice({ 53 | onSuccess: () => { 54 | message.success('删除成功'); 55 | }, 56 | onError: (err) => { 57 | message.error(get(err, 'response.data.message', '删除失败')); 58 | }, 59 | }); 60 | 61 | const onWake = useCallback(() => { 62 | if (isWaking) { 63 | return; 64 | } 65 | wakeDevice(device); 66 | }, [isWaking, device, wakeDevice]); 67 | 68 | const items: MenuProps['items'] = [ 69 | { 70 | key: 'edit', 71 | icon: , 72 | label: '编辑', 73 | onClick: actions.setTrue, 74 | }, 75 | { 76 | key: 'delete', 77 | icon: , 78 | label: '删除', 79 | onClick: () => { 80 | modal.confirm({ 81 | title: '删除', 82 | content: `确认删除设备 ${device.name} 吗?`, 83 | onOk: () => deleteDevice(device), 84 | }); 85 | }, 86 | }, 87 | ]; 88 | 89 | return ( 90 | <> 91 |
98 |
105 | 106 |
107 |
108 |
{device.name}
109 |
{device.mac}
110 |
111 | {status === DeviceStatus.Online && ( 112 |
118 | )} 119 | {(isLoading || isWaking) && ( 120 |
126 | 127 |
128 | )} 129 |
130 | 136 |
137 | 138 |
139 |
140 |
141 |
142 | 143 | 149 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /web/src/components/DeviceEdit/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | import { Form, Input, Modal, InputNumber } from "antd"; 3 | import { get } from "lodash-es"; 4 | import { Device } from "@/types/device"; 5 | import useMessage from "@/hooks/useMessage"; 6 | import { useAddDevice, useUpdateDevice } from "@/hooks/useDevices"; 7 | 8 | export type DeviceEditModel = Omit; 9 | 10 | export interface DeviceEditProps { 11 | device?: Device | null; 12 | open: boolean; 13 | onOk: () => unknown; 14 | onCancel: () => unknown; 15 | } 16 | 17 | export default function DeviceEdit({ 18 | device, 19 | open, 20 | onOk, 21 | onCancel, 22 | }: DeviceEditProps) { 23 | const [form] = Form.useForm(); 24 | const message = useMessage(); 25 | const { isMutating: addDeviceLoading, trigger: addDevice } = useAddDevice({ 26 | onSuccess: () => { 27 | onOk(); 28 | message.success("添加成功"); 29 | }, 30 | onError: (err) => { 31 | message.error(get(err, "response.data.message", "添加失败")); 32 | }, 33 | }); 34 | const { isMutating: updateDeviceLoading, trigger: updateDevice } = 35 | useUpdateDevice({ 36 | onSuccess: () => { 37 | onOk(); 38 | message.success("保存成功"); 39 | }, 40 | onError: (err) => { 41 | message.error(get(err, "response.data.message", "保存失败")); 42 | }, 43 | }); 44 | 45 | const loading = addDeviceLoading || updateDeviceLoading; 46 | 47 | const onFinish = useCallback(() => { 48 | const deviceModel = form.getFieldsValue(); 49 | if (device) { 50 | updateDevice({ 51 | ...deviceModel, 52 | uid: device.uid, 53 | }); 54 | } else { 55 | addDevice(deviceModel); 56 | } 57 | }, [form, device, updateDevice, addDevice]); 58 | 59 | useEffect(() => { 60 | if (!open) { 61 | return; 62 | } 63 | 64 | if (device) { 65 | form.setFieldsValue({ 66 | name: device.name, 67 | mac: device.mac, 68 | ip: device.ip, 69 | netmask: device.netmask, 70 | port: device.port, 71 | }); 72 | } else { 73 | form.resetFields(); 74 | } 75 | // eslint-disable-next-line react-hooks/exhaustive-deps 76 | }, [open]); 77 | 78 | return ( 79 | 94 |
102 | 120 | 121 | 122 | 140 | 141 | 142 | 160 | 161 | 162 | 176 | 177 | 178 | 198 | 199 | 200 |
201 |
202 | ); 203 | } 204 | -------------------------------------------------------------------------------- /web/src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from "react"; 2 | import ErrorElement from "../ErrorElement"; 3 | 4 | export interface ErrorBoundaryProps { 5 | children: ReactNode; 6 | } 7 | 8 | export interface ErrorBoundaryState { 9 | error: Error | null; 10 | } 11 | 12 | export default class ErrorBoundary extends Component< 13 | ErrorBoundaryProps, 14 | ErrorBoundaryState 15 | > { 16 | constructor(props: ErrorBoundaryProps) { 17 | super(props); 18 | this.state = { error: null }; 19 | } 20 | 21 | static getDerivedStateFromError(error: Error) { 22 | // Update state so the next render will show the fallback UI. 23 | return { error }; 24 | } 25 | 26 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 27 | // You can also log the error to an error reporting service 28 | console.log(error, errorInfo); 29 | } 30 | 31 | render() { 32 | const { error } = this.state; 33 | // eslint-disable-next-line react/prop-types 34 | const { children } = this.props; 35 | if (error) { 36 | // You can render any custom fallback UI 37 | return ; 38 | } 39 | 40 | return children; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web/src/components/ErrorElement/index.module.less: -------------------------------------------------------------------------------- 1 | .error { 2 | height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | text-align: center; 7 | color: #333; 8 | } 9 | -------------------------------------------------------------------------------- /web/src/components/ErrorElement/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result, theme } from 'antd'; 2 | import styles from './index.module.less'; 3 | 4 | export interface ErrorElementProps { 5 | message?: string; 6 | } 7 | 8 | export default function ErrorElement({ message }: ErrorElementProps) { 9 | const { token } = theme.useToken(); 10 | 11 | return ( 12 |
19 | window.location.reload()}> 25 | 重新加载 26 | 27 | )} 28 | /> 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /web/src/components/Header/index.module.less: -------------------------------------------------------------------------------- 1 | .header { 2 | @media (max-width: 575px) { 3 | padding-inline: 20px; 4 | } 5 | } 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | max-width: 1200px; 12 | margin: 0 auto; 13 | } 14 | 15 | .logo { 16 | font-size: 28px; 17 | font-weight: 600; 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .button { 26 | margin-left: 16px; 27 | font-size: 20px; 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /web/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Layout, Switch, theme } from "antd"; 3 | import { 4 | SunOutlined, 5 | MoonOutlined, 6 | PlusCircleOutlined, 7 | LockOutlined, 8 | GithubOutlined, 9 | } from "@ant-design/icons"; 10 | import useTheme, { ITheme } from "@/hooks/useTheme"; 11 | import AuthEdit from "../AuthEdit"; 12 | import DeviceEdit from "../DeviceEdit"; 13 | import useBoolean from "@/hooks/useBoolean"; 14 | import styles from "./index.module.less"; 15 | 16 | export default function Header() { 17 | const [themeValue, setThemeValue] = useTheme(); 18 | const [deviceEditOpen, deviceEditActions] = useBoolean(false); 19 | const [authEditOpen, authEditActions] = useBoolean(false); 20 | const { token } = theme.useToken(); 21 | const style = { 22 | color: token.colorTextLightSolid, 23 | }; 24 | 25 | const onThemeValueChange = useCallback( 26 | (checked: boolean) => { 27 | if (checked) { 28 | setThemeValue(ITheme.Dark); 29 | } else { 30 | setThemeValue(ITheme.Light); 31 | } 32 | }, 33 | [setThemeValue] 34 | ); 35 | 36 | return ( 37 | <> 38 | 39 |
40 |
Wol
41 |
42 | } 45 | unCheckedChildren={} 46 | onChange={onThemeValueChange} 47 | /> 48 |
54 | 55 |
56 |
62 | 63 |
64 |
69 | window.open("https://github.com/nashaofu/wol", "_blank") 70 | } 71 | > 72 | 73 |
74 |
75 |
76 |
77 | 78 | 83 | 84 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /web/src/components/Home/index.module.less: -------------------------------------------------------------------------------- 1 | .home { 2 | max-width: 1300px; 3 | padding: 48px 50px; 4 | margin: 0 auto; 5 | @media (max-width: 575px) { 6 | padding: 30px 20px; 7 | } 8 | } 9 | 10 | .item { 11 | transition: all 0.3s ease; 12 | &:hover { 13 | transform: translateY(-6px); 14 | box-shadow: 0 26px 40px -24px rgba(0, 0, 0, 0.8); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, Col, Empty, Row, Spin, 3 | } from 'antd'; 4 | import { useDevices } from '@/hooks/useDevices'; 5 | import DeviceCard from '@/components/DeviceCard'; 6 | import DeviceEdit from '../DeviceEdit'; 7 | import useBoolean from '@/hooks/useBoolean'; 8 | import styles from './index.module.less'; 9 | 10 | export default function Home() { 11 | const { data: devices = [], isLoading } = useDevices(); 12 | const [open, actions] = useBoolean(false); 13 | 14 | return ( 15 | <> 16 | 17 |
18 | {!devices.length && ( 19 | 20 | 23 | 24 | )} 25 | 26 | {devices.map((item) => ( 27 | 28 |
29 | 30 |
31 | 32 | ))} 33 |
34 |
35 |
36 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /web/src/components/NProgress/index.less: -------------------------------------------------------------------------------- 1 | #nprogress .bar { 2 | height: 4px; 3 | } 4 | -------------------------------------------------------------------------------- /web/src/components/NProgress/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { start, done } from 'nprogress'; 3 | import 'nprogress/nprogress.css'; 4 | import './index.less'; 5 | 6 | export default function NProgress() { 7 | useEffect(() => { 8 | start(); 9 | return () => { 10 | done(); 11 | }; 12 | }, []); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /web/src/components/Root/index.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | min-height: 100vh; 3 | } 4 | -------------------------------------------------------------------------------- /web/src/components/Root/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Layout } from 'antd'; 3 | import NProgress from '@/components/NProgress'; 4 | import Header from '@/components/Header'; 5 | import Home from '@/components/Home'; 6 | import styles from './index.module.less'; 7 | 8 | export default function Root() { 9 | return ( 10 | }> 11 | 12 |
13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /web/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'; 2 | import useSWRMutation, { SWRMutationConfiguration } from 'swr/mutation'; 3 | import fetcher from '@/utils/fetcher'; 4 | import { Auth } from '@/types/auth'; 5 | 6 | type UseSaveAuthConfig = SWRMutationConfiguration< 7 | Auth, 8 | Error, 9 | '/auth/save', 10 | Auth | null 11 | >; 12 | 13 | export function useAuth(config?: SWRConfiguration) { 14 | return useSWR( 15 | '/auth/info', 16 | (url) => fetcher.get(url), 17 | { 18 | revalidateOnFocus: false, 19 | ...config, 20 | }, 21 | ); 22 | } 23 | 24 | export function useSaveAuth(config?: UseSaveAuthConfig) { 25 | const { mutate } = useSWRConfig(); 26 | 27 | return useSWRMutation( 28 | '/auth/save', 29 | async (url, { arg }: { arg: Auth | null }) => { 30 | const resp = await fetcher.post(url, arg); 31 | return resp; 32 | }, 33 | { 34 | ...config, 35 | onSuccess: (resp, ...args) => { 36 | config?.onSuccess?.(resp, ...args); 37 | // 清理所有本地数据 38 | mutate(() => true, undefined, { revalidate: true }); 39 | }, 40 | }, 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /web/src/hooks/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | 3 | type UseBooleanOpts = boolean | (() => boolean); 4 | interface UseBooleanAction { 5 | setTrue: () => void; 6 | setFalse: () => void; 7 | } 8 | 9 | export default function useBoolean( 10 | defaultValue: UseBooleanOpts = false, 11 | ): [boolean, UseBooleanAction] { 12 | const [state, setState] = useState(defaultValue); 13 | 14 | const actions = useMemo( 15 | () => ({ 16 | setTrue: () => setState(true), 17 | setFalse: () => setState(false), 18 | }), 19 | [], 20 | ); 21 | 22 | return [state, actions]; 23 | } 24 | -------------------------------------------------------------------------------- /web/src/hooks/useDevices.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { SWRConfiguration } from 'swr'; 2 | import useSWRMutation, { SWRMutationConfiguration } from 'swr/mutation'; 3 | import { nanoid } from 'nanoid'; 4 | import fetcher from '@/utils/fetcher'; 5 | import { Device } from '@/types/device'; 6 | 7 | type UseSaveDevicesConfig = SWRMutationConfiguration< 8 | Device[], 9 | Error, 10 | '/device/save', 11 | Device[] 12 | >; 13 | 14 | export function useDevices(config?: SWRConfiguration) { 15 | return useSWR( 16 | '/device/all', 17 | async (url) => { 18 | const resp = await fetcher.get[]>(url); 19 | const devices = resp.map((item) => ({ 20 | ...item, 21 | uid: nanoid(), 22 | })); 23 | 24 | return devices; 25 | }, 26 | { 27 | revalidateOnFocus: false, 28 | ...config, 29 | }, 30 | ); 31 | } 32 | 33 | export function useSaveDevices(config?: UseSaveDevicesConfig) { 34 | const { mutate } = useDevices(); 35 | return useSWRMutation( 36 | '/device/save', 37 | async (url, { arg }: { arg: Device[] }) => { 38 | const resp = await fetcher.post[]>( 39 | url, 40 | arg.map((item) => ({ 41 | ...item, 42 | uid: undefined, 43 | })), 44 | ); 45 | 46 | const devices: Device[] = resp.map((item) => ({ 47 | ...item, 48 | uid: nanoid(), 49 | })); 50 | 51 | await mutate(devices); 52 | 53 | return devices; 54 | }, 55 | config, 56 | ); 57 | } 58 | 59 | export function useAddDevice(config?: UseSaveDevicesConfig) { 60 | const { data: devices = [] } = useDevices(); 61 | const saveDevices = useSaveDevices(config); 62 | 63 | return { 64 | ...saveDevices, 65 | trigger: (device: Omit) => { 66 | devices.push({ 67 | ...device, 68 | uid: nanoid(), 69 | }); 70 | 71 | return saveDevices.trigger(devices); 72 | }, 73 | }; 74 | } 75 | 76 | export function useUpdateDevice(config?: UseSaveDevicesConfig) { 77 | const { data: devices = [] } = useDevices(); 78 | const saveDevices = useSaveDevices(config); 79 | 80 | return { 81 | ...saveDevices, 82 | trigger: (device: Device) => { 83 | const index = devices.findIndex((item) => item.uid === device.uid); 84 | if (index !== -1) { 85 | devices[index] = device; 86 | } 87 | 88 | return saveDevices.trigger(devices); 89 | }, 90 | }; 91 | } 92 | 93 | export function useDeleteDevice(config?: UseSaveDevicesConfig) { 94 | const { data: devices = [] } = useDevices(); 95 | const saveDevices = useSaveDevices(config); 96 | 97 | return { 98 | ...saveDevices, 99 | trigger: (device: Device) => saveDevices.trigger(devices.filter((item) => item.uid !== device.uid)), 100 | }; 101 | } 102 | 103 | export function useWakeDevice( 104 | config?: SWRMutationConfiguration, 105 | ) { 106 | return useSWRMutation( 107 | '/device/wake', 108 | async (url, { arg }: { arg: Device }) => { 109 | await fetcher.post(url, arg); 110 | // 延迟 10s, 等待机器开机 111 | await new Promise((resolve) => { 112 | setTimeout(() => resolve(), 10000); 113 | }); 114 | }, 115 | config, 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /web/src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useSyncExternalStore } from 'react'; 2 | 3 | function getLocalStorage(key: string): T | undefined { 4 | try { 5 | const raw = localStorage.getItem(key); 6 | if (raw) { 7 | return JSON.parse(raw) as T; 8 | } 9 | } catch (err) { 10 | 11 | console.error(err); 12 | } 13 | 14 | return undefined; 15 | } 16 | 17 | const listeners = new Map void>>(); 18 | 19 | export default function useLocalStorage( 20 | key: string, 21 | ): [T | undefined, (val: T | undefined) => void] { 22 | const localStorageKey = `Wol.${key}`; 23 | 24 | const state = useSyncExternalStore( 25 | (onStoreChange) => { 26 | const keyListeners = listeners.get(key) ?? []; 27 | keyListeners.push(onStoreChange); 28 | listeners.set(key, keyListeners); 29 | return () => { 30 | const newKeyListeners = listeners.get(key) ?? []; 31 | listeners.set( 32 | key, 33 | newKeyListeners.filter((item) => item !== onStoreChange), 34 | ); 35 | }; 36 | }, 37 | () => getLocalStorage(localStorageKey), 38 | ); 39 | 40 | const setLocalStorage = useCallback( 41 | (value: T | undefined) => { 42 | if (value === undefined) { 43 | localStorage.removeItem(localStorageKey); 44 | } else { 45 | try { 46 | localStorage.setItem(localStorageKey, JSON.stringify(value)); 47 | } catch (err) { 48 | 49 | console.error(err); 50 | } 51 | } 52 | listeners.get(key)?.forEach((item) => item()); 53 | }, 54 | [localStorageKey, key], 55 | ); 56 | 57 | return [state, setLocalStorage]; 58 | } 59 | -------------------------------------------------------------------------------- /web/src/hooks/useMessage.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | 3 | export default function useMessage() { 4 | const { message } = App.useApp(); 5 | return message; 6 | } 7 | -------------------------------------------------------------------------------- /web/src/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | 3 | export default function useModal() { 4 | const { modal } = App.useApp(); 5 | return modal; 6 | } 7 | -------------------------------------------------------------------------------- /web/src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorage from './useLocalStorage'; 2 | 3 | export enum ITheme { 4 | Light = 'Light', 5 | Dark = 'Dark', 6 | } 7 | 8 | export default function useTheme() { 9 | return useLocalStorage('theme'); 10 | } 11 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { SWRConfig, Cache } from 'swr'; 4 | import './styles/index.less'; 5 | 6 | import ErrorBoundary from './components/ErrorBoundary'; 7 | import Wol from './Wol'; 8 | 9 | function localStorageProvider() { 10 | const key = 'Wol.app-cache'; 11 | // 初始化时,我们将数据从 `localStorage` 恢复到一个 map 中。 12 | const map = new Map(JSON.parse(localStorage.getItem(key) || '[]')); 13 | 14 | // 在卸载 app 之前,我们将所有数据写回 `localStorage` 中。 15 | window.addEventListener('beforeunload', () => { 16 | const appCache = JSON.stringify(Array.from(map.entries())); 17 | localStorage.setItem(key, appCache); 18 | }); 19 | 20 | // 我们仍然使用 map 进行读写以提高性能。 21 | return map as Cache; 22 | } 23 | 24 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | ); 33 | -------------------------------------------------------------------------------- /web/src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "antd/dist/reset.css"; 2 | 3 | ::-webkit-scrollbar { 4 | width: 8px; 5 | height: 8px; 6 | background-color: rgba(0, 0, 0, 0.1); 7 | } 8 | 9 | ::-webkit-scrollbar-track { 10 | background-color: #f5f5f5; 11 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.22); 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | background-color: rgba(0, 0, 0, 0.22); 16 | border-radius: 10px; 17 | } 18 | 19 | html { 20 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 21 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 22 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 23 | } 24 | -------------------------------------------------------------------------------- /web/src/types/auth.ts: -------------------------------------------------------------------------------- 1 | export interface Auth { 2 | username: string; 3 | password?: string; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/types/device.ts: -------------------------------------------------------------------------------- 1 | export interface Device { 2 | uid: string; 3 | name: string; 4 | mac: string; 5 | ip: string; 6 | netmask: string; 7 | port: number; 8 | } 9 | 10 | export enum DeviceStatus { 11 | Online = "Online", 12 | Offline = "Offline", 13 | } 14 | -------------------------------------------------------------------------------- /web/src/utils/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const fetcher = axios.create({ 4 | baseURL: '/api', 5 | timeout: 20000, 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | }); 10 | 11 | fetcher.interceptors.response.use( 12 | (data) => data.data, 13 | (err) => Promise.reject(err), 14 | ); 15 | 16 | export default fetcher; 17 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "ES2020"], 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "noEmit": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "isolatedModules": true, 11 | "resolveJsonModule": true, 12 | "moduleResolution": "bundler", 13 | "useDefineForClassFields": true, 14 | "allowImportingTsExtensions": true, 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /wol.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "web" 8 | } 9 | ], 10 | "settings": { 11 | "cSpell.words": [ 12 | "actix", 13 | "antd" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /wol.example.yaml: -------------------------------------------------------------------------------- 1 | user: null 2 | devices: 3 | - name: Windows 4 | mac: 00:00:00:00:00:00 5 | ip: 192.168.1.1 6 | netmask: 255.255.255.0 7 | port: 9 8 | -------------------------------------------------------------------------------- /www/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nashaofu/wol/d7c9b0908275167ce8bed74493e741ad2d289875/www/.gitkeep --------------------------------------------------------------------------------