first commit

This commit is contained in:
Yakumo Hokori
2026-02-04 22:09:54 +08:00
commit 5cecc46d7a
11 changed files with 3142 additions and 0 deletions

907
Cargo.lock generated Normal file
View File

@@ -0,0 +1,907 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cairo-rs"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
dependencies = [
"bitflags",
"cairo-sys-rs",
"glib 0.16.9",
"libc",
"once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421"
dependencies = [
"glib-sys 0.16.3",
"libc",
"system-deps",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "diskmanager"
version = "0.1.0"
dependencies = [
"glib 0.15.12",
"gtk4",
"regex",
"serde",
"serde_json",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "field-offset"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset",
"rustc_version",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gdk-pixbuf"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05"
dependencies = [
"bitflags",
"gdk-pixbuf-sys",
"gio",
"glib 0.16.9",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016"
dependencies = [
"gio-sys",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"libc",
"system-deps",
]
[[package]]
name = "gdk4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2181330ebf9d091f8ea7fed6877f7adc92114128592e1fdaeb1da28e0d01e9"
dependencies = [
"bitflags",
"cairo-rs",
"gdk-pixbuf",
"gdk4-sys",
"gio",
"glib 0.16.9",
"libc",
"pango",
]
[[package]]
name = "gdk4-sys"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de55cb49432901fe2b3534177fa06844665b9b0911d85d8601a8d8b88b7791db"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gio-sys",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"libc",
"pango-sys",
"pkg-config",
"system-deps",
]
[[package]]
name = "gio"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"gio-sys",
"glib 0.16.9",
"libc",
"once_cell",
"pin-project-lite",
"smallvec",
"thiserror",
]
[[package]]
name = "gio-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229"
dependencies = [
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"libc",
"system-deps",
"winapi",
]
[[package]]
name = "glib"
version = "0.15.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"glib-macros 0.15.13",
"glib-sys 0.15.10",
"gobject-sys 0.15.10",
"libc",
"once_cell",
"smallvec",
"thiserror",
]
[[package]]
name = "glib"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros 0.16.8",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"libc",
"once_cell",
"smallvec",
"thiserror",
]
[[package]]
name = "glib-macros"
version = "0.15.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a"
dependencies = [
"anyhow",
"heck 0.4.1",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "glib-macros"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b"
dependencies = [
"anyhow",
"heck 0.4.1",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "glib-sys"
version = "0.15.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
dependencies = [
"libc",
"system-deps",
]
[[package]]
name = "glib-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65"
dependencies = [
"libc",
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.15.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
dependencies = [
"glib-sys 0.15.10",
"libc",
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1"
dependencies = [
"glib-sys 0.16.3",
"libc",
"system-deps",
]
[[package]]
name = "graphene-rs"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ecb4d347e6d09820df3bdfd89a74a8eec07753a06bb92a3aac3ad31d04447b"
dependencies = [
"glib 0.16.9",
"graphene-sys",
"libc",
]
[[package]]
name = "graphene-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9aa82337d3972b4eafdea71e607c23f47be6f27f749aab613f1ad8ddbe6dcd6"
dependencies = [
"glib-sys 0.16.3",
"libc",
"pkg-config",
"system-deps",
]
[[package]]
name = "gsk4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "591239f5c52ca803b222124ac9c47f230cd180cee9b114c4d672e4a94b74f491"
dependencies = [
"bitflags",
"cairo-rs",
"gdk4",
"glib 0.16.9",
"graphene-rs",
"gsk4-sys",
"libc",
"pango",
]
[[package]]
name = "gsk4-sys"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "195a63f0be42529f98c3eb3bae0decfd0428ba2cc683b3e20ced88f340904ec5"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"graphene-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "gtk4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd89dba65def483a233dc4fdd3f3dab01576e3d83f80f6c9303ebe421661855e"
dependencies = [
"bitflags",
"cairo-rs",
"field-offset",
"futures-channel",
"gdk-pixbuf",
"gdk4",
"gio",
"glib 0.16.9",
"graphene-rs",
"gsk4",
"gtk4-macros",
"gtk4-sys",
"libc",
"once_cell",
"pango",
]
[[package]]
name = "gtk4-macros"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42829d621396a69b352d80b952dfcb4ecb4272506b2e10a65457013af1b395a4"
dependencies = [
"anyhow",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "gtk4-sys"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e370564e3fdacff7cffc99f7366b6a4689feb44e819d3ccee598a9a215b71605"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk4-sys",
"gio-sys",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"graphene-sys",
"gsk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "libc"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pango"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
dependencies = [
"bitflags",
"gio",
"glib 0.16.9",
"libc",
"once_cell",
"pango-sys",
]
[[package]]
name = "pango-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f"
dependencies = [
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
"libc",
"system-deps",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "system-deps"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"heck 0.5.0",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.27",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.7.14",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"

11
Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "diskmanager"
version = "0.1.0"
edition = "2021"
[dependencies]
gtk4 = "0.5"
glib = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.5"

203
src/application.rs Normal file
View File

@@ -0,0 +1,203 @@
use gtk4::prelude::*;
use gtk4::{Application, ApplicationWindow, Button, TextView, TreeView, Notebook, Box, Frame,
ScrolledWindow, Statusbar, Label, TreeViewColumn, CellRendererText, ListStore};
use std::rc::Rc;
use std::cell::RefCell;
// Data structures
#[derive(Debug, Clone)]
pub struct BlockDevice {
pub name: String, pub path: String, pub device_type: String,
pub size: String, pub fstype: String, pub mountpoint: String,
pub ro: bool, pub uuid: String, pub partuuid: String,
pub vendor: String, pub model: String, pub serial: String,
pub maj_min: String, pub pkname: String, pub children: Vec<BlockDevice>,
}
#[derive(Debug, Clone)]
pub struct RaidArray {
pub device: String, pub level: String, pub state: String,
pub array_size: String, pub active_devices: String, pub failed_devices: String,
pub spare_devices: String, pub total_devices: String, pub uuid: String,
pub name: String, pub chunk_size: String, pub mountpoint: String,
pub member_devices: Vec<RaidMember>,
}
#[derive(Debug, Clone)]
pub struct RaidMember { pub device_path: String, pub state: String, pub raid_device: String }
#[derive(Debug, Clone)]
pub struct LVMInfo { pub pvs: Vec<PV>, pub vgs: Vec<VG>, pub lvs: Vec<LV> }
#[derive(Debug, Clone)]
pub struct PV { pub pv_name: String, pub vg_name: String, pub pv_uuid: String, pub pv_size: String, pub pv_free: String, pub pv_attr: String, pub pv_fmt: String }
#[derive(Debug, Clone)]
pub struct VG { pub vg_name: String, pub vg_uuid: String, pub vg_size: String, pub vg_free: String, pub vg_attr: String, pub pv_count: String, pub lv_count: String, pub vg_alloc_percent: String, pub vg_fmt: String }
#[derive(Debug, Clone)]
pub struct LV { pub lv_name: String, pub vg_name: String, pub lv_uuid: String, pub lv_size: String, pub lv_attr: String, pub origin: String, pub snap_percent: String, pub lv_path: String, pub mountpoint: String }
pub struct DiskManagerApp {
pub window: ApplicationWindow,
pub refresh_btn: Button,
pub tabs: Notebook,
pub tree_block: TreeView,
pub tree_raid: TreeView,
pub tree_lvm: TreeView,
pub log_out: TextView,
pub status: Statusbar,
pub block_data: Rc<RefCell<Vec<BlockDevice>>>,
pub raid_data: Rc<RefCell<Vec<RaidArray>>>,
pub lvm_data: Rc<RefCell<LVMInfo>>,
pub block_store: Rc<RefCell<ListStore>>,
pub raid_store: Rc<RefCell<ListStore>>,
pub lvm_store: Rc<RefCell<ListStore>>,
}
impl DiskManagerApp {
pub fn new(app: &Application) -> Self {
let window = ApplicationWindow::new(app);
window.set_title(Some("Linux 存储管理工具"));
window.set_default_size(1000, 700);
// Main vertical box layout
let main_box = Box::new(gtk4::Orientation::Vertical, 0);
window.set_child(Some(&main_box));
// Refresh button
let refresh_btn = Button::with_label("刷新数据");
main_box.append(&refresh_btn);
// Tab widget
let tabs = Notebook::new();
main_box.append(&tabs);
// Tab 1: 块设备概览
let (tree_block, store_block, scroll_block) = Self::create_block_tab();
tabs.append_page(&scroll_block, Some(&Label::new(Some("块设备概览"))));
// Tab 2: RAID 管理
let (tree_raid, store_raid, scroll_raid) = Self::create_raid_tab();
tabs.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理"))));
// Tab 3: LVM 管理
let (tree_lvm, store_lvm, scroll_lvm) = Self::create_lvm_tab();
tabs.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理"))));
// Set tab index
tabs.set_current_page(Some(0));
// Log output frame
let log_frame = Frame::new(Some("日志输出"));
let log_scroll = ScrolledWindow::new();
let log_out = TextView::new();
log_out.set_editable(false);
log_scroll.set_child(Some(&log_out));
log_frame.set_child(Some(&log_scroll));
main_box.append(&log_frame);
// Status bar
let status = Statusbar::new();
main_box.append(&status);
let app_state = Self {
window,
refresh_btn: refresh_btn.clone(),
tabs,
tree_block,
tree_raid,
tree_lvm,
log_out,
status,
block_data: Rc::new(RefCell::new(Vec::new())),
raid_data: Rc::new(RefCell::new(Vec::new())),
lvm_data: Rc::new(RefCell::new(LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() })),
block_store: Rc::new(RefCell::new(store_block)),
raid_store: Rc::new(RefCell::new(store_raid)),
lvm_store: Rc::new(RefCell::new(store_lvm)),
};
app_state.window.show();
app_state
}
fn create_block_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 13 columns for block devices
let headers = ["设备名", "类型", "大小", "挂载点", "文件系统", "只读",
"UUID", "PARTUUID", "厂商", "型号", "序列号", "主次号", "父设备名"];
let store = ListStore::new(&[String::static_type(); 13]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
fn create_raid_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 12 columns for RAID arrays
let headers = ["阵列设备", "级别", "状态", "大小", "活动设备", "失败设备",
"备用设备", "总设备数", "UUID", "名称", "Chunk Size", "挂载点"];
let store = ListStore::new(&[String::static_type(); 12]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
fn create_lvm_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 8 columns for LVM
let headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"];
let store = ListStore::new(&[String::static_type(); 8]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
pub fn log(&self, msg: &str) {
let buffer = self.log_out.buffer();
let mut end = buffer.end_iter();
buffer.insert(&mut end, msg);
buffer.insert(&mut end, "\n");
self.status.push(0, msg);
}
}

89
src/command.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::process::{Command, Output};
use std::io::{self, Write};
use regex::Regex;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct CommandResult {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
pub struct CommandExecutor;
impl CommandExecutor {
/// Execute a shell command with optional sudo
pub fn run_command(args: &[&str], use_sudo: bool) -> CommandResult {
let mut cmd_args = if use_sudo {
vec!["sudo"]
} else {
vec![]
};
cmd_args.extend_from_slice(args);
let output = Command::new(&cmd_args[0])
.args(&cmd_args[1..])
.output();
match output {
Ok(o) => CommandResult {
success: o.status.success(),
stdout: String::from_utf8_lossy(&o.stdout).into_owned(),
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
},
Err(e) => CommandResult {
success: false,
stdout: String::new(),
stderr: e.to_string(),
},
}
}
/// Run command and parse JSON output
pub fn run_command_json(args: &[&str], use_sudo: bool) -> Option<Value> {
let result = Self::run_command(args, use_sudo);
if result.success {
serde_json::from_str(&result.stdout).ok()
} else {
None
}
}
/// Run command and extract value using regex
pub fn extract_value(output: &str, pattern: &str) -> Option<String> {
let re = Regex::new(pattern).ok()?;
re.captures(output)?.get(1).map(|m| m.as_str().to_string())
}
/// Check if device path exists
pub fn path_exists(path: &str) -> bool {
std::path::Path::new(path).exists()
}
/// Get real path (resolve symlinks)
pub fn realpath(path: &str) -> Option<String> {
let result = Self::run_command(&["realpath", path], false);
if result.success && !result.stdout.trim().is_empty() {
Some(result.stdout.trim().to_string())
} else {
None
}
}
/// Get device major:minor in hex
pub fn stat_maj_min(path: &str) -> Option<String> {
let result = Self::run_command(&["stat", "-c", "%t:%T", path], false);
if result.success {
let raw = result.stdout.trim();
if raw.contains(':') {
let parts: Vec<&str> = raw.split(':').collect();
if parts.len() == 2 {
let major_dec = i64::from_str_radix(parts[0], 16).ok()?;
return Some(format!("{}:{}", major_dec, parts[1]));
}
}
}
None
}
}

213
src/dialogs.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::command::CommandExecutor;
/// Simple dialog utilities using zenity or osascript
pub struct SimpleDialogs;
impl SimpleDialogs {
pub fn question(title: &str, message: &str) -> bool {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"No\", \"Yes\"}} default button \"No\" with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let result = CommandExecutor::run_command(&["osascript", "-e", &script], false);
result.success && result.stdout.contains("Yes")
}
#[cfg(not(target_os = "macos"))]
{
let result = CommandExecutor::run_command(
&["zenity", "--question", "--title", title, "--text", message],
false
);
result.success
}
}
pub fn info(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--info", "--title", title, "--text", message],
false
);
}
}
pub fn warning(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--warning", "--title", title, "--text", message],
false
);
}
}
pub fn error(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--error", "--title", title, "--text", message],
false
);
}
}
pub fn input(title: &str, message: &str, default: &str) -> Option<String> {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" default answer \"{}\" with title \"{}\"",
message.replace("\"", "\\\""), default.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let result = CommandExecutor::run_command(&["osascript", "-e", &script], false);
if result.success && result.stdout.contains("text returned:") {
if let Some(start) = result.stdout.find("text returned:") {
let text = &result.stdout[start + "text returned:".len()..];
return Some(text.trim().to_string());
}
}
Some(default.to_string())
}
#[cfg(not(target_os = "macos"))]
{
let result = CommandExecutor::run_command(
&["zenity", "--entry", "--title", title, "--text", message, "--entry-text", default],
false
);
if result.success && !result.stdout.trim().is_empty() {
Some(result.stdout.trim().to_string())
} else {
None
}
}
}
pub fn select(title: &str, message: &str, items: &[&str]) -> Option<(String, usize)> {
#[cfg(target_os = "macos")]
{
let items_str = items.join("\", \"");
let script = format!(
"choose from list {{\"{}\"}} with title \"{}\" with prompt \"{}\"",
items_str, title.replace("\"", "\\\""), message.replace("\"", "\\\"")
);
let result = CommandExecutor::run_command(&["osascript", "-e", &script], false);
if result.success && !result.stdout.trim().is_empty() && !result.stdout.trim().is_empty() {
let selected = result.stdout.trim().to_string();
for (i, item) in items.iter().enumerate() {
if *item == selected {
return Some((selected, i));
}
}
}
None
}
#[cfg(not(target_os = "macos"))]
{
let mut cmd = vec!["zenity", "--list", "--title", title, "--text", message, "--column", "选择"];
for item in items {
cmd.push(item);
}
let result = CommandExecutor::run_command(&cmd, false);
if result.success && !result.stdout.trim().is_empty() {
let selected = result.stdout.trim().to_string();
for (i, item) in items.iter().enumerate() {
if *item == selected {
return Some((selected, i));
}
}
}
None
}
}
pub fn scale(title: &str, message: &str, min: i32, max: i32, default: i32) -> Option<i32> {
#[cfg(target_os = "macos")]
{
// macOS doesn't have native scale dialog, use input with validation
if let Some(value) = Self::input(title, &format!("{} (范围: {}-{})", message, min, max), &default.to_string()) {
if let Ok(v) = value.parse::<i32>() {
if v >= min && v <= max {
return Some(v);
}
}
}
Some(default)
}
#[cfg(not(target_os = "macos"))]
{
let result = CommandExecutor::run_command(
&["zenity", "--scale", "--title", title, "--text", message,
"--min-value", &min.to_string(), "--max-value", &max.to_string(),
"--value", &default.to_string()],
false
);
if result.success {
if let Ok(v) = result.stdout.trim().parse::<i32>() {
return Some(v);
}
}
None
}
}
pub fn file_selection(title: &str, multiple: bool, directory: bool) -> Vec<String> {
#[cfg(target_os = "macos")]
{
let script = if directory {
format!("choose folder with title \"{}\"", title.replace("\"", "\\\""))
} else {
format!("choose file {}with title \"{}\"",
if multiple { "multiple " } else { "" },
title.replace("\"", "\\\""))
};
let result = CommandExecutor::run_command(&["osascript", "-e", &script], false);
if result.success && !result.stdout.trim().is_empty() {
result.stdout.trim().split(", ").map(|s| s.to_string()).collect()
} else {
Vec::new()
}
}
#[cfg(not(target_os = "macos"))]
{
let mut cmd = vec!["zenity", "--file-selection", "--title", title];
if multiple {
cmd.push("--multiple");
}
if directory {
cmd.push("--directory");
}
let result = CommandExecutor::run_command(&cmd, false);
if result.success && !result.stdout.trim().is_empty() {
result.stdout.trim().split("|").map(|s| s.to_string()).collect()
} else {
Vec::new()
}
}
}
}

259
src/disk_ops.rs Normal file
View File

@@ -0,0 +1,259 @@
use crate::command::CommandExecutor;
use crate::system_info::SystemInfoManager;
pub struct DiskOperations;
impl DiskOperations {
/// Mount partition to mountpoint
pub fn mount_partition(device_path: &str, mountpoint: &str) -> bool {
std::fs::create_dir_all(mountpoint).ok();
let result = CommandExecutor::run_command(&["mount", device_path, mountpoint], true);
result.success
}
/// Unmount partition
pub fn unmount_partition(device_path: &str) -> bool {
let result = CommandExecutor::run_command(&["umount", device_path], true);
result.success || result.stderr.contains("not mounted") || result.stderr.contains("未挂载")
}
/// Get disk free space info in MiB
pub fn get_disk_free_space_info_mib(disk_path: &str) -> Option<(f64, f64)> {
let result = CommandExecutor::run_command(&[
"parted", "-s", disk_path, "unit", "MiB", "print", "free"
], true);
if !result.success {
return None;
}
let mut free_spaces = Vec::new();
for line in result.stdout.lines() {
if let Some(caps) = regex::Regex::new(
r"\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)"
).ok()?.captures(line) {
if let (Some(start_str), Some(size_str)) = (
caps.get(1).and_then(|m| m.as_str().parse::<f64>().ok()),
caps.get(3).and_then(|m| m.as_str().parse::<f64>().ok())
) {
free_spaces.push((start_str, size_str));
}
}
}
free_spaces.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(s, sz)| (*s, *sz))
}
/// Create partition on disk
pub fn create_partition(
disk_path: &str,
partition_table_type: &str,
size_gb: f64,
total_disk_mib: f64,
use_max_space: bool
) -> bool {
let check = CommandExecutor::run_command(&["parted", "-s", disk_path, "print"], true);
let has_partition_table = check.success &&
!check.stdout.contains("Partition Table: unknown") &&
!check.stdout.contains("分区表unknown");
let start_mib = if !has_partition_table {
if !Dialogs::question(
"确认创建分区表",
&format!("磁盘 {} 没有分区表。您确定要创建 {} 分区表吗?此操作将擦除磁盘上的所有数据。", disk_path, partition_table_type)
) {
return false;
}
if !CommandExecutor::run_command(&["parted", "-s", disk_path, "mklabel", partition_table_type], true).success {
return false;
}
1.0
} else {
if let Some((start, _)) = Self::get_disk_free_space_info_mib(disk_path) {
if start < 1.0 { 1.0 } else { start }
} else {
return false;
}
};
let end_pos = if use_max_space {
"100%".to_string()
} else {
let end_mib = start_mib + size_gb * 1024.0;
if end_mib > total_disk_mib {
Dialogs::warning(
"警告",
&format!("请求的分区大小 ({:.1}GB) 超出了可用空间。将调整为最大可用空间。", size_gb)
);
"100%".to_string()
} else {
format!("{}MiB", end_mib)
}
};
let cmd = &["parted", "-s", disk_path, "mkpart", "primary", &format!("{}MiB", start_mib), &end_pos];
CommandExecutor::run_command(cmd, true).success
}
/// Delete partition
pub fn delete_partition(device_path: &str) -> bool {
Self::unmount_partition(device_path);
if let Ok(re) = regex::Regex::new(r"(/dev/[a-z]+)(\d+)") {
if let Some(caps) = re.captures(device_path) {
let disk_path = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let part_num = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if !disk_path.is_empty() && !part_num.is_empty() {
return CommandExecutor::run_command(
&["parted", "-s", disk_path, "rm", part_num],
true
).success;
}
}
}
false
}
/// Format partition
pub fn format_partition(device_path: &str, fstype: &str) -> bool {
let cmd = match fstype {
"ext4" => vec!["mkfs.ext4", "-F", device_path],
"xfs" => vec!["mkfs.xfs", "-f", device_path],
"ntfs" => vec!["mkfs.ntfs", "-f", device_path],
"fat32" => vec!["mkfs.vfat", "-F", "32", device_path],
_ => return false,
};
Self::unmount_partition(device_path);
CommandExecutor::run_command(&cmd, true).success
}
/// Get device UUID and fstype
pub fn get_device_details(device_path: &str) -> Option<(String, String)> {
let devices = SystemInfoManager::get_block_devices();
if let Some(dev) = SystemInfoManager::find_device_by_path(&devices, device_path) {
if !dev.fstype.is_empty() {
return Some((dev.uuid, dev.fstype));
}
}
if let Some(maj_min) = CommandExecutor::stat_maj_min(device_path) {
if let Some(dev) = SystemInfoManager::find_device_by_maj_min(&devices, &maj_min) {
if !dev.fstype.is_empty() {
return Some((dev.uuid, dev.fstype));
}
}
}
None
}
/// Add entry to /etc/fstab
pub fn add_to_fstab(_device_path: &str, mountpoint: &str, fstype: &str, uuid: &str) -> bool {
if uuid.is_empty() || mountpoint.is_empty() || fstype.is_empty() {
return false;
}
let entry = format!("UUID={} {} {} defaults 0 2", uuid, mountpoint, fstype);
CommandExecutor::run_command(
&["sh", "-c", &format!("echo '{}' >> /etc/fstab", entry)],
true
).success
}
/// Remove entry from /etc/fstab
pub fn remove_from_fstab(uuid: &str) -> bool {
CommandExecutor::run_command(
&["sed", "-i", &format!("/UUID={}/d", uuid), "/etc/fstab"],
true
).success
}
/// Wipe partition table
pub fn wipe_partition_table(device_path: &str) -> bool {
CommandExecutor::run_command(&["parted", "-s", device_path, "mklabel", "gpt"], true).success
}
}
pub struct Dialogs;
impl Dialogs {
pub fn question(title: &str, message: &str) -> bool {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"No\", \"Yes\"}} default button \"No\" with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let result = CommandExecutor::run_command(&["osascript", "-e", &script], false);
result.success && result.stdout.contains("Yes")
}
#[cfg(not(target_os = "macos"))]
{
let result = CommandExecutor::run_command(
&["zenity", "--question", "--title", title, "--text", message],
false
);
result.success
}
}
pub fn info(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--info", "--title", title, "--text", message],
false
);
}
}
pub fn warning(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--warning", "--title", title, "--text", message],
false
);
}
}
pub fn error(title: &str, message: &str) {
#[cfg(target_os = "macos")]
{
let script = format!(
"display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")
);
let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false);
}
#[cfg(not(target_os = "macos"))]
{
let _ = CommandExecutor::run_command(
&["zenity", "--error", "--title", title, "--text", message],
false
);
}
}
}

