Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e7692c109d
|
|||
|
3a3186b24a
|
|||
|
032f066307
|
|||
|
3578a39d27
|
|||
|
7947c3bae9
|
|||
|
0d474e7913
|
|||
|
65c0626910
|
@@ -0,0 +1,63 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-linux-x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Add musl target
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
- name: Install musl tools
|
||||
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||
- name: Build
|
||||
run: cargo build --release --target x86_64-unknown-linux-musl
|
||||
- name: Create archive
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
TARGET=x86_64-unknown-linux-musl
|
||||
ARCHIVE="mdrs-${VERSION}-${TARGET}.tar.gz"
|
||||
tar -czf "${ARCHIVE}" -C target/${TARGET}/release mdrs
|
||||
echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
|
||||
- name: Create release and upload asset
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
files: ${{ env.ARCHIVE }}
|
||||
|
||||
build-linux-aarch64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Install cargo-zigbuild
|
||||
run: |
|
||||
pip3 install ziglang --break-system-packages
|
||||
cargo install cargo-zigbuild --locked
|
||||
- name: Add aarch64 musl target
|
||||
run: rustup target add aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
run: cargo zigbuild --release --target aarch64-unknown-linux-musl
|
||||
- name: Create archive
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
TARGET=aarch64-unknown-linux-musl
|
||||
ARCHIVE="mdrs-${VERSION}-${TARGET}.tar.gz"
|
||||
tar -czf "${ARCHIVE}" -C target/${TARGET}/release mdrs
|
||||
echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
|
||||
- name: Create release and upload asset
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
files: ${{ env.ARCHIVE }}
|
||||
Generated
+453
-2
@@ -2,6 +2,23 @@
|
||||
# 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 = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -87,6 +104,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -232,6 +258,25 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
@@ -239,6 +284,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -254,6 +301,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
@@ -315,6 +372,12 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
@@ -330,12 +393,42 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
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 = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -363,6 +456,32 @@ version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -371,6 +490,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -495,12 +615,39 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -701,6 +848,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -922,6 +1078,15 @@ dependencies = [
|
||||
"hashbrown 0.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -950,6 +1115,16 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.95"
|
||||
@@ -974,7 +1149,10 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -983,6 +1161,12 @@ version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[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.2"
|
||||
@@ -1019,9 +1203,30 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdrs-client-rust"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -1030,16 +1235,22 @@ dependencies = [
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"flate2",
|
||||
"fs2",
|
||||
"futures",
|
||||
"os_info",
|
||||
"reqwest",
|
||||
"rpassword",
|
||||
"self-replace",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"unicode-normalization",
|
||||
"validators",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1070,6 +1281,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[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.2.0"
|
||||
@@ -1115,6 +1336,12 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1341,11 +1568,21 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1378,6 +1615,18 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
@@ -1400,6 +1649,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -1600,6 +1855,15 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
@@ -1823,6 +2087,19 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[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.38"
|
||||
@@ -1894,6 +2171,17 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[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.28"
|
||||
@@ -1959,6 +2247,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -1986,6 +2285,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -2121,6 +2426,30 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
||||
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.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -2161,6 +2490,25 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -2841,6 +3189,25 @@ dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
@@ -2910,6 +3277,20 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
@@ -2944,8 +3325,78 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdrs-client-rust"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Neuroinformatics Unit, RIKEN CBS"]
|
||||
@@ -28,3 +28,9 @@ fs2 = "0.4"
|
||||
ctrlc = "3"
|
||||
os_info = "3"
|
||||
dotenvy = "0.15"
|
||||
unicode-normalization = "0.1"
|
||||
self-replace = "1"
|
||||
tar = "0.4"
|
||||
flate2 = "1"
|
||||
zip = "2"
|
||||
tempfile = "3"
|
||||
|
||||
@@ -53,7 +53,7 @@ List registered remote hosts.
|
||||
|
||||
```shell
|
||||
mdrs config list
|
||||
mdrs config list -l
|
||||
mdrs config ls
|
||||
```
|
||||
|
||||
### config delete
|
||||
@@ -62,6 +62,7 @@ Remove a registered remote host.
|
||||
|
||||
```shell
|
||||
mdrs config delete neurodata
|
||||
mdrs config rm neurodata
|
||||
```
|
||||
|
||||
### login
|
||||
@@ -199,6 +200,14 @@ mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat
|
||||
mdrs file-metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt
|
||||
```
|
||||
|
||||
### version
|
||||
|
||||
Show the tool name and version number.
|
||||
|
||||
```shell
|
||||
mdrs version
|
||||
```
|
||||
|
||||
### help
|
||||
|
||||
Show help for a command.
|
||||
|
||||
@@ -13,5 +13,10 @@ fn main() {
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
println!("cargo:rustc-env=RUSTC_VERSION={}", version);
|
||||
// Expose the build target triple so selfupdate can match release assets.
|
||||
println!(
|
||||
"cargo:rustc-env=BUILD_TARGET={}",
|
||||
std::env::var("TARGET").unwrap_or_default()
|
||||
);
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
@@ -45,6 +45,5 @@ pub async fn chacl(
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("ACL change failed: {}", resp.status()).into());
|
||||
}
|
||||
println!("ACL changed successfully for: {}", remote_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+6
-20
@@ -64,7 +64,7 @@ pub fn config_create(remote: &str, url: &str) -> Result<(), Box<dyn std::error::
|
||||
.map(|m| m.contains_key(remote))
|
||||
.unwrap_or(false);
|
||||
if section_exists {
|
||||
return Err(format!("Remote host `{}` already exists.", remote).into());
|
||||
return Err(format!("Remote host `{}` is already exists.", remote).into());
|
||||
}
|
||||
// set url
|
||||
conf.set(remote, "url", Some(url.to_string()));
|
||||
@@ -73,7 +73,6 @@ pub fn config_create(remote: &str, url: &str) -> Result<(), Box<dyn std::error::
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
write_ini_atomic(&path, &conf)?;
|
||||
println!("Created remote host `{}`.", remote);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,18 +93,16 @@ pub fn config_update(remote: &str, url: &str) -> Result<(), Box<dyn std::error::
|
||||
.map(|m| m.contains_key(remote))
|
||||
.unwrap_or(false);
|
||||
if !section_exists {
|
||||
return Err(format!("Remote host `{}` does not exist.", remote).into());
|
||||
return Err(format!("Remote host `{}` is not exists.", remote).into());
|
||||
}
|
||||
conf.set(remote, "url", Some(url.to_string()));
|
||||
write_ini_atomic(&path, &conf)?;
|
||||
println!("Updated remote host `{}`.", remote);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn config_list(long: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn config_list() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = config_path();
|
||||
if !path.exists() {
|
||||
println!("No config file found at {}", path.to_string_lossy());
|
||||
return Ok(());
|
||||
}
|
||||
sanitize_config_file(&path)?;
|
||||
@@ -113,22 +110,12 @@ pub fn config_list(long: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut conf = Ini::new();
|
||||
let _ = conf.load(&path_str)?;
|
||||
let map = conf.get_map().unwrap_or_default();
|
||||
let mut printed = false;
|
||||
for (sec, props) in map.iter() {
|
||||
if sec == "default" {
|
||||
continue;
|
||||
}
|
||||
if !long {
|
||||
println!("{}", sec);
|
||||
printed = true;
|
||||
} else {
|
||||
let url = props.get("url").and_then(|v| v.clone()).unwrap_or_default();
|
||||
println!("{}:\t{}", sec, url);
|
||||
printed = true;
|
||||
}
|
||||
}
|
||||
if !printed {
|
||||
println!("No remotes configured");
|
||||
let url = props.get("url").and_then(|v| v.clone()).unwrap_or_default();
|
||||
println!("{}:\t{}", sec, url);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -144,7 +131,7 @@ pub fn config_delete(remote: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// fallback: reconstruct by removing the section in memory map and writing file
|
||||
let mut map = conf.get_map().unwrap_or_default();
|
||||
if map.remove(remote).is_none() {
|
||||
return Err(format!("Remote host `{}` does not exist.", remote).into());
|
||||
return Err(format!("Remote host `{}` is not exists.", remote).into());
|
||||
}
|
||||
// build new Ini from map
|
||||
let mut new_ini = Ini::new();
|
||||
@@ -154,7 +141,6 @@ pub fn config_delete(remote: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
write_ini_atomic(&path, &new_ini)?;
|
||||
println!("Deleted remote host `{}`.", remote);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ pub enum ConfigSubcommand {
|
||||
/// Update an existing remote host
|
||||
Update(ConfigUpdateArgs),
|
||||
/// List all remote hosts
|
||||
#[command(alias = "ls")]
|
||||
List(ConfigListArgs),
|
||||
/// Delete a remote host
|
||||
#[command(aliases = ["remove", "rm"])]
|
||||
Delete(ConfigDeleteArgs),
|
||||
}
|
||||
|
||||
@@ -25,10 +27,7 @@ pub struct ConfigUpdateArgs {
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ConfigListArgs {
|
||||
#[arg(short, long)]
|
||||
pub long: bool,
|
||||
}
|
||||
pub struct ConfigListArgs {}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ConfigDeleteArgs {
|
||||
|
||||
+52
-44
@@ -1,6 +1,6 @@
|
||||
use crate::commands::shared::{
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh,
|
||||
parse_remote_path,
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache,
|
||||
find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path,
|
||||
};
|
||||
|
||||
pub async fn cp(
|
||||
@@ -13,10 +13,10 @@ pub async fn cp(
|
||||
let (d_remote, d_lab, d_path) = parse_remote_path(dest_path)?;
|
||||
|
||||
if s_remote != d_remote {
|
||||
return Err("Source and destination must use the same remote.".into());
|
||||
return Err("Remote host mismatched.".into());
|
||||
}
|
||||
if s_lab != d_lab {
|
||||
return Err("Source and destination must be in the same laboratory.".into());
|
||||
return Err("Laboratory mismatched.".into());
|
||||
}
|
||||
|
||||
let cache = load_cache_with_token_refresh(&s_remote).await?;
|
||||
@@ -28,11 +28,12 @@ pub async fn cp(
|
||||
let (s_dirname, s_basename) = split_path(&s_path);
|
||||
|
||||
// If dest ends with '/', treat it as a directory and preserve src basename
|
||||
let (d_dirname, d_basename) = if dest_ends_with_slash {
|
||||
let (d_dirname, d_basename_raw) = if dest_ends_with_slash {
|
||||
(d_path.clone(), s_basename.clone())
|
||||
} else {
|
||||
split_path(&d_path)
|
||||
};
|
||||
let d_basename = nfc(&d_basename_raw);
|
||||
|
||||
let s_parent_folder = find_folder(&conn, lab_id, &s_dirname, None).await?;
|
||||
let s_parent_files = conn.list_all_files(&s_parent_folder.id).await?;
|
||||
@@ -46,12 +47,16 @@ pub async fn cp(
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!("File `{}` already exists.", d_basename).into());
|
||||
}
|
||||
if d_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.any(|f| f.name.to_lowercase() == d_basename.to_lowercase())
|
||||
{
|
||||
return Err("Cannot overwrite non-folder with folder.".into());
|
||||
if find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename).is_some() {
|
||||
return Err(format!(
|
||||
"Cannot overwrite non-folder `{}` with folder `{}`.",
|
||||
d_basename, d_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
// No-op if source and destination are identical
|
||||
if s_parent_folder.id == d_parent_folder.id && d_basename == s_basename {
|
||||
return Ok(());
|
||||
}
|
||||
let body = serde_json::json!({"folder": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
@@ -64,48 +69,51 @@ pub async fn cp(
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Copy failed: {}", resp.status()).into());
|
||||
}
|
||||
println!("Copied: {} -> {}", src_path, dest_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try source as a folder
|
||||
if let Some(src_folder) = s_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.find(|f| f.name.to_lowercase() == s_basename.to_lowercase())
|
||||
{
|
||||
if !recursive {
|
||||
let src_folder = match find_subfolder_by_name(&s_parent_folder.sub_folders, &s_basename) {
|
||||
Some(f) => f,
|
||||
None => return Err(format!("File or folder `{}` not found.", s_basename).into()),
|
||||
};
|
||||
if !recursive {
|
||||
return Err(format!("Cannot copy `{}`: Is a folder.", s_path).into());
|
||||
}
|
||||
let src_folder_id = src_folder.id.clone();
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!(
|
||||
"Cannot overwrite non-folder `{}` with folder `{}`.",
|
||||
d_basename, s_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) {
|
||||
if d_folder.id == src_folder_id {
|
||||
return Err(
|
||||
format!("{}: is a folder (use -r to copy folders)", src_path).into(),
|
||||
format!("`{}` and `{}` are the same folder.", s_path, s_path).into(),
|
||||
);
|
||||
}
|
||||
let src_folder_id = src_folder.id.clone();
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!("File `{}` already exists.", d_basename).into());
|
||||
}
|
||||
if d_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.any(|f| f.name.to_lowercase() == d_basename.to_lowercase())
|
||||
{
|
||||
return Err("Folder not empty.".into());
|
||||
}
|
||||
let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
.client
|
||||
.post(conn.build_url(&format!("v3/folders/{}/copy/", src_folder_id)))
|
||||
.headers(conn.prepare_headers())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Copy failed: {}", resp.status()).into());
|
||||
}
|
||||
println!("Copied: {} -> {}", src_path, dest_path);
|
||||
return Err(
|
||||
format!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path).into(),
|
||||
);
|
||||
}
|
||||
// No-op if source and destination are identical
|
||||
if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!("Source `{}` not found.", src_path).into())
|
||||
let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
.client
|
||||
.post(conn.build_url(&format!("v3/folders/{}/copy/", src_folder_id)))
|
||||
.headers(conn.prepare_headers())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Copy failed: {}", resp.status()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split a path into (parent_dir, basename).
|
||||
|
||||
@@ -28,6 +28,6 @@ pub async fn file_metadata(remote_path: &str, password: Option<&str>) -> Result<
|
||||
|
||||
let resp = conn.get(&format!("v3/files/{}/metadata/", file.id)).await?;
|
||||
let json: serde_json::Value = resp.json().await?;
|
||||
println!("{}", serde_json::to_string_pretty(&json)?);
|
||||
println!("{}", serde_json::to_string(&json)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+29
-48
@@ -1,56 +1,37 @@
|
||||
use crate::connection::MDRSConnection;
|
||||
use crate::commands::shared::{create_authenticated_conn, load_cache_with_token_refresh};
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
pub async fn labs(remote: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cache = load_cache_with_token_refresh(remote).await?;
|
||||
let conn = create_authenticated_conn(remote, &cache)?;
|
||||
let labs = conn.list_laboratories().await?;
|
||||
|
||||
pub async fn labs(
|
||||
conn: Arc<MDRSConnection>,
|
||||
remote_label: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Try API first
|
||||
match conn.list_laboratories().await {
|
||||
Ok(labs) => {
|
||||
println!("Laboratories:");
|
||||
for lab in labs.items {
|
||||
println!(
|
||||
" {} (PI: {}, Full: {})",
|
||||
lab.name, lab.pi_name, lab.full_name
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
// fallback to cache
|
||||
}
|
||||
let header = ("Name", "PI", "Laboratory");
|
||||
let mut w_name = header.0.len();
|
||||
let mut w_pi = header.1.len();
|
||||
let mut w_full = header.2.len();
|
||||
|
||||
for lab in &labs.items {
|
||||
w_name = w_name.max(lab.name.len());
|
||||
w_pi = w_pi.max(lab.pi_name.len());
|
||||
w_full = w_full.max(lab.full_name.len());
|
||||
}
|
||||
|
||||
// fallback: read cache file using remote_label
|
||||
let cache_path = crate::settings::SETTINGS
|
||||
.config_dirname
|
||||
.join("cache")
|
||||
.join(format!("{}.json", remote_label));
|
||||
if !cache_path.exists() {
|
||||
println!("No laboratories available (API failed and no cache)");
|
||||
return Ok(());
|
||||
}
|
||||
let text = fs::read_to_string(&cache_path)?;
|
||||
let v: serde_json::Value = serde_json::from_str(&text)?;
|
||||
// Cache stores laboratories as `{"items": [...]}` (Python-compatible format)
|
||||
let labs_arr = v
|
||||
.get("laboratories")
|
||||
.and_then(|l| l.get("items"))
|
||||
.and_then(|a| a.as_array());
|
||||
if let Some(arr) = labs_arr {
|
||||
println!("Laboratories (from cache):");
|
||||
for lab in arr {
|
||||
let name = lab.get("name").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let pi = lab.get("pi_name").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let full = lab.get("full_name").and_then(|s| s.as_str()).unwrap_or("");
|
||||
println!(" {} (PI: {}, Full: {})", name, pi, full);
|
||||
}
|
||||
} else {
|
||||
println!("No laboratories found in cache");
|
||||
println!(
|
||||
"{:<w_name$} {:<w_pi$} {:<w_full$}",
|
||||
header.0, header.1, header.2,
|
||||
w_name = w_name, w_pi = w_pi, w_full = w_full,
|
||||
);
|
||||
let sep_len = w_name + 2 + w_pi + 2 + w_full;
|
||||
println!("{}", "-".repeat(sep_len));
|
||||
|
||||
for lab in &labs.items {
|
||||
println!(
|
||||
"{:<w_name$} {:<w_pi$} {:<w_full$}",
|
||||
lab.name, lab.pi_name, lab.full_name,
|
||||
w_name = w_name, w_pi = w_pi, w_full = w_full,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ pub async fn login(
|
||||
}
|
||||
fs::rename(&tmp, &cache_file)?;
|
||||
|
||||
println!("Login successful and cached for {}.", remote);
|
||||
println!("Login Successful");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use std::fs;
|
||||
|
||||
pub fn logout(remote: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cache_path = crate::settings::SETTINGS
|
||||
.config_dirname
|
||||
.join("cache")
|
||||
.join(format!("{}.json", remote));
|
||||
if cache_path.exists() {
|
||||
fs::remove_file(&cache_path)?;
|
||||
println!("Logged out from {}", remote);
|
||||
} else {
|
||||
println!("No login cache found for {}", remote);
|
||||
std::fs::remove_file(&cache_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+5
-3
@@ -14,7 +14,7 @@ pub async fn ls(
|
||||
password: Option<&str>,
|
||||
is_json: bool,
|
||||
is_recursive: bool,
|
||||
is_quick: bool,
|
||||
is_quiet: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (remote, labname, path) = parse_remote_path(remote_path)?;
|
||||
let cache = load_cache_with_token_refresh(&remote).await?;
|
||||
@@ -29,7 +29,7 @@ pub async fn ls(
|
||||
} else {
|
||||
build_folder_json_flat(&conn, &folder, &labname).await?
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else if is_recursive {
|
||||
let prefix = format!("{}:/{}", remote, labname);
|
||||
ls_plain_recursive(&conn, folder, &labname, &prefix, password).await?;
|
||||
@@ -44,7 +44,7 @@ pub async fn ls(
|
||||
&files_sorted,
|
||||
folder.access_level_name(),
|
||||
&labname,
|
||||
!is_quick,
|
||||
!is_quiet,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ fn ls_plain_recursive<'a>(
|
||||
|
||||
print_folder_plain(&sub_folders, &files_sorted, access, labname, false);
|
||||
|
||||
println!();
|
||||
|
||||
for sf in sub_folders {
|
||||
if sf.lock {
|
||||
match password {
|
||||
|
||||
@@ -13,6 +13,6 @@ pub async fn metadata(remote_path: &str, password: Option<&str>) -> Result<(), B
|
||||
.get(&format!("v3/folders/{}/metadata/", folder.id))
|
||||
.await?;
|
||||
let json: serde_json::Value = resp.json().await?;
|
||||
println!("{}", serde_json::to_string_pretty(&json)?);
|
||||
println!("{}", serde_json::to_string(&json)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+8
-15
@@ -1,6 +1,6 @@
|
||||
use crate::commands::shared::{
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh,
|
||||
parse_remote_path,
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache,
|
||||
find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path,
|
||||
};
|
||||
|
||||
pub async fn mkdir(remote_path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -26,26 +26,19 @@ pub async fn mkdir(remote_path: &str) -> Result<(), Box<dyn std::error::Error>>
|
||||
let lab = find_lab_in_cache(&cache, &labname)?;
|
||||
let parent_folder = find_folder(&conn, lab.id, parent_path, None).await?;
|
||||
|
||||
// Check for name conflict in sub-folders
|
||||
if parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.any(|f| f.name == new_folder_name)
|
||||
{
|
||||
return Err(format!("'{}' already exists as a folder", new_folder_name).into());
|
||||
}
|
||||
// Check for name conflict in files
|
||||
// Check for name conflict in sub-folders or files
|
||||
let files = conn.list_all_files(&parent_folder.id).await?;
|
||||
if find_file_by_name(&files, new_folder_name).is_some() {
|
||||
return Err(format!("'{}' already exists as a file", new_folder_name).into());
|
||||
if find_subfolder_by_name(&parent_folder.sub_folders, new_folder_name).is_some()
|
||||
|| find_file_by_name(&files, new_folder_name).is_some()
|
||||
{
|
||||
return Err(format!("Cannot create folder `{}`: File exists.", path).into());
|
||||
}
|
||||
|
||||
let resp = conn
|
||||
.create_folder(&parent_folder.id, new_folder_name)
|
||||
.create_folder(&parent_folder.id, &nfc(new_folder_name))
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Failed to create folder: {}", resp.status()).into());
|
||||
}
|
||||
println!("Created folder: {}", new_folder_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -16,4 +16,6 @@ pub mod mv;
|
||||
pub mod rm;
|
||||
pub mod shared;
|
||||
pub mod upload;
|
||||
pub mod version;
|
||||
pub mod whoami;
|
||||
pub mod selfupdate;
|
||||
|
||||
+51
-41
@@ -1,6 +1,6 @@
|
||||
use crate::commands::shared::{
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh,
|
||||
parse_remote_path,
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache,
|
||||
find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path,
|
||||
};
|
||||
|
||||
pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -9,10 +9,10 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::erro
|
||||
let (d_remote, d_lab, d_path) = parse_remote_path(dest_path)?;
|
||||
|
||||
if s_remote != d_remote {
|
||||
return Err("Source and destination must use the same remote.".into());
|
||||
return Err("Remote host mismatched.".into());
|
||||
}
|
||||
if s_lab != d_lab {
|
||||
return Err("Source and destination must be in the same laboratory.".into());
|
||||
return Err("Laboratory mismatched.".into());
|
||||
}
|
||||
|
||||
let cache = load_cache_with_token_refresh(&s_remote).await?;
|
||||
@@ -24,11 +24,12 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::erro
|
||||
let (s_dirname, s_basename) = split_path(&s_path);
|
||||
|
||||
// If dest ends with '/', treat it as a directory and preserve src basename
|
||||
let (d_dirname, d_basename) = if dest_ends_with_slash {
|
||||
let (d_dirname, d_basename_raw) = if dest_ends_with_slash {
|
||||
(d_path.clone(), s_basename.clone())
|
||||
} else {
|
||||
split_path(&d_path)
|
||||
};
|
||||
let d_basename = nfc(&d_basename_raw);
|
||||
|
||||
let s_parent_folder = find_folder(&conn, lab_id, &s_dirname, None).await?;
|
||||
let s_parent_files = conn.list_all_files(&s_parent_folder.id).await?;
|
||||
@@ -42,12 +43,16 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::erro
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!("File `{}` already exists.", d_basename).into());
|
||||
}
|
||||
if d_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.any(|f| f.name.to_lowercase() == d_basename.to_lowercase())
|
||||
{
|
||||
return Err("Cannot overwrite non-folder with folder.".into());
|
||||
if find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename).is_some() {
|
||||
return Err(format!(
|
||||
"Cannot overwrite non-folder `{}` with folder `{}`.",
|
||||
d_basename, d_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
// No-op if source and destination are identical
|
||||
if s_parent_folder.id == d_parent_folder.id && d_basename == s_basename {
|
||||
return Ok(());
|
||||
}
|
||||
let body = serde_json::json!({"folder": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
@@ -60,43 +65,48 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::erro
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Move failed: {}", resp.status()).into());
|
||||
}
|
||||
println!("Moved: {} -> {}", src_path, dest_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try source as a folder
|
||||
if let Some(src_folder) = s_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.find(|f| f.name.to_lowercase() == s_basename.to_lowercase())
|
||||
{
|
||||
let src_folder_id = src_folder.id.clone();
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!("File `{}` already exists.", d_basename).into());
|
||||
let src_folder = match find_subfolder_by_name(&s_parent_folder.sub_folders, &s_basename) {
|
||||
Some(f) => f,
|
||||
None => return Err(format!("File or folder `{}` not found.", s_basename).into()),
|
||||
};
|
||||
let src_folder_id = src_folder.id.clone();
|
||||
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
|
||||
return Err(format!(
|
||||
"Cannot overwrite non-folder `{}` with folder `{}`.",
|
||||
d_basename, s_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) {
|
||||
if d_folder.id == src_folder_id {
|
||||
return Err(
|
||||
format!("`{}` and `{}` are the same folder.", s_path, s_path).into(),
|
||||
);
|
||||
}
|
||||
if d_parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.any(|f| f.name.to_lowercase() == d_basename.to_lowercase())
|
||||
{
|
||||
return Err("Folder not empty.".into());
|
||||
}
|
||||
let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
.client
|
||||
.post(conn.build_url(&format!("v3/folders/{}/move/", src_folder_id)))
|
||||
.headers(conn.prepare_headers())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Move failed: {}", resp.status()).into());
|
||||
}
|
||||
println!("Moved: {} -> {}", src_path, dest_path);
|
||||
return Err(
|
||||
format!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path).into(),
|
||||
);
|
||||
}
|
||||
// No-op if source and destination are identical
|
||||
if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!("Source `{}` not found.", src_path).into())
|
||||
let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename});
|
||||
let resp = conn
|
||||
.client
|
||||
.post(conn.build_url(&format!("v3/folders/{}/move/", src_folder_id)))
|
||||
.headers(conn.prepare_headers())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Move failed: {}", resp.status()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split a path into (parent_dir, basename).
|
||||
|
||||
+5
-11
@@ -1,6 +1,6 @@
|
||||
use crate::commands::shared::{
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh,
|
||||
parse_remote_path,
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache,
|
||||
find_subfolder_by_name, load_cache_with_token_refresh, parse_remote_path,
|
||||
};
|
||||
|
||||
pub async fn rm(remote_path: &str, recursive: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -36,18 +36,13 @@ pub async fn rm(remote_path: &str, recursive: bool) -> Result<(), Box<dyn std::e
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Failed to delete file: {}", resp.status()).into());
|
||||
}
|
||||
println!("Deleted file: {}", target_name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if target is a sub-folder
|
||||
if let Some(subfolder) = parent_folder
|
||||
.sub_folders
|
||||
.iter()
|
||||
.find(|f| f.name == target_name)
|
||||
{
|
||||
if let Some(subfolder) = find_subfolder_by_name(&parent_folder.sub_folders, target_name) {
|
||||
if !recursive {
|
||||
return Err(format!("'{}': Is a folder", target_name).into());
|
||||
return Err(format!("Cannot remove `{}`: Is a folder.", path).into());
|
||||
}
|
||||
let resp = conn
|
||||
.client
|
||||
@@ -59,9 +54,8 @@ pub async fn rm(remote_path: &str, recursive: bool) -> Result<(), Box<dyn std::e
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Failed to delete folder: {}", resp.status()).into());
|
||||
}
|
||||
println!("Deleted folder: {}", target_name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!("'{}': No such file or directory", target_name).into())
|
||||
Err(format!("Cannot remove `{}`: No such file or folder.", path).into())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use anyhow::{anyhow, bail};
|
||||
use reqwest::header::{AUTHORIZATION, USER_AGENT};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
const GITEA_HOST: &str = "https://git.ni.riken.jp";
|
||||
const REPO_OWNER: &str = "niu";
|
||||
const REPO_NAME: &str = "mdrs-client-rust";
|
||||
|
||||
/// Current build target triple, captured at compile time via build.rs.
|
||||
const BUILD_TARGET: &str = env!("BUILD_TARGET");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GiteaRelease {
|
||||
tag_name: String,
|
||||
assets: Vec<GiteaAsset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GiteaAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
/// Returns true if `latest` is strictly greater than `current` (semver-like comparison).
|
||||
fn is_newer(current: &str, latest: &str) -> bool {
|
||||
let parse = |s: &str| -> Vec<u64> {
|
||||
s.trim_start_matches('v')
|
||||
.split('.')
|
||||
.map(|p| p.parse::<u64>().unwrap_or(0))
|
||||
.collect()
|
||||
};
|
||||
let cur = parse(current);
|
||||
let lat = parse(latest);
|
||||
let len = cur.len().max(lat.len());
|
||||
for i in 0..len {
|
||||
let c = cur.get(i).copied().unwrap_or(0);
|
||||
let l = lat.get(i).copied().unwrap_or(0);
|
||||
if l > c {
|
||||
return true;
|
||||
}
|
||||
if l < c {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Extract the binary named `bin_name` from a `.tar.gz` archive at `archive_path`
|
||||
/// and write it to `dest_path`.
|
||||
fn extract_from_tar_gz(archive_path: &Path, bin_name: &str, dest_path: &Path) -> anyhow::Result<()> {
|
||||
use flate2::read::GzDecoder;
|
||||
use tar::Archive;
|
||||
|
||||
let file = std::fs::File::open(archive_path)?;
|
||||
let gz = GzDecoder::new(file);
|
||||
let mut archive = Archive::new(gz);
|
||||
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?;
|
||||
// Match by file name only (ignore directory prefix in archive).
|
||||
if path.file_name().and_then(|n| n.to_str()) == Some(bin_name) {
|
||||
entry.unpack(dest_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
bail!("Binary '{}' not found in archive", bin_name)
|
||||
}
|
||||
|
||||
/// Extract the binary named `bin_name` from a `.zip` archive at `archive_path`
|
||||
/// and write it to `dest_path`.
|
||||
fn extract_from_zip(archive_path: &Path, bin_name: &str, dest_path: &Path) -> anyhow::Result<()> {
|
||||
use std::io::Read;
|
||||
|
||||
let file = std::fs::File::open(archive_path)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut entry = archive.by_index(i)?;
|
||||
let entry_name = entry.name().to_owned();
|
||||
let file_name = Path::new(&entry_name)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("");
|
||||
if file_name == bin_name {
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
std::fs::write(dest_path, &buf)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
bail!("Binary '{}' not found in archive", bin_name)
|
||||
}
|
||||
|
||||
pub async fn selfupdate(yes: bool) -> anyhow::Result<()> {
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
println!("Checking for updates (current version: {current_version}, target: {BUILD_TARGET})...");
|
||||
|
||||
let api_url = format!(
|
||||
"{GITEA_HOST}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases?limit=1"
|
||||
);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client
|
||||
.get(&api_url)
|
||||
.header(USER_AGENT, format!("mdrs/{current_version}"));
|
||||
|
||||
if let Ok(token) = env::var("GITEA_TOKEN") {
|
||||
req = req.header(AUTHORIZATION, format!("Bearer {token}"));
|
||||
}
|
||||
|
||||
let resp = req.send().await?;
|
||||
if !resp.status().is_success() {
|
||||
bail!("Failed to fetch release info: HTTP {}", resp.status());
|
||||
}
|
||||
|
||||
let releases: Vec<GiteaRelease> = resp.json().await?;
|
||||
let release = releases
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No releases found"))?;
|
||||
|
||||
let latest_version = release.tag_name.trim_start_matches('v');
|
||||
|
||||
if !is_newer(current_version, latest_version) {
|
||||
println!("Already up-to-date ({current_version}).");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("New version available: {latest_version}");
|
||||
|
||||
// Find the asset matching the current build target.
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.contains(BUILD_TARGET))
|
||||
.ok_or_else(|| {
|
||||
let names: Vec<&str> = release.assets.iter().map(|a| a.name.as_str()).collect();
|
||||
anyhow!(
|
||||
"No release asset found for target '{BUILD_TARGET}'. \
|
||||
Available assets: {}",
|
||||
names.join(", ")
|
||||
)
|
||||
})?;
|
||||
|
||||
println!("Asset: {}", asset.name);
|
||||
|
||||
if !yes {
|
||||
print!("Update to version {latest_version}? [y/N] ");
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
if !input.trim().eq_ignore_ascii_case("y") {
|
||||
println!("Update cancelled.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Download the asset to a temporary directory.
|
||||
let tmp_dir = tempfile::Builder::new()
|
||||
.prefix("mdrs-selfupdate-")
|
||||
.tempdir()?;
|
||||
let archive_path = tmp_dir.path().join(&asset.name);
|
||||
|
||||
println!("Downloading {}...", asset.browser_download_url);
|
||||
|
||||
let mut download_req = client
|
||||
.get(&asset.browser_download_url)
|
||||
.header(USER_AGENT, format!("mdrs/{current_version}"));
|
||||
|
||||
if let Ok(token) = env::var("GITEA_TOKEN") {
|
||||
download_req = download_req.header(AUTHORIZATION, format!("Bearer {token}"));
|
||||
}
|
||||
|
||||
let download_resp = download_req.send().await?;
|
||||
if !download_resp.status().is_success() {
|
||||
bail!(
|
||||
"Failed to download asset: HTTP {}",
|
||||
download_resp.status()
|
||||
);
|
||||
}
|
||||
|
||||
let bytes = download_resp.bytes().await?;
|
||||
std::fs::write(&archive_path, &bytes)?;
|
||||
|
||||
// Extract the binary from the archive.
|
||||
let bin_name = if cfg!(windows) { "mdrs.exe" } else { "mdrs" };
|
||||
let new_bin = tmp_dir.path().join(bin_name);
|
||||
let name = asset.name.as_str();
|
||||
|
||||
if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
|
||||
extract_from_tar_gz(&archive_path, bin_name, &new_bin)?;
|
||||
} else if name.ends_with(".zip") {
|
||||
extract_from_zip(&archive_path, bin_name, &new_bin)?;
|
||||
} else {
|
||||
bail!("Unsupported archive format: {}", asset.name);
|
||||
}
|
||||
|
||||
// Make the extracted binary executable on Unix.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&new_bin)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&new_bin, perms)?;
|
||||
}
|
||||
|
||||
// Atomically replace the current executable.
|
||||
self_replace::self_replace(&new_bin)?;
|
||||
|
||||
println!("Successfully updated to version {latest_version}.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+18
-5
@@ -1,11 +1,12 @@
|
||||
use crate::models::file::File;
|
||||
use crate::models::folder::FolderDetail;
|
||||
use crate::models::folder::{FolderDetail, FolderSimple};
|
||||
use crate::connection::MDRSConnection;
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache structs — matching Python's cache format exactly
|
||||
@@ -376,6 +377,11 @@ pub fn find_lab_in_cache<'a>(
|
||||
.ok_or_else(|| format!("Laboratory `{}` not found.", labname).into())
|
||||
}
|
||||
|
||||
/// Apply Unicode NFC normalization to a string.
|
||||
pub fn nfc(s: &str) -> String {
|
||||
s.chars().nfc().collect()
|
||||
}
|
||||
|
||||
/// Resolve a folder by path using the API (GET v3/folders/?path=...&laboratory_id=...)
|
||||
pub async fn find_folder(
|
||||
conn: &MDRSConnection,
|
||||
@@ -383,7 +389,8 @@ pub async fn find_folder(
|
||||
path: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<FolderDetail, Box<dyn std::error::Error>> {
|
||||
let folders = conn.list_folders_by_path(lab_id, path).await?;
|
||||
let normalized_path = nfc(path);
|
||||
let folders = conn.list_folders_by_path(lab_id, &normalized_path).await?;
|
||||
if folders.is_empty() {
|
||||
return Err(format!("Folder `{}` not found.", path).into());
|
||||
}
|
||||
@@ -409,10 +416,16 @@ pub async fn find_folder(
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
/// Find a file by name (case-insensitive) in a file list
|
||||
/// Find a file by name (NFC-normalized, case-insensitive) in a file list.
|
||||
pub fn find_file_by_name<'a>(files: &'a [File], name: &str) -> Option<&'a File> {
|
||||
let name_lower = name.to_lowercase();
|
||||
files.iter().find(|f| f.name.to_lowercase() == name_lower)
|
||||
let name_lower = nfc(name).to_lowercase();
|
||||
files.iter().find(|f| nfc(&f.name).to_lowercase() == name_lower)
|
||||
}
|
||||
|
||||
/// Find a sub-folder by name (NFC-normalized, case-insensitive).
|
||||
pub fn find_subfolder_by_name<'a>(subfolders: &'a [FolderSimple], name: &str) -> Option<&'a FolderSimple> {
|
||||
let name_lower = nfc(name).to_lowercase();
|
||||
subfolders.iter().find(|f| nfc(&f.name).to_lowercase() == name_lower)
|
||||
}
|
||||
|
||||
/// Format an ISO 8601 timestamp as "YYYY/MM/DD HH:MM:SS"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::models::folder::FolderSimple;
|
||||
use crate::commands::shared::{
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh,
|
||||
parse_remote_path,
|
||||
create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache,
|
||||
load_cache_with_token_refresh, nfc, parse_remote_path,
|
||||
};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::path::PathBuf;
|
||||
@@ -128,10 +128,10 @@ async fn find_or_create_folder(
|
||||
existing: &[FolderSimple],
|
||||
name: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
if let Some(sf) = existing.iter().find(|f| f.name == name) {
|
||||
if let Some(sf) = existing.iter().find(|f| nfc(&f.name).to_lowercase() == nfc(name).to_lowercase()) {
|
||||
return Ok(sf.id.clone());
|
||||
}
|
||||
let resp = conn.create_folder(parent_id, name).await?;
|
||||
let resp = conn.create_folder(parent_id, &nfc(name)).await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Failed to create remote folder: {}", name).into());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn version() {
|
||||
println!("{} {}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
+6
-1
@@ -103,7 +103,12 @@ impl MDRSConnection {
|
||||
parent_id: &str,
|
||||
folder_name: &str,
|
||||
) -> reqwest::Result<reqwest::Response> {
|
||||
let body = serde_json::json!({"parent": parent_id, "name": folder_name});
|
||||
let body = serde_json::json!({
|
||||
"name": folder_name,
|
||||
"parent_id": parent_id,
|
||||
"description": "",
|
||||
"template_id": -1,
|
||||
});
|
||||
self.client
|
||||
.post(self.build_url("v3/folders/"))
|
||||
.headers(self.prepare_headers())
|
||||
|
||||
+26
-20
@@ -62,7 +62,7 @@ enum Commands {
|
||||
#[arg(short = 'r', long)]
|
||||
recursive: bool,
|
||||
#[arg(short = 'q', long)]
|
||||
quick: bool,
|
||||
quiet: bool,
|
||||
},
|
||||
Whoami {
|
||||
remote: String,
|
||||
@@ -109,6 +109,15 @@ enum Commands {
|
||||
password: Option<String>,
|
||||
remote_path: String,
|
||||
},
|
||||
/// Show the version of this tool
|
||||
Version,
|
||||
/// Update this binary to the latest release
|
||||
#[command(name = "selfupdate")]
|
||||
SelfUpdate {
|
||||
/// Skip the confirmation prompt
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Print the error message in Python-compatible format and exit with code 2.
|
||||
@@ -159,8 +168,8 @@ fn main() {
|
||||
handle_error(e);
|
||||
}
|
||||
}
|
||||
ConfigSubcommand::List(args) => {
|
||||
if let Err(e) = crate::commands::config::config_list(args.long) {
|
||||
ConfigSubcommand::List(_) => {
|
||||
if let Err(e) = crate::commands::config::config_list() {
|
||||
handle_error(e);
|
||||
}
|
||||
}
|
||||
@@ -249,7 +258,7 @@ fn main() {
|
||||
password,
|
||||
json,
|
||||
recursive,
|
||||
quick,
|
||||
quiet,
|
||||
} => {
|
||||
if let Err(e) = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
@@ -258,7 +267,7 @@ fn main() {
|
||||
password.as_deref(),
|
||||
*json,
|
||||
*recursive,
|
||||
*quick,
|
||||
*quiet,
|
||||
))
|
||||
{
|
||||
handle_error(e);
|
||||
@@ -273,21 +282,9 @@ fn main() {
|
||||
}
|
||||
Commands::Labs { remote } => {
|
||||
let remote = remote.trim_end_matches(':');
|
||||
match crate::commands::config::get_remote_url(remote) {
|
||||
Ok(Some(url)) => {
|
||||
let conn = std::sync::Arc::new(crate::connection::MDRSConnection::new(&url));
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
if let Err(e) = rt.block_on(crate::commands::labs::labs(conn.clone(), remote)) {
|
||||
handle_error(e);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
eprintln!("Error: Remote host `{}` is not configured", remote);
|
||||
std::process::exit(2);
|
||||
}
|
||||
Err(e) => {
|
||||
handle_error(e);
|
||||
}
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
if let Err(e) = rt.block_on(commands::labs::labs(remote)) {
|
||||
handle_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,5 +353,14 @@ fn main() {
|
||||
handle_error(e);
|
||||
}
|
||||
}
|
||||
Commands::Version => {
|
||||
commands::version::version();
|
||||
}
|
||||
Commands::SelfUpdate { yes } => {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
if let Err(e) = rt.block_on(commands::selfupdate::selfupdate(*yes)) {
|
||||
handle_error(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user