From 2da7aab3e5981bf75f4d29efc18e90c6124dc937 Mon Sep 17 00:00:00 2001 From: voson Date: Thu, 19 Mar 2026 11:01:43 +0800 Subject: [PATCH] feat(upgrade): add self-update command from Gitea Release - Add secrets upgrade command: --check to verify, default to download and replace binary - No database or master key required - Support tar.gz and zip artifacts from Gitea Release Made-with: Cursor --- AGENTS.md | 15 ++ Cargo.lock | 576 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 + README.md | 8 + src/commands/config.rs | 1 + src/commands/mod.rs | 1 + src/commands/upgrade.rs | 198 ++++++++++++++ src/db.rs | 2 +- src/main.rs | 23 +- 9 files changed, 827 insertions(+), 4 deletions(-) create mode 100644 src/commands/upgrade.rs diff --git a/AGENTS.md b/AGENTS.md index cdf587d..ecfcb70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ secrets/ update.rs # update 命令:增量更新,CAS 并发保护,含历史快照 rollback.rs # rollback / history 命令:版本回滚与历史查看 run.rs # inject / run 命令:临时环境变量注入 + upgrade.rs # upgrade 命令:检查并下载最新版本,自动替换二进制 scripts/ setup-gitea-actions.sh # 配置 Gitea Actions 变量与 Secrets .gitea/workflows/ @@ -405,6 +406,20 @@ secrets run -n refining --kind service --name gitea -- printenv --- +### upgrade — 自动更新 CLI 二进制 + +从 Gitea Release 下载最新版本并替换当前二进制,无需数据库连接或主密钥。 + +```bash +# 检查是否有新版本(不下载) +secrets upgrade --check + +# 下载并安装最新版本 +secrets upgrade +``` + +--- + ### config — 配置管理(无需主密钥) ```bash diff --git a/Cargo.lock b/Cargo.lock index 97d2102..406afc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -138,6 +144,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -217,6 +229,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -372,6 +390,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -515,12 +542,40 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "flume" version = "0.11.1" @@ -635,8 +690,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -647,7 +718,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -728,6 +799,106 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -881,6 +1052,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -977,6 +1164,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -998,6 +1191,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1023,6 +1222,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -1174,6 +1383,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -1256,6 +1471,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1265,6 +1535,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1278,10 +1554,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.0" @@ -1303,6 +1589,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1312,6 +1608,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.0" @@ -1364,6 +1669,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + [[package]] name = "ring" version = "0.17.14" @@ -1419,6 +1762,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.36" @@ -1439,6 +1801,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1481,17 +1844,24 @@ dependencies = [ "chrono", "clap", "dirs", + "flate2", "keyring", "rand 0.10.0", + "reqwest", "rpassword", + "self-replace", + "semver", "serde", "serde_json", "sqlx", + "tar", + "tempfile", "tokio", "toml", "tracing", "tracing-subscriber", "uuid", + "zip", ] [[package]] @@ -1530,6 +1900,17 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "semver" version = "1.0.27" @@ -1657,6 +2038,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.12" @@ -1939,6 +2326,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1950,6 +2346,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -2032,6 +2452,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2082,6 +2512,51 @@ version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +[[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", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[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" @@ -2144,6 +2619,18 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.19.0" @@ -2252,6 +2739,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2295,6 +2791,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2361,6 +2871,26 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -2779,6 +3309,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.1" @@ -2882,8 +3422,40 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 0c534c3..45e000f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,21 @@ argon2 = { version = "0.5.3", features = ["std"] } chrono = { version = "0.4.44", features = ["serde"] } clap = { version = "4.6.0", features = ["derive"] } dirs = "6.0.0" +flate2 = "1.1.9" keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "linux-native"] } rand = "0.10.0" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } rpassword = "7.4.0" +self-replace = "1.5.0" +semver = "1.0.27" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] } +tar = "0.4.44" +tempfile = "3.19" tokio = { version = "1.50.0", features = ["full"] } toml = "1.0.7" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.22.0", features = ["serde"] } +zip = { version = "8.2.0", default-features = false, features = ["deflate"] } diff --git a/README.md b/README.md index 77654f5..5633bbf 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ cargo build --release # 或从 Release 页面下载预编译二进制 ``` +已有旧版本时,可执行 `secrets upgrade` 自动下载最新版并替换。 + ## 首次使用(每台设备各执行一次) ```bash @@ -96,6 +98,7 @@ secrets add --help secrets update --help secrets delete --help secrets config --help +secrets upgrade --help # 检查并更新 CLI 版本 # ── search ────────────────────────────────────────────────────────────────── secrets search --summary --limit 20 # 发现概览 @@ -135,6 +138,10 @@ secrets config set-db "postgres://postgres:@:/secrets" # secrets config show # 密码脱敏展示 secrets config path # 打印配置文件路径 +# ── upgrade ────────────────────────────────────────────────────────────────── +secrets upgrade --check # 仅检查是否有新版本 +secrets upgrade # 下载并安装最新版(从 Gitea Release) + # ── 调试 ────────────────────────────────────────────────────────────────────── secrets --verbose search -q mqtt RUST_LOG=secrets=trace secrets search @@ -185,6 +192,7 @@ src/ search.rs # 多条件查询,支持 -f/-o/--summary/--limit/--offset/--sort delete.rs # 删除 update.rs # 增量更新(合并 tags/metadata/encrypted) + upgrade.rs # 从 Gitea Release 自更新 scripts/ setup-gitea-actions.sh # 配置 Gitea Actions 变量与 Secrets ``` diff --git a/src/commands/config.rs b/src/commands/config.rs index 48e0551..d69479e 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -9,6 +9,7 @@ pub async fn run(action: crate::ConfigAction) -> Result<()> { .await .map_err(|e| anyhow::anyhow!("Database connection failed: {}", e))?; drop(pool); + println!("Database connection successful."); let cfg = Config { database_url: Some(url.clone()), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ee9a8b9..305ec8f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,3 +6,4 @@ pub mod rollback; pub mod run; pub mod search; pub mod update; +pub mod upgrade; diff --git a/src/commands/upgrade.rs b/src/commands/upgrade.rs new file mode 100644 index 0000000..a6794ea --- /dev/null +++ b/src/commands/upgrade.rs @@ -0,0 +1,198 @@ +use anyhow::{Context, Result, bail}; +use flate2::read::GzDecoder; +use serde::Deserialize; +use std::io::{Cursor, Read, Write}; + +const GITEA_API: &str = "https://gitea.refining.dev/api/v1/repos/refining/secrets/releases/latest"; + +const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, Deserialize)] +struct Release { + tag_name: String, + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct Asset { + name: String, + browser_download_url: String, +} + +/// Detect the asset suffix for the current platform/arch at compile time. +fn platform_asset_suffix() -> Result<&'static str> { + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + { + Ok("x86_64-linux-musl.tar.gz") + } + + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + Ok("aarch64-macos.tar.gz") + } + + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + { + Ok("x86_64-macos.tar.gz") + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + { + Ok("x86_64-windows.zip") + } + + #[cfg(not(any( + all(target_os = "linux", target_arch = "x86_64"), + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "macos", target_arch = "x86_64"), + all(target_os = "windows", target_arch = "x86_64"), + )))] + bail!( + "Unsupported platform: {}/{}", + std::env::consts::OS, + std::env::consts::ARCH + ) +} + +/// Strip the "secrets-" prefix from the tag and parse as semver. +fn parse_tag_version(tag: &str) -> Result { + let ver_str = tag + .strip_prefix("secrets-") + .with_context(|| format!("unexpected tag format: {tag}"))?; + semver::Version::parse(ver_str) + .with_context(|| format!("failed to parse version from tag: {tag}")) +} + +/// Extract the binary from a tar.gz archive (first file whose name == "secrets"). +fn extract_from_targz(bytes: &[u8]) -> Result> { + let gz = GzDecoder::new(Cursor::new(bytes)); + let mut archive = tar::Archive::new(gz); + for entry in archive.entries().context("failed to read tar entries")? { + let mut entry = entry.context("bad tar entry")?; + let path = entry.path().context("bad tar entry path")?.into_owned(); + let fname = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default(); + if fname == "secrets" || fname == "secrets.exe" { + let mut buf = Vec::new(); + entry.read_to_end(&mut buf).context("read tar entry")?; + return Ok(buf); + } + } + bail!("binary not found inside tar.gz archive") +} + +/// Extract the binary from a zip archive (first file whose name matches). +#[cfg(target_os = "windows")] +fn extract_from_zip(bytes: &[u8]) -> Result> { + let reader = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(reader).context("failed to open zip archive")?; + for i in 0..archive.len() { + let mut file = archive.by_index(i).context("bad zip entry")?; + let fname = file.name().to_owned(); + if fname.ends_with("secrets.exe") || fname.ends_with("secrets") { + let mut buf = Vec::new(); + file.read_to_end(&mut buf).context("read zip entry")?; + return Ok(buf); + } + } + bail!("binary not found inside zip archive") +} + +pub async fn run(check_only: bool) -> Result<()> { + let current = semver::Version::parse(CURRENT_VERSION).context("invalid current version")?; + + println!("Current version: v{current}"); + println!("Checking for updates..."); + + let client = reqwest::Client::builder() + .user_agent(format!("secrets-cli/{CURRENT_VERSION}")) + .build() + .context("failed to build HTTP client")?; + + let release: Release = client + .get(GITEA_API) + .send() + .await + .context("failed to fetch release info from Gitea")? + .error_for_status() + .context("Gitea API returned an error")? + .json() + .await + .context("failed to parse release JSON")?; + + let latest = parse_tag_version(&release.tag_name)?; + + if latest <= current { + println!("Already up to date (v{current})"); + return Ok(()); + } + + println!("New version available: v{latest}"); + + if check_only { + println!("Run `secrets upgrade` to update."); + return Ok(()); + } + + let suffix = platform_asset_suffix()?; + let asset = release + .assets + .iter() + .find(|a| a.name.ends_with(suffix)) + .with_context(|| { + format!( + "no asset found for this platform (looking for suffix: {suffix})\navailable: {}", + release + .assets + .iter() + .map(|a| a.name.as_str()) + .collect::>() + .join(", ") + ) + })?; + + println!("Downloading {}...", asset.name); + + let bytes = client + .get(&asset.browser_download_url) + .send() + .await + .context("download failed")? + .error_for_status() + .context("download returned an error")? + .bytes() + .await + .context("failed to read download body")?; + + println!("Extracting..."); + + let binary = if suffix.ends_with(".tar.gz") { + extract_from_targz(&bytes)? + } else { + #[cfg(target_os = "windows")] + { + extract_from_zip(&bytes)? + } + #[cfg(not(target_os = "windows"))] + bail!("zip extraction is only supported on Windows") + }; + + // Write to a temporary file, set executable permission, then atomically replace. + let mut tmp = tempfile::NamedTempFile::new().context("failed to create temp file")?; + tmp.write_all(&binary) + .context("failed to write temp binary")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o755); + std::fs::set_permissions(tmp.path(), perms).context("failed to chmod temp binary")?; + } + + self_replace::self_replace(tmp.path()).context("failed to replace current binary")?; + + println!("Updated: v{current} → v{latest}"); + Ok(()) +} diff --git a/src/db.rs b/src/db.rs index 89eb00f..2573b69 100644 --- a/src/db.rs +++ b/src/db.rs @@ -6,7 +6,7 @@ pub async fn create_pool(database_url: &str) -> Result { tracing::debug!("connecting to database"); let pool = PgPoolOptions::new() .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(10)) + .acquire_timeout(std::time::Duration::from_secs(5)) .connect(database_url) .await?; tracing::debug!("database connection established"); diff --git a/src/main.rs b/src/main.rs index faf6435..5315a91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -388,6 +388,22 @@ EXAMPLES: #[arg(last = true, required = true)] command: Vec, }, + + /// Check for a newer version and update the binary in-place. + /// + /// Downloads the latest release from Gitea and replaces the current binary. + /// No database connection or master key required. + #[command(after_help = "EXAMPLES: + # Check for updates only (no download) + secrets upgrade --check + + # Download and install the latest version + secrets upgrade")] + Upgrade { + /// Only check if a newer version is available; do not download + #[arg(long)] + check: bool, + }, } #[derive(Subcommand)] @@ -422,6 +438,11 @@ async fn main() -> Result<()> { return commands::config::run(action).await; } + // upgrade needs no database or master key either + if let Commands::Upgrade { check } = cli.command { + return commands::upgrade::run(check).await; + } + let db_url = config::resolve_db_url(&cli.db_url)?; let pool = db::create_pool(&db_url).await?; db::migrate(&pool).await?; @@ -435,7 +456,7 @@ async fn main() -> Result<()> { // except delete which operates on plaintext metadata only. match cli.command { - Commands::Init | Commands::Config { .. } => unreachable!(), + Commands::Init | Commands::Config { .. } | Commands::Upgrade { .. } => unreachable!(), Commands::Add { namespace,