289
src/lvm_ops.rs Normal file
View File

@@ -0,0 +1,289 @@
use crate::command::CommandExecutor;
use crate::disk_ops::Dialogs;
pub struct LvmOperations;
impl LvmOperations {
fn execute_command(
args: &[&str],
error_msg: &str,
suppress_errors: Option<&[&str]>
) -> (bool, String, String) {
let result = CommandExecutor::run_command(args, true);
let should_suppress = if let Some(patterns) = suppress_errors {
patterns.iter().any(|p| result.stderr.contains(p))
} else {
false
};
if !result.success && !should_suppress {
Dialogs::error(error_msg, &result.stderr);
}
(result.success, result.stdout, result.stderr)
}
pub fn create_pv(device_path: &str) -> bool {
if !Dialogs::question(
"确认创建物理卷",
&format!("您确定要在设备 {} 上创建物理卷吗?\n此操作将覆盖设备上的数据。", device_path)
) {
return false;
}
let (success, _, stderr) = Self::execute_command(
&["pvcreate", "-y", device_path],
&format!("{} 上创建物理卷失败", device_path),
Some(&["device is partitioned"])
);
if success {
Dialogs::info("成功", &format!("物理卷已在 {} 上创建", device_path));
return true;
}
if stderr.contains("device is partitioned") {
if Dialogs::question(
"设备已分区",
&format!("设备 {} 已分区。您是否要擦除分区表并将其用于物理卷?", device_path)
) {
let (mklabel_success, _, _) = Self::execute_command(
&["parted", "-s", device_path, "mklabel", "gpt"],
&format!("擦除 {} 上的分区表失败", device_path),
None
);
if mklabel_success {
let (retry_success, _, _) = Self::execute_command(
&["pvcreate", "-y", device_path],
&format!("{} 上创建物理卷失败(重试)", device_path),
None
);
if retry_success {
Dialogs::info("成功", &format!("物理卷已在 {} 上创建", device_path));
return true;
}
}
} else {
Dialogs::info("信息", "未在已分区设备上创建物理卷");
}
}
false
}
pub fn delete_pv(device_path: &str) -> bool {
if !Dialogs::question(
"确认删除物理卷",
&format!("您确定要删除物理卷 {} 吗?\n如果该物理卷属于某个卷组,请先将其从卷组中移除。", device_path)
) {
return false;
}
let (success, _, _) = Self::execute_command(
&["pvremove", "-y", device_path],
&format!("删除物理卷 {} 失败", device_path),
None
);
if success {
Dialogs::info("成功", &format!("物理卷 {} 已删除", device_path));
}
success
}
pub fn create_vg(vg_name: &str, pv_paths: &[String]) -> bool {
if !Dialogs::question(
"确认创建卷组",
&format!("您确定要使用物理卷 {} 创建卷组 {} 吗?", pv_paths.join(", "), vg_name)
) {
return false;
}
let mut cmd: Vec<&str> = vec!["vgcreate", vg_name];
for pv in pv_paths {
cmd.push(pv);
}
let (success, _, _) = Self::execute_command(
&cmd,
&format!("创建卷组 {} 失败", vg_name),
None
);
if success {
Dialogs::info("成功", &format!("卷组 {} 已创建", vg_name));
}
success
}
pub fn delete_vg(vg_name: &str) -> bool {
if !Dialogs::question(
"确认删除卷组",
&format!("您确定要删除卷组 {} 吗?\n此操作将删除所有属于该卷组的逻辑卷!", vg_name)
) {
return false;
}
let (success, _, _) = Self::execute_command(
&["vgremove", "-y", "-f", vg_name],
&format!("删除卷组 {} 失败", vg_name),
None
);
if success {
Dialogs::info("成功", &format!("卷组 {} 已删除", vg_name));
}
success
}
pub fn create_lv(lv_name: &str, vg_name: &str, size_gb: f64, use_max_space: bool) -> bool {
let message = if use_max_space {
format!("您确定要在卷组 {} 中创建逻辑卷 {} 吗?\n使用卷组所有可用空间。", vg_name, lv_name)
} else {
format!("您确定要在卷组 {} 中创建 {}GB 的逻辑卷 {} 吗?", vg_name, size_gb, lv_name)
};
if !Dialogs::question("确认创建逻辑卷", &message) {
return false;
}
let size_str = format!("{}G", size_gb);
let cmd: Vec<&str> = if use_max_space {
vec!["lvcreate", "-y", "-l", "100%FREE", "-n", lv_name, vg_name]
} else {
vec!["lvcreate", "-y", "-L", &size_str, "-n", lv_name, vg_name]
};
let (success, _, _) = Self::execute_command(
&cmd,
&format!("创建逻辑卷 {}/{} 失败", vg_name, lv_name),
None
);
if success {
Dialogs::info("成功", &format!("逻辑卷 {}/{} 已创建", vg_name, lv_name));
}
success
}
pub fn delete_lv(lv_name: &str, vg_name: &str) -> bool {
if !Dialogs::question(
"确认删除逻辑卷",
&format!("您确定要删除逻辑卷 {}/{} 吗?\n此操作将擦除所有数据!", vg_name, lv_name)
) {
return false;
}
let path = format!("{}/{}", vg_name, lv_name);
let (success, _, _) = Self::execute_command(
&["lvremove", "-y", &path],
&format!("删除逻辑卷 {} 失败", path),
None
);
if success {
Dialogs::info("成功", &format!("逻辑卷 {} 已删除", path));
}
success
}
pub fn activate_lv(lv_name: &str, vg_name: &str) -> bool {
let path = format!("{}/{}", vg_name, lv_name);
let (success, _, _) = Self::execute_command(
&["lvchange", "-ay", &path],
&format!("激活逻辑卷 {} 失败", path),
None
);
if success {
Dialogs::info("成功", &format!("逻辑卷 {} 已激活", path));
}
success
}
pub fn deactivate_lv(lv_name: &str, vg_name: &str) -> bool {
let path = format!("{}/{}", vg_name, lv_name);
let (success, _, _) = Self::execute_command(
&["lvchange", "-an", &path],
&format!("停用逻辑卷 {} 失败", path),
None
);
if success {
Dialogs::info("成功", &format!("逻辑卷 {} 已停用", path));
}
success
}
pub fn extend_vg(vg_name: &str, pv_path: &str) -> bool {
if !Dialogs::question(
"确认扩展卷组",
&format!("您确定要将物理卷 {} 添加到卷组 {} 吗?", pv_path, vg_name)
) {
return false;
}
let (success, _, _) = Self::execute_command(
&["vgextend", vg_name, pv_path],
&format!("扩展卷组 {} 失败", vg_name),
None
);
if success {
Dialogs::info("成功", &format!("卷组 {} 已扩展", vg_name));
}
success
}
pub fn reduce_vg(vg_name: &str, pv_path: &str) -> bool {
if !Dialogs::question(
"确认缩减卷组",
&format!("您确定要从卷组 {} 中移除物理卷 {} 吗?\n确保该物理卷未被使用。", vg_name, pv_path)
) {
return false;
}
let (success, _, _) = Self::execute_command(
&["vgreduce", vg_name, pv_path],
&format!("缩减卷组 {} 失败", vg_name),
None
);
if success {
Dialogs::info("成功", &format!("卷组 {} 已缩减", vg_name));
}
success
}
pub fn pvmove(from_pv: &str) -> bool {
if !Dialogs::question(
"确认移动数据",
&format!("您确定要将物理卷 {} 上的数据移动到其他物理卷吗?\n此操作可能需要较长时间。", from_pv)
) {
return false;
}
let (success, _, _) = Self::execute_command(
&["pvmove", from_pv],
&format!("移动物理卷 {} 上的数据失败", from_pv),
None
);
if success {
Dialogs::info("成功", &format!("已完成移动物理卷 {} 上的数据", from_pv));
}
success
}
}

