first commit
This commit is contained in:
907
Cargo.lock
generated
Normal file
907
Cargo.lock
generated
Normal 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
11
Cargo.toml
Normal 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
203
src/application.rs
Normal 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
89
src/command.rs
Normal 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
213
src/dialogs.rs
Normal 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
259
src/disk_ops.rs
Normal 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
289
src/lvm_ops.rs
Normal 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
354
src/main.rs
Normal 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<>k4::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
246
src/raid_ops.rs
Normal 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
421
src/system_info.rs
Normal 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
150
src/ui.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user