commit 2e4f0e947b70e0a22bb73e36543bd42a022c498e Author: CNWei Date: Fri Oct 16 13:46:20 2026 +0800 init: 本地静态页面托管服务 - 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8cabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4db409c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1009 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stash-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "colored", + "stash-core", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "stash-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "mime_guess", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower 0.4.13", + "tower-http", + "tracing", + "walkdir", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cf837a0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +resolver = "2" +members = [ + "stash-core", + "stash-cli", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[workspace.dependencies] +tokio = { version = "1.38", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +thiserror = "1.0" +anyhow = "1.0" \ No newline at end of file diff --git a/stash-cli/Cargo.toml b/stash-cli/Cargo.toml new file mode 100644 index 0000000..a68f627 --- /dev/null +++ b/stash-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stash-cli" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[[bin]] +name = "stash" +path = "src/main.rs" + +[dependencies] +stash-core = { path = "../stash-core",version = "0.1.0" } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +clap = { version = "4.6.7", features = ["derive"] } +anyhow = { workspace = true } +colored = "3.1.1" \ No newline at end of file diff --git a/stash-cli/src/main.rs b/stash-cli/src/main.rs new file mode 100644 index 0000000..3fe2541 --- /dev/null +++ b/stash-cli/src/main.rs @@ -0,0 +1,138 @@ +use std::path::PathBuf; +use clap::Parser; +use colored::*; +use stash_core::{Config, start}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Parser, Debug)] +#[command( + name = "stash", + author = "stash developers", + version, + about = "🚀 本地静态页面托管与多项目管理工具", + long_about = "支持本地静态页面按目录隔离托管,支持前端直接调用同源接口,提供清爽的项目导航列表。" +)] +struct Cli { + /// 静态页面托管根目录(支持相对路径和绝对路径) + #[arg(short, long, default_value = "./public", value_name = "DIR")] + root: PathBuf, + + /// 监听端口 + #[arg(short, long, default_value_t = 8080, value_name = "PORT")] + port: u16, + + /// 绑定监听地址(默认 127.0.0.1,如需局域网访问可传 0.0.0.0) + #[arg(long, default_value = "127.0.0.1", value_name = "HOST")] + host: String, + + /// 关闭根目录项目导航列表功能(仅托管静态文件) + #[arg(long, default_value_t = false)] + no_listing: bool, + + /// 关闭 CORS 跨域标头注入 + #[arg(long, default_value_t = false)] + no_cors: bool, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // 1. 初始化 tracing 日志 + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // 2. 解析命令行输入 + let cli = Cli::parse(); + + // 检查目录是否存在,不存在则友好提示并自动创建默认 public + if !cli.root.exists() { + if cli.root == PathBuf::from("./public") { + println!( + "{} 默认托管目录 {} 不存在,正在自动创建...", + "[INFO]".cyan(), + "./public".yellow() + ); + std::fs::create_dir_all(&cli.root)?; + } else { + eprintln!( + "{} 指定的托管目录不存在: {}", + "[ERROR]".red().bold(), + cli.root.display().to_string().yellow() + ); + std::process::exit(1); + } + } + + let abs_root = cli.root.canonicalize().unwrap_or_else(|_| cli.root.clone()); + + // 3. 构造 stash-core 配置并启动 + let config = Config { + root: abs_root.clone(), + port: cli.port, + host: cli.host, + directory_listing: !cli.no_listing, + enable_cors: !cli.no_cors, + }; + + let handle = match start(config).await { + Ok(h) => h, + Err(err) => { + eprintln!("{} 启动失败: {}", "[FATAL]".red().bold(), err); + std::process::exit(1); + } + }; + + // 4. 打印启动成功 Banner + println!(); + println!("{}", "==================================================".bright_blue()); + println!(" ✨ {} {}", "Stash Server".green().bold(), format!("v{}", env!("CARGO_PKG_VERSION")).dimmed()); + println!("{}", "==================================================".bright_blue()); + println!(" ➜ 本地访问: {}", format!("http://{}", handle.local_addr).cyan().bold().underline()); + println!(" ➜ 托管路径: {}", abs_root.display().to_string().white()); + println!(" ➜ 目录导航页: {}", if !cli.no_listing { "开启".green() } else { "关闭".red() }); + println!(" ➜ 接口列表: {}", format!("http://{}/api/pages", handle.local_addr).dimmed()); + println!("{}", "--------------------------------------------------".bright_blue()); + println!(" 按 {} 停止服务", "Ctrl+C".yellow().bold()); + println!(); + + // 5. 跨平台捕获退出信号 + wait_for_shutdown_signal().await; + + println!("\n{} 正在停止服务并释放端口...", "[SYS]".yellow()); + handle.stop(); + + // 留出时间让 tokio 优雅关闭 socket 句柄 + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + println!("{} 服务已安全退出。\n", "[DONE]".green().bold()); + + Ok(()) +} + +/// 监听停机信号(支持 Windows Ctrl+C 以及 Linux/macOS SIGINT/SIGTERM) +async fn wait_for_shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("无法注册 Ctrl+C 信号处理器"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("无法注册 SIGTERM 信号处理器") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} \ No newline at end of file diff --git a/stash-core/Cargo.toml b/stash-core/Cargo.toml new file mode 100644 index 0000000..cdcb6db --- /dev/null +++ b/stash-core/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "stash-core" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[dependencies] +tokio = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } + +# Web 核心与静态托管 +axum = "0.8.9" +tower-http = { version = "0.7.1", features = ["fs", "cors", "trace"] } +tower = { version = "0.4", features = ["util"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +walkdir = "2.4" +mime_guess = "2.0" \ No newline at end of file diff --git a/stash-core/assets/404.html b/stash-core/assets/404.html new file mode 100644 index 0000000..a5135a8 --- /dev/null +++ b/stash-core/assets/404.html @@ -0,0 +1,86 @@ + + + + + + 404 - 资源未找到 + + + +
+
404
+

资源或页面不存在

+

请求的路径 __PATH__ 不存在或缺少 index.html

+ 返回应用大厅 +
+ + \ No newline at end of file diff --git a/stash-core/assets/index.html b/stash-core/assets/index.html new file mode 100644 index 0000000..719c485 --- /dev/null +++ b/stash-core/assets/index.html @@ -0,0 +1,212 @@ + + + + + + Stash 本地应用控制台 + + + +
+
+
+
+ + + + + + + + +

本地前端应用

+
+
•••
+
+ +
+ __CARDS__ +
+
+
+ + \ No newline at end of file diff --git a/stash-core/src/config.rs b/stash-core/src/config.rs new file mode 100644 index 0000000..4669ea4 --- /dev/null +++ b/stash-core/src/config.rs @@ -0,0 +1,27 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct Config { + /// 静态页面托管根目录(存放一个或多个前端项目) + pub root: PathBuf, + /// 监听端口 + pub port: u16, + /// 监听地址,默认 127.0.0.1 + pub host: String, + /// 是否在根路径没有 index.html 时展示项目仪表盘 + pub directory_listing: bool, + /// 是否注入 CORS 标头 + pub enable_cors: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + root: PathBuf::from("./public"), + port: 8080, + host: "127.0.0.1".to_string(), + directory_listing: true, + enable_cors: true, + } + } +} \ No newline at end of file diff --git a/stash-core/src/error.rs b/stash-core/src/error.rs new file mode 100644 index 0000000..e80faf4 --- /dev/null +++ b/stash-core/src/error.rs @@ -0,0 +1,42 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StashError { + #[error("IO 错误: {0}")] + Io(#[from] std::io::Error), + + #[error("目录不存在或非法: {0}")] + InvalidDirectory(String), + + #[error("网络地址解析失败: {0}")] + AddrParse(#[from] std::net::AddrParseError), + + #[error("端口绑定失败: {0}")] + ServerBind(String), + + #[error("未找到资源: {0}")] + NotFound(String), +} + +impl IntoResponse for StashError { + fn into_response(self) -> Response { + let (status, err_msg) = match self { + StashError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + StashError::InvalidDirectory(msg) => (StatusCode::BAD_REQUEST, msg), + _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + }; + + let body = Json(json!({ + "error": err_msg, + "status": status.as_u16(), + })); + + (status, body).into_response() + } +} \ No newline at end of file diff --git a/stash-core/src/lib.rs b/stash-core/src/lib.rs new file mode 100644 index 0000000..0e43e3d --- /dev/null +++ b/stash-core/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod error; +pub mod listing; +pub mod server; + +pub use config::Config; +pub use error::StashError; +pub use listing::{scan_projects, AppProject}; +pub use server::{start, ServerHandle}; \ No newline at end of file diff --git a/stash-core/src/listing.rs b/stash-core/src/listing.rs new file mode 100644 index 0000000..a630b94 --- /dev/null +++ b/stash-core/src/listing.rs @@ -0,0 +1,75 @@ +use serde::Serialize; +use std::path::Path; + +const INDEX_TEMPLATE: &str = include_str!("../assets/index.html"); +const NOT_FOUND_TEMPLATE: &str = include_str!("../assets/404.html"); + +#[derive(Debug, Serialize, Clone)] +pub struct AppProject { + pub name: String, + pub route_path: String, +} + +pub fn scan_projects(root: &Path) -> Vec { + let mut projects = Vec::new(); + + let Ok(read_dir) = std::fs::read_dir(root) else { + return projects; + }; + + for item in read_dir.flatten() { + let path = item.path(); + let name = item.file_name().to_string_lossy().to_string(); + + if name.starts_with('.') { + continue; + } + + if path.is_dir() { + let has_index = path.join("index.html").exists() || path.join("index.htm").exists(); + if has_index { + let route_path = format!("/{}/", name); + projects.push(AppProject { name, route_path }); + } + } + } + + projects.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + projects +} + +pub fn render_dashboard(projects: &[AppProject]) -> String { + let mut cards_html = String::new(); + + for proj in projects { + // 提取首字符作为圆形头像字母 + let initial = proj.name.chars().next().unwrap_or('A').to_uppercase().to_string(); + + cards_html.push_str(&format!( + r#" + +
{initial}
+
+ {name} + 本地托管应用 • {url} +
+
+ "#, + url = proj.route_path, + name = proj.name, + initial = initial + )); + } + + let final_cards = if cards_html.is_empty() { + r#"
当前目录下暂无含有 index.html 的前端项目
"#.to_string() + } else { + cards_html + }; + + INDEX_TEMPLATE.replace("__CARDS__", &final_cards) +} + +pub fn render_not_found(path: &str) -> String { + NOT_FOUND_TEMPLATE.replace("__PATH__", path) +} \ No newline at end of file diff --git a/stash-core/src/server.rs b/stash-core/src/server.rs new file mode 100644 index 0000000..1b12bdd --- /dev/null +++ b/stash-core/src/server.rs @@ -0,0 +1,135 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use axum::{ + extract::{OriginalUri, State}, + http::{header, StatusCode}, + response::{Html, IntoResponse, Response}, + routing::get, + Json, Router, +}; +use tower_http::{ + cors::{Any, CorsLayer}, + services::ServeDir, + trace::TraceLayer, +}; +use tokio::sync::oneshot; +use tracing::{info, warn}; + +use crate::config::Config; +use crate::error::StashError; +use crate::listing::{render_dashboard, render_not_found, scan_projects}; + +#[derive(Clone)] +pub struct AppState { + pub config: Arc, +} + +pub struct ServerHandle { + shutdown_tx: oneshot::Sender<()>, + pub local_addr: SocketAddr, +} + +impl ServerHandle { + pub fn stop(self) { + let _ = self.shutdown_tx.send(()); + } +} + +/// 根路径处理器 +async fn root_handler(State(state): State) -> Result { + let index_file = state.config.root.join("index.html"); + + // 1. 如果托管目录本身就是一个完整的前端项目(含 index.html),优先直接展示 + if index_file.exists() { + let content = tokio::fs::read_to_string(&index_file).await?; + return Ok(([(header::CONTENT_TYPE, "text/html; charset=utf-8")], Html(content)).into_response()); + } + + // 2. 否则展示筛选后的可用前端应用大厅 + let projects = scan_projects(&state.config.root); + let html = render_dashboard(&projects); + Ok(Html(html).into_response()) +} + +/// 全局 404 自定义页面处理器 +async fn not_found_handler(OriginalUri(uri): OriginalUri) -> impl IntoResponse { + let html = render_not_found(&uri.path()); + (StatusCode::NOT_FOUND, Html(html)) +} + +/// API: 健康检查 +async fn health_check() -> impl IntoResponse { + Json(serde_json::json!({ + "status": "ok", + "service": "stash" + })) +} + +/// API: 项目列表 +async fn list_projects_api(State(state): State) -> impl IntoResponse { + let projects = scan_projects(&state.config.root); + Json(serde_json::json!({ + "projects": projects + })) +} + +pub async fn start(config: Config) -> Result { + if !config.root.exists() { + return Err(StashError::InvalidDirectory( + config.root.display().to_string(), + )); + } + + let shared_state = AppState { + config: Arc::new(config.clone()), + }; + + // 静态文件服务:找不到静态资源时 fallback 到自定义 not_found_handler + let serve_dir = ServeDir::new(&config.root) + .append_index_html_on_directories(true) + .fallback(axum::handler::HandlerWithoutStateExt::into_service(not_found_handler)); + + let mut app = Router::new() + .route("/api/health", get(health_check)) + .route("/api/projects", get(list_projects_api)) + .route("/", get(root_handler)) + .fallback_service(serve_dir) + .with_state(shared_state) + .layer(TraceLayer::new_for_http()); + + if config.enable_cors { + app = app.layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any), + ); + } + + let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| StashError::ServerBind(format!("端口 {} 绑定失败: {}", config.port, e)))?; + + let local_addr = listener.local_addr()?; + info!("Stash 服务运行于: http://{}", local_addr); + + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + + tokio::spawn(async move { + let server = axum::serve(listener, app) + .with_graceful_shutdown(async move { + let _ = shutdown_rx.await; + info!("Stash 优雅停机..."); + }); + + if let Err(err) = server.await { + warn!("服务异常退出: {}", err); + } + }); + + Ok(ServerHandle { + shutdown_tx, + local_addr, + }) +} \ No newline at end of file