354
src/main.rs Normal file
View File

@@ -0,0 +1,354 @@
mod command;
mod system_info;
mod disk_ops;
mod raid_ops;
mod lvm_ops;
use gtk4::prelude::*;
use gtk4::{Application, ApplicationWindow, Button, TextView, TreeView, Notebook, Box, Frame,
ScrolledWindow, Statusbar, Label, TreeViewColumn, CellRendererText, ListStore};
use std::rc::Rc;
use std::cell::RefCell;
use crate::system_info::{SystemInfoManager, BlockDevice, RaidArray, LVMInfo};
pub struct DiskManagerApp {
window: ApplicationWindow,
refresh_btn: Button,
tabs: Notebook,
tree_block: TreeView,
tree_raid: TreeView,
tree_lvm: TreeView,
log_out: TextView,
status: Statusbar,
block_data: Rc<RefCell<Vec<BlockDevice>>>,
raid_data: Rc<RefCell<Vec<RaidArray>>>,
lvm_data: Rc<RefCell<LVMInfo>>,
block_store: Rc<RefCell<ListStore>>,
raid_store: Rc<RefCell<ListStore>>,
lvm_store: Rc<RefCell<ListStore>>,
}
impl DiskManagerApp {
pub fn new(app: &Application) -> Self {
let window = ApplicationWindow::new(app);
window.set_title(Some("Linux 存储管理工具"));
window.set_default_size(1000, 700);
let main_box = Box::new(gtk4::Orientation::Vertical, 0);
window.set_child(Some(&main_box));
let refresh_btn = Button::with_label("刷新数据");
main_box.append(&refresh_btn);
let tabs = Notebook::new();
main_box.append(&tabs);
let (tree_block, store_block, scroll_block) = Self::create_block_tab();
tabs.append_page(&scroll_block, Some(&Label::new(Some("块设备概览"))));
let (tree_raid, store_raid, scroll_raid) = Self::create_raid_tab();
tabs.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理"))));
let (tree_lvm, store_lvm, scroll_lvm) = Self::create_lvm_tab();
tabs.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理"))));
tabs.set_current_page(Some(0));
let log_frame = Frame::new(Some("日志输出"));
let log_scroll = ScrolledWindow::new();
let log_out = TextView::new();
log_out.set_editable(false);
log_scroll.set_child(Some(&log_out));
log_frame.set_child(Some(&log_scroll));
main_box.append(&log_frame);
let status = Statusbar::new();
main_box.append(&status);
let app_state = Self {
window,
refresh_btn: refresh_btn.clone(),
tabs,
tree_block,
tree_raid,
tree_lvm,
log_out,
status,
block_data: Rc::new(RefCell::new(Vec::new())),
raid_data: Rc::new(RefCell::new(Vec::new())),
lvm_data: Rc::new(RefCell::new(LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() })),
block_store: Rc::new(RefCell::new(store_block)),
raid_store: Rc::new(RefCell::new(store_raid)),
lvm_store: Rc::new(RefCell::new(store_lvm)),
};
{
let app = app_state.clone();
refresh_btn.connect_clicked(move |_| {
app.log("开始刷新所有设备信息...");
app.refresh_block_devices();
app.refresh_raid();
app.refresh_lvm();
app.log("所有设备信息刷新完成。");
});
}
app_state.window.show();
app_state
}
fn create_block_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
let headers = ["设备名", "类型", "大小", "挂载点", "文件系统", "只读",
"UUID", "PARTUUID", "厂商", "型号", "序列号", "主次号", "父设备名"];
let store = ListStore::new(&[String::static_type(); 13]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
fn create_raid_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
let headers = ["阵列设备", "级别", "状态", "大小", "活动设备", "失败设备",
"备用设备", "总设备数", "UUID", "名称", "Chunk Size", "挂载点"];
let store = ListStore::new(&[String::static_type(); 12]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
fn create_lvm_tab() -> (TreeView, ListStore, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
let headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"];
let store = ListStore::new(&[String::static_type(); 8]);
tree.set_model(Some(&store));
for (i, header) in headers.iter().enumerate() {
let col = TreeViewColumn::new();
let cell = CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(header);
col.set_resizable(true);
col.add_attribute(&cell, "text", i as i32);
tree.append_column(&col);
}
(tree, store, scrolled)
}
pub fn log(&self, msg: &str) {
let buffer = self.log_out.buffer();
let mut end = buffer.end_iter();
buffer.insert(&mut end, msg);
buffer.insert(&mut end, "\n");
self.status.push(0, msg);
}
pub fn refresh_block_devices(&self) {
self.log("刷新块设备信息...");
let devices = SystemInfoManager::get_block_devices();
*self.block_data.borrow_mut() = devices;
let store = self.block_store.borrow_mut();
store.clear();
for dev in self.block_data.borrow().iter() {
Self::add_block_device(&store, dev, None);
}
self.log("块设备信息刷新成功。");
}
fn add_block_device(store: &ListStore, dev: &BlockDevice, _parent: Option<&gtk4::TreeIter>) {
let iter = store.append();
let ro_text = if dev.ro { "" } else { "" }.to_string();
store.set(&iter, &[
(0, &dev.name),
(1, &dev.device_type),
(2, &dev.size),
(3, &dev.mountpoint),
(4, &dev.fstype),
(5, &ro_text),
(6, &dev.uuid),
(7, &dev.partuuid),
(8, &dev.vendor),
(9, &dev.model),
(10, &dev.serial),
(11, &dev.maj_min),
(12, &dev.pkname),
]);
for child in &dev.children {
Self::add_block_device(store, child, Some(&iter));
}
}
pub fn refresh_raid(&self) {
self.log("刷新 RAID 信息...");
let arrays = SystemInfoManager::get_mdadm_arrays();
*self.raid_data.borrow_mut() = arrays;
let store = self.raid_store.borrow_mut();
store.clear();
for array in self.raid_data.borrow().iter() {
let iter = store.append();
store.set(&iter, &[
(0, &array.device),
(1, &array.level),
(2, &array.state),
(3, &array.array_size),
(4, &array.active_devices),
(5, &array.failed_devices),
(6, &array.spare_devices),
(7, &array.total_devices),
(8, &array.uuid),
(9, &array.name),
(10, &array.chunk_size),
(11, &array.mountpoint),
]);
}
self.log("RAID 信息刷新成功。");
}
pub fn refresh_lvm(&self) {
self.log("刷新 LVM 信息...");
let info = SystemInfoManager::get_lvm_info();
*self.lvm_data.borrow_mut() = info;
let store = self.lvm_store.borrow_mut();
store.clear();
let info = self.lvm_data.borrow();
// PVs section
let pv_iter = store.append();
store.set(&pv_iter, &[
(0, &"物理卷 (PVs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"")
]);
for pv in &info.pvs {
let iter = store.append();
store.set(&iter, &[
(0, &pv.pv_name),
(1, &pv.pv_size),
(2, &pv.pv_attr),
(3, &pv.pv_uuid),
(4, &format!("VG: {}", pv.vg_name)),
(5, &format!("空闲: {}", pv.pv_free)),
(6, &pv.pv_fmt),
(7, &""),
]);
}
// VGs section
let vg_iter = store.append();
store.set(&vg_iter, &[
(0, &"卷组 (VGs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"")
]);
for vg in &info.vgs {
let iter = store.append();
store.set(&iter, &[
(0, &vg.vg_name),
(1, &vg.vg_size),
(2, &vg.vg_attr),
(3, &vg.vg_uuid),
(4, &format!("PVs: {}, LVs: {}", vg.pv_count, vg.lv_count)),
(5, &format!("空闲: {}, 已分配: {}%", vg.vg_free, vg.vg_alloc_percent)),
(6, &vg.vg_fmt),
(7, &""),
]);
}
// LVs section
let lv_iter = store.append();
store.set(&lv_iter, &[
(0, &"逻辑卷 (LVs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"")
]);
for lv in &info.lvs {
let iter = store.append();
store.set(&iter, &[
(0, &lv.lv_name),
(1, &lv.lv_size),
(2, &lv.lv_attr),
(3, &lv.lv_uuid),
(4, &format!("VG: {}, Origin: {}", lv.vg_name, lv.origin)),
(5, &format!("快照: {}%", lv.snap_percent)),
(6, &lv.lv_path),
(7, &lv.mountpoint),
]);
}
self.log("LVM 信息刷新成功。");
}
}
impl Clone for DiskManagerApp {
fn clone(&self) -> Self {
Self {
window: self.window.clone(),
refresh_btn: self.refresh_btn.clone(),
tabs: self.tabs.clone(),
tree_block: self.tree_block.clone(),
tree_raid: self.tree_raid.clone(),
tree_lvm: self.tree_lvm.clone(),
log_out: self.log_out.clone(),
status: self.status.clone(),
block_data: self.block_data.clone(),
raid_data: self.raid_data.clone(),
lvm_data: self.lvm_data.clone(),
block_store: self.block_store.clone(),
raid_store: self.raid_store.clone(),
lvm_store: self.lvm_store.clone(),
}
}
}
fn main() {
let app = Application::new(
Some("com.diskmanager.app"),
Default::default(),
);
app.connect_activate(|app| {
let _window = DiskManagerApp::new(app);
});
app.run();
}

246
src/raid_ops.rs Normal file
View File

@@ -0,0 +1,246 @@
use crate::command::CommandExecutor;
use crate::system_info::{SystemInfoManager, RaidArray};
use crate::disk_ops::Dialogs;
pub struct RaidOperations;
impl RaidOperations {
fn execute_command(args: &[&str], error_msg: &str, suppress_errors: Option<&[&str]>)-> (bool, String, String) {
let result = CommandExecutor::run_command(args, true);
let should_suppress = if let Some(patterns) = suppress_errors {
patterns.iter().any(|p| result.stderr.contains(p))
} else {
false
};
if !result.success && !should_suppress {
Dialogs::error(error_msg, &result.stderr);
}
(result.success, result.stdout, result.stderr)
}
fn get_next_md_name() -> String {
let mut existing = std::collections::HashSet::new();
let arrays = SystemInfoManager::get_mdadm_arrays();
for array in arrays {
if let Some(name) = array.device.strip_prefix("/dev/md") {
if let Ok(num) = name.parse::<u32>() {
existing.insert(num);
}
}
}
let mut num = 0u32;
while existing.contains(&num) {
num += 1;
}
format!("/dev/md{}", num)
}
pub fn create_raid_array(devices: &[String], level: &str, chunk_size: &str) -> bool {
let device_count = devices.len();
match level {
"raid0" | "0" => if device_count < 1 {
Dialogs::error("错误", "RAID0 至少需要1个设备");
return false;
},
"raid1" | "1" => if device_count < 2 {
Dialogs::error("错误", "RAID1 至少需要2个设备");
return false;
},
"raid5" | "5" => if device_count < 3 {
Dialogs::error("错误", "RAID5 至少需要3个设备");
return false;
},
"raid6" | "6" => if device_count < 4 {
Dialogs::error("错误", "RAID6 至少需要4个设备");
return false;
},
"raid10" | "10" => if device_count < 2 {
Dialogs::error("错误", "RAID10 至少需要2个设备");
return false;
},
_ => {},
}
if !Dialogs::question(
"确认创建 RAID 阵列",
&format!("您确定要使用设备 {} 创建 RAID {} 阵列吗?\n此操作将销毁设备上的所有数据!",
devices.join(", "), level)
) {
return false;
}
for dev in devices {
let (success, _, stderr) = Self::execute_command(
&["mdadm", "--zero-superblock", "--force", dev],
&format!("清除设备 {} 上的 RAID 超级块失败", dev),
Some(&["Unrecognised md component device"])
);
if !success && !stderr.contains("Unrecognised md component device") {
return false;
}
}
let array_name = Self::get_next_md_name();
let level_num = level.trim_start_matches("raid");
let mut cmd: Vec<String> = vec!["mdadm".to_string(), "--create".to_string(), array_name.clone(),
format!("--level={}", level_num), format!("--raid-devices={}", device_count)];
if ["raid0", "raid5", "raid6", "raid10", "0", "5", "6", "10"].contains(&level) {
cmd.push(format!("--chunk={}K", chunk_size));
}
cmd.push("--force".to_string());
for dev in devices {
cmd.push(dev.clone());
}
let cmd_args: Vec<&str> = cmd.iter().map(|s| s.as_str()).collect();
let (success, stdout, _) = Self::execute_command(&cmd_args, "创建 RAID 阵列失败", None);
if success {
Dialogs::info("成功", &format!("成功创建 RAID {} 阵列 {}", level, array_name));
let (scan_success, scan_stdout, _) = Self::execute_command(
&["mdadm", "--examine", "--scan"],
"扫描 mdadm 配置失败",
None
);
if scan_success {
let _ = CommandExecutor::run_command(&["mkdir", "-p", "/etc"], false);
let _ = CommandExecutor::run_command(
&["sh", "-c", &format!("echo '{}' | tee -a /etc/mdadm.conf", scan_stdout.trim())],
true
);
}
true
} else {
if stdout.contains("Array name") && stdout.contains("is in use already") {
Dialogs::error("错误", "阵列名称已被占用,请尝试停止或删除现有阵列");
}
false
}
}
pub fn stop_raid_array(array_path: &str) -> bool {
if !Dialogs::question(
"确认停止 RAID 阵列",
&format!("您确定要停止 RAID 阵列 {} 吗?\n停止阵列将使其无法访问。", array_path)
) {
return false;
}
let _ = CommandExecutor::run_command(&["umount", array_path], true);
let (success, _, _) = Self::execute_command(
&["mdadm", "--stop", array_path],
&format!("停止 RAID 阵列 {} 失败", array_path),
Some(&["not mounted", "未挂载"])
);
if success {
Dialogs::info("成功", &format!("成功停止 RAID 阵列 {}", array_path));
}
success
}
pub fn delete_active_raid_array(array: &RaidArray) -> bool {
if !Dialogs::question(
"确认删除 RAID 阵列",
&format!("您确定要删除 RAID 阵列 {} 吗?\n此操作将停止阵列并清除成员设备上的数据!", array.device)
) {
return false;
}
if !Self::stop_raid_array(&array.device) {
Dialogs::error("错误", "无法停止阵列");
return false;
}
let mut all_cleared = true;
for member in &array.member_devices {
let (success, _, stderr) = Self::execute_command(
&["mdadm", "--zero-superblock", "--force", &member.device_path],
&format!("清除设备 {} 上的 RAID 超级块失败", member.device_path),
Some(&["Unrecognised md component device"])
);
if !success && !stderr.contains("Unrecognised md component device") {
all_cleared = false;
}
}
let _ = Self::delete_config_from_mdadm_conf(&array.uuid);
if all_cleared {
Dialogs::info("成功", &format!("成功删除 RAID 阵列 {}", array.device));
} else {
Dialogs::error("错误", "删除完成,但部分设备超级块未能完全清除");
}
true
}
pub fn delete_configured_raid_array(uuid: &str) -> bool {
if !Dialogs::question(
"确认删除 RAID 阵列配置",
&format!("您确定要删除 UUID 为 {} 的 RAID 阵列配置吗?\n此操作只删除配置文件中的条目。", uuid)
) {
return false;
}
let result = Self::delete_config_from_mdadm_conf(uuid);
if result {
Dialogs::info("成功", "成功删除 RAID 阵列配置");
}
result
}
fn delete_config_from_mdadm_conf(uuid: &str) -> bool {
for conf_path in ["/etc/mdadm/mdadm.conf", "/etc/mdadm.conf"] {
if CommandExecutor::path_exists(conf_path) {
let content = CommandExecutor::run_command(&["cat", conf_path], false);
if content.success {
let lines: Vec<&str> = content.stdout.lines()
.filter(|line| !line.trim_start().starts_with("ARRAY") ||
!line.contains(uuid))
.collect();
if lines.len() != content.stdout.lines().count() {
let new_content = lines.join("\n");
let _ = CommandExecutor::run_command(
&["sh", "-c", &format!("echo '{}' > {}", new_content, conf_path)],
true
);
return true;
}
}
}
}
false
}
pub fn activate_raid_array(array_path: &str, uuid: &str) -> bool {
let (success, _, _) = Self::execute_command(
&["mdadm", "--assemble", array_path, "--uuid", uuid],
&format!("激活 RAID 阵列 {} 失败", array_path),
Some(&["already active"])
);
if success {
Dialogs::info("成功", &format!("成功激活 RAID 阵列 {}", array_path));
}
success
}
}

421
src/system_info.rs Normal file
View File

@@ -0,0 +1,421 @@
use crate::command::CommandExecutor;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct BlockDevice {
pub name: String,
pub path: String,
pub device_type: String,
pub size: String,
pub fstype: String,
pub mountpoint: String,
pub ro: bool,
pub uuid: String,
pub partuuid: String,
pub vendor: String,
pub model: String,
pub serial: String,
pub maj_min: String,
pub pkname: String,
pub children: Vec<BlockDevice>,
}
#[derive(Debug, Clone)]
pub struct RaidMember {
pub number: String,
pub major: String,
pub minor: String,
pub raid_device: String,
pub state: String,
pub device_path: String,
}
#[derive(Debug, Clone)]
pub struct RaidArray {
pub device: String,
pub level: String,
pub state: String,
pub array_size: String,
pub active_devices: String,
pub failed_devices: String,
pub spare_devices: String,
pub total_devices: String,
pub uuid: String,
pub name: String,
pub chunk_size: String,
pub mountpoint: String,
pub member_devices: Vec<RaidMember>,
}
#[derive(Debug, Clone)]
pub struct PV {
pub pv_name: String,
pub vg_name: String,
pub pv_uuid: String,
pub pv_size: String,
pub pv_free: String,
pub pv_attr: String,
pub pv_fmt: String,
}
#[derive(Debug, Clone)]
pub struct VG {
pub vg_name: String,
pub vg_uuid: String,
pub vg_size: String,
pub vg_free: String,
pub vg_attr: String,
pub pv_count: String,
pub lv_count: String,
pub vg_alloc_percent: String,
pub vg_fmt: String,
}
#[derive(Debug, Clone)]
pub struct LV {
pub lv_name: String,
pub vg_name: String,
pub lv_uuid: String,
pub lv_size: String,
pub lv_attr: String,
pub origin: String,
pub snap_percent: String,
pub lv_path: String,
pub mountpoint: String,
}
#[derive(Debug, Clone)]
pub struct LVMInfo {
pub pvs: Vec<PV>,
pub vgs: Vec<VG>,
pub lvs: Vec<LV>,
}
pub struct SystemInfoManager;
impl SystemInfoManager {
/// Get all block devices using lsblk
pub fn get_block_devices() -> Vec<BlockDevice> {
let result = CommandExecutor::run_command(&[
"lsblk", "-J", "-o",
"NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH"
], false);
if let Some(data) = CommandExecutor::run_command_json(&[
"lsblk", "-J", "-o",
"NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH"
], false) {
if let Some(devs) = data.get("blockdevices").and_then(|v| v.as_array()) {
return devs.iter().filter_map(|d| Self::parse_device(d)).collect();
}
}
Vec::new()
}
fn parse_device(d: &Value) -> Option<BlockDevice> {
let name = d.get("NAME")?.as_str()?.to_string();
Some(BlockDevice {
name: name.clone(),
path: d.get("PATH").and_then(|v| v.as_str()).unwrap_or(&format!("/dev/{}", name)).to_string(),
device_type: d.get("TYPE")?.as_str()?.to_string(),
size: d.get("SIZE")?.as_str()?.to_string(),
fstype: d.get("FSTYPE")?.as_str()?.to_string(),
mountpoint: d.get("MOUNTPOINT")?.as_str()?.to_string(),
ro: d.get("RO")?.as_u64()? == 1,
uuid: d.get("UUID")?.as_str()?.to_string(),
partuuid: d.get("PARTUUID")?.as_str()?.to_string(),
vendor: d.get("VENDOR")?.as_str()?.to_string(),
model: d.get("MODEL")?.as_str()?.to_string(),
serial: d.get("SERIAL")?.as_str()?.to_string(),
maj_min: d.get("MAJ:MIN")?.as_str()?.to_string(),
pkname: d.get("PKNAME")?.as_str()?.to_string(),
children: d.get("children").and_then(|v| v.as_array())
.map(|c| c.iter().filter_map(|d| Self::parse_device(d)).collect())
.unwrap_or_default(),
})
}
/// Find device by path recursively
pub fn find_device_by_path(devices: &[BlockDevice], target_path: &str) -> Option<BlockDevice> {
for dev in devices {
if dev.path == target_path {
return Some(dev.clone());
}
if let Some(found) = Self::find_in_children(&dev.children, target_path) {
return Some(found);
}
}
None
}
fn find_in_children(children: &[BlockDevice], target_path: &str) -> Option<BlockDevice> {
for child in children {
if child.path == target_path {
return Some(child.clone());
}
if let Some(found) = Self::find_in_children(&child.children, target_path) {
return Some(found);
}
}
None
}
/// Find device by maj:min
pub fn find_device_by_maj_min(devices: &[BlockDevice], target_maj_min: &str) -> Option<BlockDevice> {
for dev in devices {
if dev.maj_min == target_maj_min {
return Some(dev.clone());
}
if let Some(found) = Self::find_maj_min_in_children(&dev.children, target_maj_min) {
return Some(found);
}
}
None
}
fn find_maj_min_in_children(children: &[BlockDevice], target_maj_min: &str) -> Option<BlockDevice> {
for child in children {
if child.maj_min == target_maj_min {
return Some(child.clone());
}
if let Some(found) = Self::find_maj_min_in_children(&child.children, target_maj_min) {
return Some(found);
}
}
None
}
/// Get mountpoint for device
pub fn get_mountpoint_for_device(device_path: &str) -> Option<String> {
let devices = Self::get_block_devices();
// Try direct path lookup
if let Some(dev) = Self::find_device_by_path(&devices, device_path) {
if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" {
return Some(dev.mountpoint);
}
}
// Try resolving symlink
if let Some(resolved) = CommandExecutor::realpath(device_path) {
if let Some(dev) = Self::find_device_by_path(&devices, &resolved) {
if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" {
return Some(dev.mountpoint);
}
}
}
// Try by maj:min
if let Some(maj_min) = CommandExecutor::stat_maj_min(device_path) {
if let Some(dev) = Self::find_device_by_maj_min(&devices, &maj_min) {
if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" {
return Some(dev.mountpoint);
}
}
}
None
}
/// Get all RAID arrays
pub fn get_mdadm_arrays() -> Vec<RaidArray> {
let mut arrays = Vec::new();
// Get active arrays from mdadm --detail --scan
let scan_result = CommandExecutor::run_command(&["mdadm", "--detail", "--scan"], true);
if scan_result.success {
for line in scan_result.stdout.lines() {
if line.starts_with("ARRAY") {
if let Some((device, uuid)) = Self::parse_array_line(line) {
let detail = CommandExecutor::run_command(&["mdadm", "--detail", &device], true);
if detail.success {
if let Some(array) = Self::parse_mdadm_detail(&device, &uuid, &detail.stdout) {
arrays.push(array);
}
}
}
}
}
}
arrays
}
fn parse_array_line(line: &str) -> Option<(String, String)> {
let re = regex::Regex::new(r"ARRAY\s+(\S+)(?:\s+\S+=\S+)*\s+UUID=([0-9a-f:]+)").ok()?;
let caps = re.captures(line)?;
Some((
caps.get(1)?.as_str().to_string(),
caps.get(2)?.as_str().to_string(),
))
}
fn parse_mdadm_detail(device: &str, uuid: &str, output: &str) -> Option<RaidArray> {
let name = CommandExecutor::extract_value(output, r"Name\s*:\s*(.+)")
.unwrap_or_else(|| std::path::Path::new(device).file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| device.to_string()));
let state = CommandExecutor::extract_value(output, r"State\s*:\s*(.+)")
.unwrap_or_else(|| {
if output.to_lowercase().contains("clean") && output.to_lowercase().contains("active") {
"Active, Clean".to_string()
} else if output.to_lowercase().contains("degraded") {
"Degraded".to_string()
} else {
"N/A".to_string()
}
});
let mut members = Vec::new();
let re = regex::Regex::new(r"\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([^\s]+(?:\s+[^\s]+)*)\s+(/dev/\S+)").ok()?;
for line in output.lines() {
if let Some(caps) = re.captures(line) {
members.push(RaidMember {
number: caps.get(1)?.as_str().to_string(),
major: caps.get(2)?.as_str().to_string(),
minor: caps.get(3)?.as_str().to_string(),
raid_device: caps.get(4)?.as_str().to_string(),
state: caps.get(5)?.as_str().to_string().trim().to_string(),
device_path: caps.get(6)?.as_str().to_string(),
});
}
}
let mountpoint = Self::get_mountpoint_for_device(device);
Some(RaidArray {
device: device.to_string(),
level: CommandExecutor::extract_value(output, r"Raid Level\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
state,
array_size: CommandExecutor::extract_value(output, r"Array Size\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
active_devices: CommandExecutor::extract_value(output, r"Active Devices\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
failed_devices: CommandExecutor::extract_value(output, r"Failed Devices\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
spare_devices: CommandExecutor::extract_value(output, r"Spare Devices\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
total_devices: CommandExecutor::extract_value(output, r"Raid Devices\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
uuid: uuid.to_string(),
name,
chunk_size: CommandExecutor::extract_value(output, r"Chunk Size\s*:\s*(.+)")
.unwrap_or_else(|| "N/A".to_string()),
mountpoint: mountpoint.unwrap_or_default(),
member_devices: members,
})
}
/// Get LVM info
pub fn get_lvm_info() -> LVMInfo {
let mut info = LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() };
// Get PVs
if let Some(data) = CommandExecutor::run_command_json(&["pvs", "--reportformat", "json"], false) {
if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) {
if let Some(pvs) = report.get("pv").and_then(|v| v.as_array()) {
for p in pvs {
info.pvs.push(PV {
pv_name: p.get("pv_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_name: p.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_uuid: p.get("pv_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_size: p.get("pv_size").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_free: p.get("pv_free").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_attr: p.get("pv_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_fmt: p.get("pv_fmt").and_then(|v| v.as_str()).unwrap_or("").to_string(),
});
}
}
}
}
// Get VGs
if let Some(data) = CommandExecutor::run_command_json(&["vgs", "--reportformat", "json"], false) {
if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) {
if let Some(vgs) = report.get("vg").and_then(|v| v.as_array()) {
for v in vgs {
info.vgs.push(VG {
vg_name: v.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_uuid: v.get("vg_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_size: v.get("vg_size").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_free: v.get("vg_free").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_attr: v.get("vg_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(),
pv_count: v.get("pv_count").and_then(|v| v.as_str()).unwrap_or("").to_string(),
lv_count: v.get("lv_count").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_alloc_percent: v.get("vg_alloc_percent").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_fmt: v.get("vg_fmt").and_then(|v| v.as_str()).unwrap_or("").to_string(),
});
}
}
}
}
// Get LVs
if let Some(data) = CommandExecutor::run_command_json(&[
"lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path",
"--reportformat", "json"
], false) {
if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) {
if let Some(lvs) = report.get("lv").and_then(|v| v.as_array()) {
for l in lvs {
let lv_path = l.get("lv_path").and_then(|v| v.as_str()).unwrap_or("").to_string();
let mountpoint = Self::get_mountpoint_for_device(&lv_path);
info.lvs.push(LV {
lv_name: l.get("lv_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vg_name: l.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
lv_uuid: l.get("lv_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(),
lv_size: l.get("lv_size").and_then(|v| v.as_str()).unwrap_or("").to_string(),
lv_attr: l.get("lv_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(),
origin: l.get("origin").and_then(|v| v.as_str()).unwrap_or("").to_string(),
snap_percent: l.get("snap_percent").and_then(|v| v.as_str()).unwrap_or("").to_string(),
lv_path,
mountpoint: mountpoint.unwrap_or_default(),
});
}
}
}
}
info
}
/// Get unallocated partitions
pub fn get_unallocated_partitions() -> Vec<String> {
let mut candidates = Vec::new();
let devices = Self::get_block_devices();
fn process_dev(dev: &BlockDevice, candidates: &mut Vec<String>) {
let mountpoint = &dev.mountpoint;
let fstype = &dev.fstype;
if !mountpoint.is_empty() && mountpoint != "[SWAP]" {
for child in &dev.children { process_dev(child, candidates); }
return;
}
if fstype == "LVM2_member" || fstype == "linux_raid_member" {
for child in &dev.children { process_dev(child, candidates); }
return;
}
if dev.device_type == "disk" && dev.children.is_empty() {
candidates.push(dev.path.clone());
} else if dev.device_type == "part" {
candidates.push(dev.path.clone());
}
if dev.device_type == "disk" && !dev.children.is_empty() {
for child in &dev.children { process_dev(child, candidates); }
}
}
for dev in &devices { process_dev(dev, &mut candidates); }
candidates.sort();
candidates.dedup();
candidates
}
}

150
src/ui.rs Normal file
View File

@@ -0,0 +1,150 @@
use gtk4::{prelude::*, Box, Button, TextView, TreeView, Notebook, Statusbar,
ScrolledWindow, Frame, Label, ApplicationWindow};
use std::rc::Rc;
pub struct DiskManagerUI {
pub main_container: Box,
pub refresh_button: Button,
pub tab_widget: Notebook,
pub tree_block_devices: TreeView,
pub tree_raid: TreeView,
pub tree_lvm: TreeView,
pub log_output: TextView,
pub statusbar: Statusbar,
}
impl DiskManagerUI {
pub fn new() -> Rc<Self> {
// Main vertical box
let main_container = Box::new(gtk4::Orientation::Vertical, 0);
// Refresh button
let refresh_button = Button::with_label("刷新数据");
main_container.append(&refresh_button);
// Tab widget
let tab_widget = Notebook::new();
main_container.append(&tab_widget);
// Tab 1: 块设备概览
let (tree_block_devices, scroll_block) = Self::create_block_device_tab();
tab_widget.append_page(&scroll_block, Some(&Label::new(Some("块设备概览"))));
// Tab 2: RAID 管理
let (tree_raid, scroll_raid) = Self::create_raid_tab();
tab_widget.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理"))));
// Tab 3: LVM 管理
let (tree_lvm, scroll_lvm) = Self::create_lvm_tab();
tab_widget.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理"))));
tab_widget.set_current_page(Some(0));
// Log output frame
let log_frame = Frame::new(Some("日志输出"));
let log_scroll = ScrolledWindow::new();
let log_output = TextView::new();
log_output.set_editable(false);
log_scroll.set_child(Some(&log_output));
log_frame.set_child(Some(&log_scroll));
main_container.append(&log_frame);
// Status bar
let statusbar = Statusbar::new();
main_container.append(&statusbar);
Rc::new(Self {
main_container,
refresh_button,
tab_widget,
tree_block_devices,
tree_raid,
tree_lvm,
log_output,
statusbar,
})
}
fn create_block_device_tab() -> (TreeView, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 13 columns
let columns = [
("设备名", 0), ("类型", 1), ("大小", 2), ("挂载点", 3),
("文件系统", 4), ("只读", 5), ("UUID", 6), ("PARTUUID", 7),
("厂商", 8), ("型号", 9), ("序列号", 10), ("主次号", 11), ("父设备名", 12)
];
for (title, idx) in columns.iter() {
let col = gtk4::TreeViewColumn::new();
let cell = gtk4::CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(title);
col.set_resizable(true);
col.add_attribute(&cell, "text", *idx);
tree.append_column(&col);
}
(tree, scrolled)
}
fn create_raid_tab() -> (TreeView, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 12 columns
let columns = [
("阵列设备", 0), ("级别", 1), ("状态", 2), ("大小", 3),
("活动设备", 4), ("失败设备", 5), ("备用设备", 6),
("总设备数", 7), ("UUID", 8), ("名称", 9),
("Chunk Size", 10), ("挂载点", 11)
];
for (title, idx) in columns.iter() {
let col = gtk4::TreeViewColumn::new();
let cell = gtk4::CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(title);
col.set_resizable(true);
col.add_attribute(&cell, "text", *idx);
tree.append_column(&col);
}
(tree, scrolled)
}
fn create_lvm_tab() -> (TreeView, ScrolledWindow) {
let scrolled = ScrolledWindow::new();
let tree = TreeView::new();
scrolled.set_child(Some(&tree));
// 8 columns
let columns = [
("名称", 0), ("大小", 1), ("属性", 2), ("UUID", 3),
("关联", 4), ("空闲/已用", 5), ("路径/格式", 6), ("挂载点", 7)
];
for (title, idx) in columns.iter() {
let col = gtk4::TreeViewColumn::new();
let cell = gtk4::CellRendererText::new();
col.pack_start(&cell, true);
col.set_title(title);
col.set_resizable(true);
col.add_attribute(&cell, "text", *idx);
tree.append_column(&col);
}
(tree, scrolled)
}
pub fn log(&self, message: &str) {
let buffer = self.log_output.buffer();
let mut end = buffer.end_iter();
buffer.insert(&mut end, message);
buffer.insert(&mut end, "\n");
self.statusbar.push(0, message);
}
}