diff --git a/.rustfmt.toml b/.rustfmt.toml index 7b60896d..d4f9254f 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,6 @@ edition = "2021" imports_layout = "HorizontalVertical" imports_granularity = "Module" -group_imports = "One" \ No newline at end of file +group_imports = "One" +indent_style = "Block" +reorder_imports = true diff --git a/Cargo.lock b/Cargo.lock index 3a68dcef..ce4d4b93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -9,48 +9,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] -name = "ahash" -version = "0.8.8" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", + "memchr", ] [[package]] -name = "aho-corasick" -version = "1.0.1" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "memchr", + "winapi", ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "anstream" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "anstyle-parse" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ - "libc", + "utf8parse", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", ] [[package]] @@ -78,9 +99,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake2" @@ -95,15 +116,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cfg-if" @@ -113,21 +128,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "chrono" -version = "0.4.31" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "windows-targets 0.48.0", -] +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" @@ -139,34 +142,45 @@ dependencies = [ "atty", "bitflags 1.3.2", "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", + "textwrap", + "unicode-width 0.1.13", "vec_map", "yaml-rust", ] [[package]] name = "clap" -version = "3.2.25" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ - "bitflags 1.3.2", - "clap_lex", - "indexmap 1.9.2", - "once_cell", - "textwrap 0.16.0", + "clap_builder", ] [[package]] -name = "clap_lex" -version = "0.2.4" +name = "clap_builder" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ - "os_str_bytes", + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", ] +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "console" version = "0.15.5" @@ -176,21 +190,15 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.13", "windows-sys 0.42.0", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] @@ -237,9 +245,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -247,37 +255,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.48", + "strsim 0.11.1", + "syn 2.0.90", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.48", -] - -[[package]] -name = "defer-drop" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" -dependencies = [ - "crossbeam-channel", - "once_cell", + "syn 2.0.90", ] [[package]] @@ -291,33 +289,33 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.90", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.48", + "syn 2.0.90", ] [[package]] @@ -362,6 +360,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "env_logger" version = "0.7.1" @@ -397,7 +401,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "log", "scopeguard", "uuid", @@ -409,11 +413,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fuzzy-muff" -version = "0.3.10" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331179e18cf2ef4a2fa3b48e8885d6a2efe8947167231135f72303cf8fd1d0ba" +checksum = "97bdaa384c9993076604f4050e57ef2afc46f7c15c70db5564d68cb8e665747a" dependencies = [ "thread_local", ] @@ -445,24 +455,24 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ - "ahash 0.3.8", + "ahash", "autocfg", "rayon", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "ahash 0.8.8", + "foldhash", "rayon", ] @@ -475,33 +485,23 @@ dependencies = [ "libc", ] -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "httm" -version = "0.36.5" +version = "0.44.1" dependencies = [ - "ahash 0.8.8", - "clap 3.2.25", + "clap 4.5.23", "crossbeam-channel", "exacl", - "hashbrown 0.14.3", - "indicatif 0.17.7", + "foldhash", + "hashbrown 0.15.2", + "indicatif 0.17.9", "itertools", "libc", "lms", "lscolors", - "nix 0.28.0", + "nix 0.29.0", "nu-ansi-term", "number_prefix 0.4.0", - "once_cell", "proc-mounts", "rayon", "realpath-ext", @@ -523,45 +523,12 @@ dependencies = [ "quick-error", ] -[[package]] -name = "iana-time-zone" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.3" @@ -569,7 +536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -586,30 +553,21 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.7" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix 0.4.0", "portable-atomic", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", + "web-time", ] [[package]] name = "itertools" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -622,24 +580,25 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" @@ -667,16 +626,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lscolors" -version = "0.17.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53304fff6ab1e597661eee37e42ea8c47a146fca280af902bb76bff8a896e523" +checksum = "61183da5de8ba09a58e330d55e5ea796539d8443bd00fdeb863eac39724aa4ab" dependencies = [ + "aho-corasick", "nu-ansi-term", ] @@ -708,11 +668,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -720,21 +680,18 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "num-traits" -version = "0.2.15" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num_threads" @@ -769,12 +726,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "partition-identity" version = "0.3.0" @@ -798,9 +749,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -831,9 +782,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -882,9 +833,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -894,9 +845,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -905,17 +856,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -948,32 +899,33 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ - "indexmap 2.2.3", + "indexmap", "itoa", + "memchr", "ryu", "serde", ] @@ -986,9 +938,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -1009,9 +961,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1040,12 +992,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1054,15 +1006,9 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "unicode-width", + "unicode-width 0.1.13", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.39" @@ -1085,9 +1031,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -1095,13 +1041,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -1117,21 +1064,14 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ + "num-conv", "time-core", ] -[[package]] -name = "timer" -version = "0.2.0" -source = "git+https://github.com/kimono-koans/timer.rs#4ba32a90432c50224d5a2c447cb213e8432b10a8" -dependencies = [ - "chrono", -] - [[package]] name = "tuikit" version = "0.5.0" @@ -1143,31 +1083,25 @@ dependencies = [ "log", "nix 0.24.3", "term", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] name = "two_percent" -version = "0.10.39" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af460e47d0734aeda8ec1b396ee4300f057a03b950db0473b113c142d04e7bf2" +checksum = "aceaec31deda2e962c2a22f7e2a77cc21a10c9622e54d82f7b657a04acb03647" dependencies = [ - "bitflags 2.4.2", - "chrono", + "bitflags 2.6.0", "crossbeam-channel", - "defer-drop", "derive_builder", "fuzzy-muff", - "lazy_static", "libc", "log", - "nix 0.28.0", "rayon", "regex", - "time", - "timer", "tuikit", - "unicode-width", + "unicode-width 0.2.0", "vte", "which", ] @@ -1186,9 +1120,15 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "utf8parse" @@ -1242,34 +1182,34 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1277,34 +1217,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "which" -version = "6.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" dependencies = [ "either", - "home", - "once_cell", + "env_home", "rustix", - "windows-sys 0.52.0", + "winsafe", ] [[package]] @@ -1338,15 +1287,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.0", -] - [[package]] name = "windows-sys" version = "0.42.0" @@ -1362,52 +1302,38 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.0", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.0" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1418,15 +1344,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1436,15 +1356,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1454,15 +1368,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1472,15 +1386,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1490,15 +1398,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1508,15 +1410,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1526,21 +1422,21 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", "linux-raw-sys", @@ -1552,23 +1448,3 @@ name = "yaml-rust" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" - -[[package]] -name = "zerocopy" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] diff --git a/Cargo.toml b/Cargo.toml index c7d4a509..9a286dc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "httm" authors = ["Robert Swinford gmail.com>"] -version = "0.36.5" +version = "0.44.1" edition = "2021" keywords = ["zfs", "backup", "restore", "cli-utility", "snapshot"] description = "A CLI tool for viewing snapshot file versions on ZFS and btrfs datasets" @@ -27,50 +27,48 @@ std = ["xattrs", "malloc_trim"] # acls feature - requires libacl1-dev to build acls = ["exacl"] xattrs = ["xattr"] -malloc_trim = ["libc", "skim/malloc_trim"] -setpriority = ["libc"] +malloc_trim = ["skim/malloc_trim", "libc"] licensing = ["lms", "itertools"] [target.'cfg(unix)'.dependencies] +libc = { version = "0.2.169", default-features = false, optional = true } exacl = { version = "0.12.0", default-features = false, optional = true } -xattr = { version = "1.3.1", default-features = false, optional = true } -libc = { version = "0.2.152", default-features = false, optional = true } +xattr = { version = "1.4.0", default-features = false, optional = true } [dependencies] -ahash = { version = "0.8.8", default-features = false } -clap = { version = "3.2.25", default-features = false, features = [ - "cargo", +foldhash = { version = "0.1.4", default-features = true } +clap = { version = "4.5.23", default-features = true, features = [ "std", + "cargo", ] } -crossbeam-channel = { version = "0.5.11", default-features = false } -time = { version = "0.3.31", default-features = false, features = [ +crossbeam-channel = { version = "0.5.14", default-features = false } +time = { version = "0.3.37", default-features = false, features = [ "formatting", "local-offset", ] } number_prefix = { version = "0.4.0", default-features = false } -skim = { version = "0.10.39", default-features = false, package = "two_percent" } -nu-ansi-term = { version = "0.50.0", default-features = false } -lscolors = { version = "0.17.0", default-features = false, features = [ +skim = { version = "0.12.3", default-features = false, package = "two_percent" } +nu-ansi-term = { version = "0.50.1", default-features = false } +lscolors = { version = "0.20.0", default-features = false, features = [ "nu-ansi-term", ] } -terminal_size = { version = "0.3.0", default-features = false } -which = { version = "6.0.0", default-features = false } -rayon = { version = "1.8.1", default-features = false } -indicatif = { version = "0.17.7", default-features = false } +terminal_size = { version = "0.4.1", default-features = false } +which = { version = "7.0.1", default-features = false } +rayon = { version = "1.10.0", default-features = false } +indicatif = { version = "0.17.9", default-features = false } proc-mounts = { version = "0.3.0", default-features = false } -once_cell = { version = "1.19.0", default-features = false } -hashbrown = { version = "0.14.3", default-features = false, features = [ +hashbrown = { version = "0.15.2", default-features = false, features = [ "rayon", - "ahash", "inline-more", + "default-hasher", ] } -nix = { version = "0.28.0", default-features = false, features = [ +nix = { version = "0.29.0", default-features = false, features = [ "fs", "user", "zerocopy", ] } -serde = { version = "1.0.195", default-features = false } -serde_json = { version = "1.0.111", default-features = false, features = [ +serde = { version = "1.0.217", default-features = false } +serde_json = { version = "1.0.135", default-features = false, features = [ "preserve_order", ] } realpath-ext = { version = "0.1.3", default-features = false, features = [ @@ -78,14 +76,11 @@ realpath-ext = { version = "0.1.3", default-features = false, features = [ ] } # these are strictly not required to build, only included for attribution sake (to be picked up by cargo_about) lms = { version = "0.4.0", default-features = false, optional = true } -itertools = { version = "0.12.0", default-features = false, optional = true } - -[patch.crates-io] -timer = { git = "https://github.com/kimono-koans/timer.rs" } +itertools = { version = "0.14.0", default-features = false, optional = true } [package.metadata.deb] maintainer = "kimono koans " -copyright = "2023, Robert Swinford gmail.com>" +copyright = "2024, Robert Swinford gmail.com>" extended-description = """\ Prints the size, date and corresponding locations of available unique versions of files \ diff --git a/README.md b/README.md index b67de2e2..2ad4858b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ *The dream of a CLI Time Machine is still alive with `httm`.* -`httm` prints the size, date and corresponding locations of available unique versions (deduplicated by modify time and size) of files residing on snapshots, but can also be used *interactively* to select and restore files, even snapshot mounts by file! `httm` might change the way you use snapshots (because ZFS/BTRFS/NILFS2 aren't designed for finding for unique file versions) or the Time Machine concept (because `httm` is very fast!). +`httm` prints the size, date and corresponding locations of available unique versions (deduplicated by modify time and size) of files residing on snapshots, but can also be used *interactively* to select and restore files, even snapshot mounts by file! `httm` might change the way you use snapshots (because ZFS/BTRFS/NILFS2 aren't designed to find unique file versions) or the Time Machine concept (because `httm` is very fast!). `httm` boasts an array of seductive features, like: @@ -19,17 +19,17 @@ * Guard any restore actions with precautionary snapshots * List snapshot names, even prune snapshots, which include a file name * Shortcut features: only display last snapshot, omit duplicates of the live file, etc. -* Uniqueness level: Like `rsync`, `httm` can determine whether file is unique based solely on metadata, or use checksums +* Verification level: Like `rsync`, `httm` can determine whether file is unique based solely on metadata, or use checksums * 4 native interactive modes: browse, select, prune and restore * ANSI `ls` colors from your environment * Detect and display categories of unique file versions available (`multiple`, `single`, `single-with-snap`,..., etc.) -* Select from several formatting styles (newline, null, tab delimited, JSON, etc.). Parseable ... or not ... oh my! +* Select from several formatting styles (newline, null, tab delimited, JSON, CSV, etc.). Parseable ... or not ... oh my! * Packaged scripts which help you, and show you how to, use `httm`: [ounce](https://github.com/kimono-koans/httm/blob/master/scripts/ounce.bash), [bowie](https://github.com/kimono-koans/httm/blob/master/scripts/bowie.bash), [equine](https://github.com/kimono-koans/httm/blob/master/scripts/equine.bash), and [nicotine](https://github.com/kimono-koans/httm/blob/master/scripts/nicotine.bash) -* Supports ZFS/BTRFS/NILFS2 snapshots and Time Machine backups! +* Supports ZFS/BTRFS/NILFS2 snapshots, and Restic and Time Machine backups! Use in combination with you favorite shell's hot keys for even more fun. -Inspired by the [findoid](https://github.com/jimsalterjrs/sanoid) script, [fzf](https://github.com/junegunn/fzf), [skim]() and many [zsh](https://www.zsh.org) key bindings. +Inspired by the [findoid](https://github.com/jimsalterjrs/sanoid) script, [fzf](https://github.com/junegunn/fzf), [skim](https://github.com/lotabout/skim) and many [zsh](https://www.zsh.org) key bindings. ## Install via Native Packages @@ -121,7 +121,7 @@ On some Linux distributions, which include old versions of `libc`, `cargo` may r ## Example Usage -Note: Users may need to use `sudo` (or equivalent) to view versions on BTRFS or NILFS2 datasets, as BTRFS or NILFS2 snapshots may require root permissions in order to be visible. +Note: Users may need to use `sudo` (or equivalent) to view versions on BTRFS or NILFS2 datasets, or Restic repositories, as BTRFS or NILFS2 snapshots or Restic repositories may require root permissions in order to be visible. Restic and Time Machine backups also require an additional flag, see further discussion of Restic `--alt-store` in the below. Like other UNIX utilities (such as `cat`, `uniq`, `sort`), if you include no path/s as arguments, then `httm` will pause waiting for input on stdin: @@ -144,34 +144,34 @@ Print all unique versions of your history file, as formatted JSON: ➜ httm --json ~/.histfile ``` -Print all files on snapshots deleted from your home directory, recursive: +Browse all files in your home directory, recursively, and view unique versions on local snapshots: ```bash -➜ httm -d -R ~ +➜ httm -b -R ~ ``` -Print all files on snapshots deleted from your home directory, recursive, newline delimited, piped to a text file: +Print all files on snapshots deleted from your home directory, recursive: ```bash -# pseudo live file versions -➜ httm -d -n -R --no-snap ~ > pseudo-live-versions.txt -# unique snapshot versions -➜ httm -d -n -R --no-live ~ > deleted-unique-versions.txt +➜ httm -d -R ~ ``` -Browse all files in your home directory, recursively, and view unique versions on local snapshots: +Browse all files deleted from your home directory, recursively, and view unique versions on all local and alternative replicated dataset snapshots: ```bash -➜ httm -b -R ~ +➜ httm -d=only -b -a -R ~ ``` -Browse all files deleted from your home directory, recursively, and view unique versions on all local and alternative replicated dataset snapshots: +Print all files on snapshots deleted from your home directory, recursive, newline delimited, piped to a text file: ```bash -➜ httm -d=only -b -a -R ~ +# pseudo live file versions +➜ httm -d -n -R --no-snap ~ > pseudo-live-versions.txt +# unique snapshot versions +➜ httm -d -n -R --no-live ~ > deleted-unique-versions.txt ``` -Browse all files in your home directory, recursively, and view unique versions on local snapshots, to select and ultimately restore to your working directory, in overwrite mode: +Browse all files in your home directory, recursively, and view unique versions on local snapshots, to select and ultimately restore to its original location, in overwrite mode: ```bash ➜ httm -r=overwrite -R ~ @@ -198,6 +198,9 @@ View unique versions of a file for recovery (shortcut, no need to browse a direc # search for the text "pattern" among snapshots of httm manpage ➜ httm -n --omit-ditto /usr/share/man/man1/httm.1.gz | xargs rg "pattern" -z +# similarly, print the directory sizes of each unique snapshot +➜ httm -n --omit-ditto /srv/downloads | xargs -I{} du -sh "{}" + # print all unique versions of your `/var/log/syslog` file, # newline delimited piped to `find` to print only versions # with modify times of less than 1 day from right now. @@ -239,7 +242,7 @@ Snapshot the dataset upon which `/etc/samba/smb.conf` is located: ➜ sudo httm -S /etc/samba/smb.conf ``` -Browse all files, recursively, in a folder backed up via `rsync` to a remote share, and view unique versions on remote snapshots directly (only available for BTRFS Snapper and ZFS datasets). +Browse all files, recursively, in a folder backed up via `rsync` to a remote share, and view unique versions on remote snapshots directly (only available for BTRFS Snapper and ZFS datasets). (Note: Remember to make ZFS snapshots visible in your `smb.conf` with `zfsacl:expose_snapdir=True`). ```bash # mount the share @@ -273,7 +276,10 @@ Mounting sparse bundle (this may include an fsck): Backups of kiev ... Discovering backup locations (this can take a few seconds)... Mounting snapshots... ... -➜ httm .zshrc +# restic users can do something similar by: +# 1. mounting their repositories: restic -r /path/to/repo mount /path/to/mountpoint +# 2. invoking httm with --alt-store: httm --alt-store=restic .zshrc +➜ httm --alt-store=timemachine .zshrc ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Tue May 09 22:57:09 2023 6.7 KiB "/Volumes/.timemachine/842A693F-CB54-4C5A-9AB1-C73681D4DFCD/2023-11-08-212757.backup/2023-11-08-212757.backup/Data/Users/kimono/.zshrc" Sun Nov 12 20:29:57 2023 6.7 KiB "/Volumes/.timemachine/842A693F-CB54-4C5A-9AB1-C73681D4DFCD/2023-11-18-011056.backup/2023-11-18-011056.backup/Data/Users/kimono/.zshrc" diff --git a/cargo_about/about.toml b/cargo_about/about.toml index c1abde42..0fefff08 100644 --- a/cargo_about/about.toml +++ b/cargo_about/about.toml @@ -1,8 +1,13 @@ -accepted = ["Apache-2.0", "MIT", "MPL-2.0", "Unicode-TOU", "Unicode-DFS-2016", "BSD-3-Clause"] -targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl", +accepted = [ + "Apache-2.0", + "MIT", + "MPL-2.0", + "Unicode-TOU", + "Unicode-DFS-2016", + "BSD-3-Clause", + "Zlib", ] +targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] ignore-build-dependencies = false ignore-dev-dependencies = false -ignore-transitive-dependencies = false \ No newline at end of file +ignore-transitive-dependencies = false diff --git a/httm.1 b/httm.1 index c69cc07e..2fde54ed 100644 --- a/httm.1 +++ b/httm.1 @@ -1,302 +1,125 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH HTTM "1" "February 2024" "httm 0.36.5" "User Commands" +.TH HTTM "1" "January 2025" "httm 0.44.1" "User Commands" .SH NAME -httm \- manual page for httm 0.36.5 +httm \- manual page for httm 0.44.1 +.SH SYNOPSIS +.B httm +[\fI\,OPTIONS\/\fR] [\fI\,INPUT_FILES\/\fR]... .SH DESCRIPTION -httm 0.36.5 -httm prints the size, date and corresponding locations of available unique versions of files -residing on snapshots. May also be used interactively to select and restore from such versions, and -even to snapshot datasets which contain certain files. -.SS "USAGE:" -.IP -httm [OPTIONS] [\-\-] [INPUT_FILES]... -.SS "ARGS:" -.TP -... -in any non\-interactive mode, put requested paths here. If you include -no paths as arguments, then httm will pause waiting for input on stdin. -In any interactive mode, this is the directory search path. If no -directory is specified, httm will use the current working directory. -.SS "OPTIONS:" -.HP +httm prints the size, date and corresponding locations of available unique versions of files residing on snapshots. May also be used interactively to select and restore from such versions, and even to snapshot datasets which contain certain files. +.SS "Arguments:" +.TP +[INPUT_FILES]... +in any non\-interactive mode, put requested paths here. If you include no paths as arguments, then httm will pause waiting for input on stdin. In any interactive mode, this is the directory search path. If no directory is specified, httm will use the current working directory. +.SH OPTIONS +.TP \fB\-b\fR, \fB\-\-browse\fR -.IP -interactive browse and search a specified directory to display unique file versions. -[aliases: interactive] -.HP -\fB\-s\fR, \fB\-\-select[=\fR] +interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to dump to stdout. This argument optionally takes a value. Default behavior/value is to simply print the path name, but, if the path is a file, the user can print the file's contents by giving the value "contents", or print the PREVIEW output by giving the value "preview". [possible values: path, contents, preview] +.TP +\fB\-r\fR, \fB\-\-restore[=\fR] +interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to restore. This argument optionally takes a value. Default behavior/value is a non\-destructive "copy" to the current working directory with a new name, so as not to overwrite any "live" file version. However, the user may specify "overwrite" (or "yolo") to restore to the same file location. Note, "overwrite" can be a DESTRUCTIVE operation. Overwrite mode will attempt to preserve attributes, like the permissions/mode, timestamps, xattrs and ownership of the selected snapshot file version (this is and will likely remain a UNIX only feature). In order to preserve such attributes in "copy" mode, specify the "copy\-and\-preserve" value. User may also specify "guard". Guard mode has the same semantics as "overwrite" but will attempt to take a precautionary snapshot before any overwrite action occurs. Note: Guard mode is a ZFS only option. User may also set via the HTTM_RESTORE_MODE environment variable. [possible values: copy, copy\-and\-preserve, overwrite, yolo, guard] +.TP +\fB\-d\fR, \fB\-\-deleted[=\fR] +show deleted files in interactive modes. In non\-interactive modes, do a search for all files deleted from a specified directory. This argument optionally takes a value. The default behavior/value is "all". If "only" is specified, then, in the interactive modes, non\-deleted files will be excluded from the search. If "single" is specified, then, deleted files behind deleted directories, (that is \fB\-\-\fR files with a depth greater than one) will be ignored. [possible values: all, single, only] +.TP \fB\-R\fR, \fB\-\-recursive\fR -.IP -recurse into the selected directory to find more files. Only available in interactive -and deleted file modes. -.HP +recurse into the selected directory to find more files. Only available in interactive and deleted file modes. +.TP \fB\-a\fR, \fB\-\-alt\-replicated\fR -.IP -automatically discover locally replicated datasets and list their snapshots as well. -NOTE: Be certain such replicated datasets are mounted before use. httm will silently -ignore unmounted datasets in the interactive modes. -.HP -\fB\-p\fR, \fB\-\-preview[=\fR...] -.IP -user may specify a command to preview snapshots while in a snapshot selection view. -This argument optionally takes a value specifying the command to be executed. The -default value/command, if no command value specified, is a 'bowie' formatted 'diff'. -User defined commands must specify the snapshot file name "{snap_file}" and the live -file name "{live_file}" within their shell command. NOTE: 'bash' is required to -bootstrap any preview script, even if user defined preview commands or script is written -in a different language. -.HP -\fB\-\-uniqueness[=\fR...] -.IP -comparing file versions solely on the basis of size and modify time (the default -"metadata" behavior) may return what appear to be "false positives", in the sense that, -modify time is not a precise measure of whether a file has actually changed. A program -might overwrite a file with the same contents, or a user can simply update the modify -time via 'touch'. If only this flag is specified, the "contents" option compares the -actual file contents of file versions, if their sizes match, and overrides the default -"metadata" behavior. The "contents" option can be expensive, as the file versions need -to be read back and compared, and should probably only be used for smaller files. Given -how expensive this operation can be, for larger files or files with many versions, -"contents" option is not shown in Interactive browse mode, but after a selection is -made, can be utilized in Select or Restore modes. The "all" or "no\-filter" option dumps -all snapshot versions, and no attempt is made to determine if the file versions are -distinct. [aliases: unique] [possible values: all, no\-filter, metadata, contents] -.HP +automatically discover locally replicated datasets and list their snapshots as well. NOTE: Be certain such replicated datasets are mounted before use. httm will silently ignore unmounted datasets in the interactive modes. +.TP +\fB\-p\fR, \fB\-\-preview[=\fR] +user may specify a command to preview snapshots while in a snapshot selection view. This argument optionally takes a value specifying the command to be executed. The default value/command, if no command value specified, is a 'bowie' formatted 'diff'. User defined commands must specify the snapshot file name "{snap_file}" and the live file name "{live_file}" within their shell command. NOTE: 'bash' is required to bootstrap any preview script, even if user defined preview commands or script is written in a different language. +.TP +\fB\-\-dedup\-by[=\fR] +comparing file versions solely on the basis of size and modify time (the default "metadata" behavior) may return what appear to be "false positives", in the sense that, modify time is not a precise measure of whether a file has actually changed. A program might overwrite a file with the same contents, or a user can simply update the modify time via 'touch'. If only this flag is specified, the "contents" option compares the actual file contents of file versions, if their sizes match, and overrides the default "metadata" behavior. The "contents" option can be expensive, as the file versions need to be read back and compared, and should probably only be used for smaller files. Given how expensive this operation can be, for larger files or files with many versions, "contents" option is not shown in Interactive browse mode, but after a selection is made, can be utilized, when enabled, in Select or Restore modes. The "disable" "all" or "no\-filter" option dumps all snapshot versions, and no attempt is made to determine if the file versions are distinct. [aliases: unique, uniqueness] [possible values: disable, all, no\-filter, metadata, contents] +.TP \fB\-e\fR, \fB\-\-exact\fR -.IP -use exact pattern matching for searches in the interactive modes (in contrast to the -default fuzzy searching). -.HP -\fB\-S\fR, \fB\-\-snap[=\fR...] +use exact pattern matching for searches in the interactive modes (in contrast to the default fuzzy searching). .TP -snapshot a file/s most immediate mount. -This argument optionally takes a value for a +\fB\-S\fR, \fB\-\-snap[=\fR] +snapshot a file/s most immediate mount. This argument optionally takes a value for a snapshot suffix. The default suffix is 'httmSnapFileMount'. Note: This is a ZFS only option which requires either superuser or 'zfs allow' privileges. .TP -snapshot suffix. -The default suffix is 'httmSnapFileMount'. Note: This is a ZFS only -.IP -option which requires either superuser or 'zfs allow' privileges. [aliases: snap\-file, -snapshot, snap\-file\-mount] -.HP \fB\-\-list\-snaps[=\fR] +display snapshots names for a file. This argument optionally takes a value. By default, this argument will return all available snapshot names. When the DEDUP_BY flag is not specified but the LIST_SNAPS is, the default DEDUP_BY level is "all" snapshots. User may limit type of snapshots returned via specifying the DEDUP_BY flag. The user may also omit the most recent "n" snapshots from any list. By appending a comma, this argument also filters those snapshots which contain the specified pattern/s. A value of "5,prep_Apt" would return the snapshot names of only the last 5 (at most) of all snapshot versions which contain "prep_Apt". The value "native" will restrict selection to only 'httm' native snapshot suffix values, like "httmSnapFileMount" and "ounceSnapFileMount". Note: This is a ZFS and btrfs only option. .TP -display snapshots names for a file. -This argument optionally takes a value. By +\fB\-\-prune\fR +prune all snapshot/s which contain the input file/s on that file's most immediate mount via "zfs destroy". "zfs destroy" is a DESTRUCTIVE operation which *does not* only apply to the file in question, but the entire snapshot upon which it resides. Careless use may cause you to lose snapshot data you care about. This argument requires and will be filtered according to any values specified at LIST_SNAPS. User may also enable SELECT mode to make a granular selection of specific snapshots to prune. Note: This is a ZFS only option. .TP -default, this argument will return all available snapshot names. -User may limit type of +\fB\-\-roll\-forward=\fR +traditionally 'zfs rollback' is a destructive operation, whereas httm roll\-forward is non\-destructive. httm will copy only files and their attributes that have changed since a specified snapshot, from that snapshot, to its live dataset. httm will also take two precautionary snapshots, one before and one after the copy. Should the roll forward fail for any reason, httm will roll back to the pre\-execution state. Caveats: This is a ZFS only option which requires super user privileges. Not all filesystem features are supported (for instance, Solaris door or sockets on the snapshot) and will cause a roll forward to fail. Certain special/files objects will be copied or recreated, but are not guaranteed to be in the same state as the snapshot (for instance, fifos).The block clone copying so many file in parallel may also cause a kernel crash on some configurations, and is therefore disabled in this mode. .TP -snapshots returned via the UNIQUENESS flag. -The user may also omit the most recent "n" +\fB\-m\fR, \fB\-\-file\-mount[=\fR] +by default, display the all mount point/s of all dataset/s which contain/s the input file/s. This argument optionally takes a value to display other information about the path. Possible values are: "mount" or "target" or "directory", return the directory upon which the underlying dataset or device of the mount, "source" or "device" or "dataset", return the underlying dataset/device of the mount, and, "relative\-path" or "relative", return the path relative to the underlying dataset/device of the mount. [aliases: mount] [possible values: source, target, mount, directory, device, dataset, relative\-path, relative, relpath] .TP -snapshots from any list. -By appending a comma, this argument also filters those +\fB\-l\fR, \fB\-\-last\-snap[=\fR] +automatically select and print the path of last\-in\-time unique snapshot version for the input file. This argument optionally takes a value. Possible values are: "any", return the last in time snapshot version, this is the default behavior/value, "ditto", return only last snaps which are the same as the live file version, "no\-ditto\-exclusive", return only a last snap which is not the same as the live version (argument "\-\-no\-ditto" is an alias for this option), "no\-ditto\-inclusive", return a last snap which is not the same as the live version, or should none exist, return the live file, and, "none" or "without", return the live file only for those files without a last snapshot. [aliases: last, latest] [possible values: any, ditto, no\-ditto, no\-ditto\-exclusive, no\-ditto\-inclusive, none, without] .TP -snapshots which contain the specified pattern/s. -A value of "5,prep_Apt" would return -.IP -the snapshot names of only the last 5 (at most) of all snapshot versions which contain -"prep_Apt". The value "native" will restrict selection to only 'httm' native snapshot -suffix values, like "httmSnapFileMount" and "ounceSnapFileMount". Note: This is a ZFS -only option. -.HP -\fB\-\-prune\fR -.IP -prune all snapshot/s which contain the input file/s on that file's most immediate mount -via "zfs destroy". "zfs destroy" is a DESTRUCTIVE operation which *does not* only apply -to the file in question, but the entire snapshot upon which it resides. Careless use -may cause you to lose snapshot data you care about. This argument requires and will be -filtered according to any values specified at LIST_SNAPS. User may also enable SELECT -mode to make a granular selection of specific snapshots to prune. Note: This is a ZFS -only option. -.HP -\fB\-\-roll\-forward=\fR -.IP -traditionally 'zfs rollback' is a destructive operation, whereas httm roll\-forward is -non\-destructive. httm will copy only files and their attributes that have changed since -a specified snapshot, from that snapshot, to its live dataset. httm will also take two -precautionary snapshots, one before and one after the copy. Should the roll forward -fail for any reason, httm will roll back to the pre\-execution state. Caveats: This is a -ZFS only option which requires super user privileges. -.HP -\fB\-m\fR, \fB\-\-file\-mount[=\fR...] -.IP -by default, display the all mount point/s of all dataset/s which contain/s the input -file/s. This argument optionally takes a value to display other information about the -path. Possible values are: "mount" or "target" or "directory", return the directory -upon which the underlying dataset or device of the mount, "source" or "device" or -"dataset", return the underlying dataset/device of the mount, and, "relative\-path" or -"relative", return the path relative to the underlying dataset/device of the mount. -[aliases: mount] [possible values: source, target, mount, directory, device, dataset, -relative\-path, relative, relpath] -.HP -\fB\-l\fR, \fB\-\-last\-snap[=\fR...] -.IP -automatically select and print the path of last\-in\-time unique snapshot version for the -input file. This argument optionally takes a value. Possible values are: "any", return -the last in time snapshot version, this is the default behavior/value, "ditto", return -only last snaps which are the same as the live file version, "no\-ditto\-exclusive", -return only a last snap which is not the same as the live version (argument "\-\-no\-ditto" -is an alias for this option), "no\-ditto\-inclusive", return a last snap which is not the -same as the live version, or should none exist, return the live file, and, "none" or -"without", return the live file only for those files without a last snapshot. [aliases: -last, latest] [possible values: any, ditto, no\-ditto, no\-ditto\-exclusive, -no\-ditto\-inclusive, none, without] -.HP \fB\-n\fR, \fB\-\-raw\fR -.IP -display the snapshot locations only, without extraneous information, delimited by a -NEWLINE character. [aliases: newline] -.HP +display the snapshot locations only, without extraneous information, delimited by a NEWLINE character. [aliases: newline] +.TP \fB\-0\fR, \fB\-\-zero\fR -.IP -display the snapshot locations only, without extraneous information, delimited by a NULL -character. [aliases: null] -.HP +display the snapshot locations only, without extraneous information, delimited by a NULL character. [aliases: null] +.TP +\fB\-\-csv\fR +display all information, delimited by a comma. +.TP \fB\-\-not\-so\-pretty\fR -.IP -display the ordinary output, but tab delimited, without any pretty border lines. -[aliases: tabs, plain\-jane, not\-pretty] -.HP +display the ordinary output, but tab delimited, without any pretty border lines. [aliases: tabs, plain\-jane, not\-pretty] +.TP \fB\-\-json\fR -.IP display the ordinary output, but as formatted JSON. -.HP +.TP \fB\-\-omit\-ditto\fR +omit display of the snapshot version which may be identical to the live version. By default, `httm` displays all snapshot versions and the live version). .TP -omit display of the snapshot version which may be identical to the live version. -By -.IP -default, `httm` displays all snapshot versions and the live version). -.HP \fB\-\-no\-filter\fR -.IP -by default, in the interactive modes, httm will filter out files residing upon -non\-supported datasets (like ext4, tmpfs, procfs, sysfs, or devtmpfs, etc.), and within -any "common" snapshot paths. Here, one may select to disable such filtering. httm, -however, will always show the input path, and results from behind any input path when -that is the path being searched. -.HP +by default, in the interactive modes, httm will filter out files residing upon non\-supported datasets (like ext4, tmpfs, procfs, sysfs, or devtmpfs, etc.), and within any "common" snapshot paths. Here, one may select to disable such filtering. httm, however, will always show the input path, and results from behind any input path when that is the path being searched. +.TP \fB\-\-no\-hidden\fR -.IP -do not show information regarding hidden files and directories (those that start with a -\&'.') in the recursive or interactive modes. -.HP +do not show information regarding hidden files and directories (those that start with a '.') in the recursive or interactive modes. +.TP \fB\-\-one\-filesystem\fR -.IP -limit recursive search to file and directories on the same filesystem/device as the -target directory. -.HP +limit recursive search to file and directories on the same filesystem/device as the target directory. +.TP \fB\-\-no\-traverse\fR +in recursive mode, don't traverse symlinks. Although httm does its best to prevent searching pathologically recursive symlink\-ed paths, here, you may disable symlink traversal completely. NOTE: httm will never traverse symlinks when a requested recursive search is on the root/base directory ("/"). .TP -in recursive mode, don't traverse symlinks. -Although httm does its best to prevent -.IP -searching pathologically recursive symlink\-ed paths, here, you may disable symlink -traversal completely. NOTE: httm will never traverse symlinks when a requested -recursive search is on the root/base directory ("/"). -.HP \fB\-\-no\-live\fR -.IP -only display information concerning snapshot versions (display no information regarding -live versions of files or directories). [aliases: dead, disco] -.HP +only display information concerning snapshot versions (display no information regarding live versions of files or directories). [aliases: dead, disco] +.TP +\fB\-\-alt\-store=\fR +give priority to discovered alternative backups stores, like Restic, and Time Machine. If this flag is specified, httm will drop non\-alternative store datasets and place said alternative backups store snapshots, as snapshots for the root mount point ("/"). Before use, be careful that the repository is mounted. You may need superuser privileges to view a repository mounted with superuser permission. httm also includes a helper script called "equine" which can assist you in mounting remote and local Time Machine snapshots. [possible values: restic, timemachine] +.TP \fB\-\-no\-snap\fR -.IP -only display information concerning 'pseudo\-live' versions in any Display Recursive mode -(in \fB\-\-deleted\fR, \fB\-\-recursive\fR, but non\-interactive modes). Useful for finding the "files -that once were" and displaying only those pseudo\-live/zombie files. [aliases: undead, -zombie] -.HP -\fB\-\-map\-aliases\fR -.IP -manually map a local directory (eg. "/Users/") as an alias of a mount point -for ZFS or btrfs, such as the local mount point for a backup on a remote share (eg. -"/Volumes/Home"). This option is useful if you wish to view snapshot versions from -within the local directory you back up to your remote share. This option requires a -value. Such a value is delimited by a colon, ':', and is specified in the form -: (eg. \fB\-\-map\-aliases\fR /Users/:/Volumes/Home). Multiple -maps may be specified delimited by a comma, ','. You may also set via the environment -variable HTTM_MAP_ALIASES. [aliases: aliases] -.HP -\fB\-\-num\-versions[=\fR...] -.IP -detect and display the number of unique versions available (e.g. one, "1", version is -available if either a snapshot version exists, and is identical to live version, or only -a live version exists). This argument optionally takes a value. The default value, -"all", will print the filename and number of versions, "graph" will print the filename -and a line of characters representing the number of versions, "single" will print only -filenames which only have one version, (and "single\-no\-snap" will print those without a -snap taken, and "single\-with\-snap" will print those with a snap taken), and "multiple" -will print only filenames which only have multiple versions. [possible values: all, -graph, single, single\-no\-snap, single\-with\-snap, multiple] -.HP +only display information concerning 'pseudo\-live' versions in any Display Recursive mode (in \fB\-\-deleted\fR, \fB\-\-recursive\fR, but non\-interactive modes). Useful for finding the "files that once were" and displaying only those pseudo\-live/zombie files. [aliases: undead, zombie] +.TP +\fB\-\-map\-aliases\fR [] +manually map a local directory (eg. "/Users/") as an alias of a mount point for ZFS or btrfs, such as the local mount point for a backup on a remote share (eg. "/Volumes/Home"). This option is useful if you wish to view snapshot versions from within the local directory you back up to a remote network share. This option requires a value. Such a value is delimited by a colon, ':', and is specified in the form : (eg. \fB\-\-map\-aliases\fR /Users/:/Volumes/Home). Multiple maps may be specified delimited by a comma, ','. You may also set via the environment variable HTTM_MAP_ALIASES. [aliases: aliases] +.TP +\fB\-\-num\-versions[=\fR] +detect and display the number of unique versions available (e.g. one, "1", version is available if either a snapshot version exists, and is identical to live version, or only a live version exists). This argument optionally takes a value. The default value, "all", will print the filename and number of versions, "graph" will print the filename and a line of characters representing the number of versions, "single" will print only filenames which only have one version, (and "single\-no\-snap" will print those without a snap taken, and "single\-with\-snap" will print those with a snap taken), and "multiple" will print only filenames which only have multiple versions. [possible values: all, graph, single, single\-no\-snap, single\-with\-snap, multiple] +.TP \fB\-\-utc\fR -.IP use UTC for date display and timestamps -.HP +.TP \fB\-\-no\-clones\fR -.IP -by default, when copying files from snapshots, httm will first attempt a zero copy -"reflink" clone on systems that support it. Here, you may disable that behavior, and -force httm to use the fall back diff copy behavior as the default. You may also set an -environment variable to any value, "HTTM_NO_CLONE" to disable. -.HP +by default, when copying files from snapshots, httm will first attempt a zero copy "reflink" clone on systems that support it. Here, you may disable that behavior, and force httm to use the fall back diff copy behavior as the default. You may also set an environment variable to any value, "HTTM_NO_CLONE" to disable. +.TP \fB\-\-debug\fR -.IP print configuration and debugging info -.HP +.TP \fB\-\-install\-zsh\-hot\-keys\fR -.IP install zsh hot keys to the users home directory, and then exit -.HP +.TP \fB\-h\fR, \fB\-\-help\fR -.IP -Print help information -.HP +Print help +.TP \fB\-V\fR, \fB\-\-version\fR -.IP -Print version information +Print version .SH "SEE ALSO" The full documentation for .B httm diff --git a/scripts/ounce.bash b/scripts/ounce.bash index b2c223c1..5eb69dc0 100644 --- a/scripts/ounce.bash +++ b/scripts/ounce.bash @@ -41,7 +41,7 @@ $ounce only snapshots datasets when you have file changes outstanding, uncommitt (except in --trace mode). When $ounce is invoked with only a target executable and its arguments, including paths, and no additional options, -$ounce will perform a snapshot check on those paths are given as arguments to the target executable, and possibly wait +$ounce will perform a snapshot check on those paths are that given as arguments to the target executable, and possibly wait for a snapshot, before proceeding with execution of the target executable. USAGE: @@ -95,7 +95,7 @@ function print_err_exit { } function log_info { - printf "%s\n" "$*" 2>&1 | /usr/bin/logger -t ounce + printf "%s\n" "$*" 2>&1 | logger -t ounce } function prep_trace { @@ -112,6 +112,10 @@ function prep_trace { command -v awk exit 0 )" ]] || print_err_exit "'awk' is required to execute 'ounce' in trace mode. Please check that 'awk' is in your path." + [[ -n "$( + command -v logger + exit 0 + )" ]] || print_err_exit "'logger' is required to execute 'ounce' in trace mode. Please check that 'logger' is in your path." } function prep_exec { @@ -160,15 +164,15 @@ function take_snap { # mask all the errors from the first run without privileges, # let the sudo run show errors - [[ -z "$utc" ]] || httm "$utc" --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | /usr/bin/logger -t ounce - [[ -n "$utc" ]] || httm --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | /usr/bin/logger -t ounce + [[ -z "$utc" ]] || httm "$utc" --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | logger -t ounce || true + [[ -n "$utc" ]] || httm --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | logger -t ounce || true if [[ $? -ne 0 ]]; then local sudo_program sudo_program="$(prep_sudo)" - [[ -z "$utc" ]] || httm "$utc" --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | /usr/bin/logger -t ounce - [[ -n "$utc" ]] || httm --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | /usr/bin/logger -t ounce + [[ -z "$utc" ]] || httm "$utc" --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | logger -t ounce || true + [[ -n "$utc" ]] || httm --snap="$suffix" $filenames 2>&1 | grep -v "dataset already exists" | logger -t ounce || true [[ $? -eq 0 ]] || print_err_exit "'ounce' failed with a 'httm'/'zfs' snapshot error. Check you have the correct permissions to snapshot." @@ -234,9 +238,10 @@ function exec_trace { exit 0 )" - # 1) is empty, dne 2) is file, symlink or dir + # 1) is empty, dne 2) is file, symlink or dir or 3) if file is writable [[ -n "$canonical_path" ]] || continue [[ -f "$canonical_path" || -d "$canonical_path" || -L "$canonical_path" ]] || continue + [[ -w "$canonical_path" ]] || continue # 3) is file a newly created tmp file? [[ "$canonical_path" != *.swp && "$canonical_path" != ~* && "$canonical_path" != *~ && "$canonical_path" != *.tmp ]] || \ @@ -280,6 +285,11 @@ function exec_args { continue fi + if [[ ! -w "$canonical_path" ]]; then + log_info "Path is not writable: $canonical_path" + continue + fi + filenames_array+=("$canonical_path") done @@ -370,7 +380,7 @@ function ounce_of_prevention { background_pid="$!" # main exec - stdbuf -i0 -o0 -e0 strace -A -o "| stdbuf -i0 -o0 -e0 cat -u > $temp_pipe" -f -e open,openat,openat2,fsync -y --seccomp-bpf -- "$program_name" "$@" + stdbuf -i0 -o0 -e0 strace -A -o "| stdbuf -i0 -o0 -e0 cat -u > $temp_pipe" -f -e open,openat,openat2 -y --seccomp-bpf -- "$program_name" "$@" # cleanup wait "$background_pid" diff --git a/scripts/zdbstat.bash b/scripts/zdbstat.bash index 4ef3c2ec..5e538152 100644 --- a/scripts/zdbstat.bash +++ b/scripts/zdbstat.bash @@ -98,16 +98,16 @@ function dump_zfs_obj_metadata() { file_name="$1" sudo_program="$2" - source="$( zfs list -H -o name $file_name 2>/dev/null; exit 0 )" - [[ -n "$source" ]] || source="$( zfs list -H -o name $file_name 2>&1 | cut -f2 -d"'" ; exit 0 )" - inode="$( stat -c %i $file_name 2>/dev/null; exit 0 )" + source="$( zfs list -H -o name "$file_name" 2>/dev/null; exit 0 )" + [[ -n "$source" ]] || source="$( zfs list -H -o name "$file_name" 2>&1 | cut -f2 -d"'" ; exit 0 )" + inode="$( stat -c %i "$file_name" 2>/dev/null; exit 0 )" if [[ -z "$source" ]]; then - print_err_exit "Could not determine source dataset for path: $file_name" + print_err_exit "Could not determine source dataset for path: "$file_name"" fi if [[ -z "$inode" ]]; then - print_err_exit "Could not determine inode for path: $inode" + print_err_exit "Could not determine inode for path: "$inode"" fi "$sudo_program" zdb -dddddddddd "$source" "$inode" @@ -123,18 +123,18 @@ function run_loop() { for f in "$@"; do local file_name="" - file_name="$( realpath "$f" 2>/dev/null; exit 0 )" + file_name="$( readlink -e "$f" 2>/dev/null; exit 0 )" if [[ -z "$file_name" ]]; then print_err "WARN: Path likely does not exist: $f" continue fi - if [[ "$( echo $file_name | grep -c ".zfs/snapshot" )" -eq 0 ]] && \ - [[ -z "$( zfs list $file_name 2>/dev/null; exit 0 )" ]]; then - print_err "WARN: zdbstat requires a valid zfs path: $file_name" - continue - fi + if [[ "$( echo "$file_name" | grep -c ".zfs/snapshot" )" -eq 0 ]] && \ + [[ -z "$( zfs list "$file_name" 2>/dev/null; exit 0 )" ]]; then + print_err "WARN: zdbstat requires a valid zfs path: "$file_name"" + continue + fi dump_zfs_obj_metadata "$file_name" "$sudo_program" @@ -142,4 +142,4 @@ function run_loop() { } -run_loop "$@" +run_loop "$@" \ No newline at end of file diff --git a/src/background/deleted.rs b/src/background/deleted.rs index d1ededb4..31762142 100644 --- a/src/background/deleted.rs +++ b/src/background/deleted.rs @@ -15,91 +15,78 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::background::recursive::{PathProvenance, SharedRecursive}; +use crate::background::recursive::{Entries, PathProvenance}; use crate::config::generate::DeletedMode; -use crate::data::paths::{BasicDirEntryInfo, PathData}; -use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::{is_channel_closed, Never}; -use crate::lookup::deleted::{DeletedFiles, LastInTimeSet}; +use crate::data::paths::BasicDirEntryInfo; +use crate::library::results::HttmResult; use crate::GLOBAL_CONFIG; use rayon::Scope; use skim::prelude::*; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::sync::atomic::AtomicBool; -pub struct SpawnDeletedThread; +pub struct DeletedSearch { + requested_dir: BasicDirEntryInfo, + skim_tx: SkimItemSender, + hangup: Arc, +} -impl SpawnDeletedThread { +impl DeletedSearch { // "spawn" a lighter weight rayon/greenish thread for enumerate_deleted, if needed - pub fn exec( + pub fn spawn( requested_dir: &Path, deleted_scope: &Scope, skim_tx: &SkimItemSender, - hangup_rx: &Receiver, + hangup: &Arc, ) { - // canonicalize requested dir path b/c could be a symlink - let requested_dir_clone = requested_dir.to_path_buf(); - let skim_tx_clone = skim_tx.clone(); - let hangup_rx_clone = hangup_rx.clone(); + let new = Self::new(requested_dir, skim_tx.clone(), hangup.clone()); deleted_scope.spawn(move |_| { - #[cfg(feature = "setpriority")] - #[cfg(target_os = "linux")] - #[cfg(target_env = "gnu")] - { - use crate::config::generate::ExecMode; - use crate::library::utility::ThreadPriorityType; + let _ = new.run_loop(); + }) + } + + fn new(requested_dir: &Path, skim_tx: SkimItemSender, hangup: Arc) -> Self { + Self { + requested_dir: BasicDirEntryInfo::new(requested_dir.to_path_buf(), None), + skim_tx, + hangup, + } + } - let tid = std::process::id(); - if !matches!( - GLOBAL_CONFIG.exec_mode, - ExecMode::NonInteractiveRecursive(_) - ) { - match GLOBAL_CONFIG.opt_deleted_mode { - Some(DeletedMode::Only) => (), - _ => { - let _ = ThreadPriorityType::Process.nice_thread(Some(tid), 1i32); - } - } - } + fn run_loop(&self) -> HttmResult<()> { + let mut queue = vec![self.requested_dir.clone()]; + + while let Some(deleted_dir) = queue.pop() { + // check -- should deleted threads keep working? + // exit/error on disconnected channel, which closes + // at end of browse scope + if self.hangup.load(Ordering::Relaxed) { + break; } - let _ = Self::enter_directory(&requested_dir_clone, &skim_tx_clone, &hangup_rx_clone); - }) + + if let Ok(mut res) = self.enter_directory(&deleted_dir.path()) { + queue.append(&mut res); + } + } + + Ok(()) } // deleted file search for all modes - fn enter_directory( - requested_dir: &Path, - skim_tx: &SkimItemSender, - hangup_rx: &Receiver, - ) -> HttmResult<()> { + fn enter_directory(&self, requested_dir: &Path) -> HttmResult> { // check -- should deleted threads keep working? // exit/error on disconnected channel, which closes // at end of browse scope - if is_channel_closed(hangup_rx) { - return Ok(()); + if self.hangup.as_ref().load(Ordering::Relaxed) { + return Ok(Vec::new()); } - // obtain all unique deleted, unordered, unsorted, will need to fix - let vec_deleted = DeletedFiles::new(requested_dir)?.into_inner(); - - if vec_deleted.is_empty() { - return Ok(()); - } + // create entries struct here + let entries = Entries::new(requested_dir, &PathProvenance::IsPhantom, &self.skim_tx)?; // combined entries will be sent or printed, but we need the vec_dirs to recurse - let (vec_dirs, vec_files): (Vec, Vec) = - vec_deleted.into_iter().partition(|entry| { - // no need to traverse symlinks in deleted search - SharedRecursive::is_entry_dir(entry) - }); - - SharedRecursive::combine_and_send_entries( - vec_files, - &vec_dirs, - PathProvenance::IsPhantom, - requested_dir, - skim_tx, - )?; + let vec_dirs = entries.combine_and_send()?; // disable behind deleted dirs with DepthOfOne, // otherwise recurse and find all those deleted files @@ -108,123 +95,10 @@ impl SpawnDeletedThread { // are transmission errors, which are handled elsewhere if GLOBAL_CONFIG.opt_deleted_mode != Some(DeletedMode::DepthOfOne) && GLOBAL_CONFIG.opt_recursive - && !vec_dirs.is_empty() { - // get latest in time per our policy - let path_set: Vec = vec_dirs.into_iter().map(PathData::from).collect(); - - return LastInTimeSet::new(path_set)? - .iter() - .try_for_each(|deleted_dir| { - RecurseBehindDeletedDir::exec( - deleted_dir.as_path(), - requested_dir, - skim_tx, - hangup_rx, - ) - }); - } - - Ok(()) - } -} - -struct RecurseBehindDeletedDir { - vec_dirs: Vec, - deleted_dir_on_snap: PathBuf, - pseudo_live_dir: PathBuf, -} - -impl RecurseBehindDeletedDir { - // searches for all files behind the dirs that have been deleted - // recurses over all dir entries and creates pseudo live versions - // for them all, policy is to use the latest snapshot version before - // deletion - fn exec( - deleted_dir: &Path, - requested_dir: &Path, - skim_tx: &SkimItemSender, - hangup_rx: &Receiver, - ) -> HttmResult<()> { - // check -- should deleted threads keep working? - // exit/error on disconnected channel, which closes - // at end of browse scope - if is_channel_closed(hangup_rx) { - return Ok(()); - } - - let mut queue = match &deleted_dir.file_name() { - Some(dir_name) => { - let from_deleted_dir = deleted_dir - .parent() - .ok_or_else(|| HttmError::new("Not a valid directory name!"))?; - - let from_requested_dir = requested_dir; - - match RecurseBehindDeletedDir::enter_directory( - Path::new(dir_name), - from_deleted_dir, - from_requested_dir, - skim_tx, - ) { - Ok(res) if !res.vec_dirs.is_empty() => Vec::from([res]), - _ => return Ok(()), - } - } - None => return Err(HttmError::new("Not a valid directory name!").into()), - }; - - while let Some(item) = queue.pop() { - if is_channel_closed(hangup_rx) { - return Ok(()); - } - - let new = item - .vec_dirs - .into_iter() - .map(|basic_info| { - let dir_name = Path::new(basic_info.filename()); - RecurseBehindDeletedDir::enter_directory( - dir_name, - &item.deleted_dir_on_snap, - &item.pseudo_live_dir, - skim_tx, - ) - }) - .flatten(); - - queue.extend(new); + return Ok(vec_dirs); } - Ok(()) - } - - fn enter_directory( - dir_name: &Path, - from_deleted_dir: &Path, - from_requested_dir: &Path, - skim_tx: &SkimItemSender, - ) -> HttmResult { - // deleted_dir_on_snap is the path from the deleted dir on the snapshot - // pseudo_live_dir is the path from the fake, deleted directory that once was - let deleted_dir_on_snap = from_deleted_dir.to_path_buf().join(dir_name); - let pseudo_live_dir = from_requested_dir.to_path_buf().join(dir_name); - - let (vec_dirs, vec_files): (Vec, Vec) = - SharedRecursive::entries_partitioned(&deleted_dir_on_snap)?; - - SharedRecursive::combine_and_send_entries( - vec_files, - &vec_dirs, - PathProvenance::IsPhantom, - &pseudo_live_dir, - skim_tx, - )?; - - Ok(RecurseBehindDeletedDir { - vec_dirs, - deleted_dir_on_snap, - pseudo_live_dir, - }) + Ok(Vec::new()) } } diff --git a/src/background/recursive.rs b/src/background/recursive.rs index 0b1b7d35..75a4d150 100644 --- a/src/background/recursive.rs +++ b/src/background/recursive.rs @@ -15,63 +15,50 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::background::deleted::SpawnDeletedThread; +use crate::background::deleted::DeletedSearch; use crate::config::generate::{DeletedMode, ExecMode}; use crate::data::paths::{BasicDirEntryInfo, PathData}; -use crate::data::selection::SelectionCandidate; -use crate::display_versions::wrapper::VersionsDisplayWrapper; +use crate::display::wrapper::DisplayWrapper; use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::{ - is_channel_closed, path_is_filter_dir, print_output_buf, HttmIsDir, Never, -}; -use crate::parse::mounts::MaxLen; -use crate::{VersionsMap, BTRFS_SNAPPER_HIDDEN_DIRECTORY, GLOBAL_CONFIG, ZFS_HIDDEN_DIRECTORY}; -use once_cell::sync::Lazy; +use crate::library::utility::print_output_buf; +use crate::lookup::deleted::DeletedFiles; +use crate::{VersionsMap, GLOBAL_CONFIG}; use rayon::{Scope, ThreadPool}; use skim::prelude::*; use std::fs::read_dir; -use std::os::unix::fs::MetadataExt; use std::path::Path; +use std::sync::atomic::AtomicBool; use std::sync::Arc; -static OPT_REQUESTED_DIR_DEV: Lazy = Lazy::new(|| { - GLOBAL_CONFIG - .opt_requested_dir - .as_ref() - .expect("opt_requested_dir should be Some value at this point in execution") - .symlink_metadata() - .expect("Cannot read metadata for directory requested for search.") - .dev() -}); - -static FILTER_DIRS_MAX_LEN: Lazy = - Lazy::new(|| GLOBAL_CONFIG.dataset_collection.filter_dirs.max_len()); - #[derive(Clone, Copy)] pub enum PathProvenance { FromLiveDataset, IsPhantom, } -pub struct RecursiveSearch; - -impl RecursiveSearch { - pub fn exec(requested_dir: &Path, skim_tx: SkimItemSender, hangup_rx: Receiver) { - fn run_loop( - requested_dir: &Path, - skim_tx: SkimItemSender, - hangup_rx: Receiver, - opt_deleted_scope: Option<&Scope>, - ) { - // this runs the main loop for live file searches, see the referenced struct below - // we are in our own detached system thread, so print error and exit if error trickles up - RecursiveMainLoop::exec(requested_dir, opt_deleted_scope, &skim_tx, &hangup_rx) - .unwrap_or_else(|error| { - eprintln!("Error: {error}"); - std::process::exit(1) - }); +pub struct RecursiveSearch<'a> { + requested_dir: &'a Path, + skim_tx: SkimItemSender, + hangup: Arc, + started: Arc, +} + +impl<'a> RecursiveSearch<'a> { + pub fn new( + requested_dir: &'a Path, + skim_tx: SkimItemSender, + hangup: Arc, + started: Arc, + ) -> Self { + Self { + requested_dir, + skim_tx, + hangup, + started, } + } + pub fn exec(&self) { if GLOBAL_CONFIG.opt_deleted_mode.is_some() { // thread pool allows deleted to have its own scope, which means // all threads must complete before the scope exits. this is important @@ -82,56 +69,60 @@ impl RecursiveSearch { .expect("Could not initialize rayon threadpool for recursive deleted search"); pool.in_place_scope(|deleted_scope| { - run_loop(requested_dir, skim_tx, hangup_rx, Some(deleted_scope)) + self.run_loop(Some(deleted_scope)); }) } else { - run_loop(requested_dir, skim_tx, hangup_rx, None) + self.run_loop(None); } } -} -// this is the main loop to recurse all files -pub struct RecursiveMainLoop; + fn run_loop(&self, opt_deleted_scope: Option<&Scope>) { + // this runs the main loop for live file searches, see the referenced struct below + // we are in our own detached system thread, so print error and exit if error trickles up + self.loop_body(opt_deleted_scope).unwrap_or_else(|error| { + eprintln!("ERROR: {error}"); + std::process::exit(1) + }); + } -impl RecursiveMainLoop { - fn exec( - requested_dir: &Path, - opt_deleted_scope: Option<&Scope>, - skim_tx: &SkimItemSender, - hangup_rx: &Receiver, - ) -> HttmResult<()> { + fn loop_body(&self, opt_deleted_scope: Option<&Scope>) -> HttmResult<()> { // the user may specify a dir for browsing, // but wants to restore that directory, // so here we add the directory and its parent as a selection item - let dot_as_entry = BasicDirEntryInfo { - path: requested_dir.to_path_buf(), - file_type: Some(requested_dir.metadata()?.file_type()), - }; - + let dot_as_entry = BasicDirEntryInfo::new( + self.requested_dir.to_path_buf(), + Some(self.requested_dir.metadata()?.file_type()), + ); let mut initial_vec_dirs = vec![dot_as_entry]; - if let Some(parent) = requested_dir.parent() { - let double_dot_as_entry = BasicDirEntryInfo { - path: parent.to_path_buf(), - file_type: Some(parent.metadata()?.file_type()), - }; + if let Some(parent) = self.requested_dir.parent() { + let double_dot_as_entry = + BasicDirEntryInfo::new(parent.to_path_buf(), Some(parent.metadata()?.file_type())); initial_vec_dirs.push(double_dot_as_entry) } - SharedRecursive::combine_and_send_entries( - vec![], - &initial_vec_dirs, - PathProvenance::FromLiveDataset, - requested_dir, - skim_tx, - )?; + let initial_entries = Entries { + requested_dir: self.requested_dir, + is_phantom: &PathProvenance::FromLiveDataset, + skim_tx: &self.skim_tx, + vec_dirs: initial_vec_dirs, + vec_files: Vec::new(), + }; + + initial_entries.combine_and_send()?; // runs once for non-recursive but also "primes the pump" // for recursive to have items available, also only place an // error can stop execution - let mut queue: Vec = - Self::enter_directory(requested_dir, opt_deleted_scope, skim_tx, hangup_rx)?; + let mut queue: Vec = Self::enter_directory( + self.requested_dir, + opt_deleted_scope, + &self.skim_tx, + &self.hangup, + )?; + + self.started.store(true, Ordering::SeqCst); if GLOBAL_CONFIG.opt_recursive { // condition kills iter when user has made a selection @@ -140,16 +131,19 @@ impl RecursiveMainLoop { // check -- should deleted threads keep working? // exit/error on disconnected channel, which closes // at end of browse scope - if is_channel_closed(hangup_rx) { + if self.hangup.load(Ordering::Relaxed) { break; } // no errors will be propagated in recursive mode // far too likely to run into a dir we don't have permissions to view - if let Ok(items) = - Self::enter_directory(&item.path, opt_deleted_scope, skim_tx, hangup_rx) - { - queue.extend(items) + if let Ok(mut items) = Self::enter_directory( + &item.path(), + opt_deleted_scope, + &self.skim_tx, + &self.hangup, + ) { + queue.append(&mut items) } } } @@ -161,193 +155,140 @@ impl RecursiveMainLoop { requested_dir: &Path, opt_deleted_scope: Option<&Scope>, skim_tx: &SkimItemSender, - hangup_rx: &Receiver, + hangup: &Arc, ) -> HttmResult> { // combined entries will be sent or printed, but we need the vec_dirs to recurse - let (vec_dirs, vec_files): (Vec, Vec) = - SharedRecursive::entries_partitioned(requested_dir)?; - - SharedRecursive::combine_and_send_entries( - vec_files, - &vec_dirs, - PathProvenance::FromLiveDataset, - requested_dir, - skim_tx, - )?; + let entries = Entries::new(requested_dir, &PathProvenance::FromLiveDataset, skim_tx)?; if let Some(deleted_scope) = opt_deleted_scope { - SpawnDeletedThread::exec(requested_dir, deleted_scope, skim_tx, hangup_rx); + DeletedSearch::spawn(requested_dir, deleted_scope, skim_tx, hangup); } - Ok(vec_dirs) + // entries struct is consumed, but we return vec_dirs here to continue to feed the queue + entries.combine_and_send() } } -pub struct SharedRecursive; - -impl SharedRecursive { - pub fn combine_and_send_entries( - vec_files: Vec, - vec_dirs: &[BasicDirEntryInfo], - is_phantom: PathProvenance, - requested_dir: &Path, - skim_tx: &SkimItemSender, - ) -> HttmResult<()> { - let mut combined = vec_files; - combined.extend_from_slice(vec_dirs); +pub struct Entries<'a> { + pub requested_dir: &'a Path, + pub is_phantom: &'a PathProvenance, + pub skim_tx: &'a SkimItemSender, + pub vec_dirs: Vec, + pub vec_files: Vec, +} - let entries = match is_phantom { +impl<'a> Entries<'a> { + #[inline(always)] + pub fn new( + requested_dir: &'a Path, + is_phantom: &'a PathProvenance, + skim_tx: &'a SkimItemSender, + ) -> HttmResult { + // separates entries into dirs and files + let (vec_dirs, vec_files) = match is_phantom { PathProvenance::FromLiveDataset => { - // live - not phantom - match GLOBAL_CONFIG.opt_deleted_mode { - Some(DeletedMode::Only) => return Ok(()), - Some(DeletedMode::DepthOfOne | DeletedMode::All) | None => { - // never show live files is display recursive/deleted only file mode - if matches!( - GLOBAL_CONFIG.exec_mode, - ExecMode::NonInteractiveRecursive(_) - ) { - return Ok(()); - } - combined - } - } + read_dir(requested_dir)? + .flatten() + // checking file_type on dir entries is always preferable + // as it is much faster than a metadata call on the path + .map(|dir_entry| BasicDirEntryInfo::from(&dir_entry)) + .filter(|entry| entry.all_exclusions()) + .partition(|entry| entry.is_entry_dir()) } PathProvenance::IsPhantom => { - // deleted - phantom - Self::pseudo_live_versions(combined, requested_dir) + // obtain all unique deleted, unordered, unsorted, will need to fix + DeletedFiles::new(&requested_dir)? + .into_inner() + .into_iter() + .filter(|entry| entry.all_exclusions()) + .partition(|entry| entry.is_entry_dir()) } }; - Self::display_or_transmit(entries, is_phantom, skim_tx) + Ok(Self { + requested_dir, + is_phantom, + skim_tx, + vec_dirs, + vec_files, + }) } - pub fn entries_partitioned( - requested_dir: &Path, - ) -> HttmResult<(Vec, Vec)> { - // separates entries into dirs and files - let (vec_dirs, vec_files) = read_dir(requested_dir)? - .flatten() - // checking file_type on dir entries is always preferable - // as it is much faster than a metadata call on the path - .map(|dir_entry| BasicDirEntryInfo::from(&dir_entry)) - .filter(|entry| { - if GLOBAL_CONFIG.opt_no_filter { - return true; - } - - if GLOBAL_CONFIG.opt_no_hidden - && entry.filename().to_string_lossy().starts_with('.') - { - return false; - } - - if GLOBAL_CONFIG.opt_one_filesystem { - match entry.path.metadata() { - Ok(path_md) if *OPT_REQUESTED_DIR_DEV == path_md.dev() => {} - _ => { - // if we can't read the metadata for a path, - // we probably shouldn't show it either - return false; - } - } - } + #[inline(always)] + pub fn combine_and_send(self) -> HttmResult> { + let mut combined = self.vec_files; + combined.extend_from_slice(&self.vec_dirs); - if let Ok(file_type) = entry.filetype() { - if file_type.is_dir() { - return !Self::is_filter_dir(entry); + let entries_ready_to_send = match self.is_phantom { + PathProvenance::FromLiveDataset => { + // live - not phantom + match GLOBAL_CONFIG.opt_deleted_mode { + Some(DeletedMode::Only) => Vec::new(), + _ if matches!( + GLOBAL_CONFIG.exec_mode, + ExecMode::NonInteractiveRecursive(_) + ) => + { + Vec::new() } + _ => combined, } - - true - }) - .partition(Self::is_entry_dir); - - Ok((vec_dirs, vec_files)) - } - - pub fn is_entry_dir(entry: &BasicDirEntryInfo) -> bool { - // must do is_dir() look up on DirEntry file_type() as look up on Path will traverse links! - if GLOBAL_CONFIG.opt_no_traverse { - if let Ok(file_type) = entry.filetype() { - return file_type.is_dir(); } - } - - entry.httm_is_dir() - } - - fn is_filter_dir(entry: &BasicDirEntryInfo) -> bool { - // FYI path is always a relative path, but no need to canonicalize as - // partial eq for paths is comparison of components iter - let path = entry.path.as_path(); - - // never check the hidden snapshot directory for live files (duh) - // didn't think this was possible until I saw a SMB share return - // a .zfs dir entry - if path.ends_with(ZFS_HIDDEN_DIRECTORY) || path.ends_with(BTRFS_SNAPPER_HIDDEN_DIRECTORY) { - return true; - } - - // is a common btrfs snapshot dir? - if let Some(common_snap_dir) = &GLOBAL_CONFIG.dataset_collection.opt_common_snap_dir { - if path == *common_snap_dir { - return true; - } - } - - // check whether user requested this dir specifically, then we will show - if let Some(user_requested_dir) = GLOBAL_CONFIG.opt_requested_dir.as_ref() { - if user_requested_dir.as_path() == path { - return false; + PathProvenance::IsPhantom => { + // this function creates dummy "live versions" values to match deleted files + // which have been found on snapshots, so we return to the user "the path that + // once was" in their browse panel + combined + .into_iter() + .filter_map(|entry| entry.into_pseudo_live_version(self.requested_dir)) + .collect() } - } + }; - // finally : is a non-supported dataset? - // bailout easily if path is larger than max_filter_dir len - if path.components().count() > *FILTER_DIRS_MAX_LEN { - return false; - } + DisplayOrTransmit::new(entries_ready_to_send, self.is_phantom, self.skim_tx).exec()?; - path_is_filter_dir(path) + // here we consume the struct after sending the entries, + // however we still need the dirs to populate the loop's queue + // so we return the vec of dirs here + Ok(self.vec_dirs) } +} - // this function creates dummy "live versions" values to match deleted files - // which have been found on snapshots, we return to the user "the path that - // once was" in their browse panel - fn pseudo_live_versions( - entries: Vec, - pseudo_live_dir: &Path, - ) -> Vec { - entries - .into_iter() - .map(|basic_info| BasicDirEntryInfo { - path: pseudo_live_dir.join(basic_info.path.file_name().unwrap_or_default()), - file_type: basic_info.file_type, - }) - .collect() +struct DisplayOrTransmit<'a> { + combined_entries: Vec, + is_phantom: &'a PathProvenance, + skim_tx: &'a SkimItemSender, +} + +impl<'a> DisplayOrTransmit<'a> { + fn new( + combined_entries: Vec, + is_phantom: &'a PathProvenance, + skim_tx: &'a SkimItemSender, + ) -> Self { + Self { + combined_entries, + is_phantom, + skim_tx, + } } - fn display_or_transmit( - entries: Vec, - is_phantom: PathProvenance, - skim_tx: &SkimItemSender, - ) -> HttmResult<()> { + fn exec(self) -> HttmResult<()> { // send to the interactive view, or print directly, never return back match &GLOBAL_CONFIG.exec_mode { - ExecMode::Interactive(_) => Self::transmit(entries, is_phantom, skim_tx)?, + ExecMode::Interactive(_) => self.transmit()?, ExecMode::NonInteractiveRecursive(progress_bar) => { - if entries.is_empty() { + if self.combined_entries.is_empty() { if GLOBAL_CONFIG.opt_recursive { progress_bar.tick(); } else { eprintln!( - "NOTICE: httm could not find any deleted files at this directory level. \ - Perhaps try specifying a deleted mode in combination with \"--recursive\"." - ) + "NOTICE: httm could not find any deleted files at this directory level. \ + Perhaps try specifying a deleted mode in combination with \"--recursive\"." + ) } } else { - NonInteractiveRecursiveWrapper::print(entries)?; + self.display()?; // keeps spinner from squashing last line of output if GLOBAL_CONFIG.opt_recursive { @@ -361,20 +302,30 @@ impl SharedRecursive { Ok(()) } - fn transmit( - entries: Vec, - is_phantom: PathProvenance, - skim_tx: &SkimItemSender, - ) -> HttmResult<()> { + fn transmit(self) -> HttmResult<()> { // don't want a par_iter here because it will block and wait for all // results, instead of printing and recursing into the subsequent dirs - entries + self.combined_entries .into_iter() .try_for_each(|basic_info| { - skim_tx.try_send(Arc::new(SelectionCandidate::new(basic_info, is_phantom))) + self.skim_tx + .try_send(Arc::new(basic_info.into_selection(&self.is_phantom))) }) .map_err(std::convert::Into::into) } + + fn display(self) -> HttmResult<()> { + let pseudo_live_set: Vec = self + .combined_entries + .into_iter() + .map(PathData::from) + .collect(); + + let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &pseudo_live_set)?; + let output_buf = DisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); + + print_output_buf(&output_buf) + } } // this is wrapper for non-interactive searches, which will be executed through the SharedRecursive fns @@ -386,11 +337,12 @@ impl NonInteractiveRecursiveWrapper { pub fn exec() -> HttmResult<()> { // won't be sending anything anywhere, this just allows us to reuse enumerate_directory let (dummy_skim_tx, _): (SkimItemSender, SkimItemReceiver) = unbounded(); - let (hangup_tx, hangup_rx): (Sender, Receiver) = bounded(0); + let started = Arc::new(AtomicBool::new(true)); + let hangup = Arc::new(AtomicBool::new(false)); match &GLOBAL_CONFIG.opt_requested_dir { Some(requested_dir) => { - RecursiveSearch::exec(requested_dir, dummy_skim_tx, hangup_rx); + RecursiveSearch::new(requested_dir, dummy_skim_tx, hangup, started).exec(); } None => { return Err(HttmError::new( @@ -402,13 +354,4 @@ impl NonInteractiveRecursiveWrapper { Ok(()) } - - fn print(entries: Vec) -> HttmResult<()> { - let pseudo_live_set: Vec = entries.into_iter().map(PathData::from).collect(); - - let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &pseudo_live_set)?; - let output_buf = VersionsDisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); - - print_output_buf(&output_buf) - } } diff --git a/src/config/generate.rs b/src/config/generate.rs index 06e0a91d..ed88f862 100644 --- a/src/config/generate.rs +++ b/src/config/generate.rs @@ -17,15 +17,15 @@ use crate::config::install_hot_keys::install_hot_keys; use crate::data::filesystem_info::FilesystemInfo; -use crate::data::paths::PathDeconstruction; -use crate::data::paths::{PathData, ZfsSnapPathGuard}; +use crate::data::paths::{PathData, PathDeconstruction, ZfsSnapPathGuard}; +use crate::filesystem::mounts::{FilesystemType, ROOT_PATH}; use crate::library::results::{HttmError, HttmResult}; use crate::library::utility::{pwd, HttmIsDir}; use crate::lookup::file_mounts::MountDisplay; -use crate::ROOT_DIRECTORY; -use clap::{crate_name, crate_version, Arg, ArgMatches, OsValues}; +use clap::parser::ValuesRef; +use clap::{crate_name, crate_version, Arg, ArgAction, ArgMatches}; use indicatif::ProgressBar; -use rayon::prelude::*; +use rayon::iter::{ParallelBridge, ParallelIterator}; use std::io::Read; use std::ops::Index; use std::path::{Path, PathBuf}; @@ -79,10 +79,21 @@ pub enum RestoreMode { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PrintMode { - FormattedDefault, - FormattedNotPretty, - RawNewline, - RawZero, + Formatted(FormattedMode), + Raw(RawMode), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RawMode { + Csv, + Newline, + Zero, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FormattedMode { + Default, + NotPretty, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -93,10 +104,10 @@ pub enum DeletedMode { } #[derive(Debug, Clone)] -pub enum ListSnapsOfType { - All, - UniqueMetadata, - UniqueContents, +pub enum DedupBy { + Disable, + Metadata, + Contents, } #[derive(Debug, Clone)] @@ -133,20 +144,20 @@ const NATIVE_SNAP_SUFFIXES: [&str; 4] = [ ]; fn parse_args() -> ArgMatches { - clap::Command::new(crate_name!()) - .about("httm prints the size, date and corresponding locations of available unique versions of files residing on snapshots. \ + clap::command!(crate_name!()) + .about("httm prints the size, date and corresponding locations of available unique versions of files residing on snapshots. \ May also be used interactively to select and restore from such versions, and even to snapshot datasets which contain certain files.") .version(crate_version!()) .arg( Arg::new("INPUT_FILES") - .help("in any non-interactive mode, put requested paths here. If you include no paths as arguments, \ - then httm will pause waiting for input on stdin. In any interactive mode, \ + .help("in any non-interactive mode, put requested paths here. If you include no paths as arguments, \ + then httm will pause waiting for input on stdin. In any interactive mode, \ this is the directory search path. If no directory is specified, \ httm will use the current working directory.") - .takes_value(true) - .multiple_values(true) - .value_parser(clap::builder::ValueParser::os_string()) + .value_parser(clap::value_parser!(PathBuf)) + .num_args(0..) .display_order(1) + .action(ArgAction::Append) ) .arg( Arg::new("BROWSE") @@ -156,56 +167,57 @@ fn parse_args() -> ArgMatches { .visible_alias("interactive") .help("interactive browse and search a specified directory to display unique file versions.") .display_order(2) + .action(ArgAction::SetTrue) ) .arg( Arg::new("SELECT") .short('s') .long("select") - .takes_value(true) + .value_parser(["path", "contents", "preview"]) + .num_args(0..=1) .default_missing_value("path") - .possible_values(["path", "contents", "preview"]) - .min_values(0) .require_equals(true) - .help("interactive browse and search a specified directory to display unique file versions. \ - Continue to another dialog to select a snapshot version to dump to stdout. This argument optionally takes a value. \ + .help("interactive browse and search a specified directory to display unique file versions. \ + Continue to another dialog to select a snapshot version to dump to stdout. This argument optionally takes a value. \ Default behavior/value is to simply print the path name, but, if the path is a file, the user can print the file's contents by giving the value \"contents\", \ or print the PREVIEW output by giving the value \"preview\".") .conflicts_with("RESTORE") .display_order(3) + .action(ArgAction::Append) ) .arg( Arg::new("RESTORE") .short('r') .long("restore") - .takes_value(true) - .possible_values(["copy", "copy-and-preserve", "overwrite", "yolo", "guard"]) - .min_values(0) + .value_parser(["copy", "copy-and-preserve", "overwrite", "yolo", "guard"]) + .num_args(0..=1) + .default_missing_value("copy") .require_equals(true) - .help("interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to restore. \ - This argument optionally takes a value. Default behavior/value is a non-destructive \"copy\" to the current working directory with a new name, \ - so as not to overwrite any \"live\" file version. However, the user may specify \"overwrite\" (or \"yolo\") to restore to the same file location. Note, \"overwrite\" can be a DESTRUCTIVE operation. \ - Overwrite mode will attempt to preserve attributes, like the permissions/mode, timestamps, xattrs and ownership of the selected snapshot file version (this is and will likely remain a UNIX only feature). \ - In order to preserve such attributes in \"copy\" mode, specify the \"copy-and-preserve\" value. User may also specify \"guard\". \ - Guard mode has the same semantics as \"overwrite\" but will attempt to take a precautionary snapshot before any overwrite action occurs. \ - Note: Guard mode is a ZFS only option. User may also set via the HTTM_RESTORE_MODE environment variable.") + .help("interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to restore. \ + This argument optionally takes a value. Default behavior/value is a non-destructive \"copy\" to the current working directory with a new name, \ + so as not to overwrite any \"live\" file version. However, the user may specify \"overwrite\" (or \"yolo\") to restore to the same file location. Note, \"overwrite\" can be a DESTRUCTIVE operation. \ + Overwrite mode will attempt to preserve attributes, like the permissions/mode, timestamps, xattrs and ownership of the selected snapshot file version (this is and will likely remain a UNIX only feature). \ + In order to preserve such attributes in \"copy\" mode, specify the \"copy-and-preserve\" value. User may also specify \"guard\". \ + Guard mode has the same semantics as \"overwrite\" but will attempt to take a precautionary snapshot before any overwrite action occurs. \ + Note: Guard mode is a ZFS only option. User may also set via the HTTM_RESTORE_MODE environment variable.") .conflicts_with("SELECT") .display_order(4) + .action(ArgAction::Append) ) .arg( Arg::new("DELETED") .short('d') .long("deleted") - .takes_value(true) .default_missing_value("all") - .possible_values(["all", "single", "only"]) + .value_parser(["all", "single", "only"]) + .num_args(0..=1) .require_equals(true) - .min_values(0) - .require_equals(true) - .help("show deleted files in interactive modes. In non-interactive modes, do a search for all files deleted from a specified directory. \ - This argument optionally takes a value. The default behavior/value is \"all\". \ + .help("show deleted files in interactive modes. In non-interactive modes, do a search for all files deleted from a specified directory. \ + This argument optionally takes a value. The default behavior/value is \"all\". \ If \"only\" is specified, then, in the interactive modes, non-deleted files will be excluded from the search. \ If \"single\" is specified, then, deleted files behind deleted directories, (that is -- files with a depth greater than one) will be ignored.") .display_order(5) + .action(ArgAction::Append) ) .arg( Arg::new("RECURSIVE") @@ -214,47 +226,51 @@ fn parse_args() -> ArgMatches { .conflicts_with_all(&["SNAPSHOT"]) .help("recurse into the selected directory to find more files. Only available in interactive and deleted file modes.") .display_order(6) + .action(ArgAction::SetTrue) ) .arg( Arg::new("ALT_REPLICATED") .short('a') .long("alt-replicated") - .help("automatically discover locally replicated datasets and list their snapshots as well. \ - NOTE: Be certain such replicated datasets are mounted before use. \ + .aliases(["replicated"]) + .help("automatically discover locally replicated datasets and list their snapshots as well. \ + NOTE: Be certain such replicated datasets are mounted before use. \ httm will silently ignore unmounted datasets in the interactive modes.") .conflicts_with_all(&["REMOTE_DIR", "LOCAL_DIR"]) .display_order(7) + .action(ArgAction::SetTrue) ) .arg( Arg::new("PREVIEW") .short('p') .long("preview") - .help("user may specify a command to preview snapshots while in a snapshot selection view. This argument optionally takes a value specifying the command to be executed. \ - The default value/command, if no command value specified, is a 'bowie' formatted 'diff'. \ - User defined commands must specify the snapshot file name \"{snap_file}\" and the live file name \"{live_file}\" within their shell command. \ + .help("user may specify a command to preview snapshots while in a snapshot selection view. This argument optionally takes a value specifying the command to be executed. \ + The default value/command, if no command value specified, is a 'bowie' formatted 'diff'. \ + User defined commands must specify the snapshot file name \"{snap_file}\" and the live file name \"{live_file}\" within their shell command. \ NOTE: 'bash' is required to bootstrap any preview script, even if user defined preview commands or script is written in a different language.") - .takes_value(true) - .min_values(0) + .value_parser(clap::value_parser!(String)) + .num_args(0..=1) .require_equals(true) .default_missing_value("default") .display_order(8) + .action(ArgAction::Append) ) .arg( - Arg::new("UNIQUENESS") - .long("uniqueness") - .visible_aliases(&["unique"]) - .takes_value(true) + Arg::new("DEDUP_BY") + .long("dedup-by") + .value_parser(["disable", "all", "no-filter", "metadata", "contents"]) + .num_args(0..=1) + .visible_aliases(&["unique", "uniqueness"]) .default_missing_value("contents") - .possible_values(["all", "no-filter", "metadata", "contents"]) - .min_values(0) .require_equals(true) .help("comparing file versions solely on the basis of size and modify time (the default \"metadata\" behavior) may return what appear to be \"false positives\", \ - in the sense that, modify time is not a precise measure of whether a file has actually changed. A program might overwrite a file with the same contents, \ - or a user can simply update the modify time via 'touch'. If only this flag is specified, the \"contents\" option compares the actual file contents of file versions, if their sizes match, \ - and overrides the default \"metadata\" behavior. The \"contents\" option can be expensive, as the file versions need to be read back and compared, and should probably only be used for smaller files. \ + in the sense that, modify time is not a precise measure of whether a file has actually changed. A program might overwrite a file with the same contents, \ + or a user can simply update the modify time via 'touch'. If only this flag is specified, the \"contents\" option compares the actual file contents of file versions, if their sizes match, \ + and overrides the default \"metadata\" behavior. The \"contents\" option can be expensive, as the file versions need to be read back and compared, and should probably only be used for smaller files. \ Given how expensive this operation can be, for larger files or files with many versions, \"contents\" option is not shown in Interactive browse mode, \ - but after a selection is made, can be utilized in Select or Restore modes. The \"all\" or \"no-filter\" option dumps all snapshot versions, and no attempt is made to determine if the file versions are distinct.") + but after a selection is made, can be utilized, when enabled, in Select or Restore modes. The \"disable\" \"all\" or \"no-filter\" option dumps all snapshot versions, and no attempt is made to determine if the file versions are distinct.") .display_order(9) + .action(ArgAction::Append) ) .arg( Arg::new("EXACT") @@ -262,70 +278,75 @@ fn parse_args() -> ArgMatches { .long("exact") .help("use exact pattern matching for searches in the interactive modes (in contrast to the default fuzzy searching).") .display_order(10) + .action(ArgAction::SetTrue) ) .arg( Arg::new("SNAPSHOT") .short('S') .long("snap") - .takes_value(true) - .min_values(0) .require_equals(true) .default_missing_value("httmSnapFileMount") - .visible_aliases(&["snap-file", "snapshot", "snap-file-mount"]) - .help("snapshot a file/s most immediate mount. \ - This argument optionally takes a value for a snapshot suffix. The default suffix is 'httmSnapFileMount'. \ + .num_args(0..=1) + .value_parser(clap::value_parser!(String)) + .help("snapshot a file/s most immediate mount. \ + This argument optionally takes a value for a snapshot suffix. The default suffix is 'httmSnapFileMount'. \ Note: This is a ZFS only option which requires either superuser or 'zfs allow' privileges.") .conflicts_with_all(&["BROWSE", "SELECT", "RESTORE", "ALT_REPLICATED", "REMOTE_DIR", "LOCAL_DIR"]) .display_order(11) + .action(ArgAction::Append) ) .arg( Arg::new("LIST_SNAPS") .long("list-snaps") - .aliases(&["snaps-for-file", "ls-snaps", "list-snapshots"]) - .takes_value(true) - .min_values(0) + .aliases(&["snap-names", "snaps-for-file", "ls-snaps", "list-snapshots"]) + .value_parser(clap::value_parser!(String)) + .num_args(0..=1) .require_equals(true) - .multiple_values(false) - .help("display snapshots names for a file. This argument optionally takes a value. \ - By default, this argument will return all available snapshot names. \ - User may limit type of snapshots returned via the UNIQUENESS flag. \ - The user may also omit the most recent \"n\" snapshots from any list. \ - By appending a comma, this argument also filters those snapshots which contain the specified pattern/s. \ - A value of \"5,prep_Apt\" would return the snapshot names of only the last 5 (at most) of all snapshot versions which contain \"prep_Apt\". \ - The value \"native\" will restrict selection to only 'httm' native snapshot suffix values, like \"httmSnapFileMount\" and \"ounceSnapFileMount\". \ - Note: This is a ZFS only option.") + .help("display snapshots names for a file. This argument optionally takes a value. \ + By default, this argument will return all available snapshot names. \ + When the DEDUP_BY flag is not specified but the LIST_SNAPS is, the default DEDUP_BY level is \"all\" snapshots. \ + User may limit type of snapshots returned via specifying the DEDUP_BY flag. \ + The user may also omit the most recent \"n\" snapshots from any list. \ + By appending a comma, this argument also filters those snapshots which contain the specified pattern/s. \ + A value of \"5,prep_Apt\" would return the snapshot names of only the last 5 (at most) of all snapshot versions which contain \"prep_Apt\". \ + The value \"native\" will restrict selection to only 'httm' native snapshot suffix values, like \"httmSnapFileMount\" and \"ounceSnapFileMount\". \ + Note: This is a ZFS and btrfs only option.") .conflicts_with_all(&["BROWSE", "RESTORE"]) .display_order(12) + .action(ArgAction::Append) ) .arg( Arg::new("ROLL_FORWARD") .long("roll-forward") .aliases(&["roll", "spring", "spring-forward"]) - .takes_value(true) - .min_values(1) + .value_parser(clap::value_parser!(String)) + .num_args(1) .require_equals(true) - .multiple_values(false) - .help("traditionally 'zfs rollback' is a destructive operation, whereas httm roll-forward is non-destructive. \ - httm will copy only files and their attributes that have changed since a specified snapshot, from that snapshot, to its live dataset. \ - httm will also take two precautionary snapshots, one before and one after the copy. \ - Should the roll forward fail for any reason, httm will roll back to the pre-execution state. \ - Caveats: This is a ZFS only option which requires super user privileges.") + .help("traditionally 'zfs rollback' is a destructive operation, whereas httm roll-forward is non-destructive. \ + httm will copy only files and their attributes that have changed since a specified snapshot, from that snapshot, to its live dataset. \ + httm will also take two precautionary snapshots, one before and one after the copy. \ + Should the roll forward fail for any reason, httm will roll back to the pre-execution state. \ + Caveats: This is a ZFS only option which requires super user privileges. \ + Not all filesystem features are supported (for instance, Solaris door or sockets on the snapshot) and will cause a roll forward to fail. \ + Certain special/files objects will be copied or recreated, but are not guaranteed to be in the same state as the snapshot (for instance, fifos).\ + The block clone copying so many file in parallel may also cause a kernel crash on some configurations, and is therefore disabled in this mode.") .conflicts_with_all(&["BROWSE", "RESTORE", "ALT_REPLICATED", "REMOTE_DIR", "LOCAL_DIR"]) .display_order(13) + .action(ArgAction::Append) ) .arg( Arg::new("PRUNE") .long("prune") .aliases(&["purge"]) - .help("prune all snapshot/s which contain the input file/s on that file's most immediate mount via \"zfs destroy\". \ - \"zfs destroy\" is a DESTRUCTIVE operation which *does not* only apply to the file in question, but the entire snapshot upon which it resides. \ - Careless use may cause you to lose snapshot data you care about. \ - This argument requires and will be filtered according to any values specified at LIST_SNAPS. \ - User may also enable SELECT mode to make a granular selection of specific snapshots to prune. \ + .help("prune all snapshot/s which contain the input file/s on that file's most immediate mount via \"zfs destroy\". \ + \"zfs destroy\" is a DESTRUCTIVE operation which *does not* only apply to the file in question, but the entire snapshot upon which it resides. \ + Careless use may cause you to lose snapshot data you care about. \ + This argument requires and will be filtered according to any values specified at LIST_SNAPS. \ + User may also enable SELECT mode to make a granular selection of specific snapshots to prune. \ Note: This is a ZFS only option.") - .conflicts_with_all(&["BROWSE", "RESTORE", "ALT_REPLICATED", "REMOTE_DIR", "LOCAL_DIR"]) - .requires("LIST_SNAPS") + .conflicts_with_all(&["BROWSE", "RESTORE", "ALT_REPLICATED", "REMOTE_DIR", "LOCAL_DIR"]) .display_order(13) + .action(ArgAction::SetTrue) ) .arg( Arg::new("FILE_MOUNT") @@ -333,31 +354,30 @@ fn parse_args() -> ArgMatches { .long("file-mount") .alias("mount-for-file") .visible_alias("mount") - .takes_value(true) .default_missing_value("target") - .possible_values(["source", "target", "mount", "directory", "device", "dataset", "relative-path", "relative", "relpath"]) - .min_values(0) + .value_parser(["source", "target", "mount", "directory", "device", "dataset", "relative-path", "relative", "relpath"]) + .num_args(0..=1) .require_equals(true) - .help("by default, display the all mount point/s of all dataset/s which contain/s the input file/s. \ - This argument optionally takes a value to display other information about the path. Possible values are: \ + .help("by default, display the all mount point/s of all dataset/s which contain/s the input file/s. \ + This argument optionally takes a value to display other information about the path. Possible values are: \ \"mount\" or \"target\" or \"directory\", return the directory upon which the underlying dataset or device of the mount, \ \"source\" or \"device\" or \"dataset\", return the underlying dataset/device of the mount, and, \ \"relative-path\" or \"relative\", return the path relative to the underlying dataset/device of the mount.") .conflicts_with_all(&["BROWSE", "SELECT", "RESTORE"]) .display_order(14) + .action(ArgAction::Append) ) .arg( Arg::new("LAST_SNAP") .short('l') .long("last-snap") - .takes_value(true) .default_missing_value("any") .visible_aliases(&["last", "latest"]) - .possible_values(["any", "ditto", "no-ditto", "no-ditto-exclusive", "no-ditto-inclusive", "none", "without"]) - .min_values(0) + .value_parser(["any", "ditto", "no-ditto", "no-ditto-exclusive", "no-ditto-inclusive", "none", "without"]) + .num_args(0..=1) .require_equals(true) - .help("automatically select and print the path of last-in-time unique snapshot version for the input file. \ - This argument optionally takes a value. Possible values are: \ + .help("automatically select and print the path of last-in-time unique snapshot version for the input file. \ + This argument optionally takes a value. Possible values are: \ \"any\", return the last in time snapshot version, this is the default behavior/value, \ \"ditto\", return only last snaps which are the same as the live file version, \ \"no-ditto-exclusive\", return only a last snap which is not the same as the live version (argument \"--no-ditto\" is an alias for this option), \ @@ -365,6 +385,7 @@ fn parse_args() -> ArgMatches { \"none\" or \"without\", return the live file only for those files without a last snapshot.") .conflicts_with_all(&["NUM_VERSIONS", "SNAPSHOT", "FILE_MOUNT", "ALT_REPLICATED", "REMOTE_DIR", "LOCAL_DIR", "PREVIEW"]) .display_order(15) + .action(ArgAction::Append) ) .arg( Arg::new("RAW") @@ -372,8 +393,9 @@ fn parse_args() -> ArgMatches { .long("raw") .visible_alias("newline") .help("display the snapshot locations only, without extraneous information, delimited by a NEWLINE character.") - .conflicts_with_all(&["ZEROS", "NOT_SO_PRETTY"]) + .conflicts_with_all(&["ZEROS", "CSV", "NOT_SO_PRETTY"]) .display_order(16) + .action(ArgAction::SetTrue) ) .arg( Arg::new("ZEROS") @@ -381,44 +403,59 @@ fn parse_args() -> ArgMatches { .long("zero") .visible_alias("null") .help("display the snapshot locations only, without extraneous information, delimited by a NULL character.") - .conflicts_with_all(&["RAW", "NOT_SO_PRETTY"]) + .conflicts_with_all(&["RAW", "CSV", "NOT_SO_PRETTY"]) .display_order(17) + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("CSV") + .long("csv") + .help("display all information, delimited by a comma.") + .conflicts_with_all(&["RAW", "ZEROS", "NOT_SO_PRETTY", "JSON"]) + .display_order(18) + .action(ArgAction::SetTrue) ) .arg( Arg::new("NOT_SO_PRETTY") .long("not-so-pretty") .visible_aliases(&["tabs", "plain-jane", "not-pretty"]) .help("display the ordinary output, but tab delimited, without any pretty border lines.") - .conflicts_with_all(&["RAW", "ZEROS"]) - .display_order(18) + .conflicts_with_all(&["RAW", "ZEROS", "CSV"]) + .display_order(19) + .action(ArgAction::SetTrue) ) .arg( Arg::new("JSON") .long("json") .help("display the ordinary output, but as formatted JSON.") .conflicts_with_all(&["SELECT", "RESTORE"]) - .display_order(19) + .display_order(20) + .conflicts_with_all(&["CSV"]) + .action(ArgAction::SetTrue) ) .arg( Arg::new("OMIT_DITTO") .long("omit-ditto") - .help("omit display of the snapshot version which may be identical to the live version. By default, `httm` displays all snapshot versions and the live version).") + .help("omit display of the snapshot version which may be identical to the live version. By default, `httm` displays all snapshot versions and the live version).") .conflicts_with_all(&["NUM_VERSIONS"]) - .display_order(20) + .display_order(21) + .action(ArgAction::SetTrue) ) .arg( Arg::new("NO_FILTER") .long("no-filter") - .help("by default, in the interactive modes, httm will filter out files residing upon non-supported datasets (like ext4, tmpfs, procfs, sysfs, or devtmpfs, etc.), and within any \"common\" snapshot paths. \ - Here, one may select to disable such filtering. httm, however, will always show the input path, and results from behind any input path when that is the path being searched.") - .display_order(21) + .help("by default, in the interactive modes, httm will filter out files residing upon non-supported datasets (like ext4, tmpfs, procfs, sysfs, or devtmpfs, etc.), and within any \"common\" snapshot paths. \ + Here, one may select to disable such filtering. httm, however, will always show the input path, and results from behind any input path when that is the path being searched.") + .display_order(22) + .action(ArgAction::SetTrue) ) .arg( Arg::new("FILTER_HIDDEN") .long("no-hidden") .aliases(&["no-hide", "nohide", "filter-hidden"]) .help("do not show information regarding hidden files and directories (those that start with a \'.\') in the recursive or interactive modes.") - .display_order(22) + .display_order(23) + .action(ArgAction::SetTrue) ) .arg( Arg::new("ONE_FILESYSTEM") @@ -426,114 +463,138 @@ fn parse_args() -> ArgMatches { .aliases(&["same-filesystem", "single-filesystem", "one-fs", "onefs"]) .requires("RECURSIVE") .help("limit recursive search to file and directories on the same filesystem/device as the target directory.") - .display_order(23) + .display_order(24) + .action(ArgAction::SetTrue) ) .arg( Arg::new("NO_TRAVERSE") .long("no-traverse") - .help("in recursive mode, don't traverse symlinks. Although httm does its best to prevent searching pathologically recursive symlink-ed paths, \ - here, you may disable symlink traversal completely. NOTE: httm will never traverse symlinks when a requested recursive search is on the root/base directory (\"/\").") - .display_order(24) + .help("in recursive mode, don't traverse symlinks. Although httm does its best to prevent searching pathologically recursive symlink-ed paths, \ + here, you may disable symlink traversal completely. NOTE: httm will never traverse symlinks when a requested recursive search is on the root/base directory (\"/\").") + .display_order(25) + .action(ArgAction::SetTrue) ) .arg( Arg::new("NO_LIVE") .long("no-live") .visible_aliases(&["dead", "disco"]) .help("only display information concerning snapshot versions (display no information regarding live versions of files or directories).") - .display_order(25) + .display_order(26) + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("ALT_STORE") + .long("alt-store") + .alias("store") + .require_equals(true) + .value_parser(["restic", "timemachine"]) + .help("give priority to discovered alternative backups stores, like Restic, and Time Machine. \ + If this flag is specified, httm will drop non-alternative store datasets and place said alternative backups store snapshots, as snapshots for the root mount point (\"/\"). \ + Before use, be careful that the repository is mounted. You may need superuser privileges to view a repository mounted with superuser permission. \ + httm also includes a helper script called \"equine\" which can assist you in mounting remote and local Time Machine snapshots.") + .conflicts_with_all(["MAP_ALIASES"]) + .display_order(27) + .action(ArgAction::Append) ) .arg( Arg::new("NO_SNAP") .long("no-snap") .visible_aliases(&["undead", "zombie"]) - .help("only display information concerning 'pseudo-live' versions in any Display Recursive mode (in --deleted, --recursive, but non-interactive modes). \ + .help("only display information concerning 'pseudo-live' versions in any Display Recursive mode (in --deleted, --recursive, but non-interactive modes). \ Useful for finding the \"files that once were\" and displaying only those pseudo-live/zombie files.") .conflicts_with_all(&["BROWSE", "SELECT", "RESTORE", "SNAPSHOT", "LAST_SNAP", "NOT_SO_PRETTY"]) .requires("DELETED") - .display_order(26) + .display_order(28) + .action(ArgAction::SetTrue) ) .arg( Arg::new("MAP_ALIASES") .long("map-aliases") .visible_aliases(&["aliases"]) .help("manually map a local directory (eg. \"/Users/\") as an alias of a mount point for ZFS or btrfs, \ - such as the local mount point for a backup on a remote share (eg. \"/Volumes/Home\"). \ - This option is useful if you wish to view snapshot versions from within the local directory you back up to your remote share. \ - This option requires a value. Such a value is delimited by a colon, ':', and is specified in the form : \ - (eg. --map-aliases /Users/:/Volumes/Home). Multiple maps may be specified delimited by a comma, ','. \ + such as the local mount point for a backup on a remote share (eg. \"/Volumes/Home\"). \ + This option is useful if you wish to view snapshot versions from within the local directory you back up to a remote network share. \ + This option requires a value. Such a value is delimited by a colon, ':', and is specified in the form : \ + (eg. --map-aliases /Users/:/Volumes/Home). Multiple maps may be specified delimited by a comma, ','. \ You may also set via the environment variable HTTM_MAP_ALIASES.") .use_value_delimiter(true) - .takes_value(true) .value_parser(clap::builder::ValueParser::os_string()) - .display_order(27) + .num_args(0..=1) + .display_order(29) + .action(ArgAction::Append) ) .arg( Arg::new("NUM_VERSIONS") .long("num-versions") .default_missing_value("all") - .possible_values(["all", "graph", "single", "single-no-snap", "single-with-snap", "multiple"]) - .min_values(0) + .value_parser(["all", "graph", "single", "single-no-snap", "single-with-snap", "multiple"]) + .num_args(0..=1) .require_equals(true) .help("detect and display the number of unique versions available (e.g. one, \"1\", \ - version is available if either a snapshot version exists, and is identical to live version, or only a live version exists). \ - This argument optionally takes a value. The default value, \"all\", will print the filename and number of versions, \ + version is available if either a snapshot version exists, and is identical to live version, or only a live version exists). \ + This argument optionally takes a value. The default value, \"all\", will print the filename and number of versions, \ \"graph\" will print the filename and a line of characters representing the number of versions, \ \"single\" will print only filenames which only have one version, \ (and \"single-no-snap\" will print those without a snap taken, and \"single-with-snap\" will print those with a snap taken), \ and \"multiple\" will print only filenames which only have multiple versions.") .conflicts_with_all(&["LAST_SNAP", "BROWSE", "SELECT", "RESTORE", "RECURSIVE", "SNAPSHOT", "NO_LIVE", "NO_SNAP", "OMIT_DITTO"]) - .display_order(28) + .display_order(30) + .action(ArgAction::Append) ) .arg( Arg::new("REMOTE_DIR") .long("remote-dir") .hide(true) .visible_aliases(&["remote", "snap-point"]) - .help("DEPRECATED. Use MAP_ALIASES. Manually specify that mount point for ZFS (directory which contains a \".zfs\" directory) or btrfs-snapper \ - (directory which contains a \".snapshots\" directory), such as the local mount point for a remote share. You may also set via the HTTM_REMOTE_DIR environment variable.") - .takes_value(true) + .help("DEPRECATED. Use MAP_ALIASES. Manually specify that mount point for ZFS (directory which contains a \".zfs\" directory) or btrfs-snapper \ + (directory which contains a \".snapshots\" directory), such as the local mount point for a remote share. You may also set via the HTTM_REMOTE_DIR environment variable.") .value_parser(clap::builder::ValueParser::os_string()) - .display_order(29) + .display_order(31) + .action(ArgAction::Append) ) .arg( Arg::new("LOCAL_DIR") .long("local-dir") .hide(true) .visible_alias("local") - .help("DEPRECATED. Use MAP_ALIASES. Used with \"remote-dir\" to determine where the corresponding live root filesystem of the dataset is. \ - Put more simply, the \"local-dir\" is likely the directory you backup to your \"remote-dir\". If not set, httm defaults to your current working directory. \ + .help("DEPRECATED. Use MAP_ALIASES. Used with \"remote-dir\" to determine where the corresponding live root filesystem of the dataset is. \ + Put more simply, the \"local-dir\" is likely the directory you backup to your \"remote-dir\". If not set, httm defaults to your current working directory. \ You may also set via the environment variable HTTM_LOCAL_DIR.") .requires("REMOTE_DIR") - .takes_value(true) .value_parser(clap::builder::ValueParser::os_string()) - .display_order(30) + .display_order(32) + .action(ArgAction::Append) ) .arg( Arg::new("UTC") .long("utc") .help("use UTC for date display and timestamps") - .display_order(31) + .display_order(33) + .action(ArgAction::SetTrue) ) .arg( Arg::new("NO_CLONES") .long("no-clones") - .help("by default, when copying files from snapshots, httm will first attempt a zero copy \"reflink\" clone on systems that support it. \ - Here, you may disable that behavior, and force httm to use the fall back diff copy behavior as the default. \ + .help("by default, when copying files from snapshots, httm will first attempt a zero copy \"reflink\" clone on systems that support it. \ + Here, you may disable that behavior, and force httm to use the fall back diff copy behavior as the default. \ You may also set an environment variable to any value, \"HTTM_NO_CLONE\" to disable.") - .display_order(32) + .display_order(34) + .action(ArgAction::SetTrue) ) .arg( Arg::new("DEBUG") .long("debug") .help("print configuration and debugging info") - .display_order(33) + .display_order(35) + .action(ArgAction::SetTrue) ) .arg( Arg::new("ZSH_HOT_KEYS") .long("install-zsh-hot-keys") .help("install zsh hot keys to the users home directory, and then exit") .exclusive(true) - .display_order(34) + .display_order(36) + .action(ArgAction::SetTrue) ) .get_matches() } @@ -551,7 +612,7 @@ pub struct Config { pub opt_json: bool, pub opt_one_filesystem: bool, pub opt_no_clones: bool, - pub uniqueness: ListSnapsOfType, + pub dedup_by: DedupBy, pub opt_bulk_exclusion: Option, pub opt_last_snap: Option, pub opt_preview: Option, @@ -575,11 +636,11 @@ impl Config { } fn from_matches(matches: &ArgMatches) -> HttmResult { - if matches.is_present("ZSH_HOT_KEYS") { + if matches.get_flag("ZSH_HOT_KEYS") { install_hot_keys()? } - let requested_utc_offset = if matches.is_present("UTC") { + let requested_utc_offset = if matches.get_flag("UTC") { UtcOffset::UTC } else { // this fn is surprisingly finicky. it needs to be done @@ -588,28 +649,74 @@ impl Config { UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC) }; - let opt_json = matches.is_present("JSON"); + let opt_debug = matches.get_flag("DEBUG"); + + // current working directory will be helpful in a number of places + let pwd = pwd()?; + + // obtain a map of datasets, a map of snapshot directories, and possibly a map of + // alternate filesystems and map of aliases if the user requests + let mut opt_map_aliases: Option> = + matches.get_raw("MAP_ALIASES").map(|aliases| { + aliases + .map(|os_str| os_str.to_string_lossy().to_string()) + .collect() + }); + + let opt_alt_store: Option = match matches + .get_one::("ALT_STORE") + .map(|inner| inner.as_str()) + { + Some("timemachine") => Some(FilesystemType::Apfs), + Some("restic") => Some(FilesystemType::Restic(None)), + _ => None, + }; + + if opt_alt_store.is_some() && opt_map_aliases.is_some() { + eprintln!( + "WARN: httm has disabled any MAP_ALIASES in preference to an ALT_STORE specified." + ); + opt_map_aliases = None; + } + + let opt_alt_replicated = matches.get_flag("ALT_REPLICATED"); + let opt_remote_dir = matches.get_one::("REMOTE_DIR"); + let opt_local_dir = matches.get_one::("LOCAL_DIR"); + + let dataset_collection = FilesystemInfo::new( + opt_alt_replicated, + opt_debug, + opt_remote_dir, + opt_local_dir, + opt_map_aliases, + opt_alt_store, + pwd.clone(), + )?; - let mut print_mode = if matches.is_present("ZEROS") { - PrintMode::RawZero - } else if matches.is_present("RAW") { - PrintMode::RawNewline - } else if matches.is_present("NOT_SO_PRETTY") { - PrintMode::FormattedNotPretty + let opt_json = matches.get_flag("JSON"); + + let mut print_mode = if matches.get_flag("CSV") { + PrintMode::Raw(RawMode::Csv) + } else if matches.get_flag("ZEROS") { + PrintMode::Raw(RawMode::Zero) + } else if matches.get_flag("RAW") { + PrintMode::Raw(RawMode::Newline) + } else if matches.get_flag("NOT_SO_PRETTY") { + PrintMode::Formatted(FormattedMode::NotPretty) } else { - PrintMode::FormattedDefault + PrintMode::Formatted(FormattedMode::Default) }; - let opt_bulk_exclusion = if matches.is_present("NO_LIVE") { + let opt_bulk_exclusion = if matches.get_flag("NO_LIVE") { Some(BulkExclusion::NoLive) - } else if matches.is_present("NO_SNAP") { + } else if matches.get_flag("NO_SNAP") { Some(BulkExclusion::NoSnap) } else { None }; if let Some(BulkExclusion::NoSnap) = opt_bulk_exclusion { - if let PrintMode::FormattedNotPretty | PrintMode::FormattedDefault = print_mode { + if let PrintMode::Formatted(FormattedMode::Default) = print_mode { return Err(HttmError::new( "NO_SNAP is only available if RAW or ZEROS are specified.", ) @@ -618,17 +725,19 @@ impl Config { } // force a raw mode if one is not set for no_snap mode - let opt_one_filesystem = matches.is_present("ONE_FILESYSTEM"); - let opt_recursive = matches.is_present("RECURSIVE"); + let opt_one_filesystem = matches.get_flag("ONE_FILESYSTEM"); + let opt_recursive = matches.get_flag("RECURSIVE"); - let opt_exact = matches.is_present("EXACT"); - let opt_no_filter = matches.is_present("NO_FILTER"); - let opt_debug = matches.is_present("DEBUG"); - let opt_no_hidden = matches.is_present("FILTER_HIDDEN"); + let opt_exact = matches.get_flag("EXACT"); + let opt_no_filter = matches.get_flag("NO_FILTER"); + let opt_no_hidden = matches.get_flag("FILTER_HIDDEN"); let opt_no_clones = - matches.is_present("NO_CLONES") || std::env::var_os("HTTM_NO_CLONE").is_some(); + matches.get_flag("NO_CLONES") || std::env::var_os("HTTM_NO_CLONE").is_some(); - let opt_last_snap = match matches.value_of("LAST_SNAP") { + let opt_last_snap = match matches + .get_one::("LAST_SNAP") + .map(|inner| inner.as_str()) + { Some("" | "any") => Some(LastSnapMode::Any), Some("none" | "without") => Some(LastSnapMode::Without), Some("ditto") => Some(LastSnapMode::DittoOnly), @@ -637,7 +746,10 @@ impl Config { _ => None, }; - let opt_num_versions = match matches.value_of("NUM_VERSIONS") { + let opt_num_versions = match matches + .get_one::("NUM_VERSIONS") + .map(|inner| inner.as_str()) + { Some("" | "all") => Some(NumVersionsMode::AllNumerals), Some("graph") => Some(NumVersionsMode::AllGraph), Some("single") => Some(NumVersionsMode::SingleAll), @@ -648,70 +760,84 @@ impl Config { }; if matches!(opt_num_versions, Some(NumVersionsMode::AllGraph)) - && !matches!(print_mode, PrintMode::FormattedDefault) + && !matches!(print_mode, PrintMode::Formatted(FormattedMode::Default)) { return Err(HttmError::new("The NUM_VERSIONS graph mode and the RAW or ZEROS display modes are an invalid combination.").into()); } - let opt_mount_display = match matches.value_of("FILE_MOUNT") { + let opt_mount_display = match matches + .get_one::("FILE_MOUNT") + .map(|inner| inner.as_str()) + { Some("" | "mount" | "target" | "directory") => Some(MountDisplay::Target), Some("source" | "device" | "dataset") => Some(MountDisplay::Source), Some("relative-path" | "relative" | "relpath") => Some(MountDisplay::RelativePath), _ => None, }; - let opt_preview = match matches.value_of("PREVIEW") { + let opt_preview = match matches + .get_one::("PREVIEW") + .map(|inner| inner.as_str()) + { Some("" | "default") => Some("default".to_owned()), - Some(user_defined) => Some(user_defined.to_owned()), + Some(user_defined) => Some(user_defined.to_string()), None => None, }; - let mut opt_deleted_mode = match matches.value_of("DELETED") { + let mut opt_deleted_mode = match matches + .get_one::("DELETED") + .map(|inner| inner.as_str()) + { Some("" | "all") => Some(DeletedMode::All), Some("single") => Some(DeletedMode::DepthOfOne), Some("only") => Some(DeletedMode::Only), _ => None, }; - let opt_interactive_mode = if matches.is_present("RESTORE") { - let mut restore_mode = matches.value_of("RESTORE").map(|inner| inner.to_string()); + let opt_select_mode = matches.get_one::("SELECT"); + let opt_restore_mode = matches.get_one::("RESTORE"); - if matches!(restore_mode.as_deref(), Some("") | None) - && std::env::var("HTTM_RESTORE_MODE").is_ok() - { - restore_mode = std::env::var("HTTM_RESTORE_MODE").ok(); + let opt_interactive_mode = if let Some(var_restore_mode) = opt_restore_mode { + let mut restore_mode = var_restore_mode.to_string(); + + if let Ok(env_restore_mode) = std::env::var("HTTM_RESTORE_MODE") { + restore_mode = env_restore_mode; } - match restore_mode.as_deref() { - Some("guard") => Some(InteractiveMode::Restore(RestoreMode::Overwrite( + match restore_mode.as_str() { + "guard" => Some(InteractiveMode::Restore(RestoreMode::Overwrite( RestoreSnapGuard::Guarded, ))), - Some("overwrite" | "yolo") => Some(InteractiveMode::Restore( - RestoreMode::Overwrite(RestoreSnapGuard::NotGuarded), - )), - Some("copy-and-preserve") => { - Some(InteractiveMode::Restore(RestoreMode::CopyAndPreserve)) - } - Some(_) | None => Some(InteractiveMode::Restore(RestoreMode::CopyOnly)), + "overwrite" | "yolo" => Some(InteractiveMode::Restore(RestoreMode::Overwrite( + RestoreSnapGuard::NotGuarded, + ))), + "copy-and-preserve" => Some(InteractiveMode::Restore(RestoreMode::CopyAndPreserve)), + _ => Some(InteractiveMode::Restore(RestoreMode::CopyOnly)), } - } else if matches.is_present("SELECT") || opt_preview.is_some() { - match matches.value_of("SELECT") { + } else if opt_select_mode.is_some() || opt_preview.is_some() { + match opt_select_mode.map(|inner| inner.as_str()) { Some("contents") => Some(InteractiveMode::Select(SelectMode::Contents)), Some("preview") => Some(InteractiveMode::Select(SelectMode::Preview)), Some(_) | None => Some(InteractiveMode::Select(SelectMode::Path)), } // simply enable browse mode -- if deleted mode not enabled but recursive search is specified, // that is, if delete recursive search is not specified, don't error out, let user browse - } else if matches.is_present("BROWSE") || (opt_recursive && opt_deleted_mode.is_none()) { + } else if matches.get_flag("BROWSE") || (opt_recursive && opt_deleted_mode.is_none()) { Some(InteractiveMode::Browse) } else { None }; - let mut uniqueness = match matches.value_of("UNIQUENESS") { - Some("all" | "no-filter") => ListSnapsOfType::All, - Some("contents") => ListSnapsOfType::UniqueContents, - Some("metadata" | _) | None => ListSnapsOfType::UniqueMetadata, + let dedup_by = match matches + .get_one::("DEDUP_BY") + .map(|inner| inner.as_str()) + { + _ if matches.get_flag("PRUNE") => DedupBy::Disable, + Some("all" | "no-filter" | "disable") => DedupBy::Disable, + Some("contents") => DedupBy::Contents, + Some("metadata" | _) => DedupBy::Metadata, + _ if matches.contains_id("LIST_SNAPS") => DedupBy::Disable, + None => DedupBy::Metadata, }; if opt_no_hidden && !opt_recursive && opt_interactive_mode.is_none() { @@ -722,16 +848,16 @@ impl Config { } // if in last snap and select mode we will want to return a raw value, - // better to have this here. It's more confusing if we work this logic later, I think. + // better to have this here. It's more confusing if we work this logic later, I think. if opt_last_snap.is_some() && matches!(opt_interactive_mode, Some(InteractiveMode::Select(_))) { - print_mode = PrintMode::RawNewline + print_mode = PrintMode::Raw(RawMode::Newline) } let opt_snap_file_mount = - if let Some(requested_snapshot_suffix) = matches.value_of("SNAPSHOT") { - if requested_snapshot_suffix == "httmSnapFileMount" { + if let Some(requested_snapshot_suffix) = matches.get_one::("SNAPSHOT") { + if requested_snapshot_suffix == &"httmSnapFileMount" { Some(requested_snapshot_suffix.to_owned()) } else if requested_snapshot_suffix.contains(char::is_whitespace) { return Err(HttmError::new( @@ -745,44 +871,39 @@ impl Config { None }; - let opt_snap_mode_filters = if matches.is_present("LIST_SNAPS") { + let opt_snap_mode_filters = if matches.contains_id("LIST_SNAPS") { // allow selection of snaps to prune in prune mode let select_mode = matches!(opt_interactive_mode, Some(InteractiveMode::Select(_))); - if !matches.is_present("PRUNE") && select_mode { + if !matches.get_flag("PRUNE") && select_mode { eprintln!("Select mode for listed snapshots only available in PRUNE mode.") } - // default to listing all snaps in list snaps mode if unset - if !matches.is_present("UNIQUENESS") { - uniqueness = ListSnapsOfType::All; - } - - if let Some(values) = matches.value_of("LIST_SNAPS") { - Some(Self::snap_filters(values, select_mode)?) - } else { - Some(ListSnapsFilters { + match matches.get_one::("LIST_SNAPS") { + Some(value) if !value.is_empty() => Some(Self::snap_filters(value, select_mode)?), + _ => Some(ListSnapsFilters { select_mode, omit_num_snaps: 0usize, name_filters: None, - }) + }), } } else { None }; - let mut exec_mode = if let Some(full_snap_name) = matches.value_of("ROLL_FORWARD") { - ExecMode::RollForward(full_snap_name.to_string()) + let mut exec_mode = if let Some(full_snap_name) = matches.get_one::("ROLL_FORWARD") + { + ExecMode::RollForward(full_snap_name.to_owned()) } else if let Some(num_versions_mode) = opt_num_versions { ExecMode::NumVersions(num_versions_mode) } else if let Some(mount_display) = opt_mount_display { ExecMode::MountsForFiles(mount_display) - } else if matches.is_present("PRUNE") { + } else if matches.get_flag("PRUNE") { ExecMode::Prune(opt_snap_mode_filters) } else if opt_snap_mode_filters.is_some() { ExecMode::SnapsForFiles(opt_snap_mode_filters) } else if let Some(requested_snapshot_suffix) = opt_snap_file_mount { - ExecMode::SnapFileMount(requested_snapshot_suffix) + ExecMode::SnapFileMount(requested_snapshot_suffix.to_string()) } else if let Some(interactive_mode) = opt_interactive_mode { ExecMode::Interactive(interactive_mode) } else if opt_deleted_mode.is_some() { @@ -799,22 +920,10 @@ impl Config { .into()); } - // current working directory will be helpful in a number of places - let pwd = pwd()?; - - // obtain a map of datasets, a map of snapshot directories, and possibly a map of - // alternate filesystems and map of aliases if the user requests - let dataset_collection = FilesystemInfo::new( - matches.is_present("ALT_REPLICATED"), - matches.value_of_os("REMOTE_DIR"), - matches.value_of_os("LOCAL_DIR"), - matches.values_of_os("MAP_ALIASES"), - &pwd, - )?; - // paths are immediately converted to our PathData struct - let paths: Vec = - Self::paths(matches.values_of_os("INPUT_FILES"), &exec_mode, &pwd)?; + let opt_os_values = matches.get_many::("INPUT_FILES"); + + let paths: Vec = Self::paths(opt_os_values, &exec_mode, &pwd)?; // for exec_modes in which we can only take a single directory, process how we handle those here let opt_requested_dir: Option = @@ -829,9 +938,9 @@ impl Config { // doesn't make sense to follow symlinks when you're searching the whole system, // so we disable our bespoke "when to traverse symlinks" algo here, or if requested. - let opt_no_traverse = matches.is_present("NO_TRAVERSE") || { + let opt_no_traverse = matches.get_flag("NO_TRAVERSE") || { if let Some(user_requested_dir) = opt_requested_dir.as_ref() { - user_requested_dir.as_path() == Path::new(ROOT_DIRECTORY) + user_requested_dir.as_path() == ROOT_PATH.as_path() } else { false } @@ -839,17 +948,17 @@ impl Config { if !matches!(opt_deleted_mode, None | Some(DeletedMode::All)) && !opt_recursive { return Err(HttmError::new( - "Deleted modes other than \"all\" require recursive mode is enabled. Quitting.", + "Deleted modes other than \"all\" require recursive mode is enabled. Quitting.", ) .into()); } - let opt_omit_ditto = matches.is_present("OMIT_DITTO"); + let opt_omit_ditto = matches.get_flag("OMIT_DITTO"); // opt_omit_identical doesn't make sense in Display Recursive mode as no live files will exists? if opt_omit_ditto && matches!(exec_mode, ExecMode::NonInteractiveRecursive(_)) { return Err(HttmError::new( - "OMIT_DITTO not available when a deleted recursive search is specified. Quitting.", + "OMIT_DITTO not available when a deleted recursive search is specified. Quitting.", ) .into()); } @@ -865,17 +974,17 @@ impl Config { opt_bulk_exclusion, opt_recursive, opt_exact, - opt_no_filter, opt_debug, opt_no_traverse, opt_omit_ditto, opt_no_hidden, + opt_no_filter, opt_last_snap, opt_preview, opt_json, opt_one_filesystem, opt_no_clones, - uniqueness, + dedup_by, requested_utc_offset, exec_mode, print_mode, @@ -889,7 +998,7 @@ impl Config { } pub fn paths( - opt_os_values: Option, + opt_os_values: Option>, exec_mode: &ExecMode, pwd: &Path, ) -> HttmResult> { @@ -993,7 +1102,7 @@ impl Config { 0 => Some(pwd.to_path_buf()), // use our bespoke is_dir fn for determining whether a dir here see pub httm_is_dir // safe to index as we know the paths len is 1 - 1 if paths[0].httm_is_dir() => Some(paths[0].path_buf.clone()), + 1 if paths[0].httm_is_dir() => Some(paths[0].path().to_path_buf()), // handle non-directories 1 => { match exec_mode { @@ -1075,7 +1184,7 @@ impl Config { .collect(), ) } else { - Some(rest.iter().map(|item| (*item).to_string()).collect()) + Some(rest.iter().map(|item| item.to_string()).collect()) } } else { None diff --git a/src/data/filesystem_info.rs b/src/data/filesystem_info.rs index 70cbe159..9180abff 100644 --- a/src/data/filesystem_info.rs +++ b/src/data/filesystem_info.rs @@ -15,13 +15,18 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::library::results::HttmResult; -use crate::parse::aliases::MapOfAliases; -use crate::parse::alts::MapOfAlts; -use crate::parse::mounts::{BaseFilesystemInfo, FilterDirs, MapOfDatasets}; -use crate::parse::snaps::MapOfSnaps; -use clap::OsValues; -use std::ffi::OsStr; +use crate::filesystem::aliases::MapOfAliases; +use crate::filesystem::alts::MapOfAlts; +use crate::filesystem::mounts::{ + BaseFilesystemInfo, + FilesystemType, + FilterDirs, + MapOfDatasets, + TM_DIR_LOCAL_PATH, + TM_DIR_REMOTE_PATH, +}; +use crate::filesystem::snaps::MapOfSnaps; +use crate::library::results::{HttmError, HttmResult}; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -37,18 +42,57 @@ pub struct FilesystemInfo { // key: local dir, val: (remote dir, fstype) pub opt_map_of_aliases: Option, // opt single dir to to be filtered re: btrfs common snap dir - pub opt_common_snap_dir: Option, + pub opt_common_snap_dir: Option>, + // opt possible opt store type + pub opt_alt_store: Option, } impl FilesystemInfo { pub fn new( opt_alt_replicated: bool, - opt_remote_dir: Option<&OsStr>, - opt_local_dir: Option<&OsStr>, - opt_map_aliases: Option, - pwd: &Path, + opt_debug: bool, + opt_remote_dir: Option<&String>, + opt_local_dir: Option<&String>, + opt_raw_aliases: Option>, + opt_alt_store: Option, + pwd: PathBuf, ) -> HttmResult { - let base_fs_info = BaseFilesystemInfo::new()?; + let mut base_fs_info = BaseFilesystemInfo::new(opt_debug, &opt_alt_store)?; + + // only create a map of aliases if necessary (aliases conflicts with alt stores) + let opt_map_of_aliases = MapOfAliases::new( + &base_fs_info.map_of_datasets, + opt_raw_aliases, + opt_remote_dir, + opt_local_dir, + &pwd, + )?; + + // prep any blob repos + let mut opt_alt_store = opt_alt_store; + + match opt_alt_store { + Some(ref repo_type) => { + base_fs_info.from_blob_repo(&repo_type, opt_debug)?; + } + None if base_fs_info.map_of_datasets.is_empty() => { + // auto enable time machine alt store on mac when no datasets available, no working aliases, and paths exist + if cfg!(target_os = "macos") + && opt_map_of_aliases.is_none() + && TM_DIR_REMOTE_PATH.exists() + && TM_DIR_LOCAL_PATH.exists() + { + opt_alt_store.replace(FilesystemType::Apfs); + base_fs_info.from_blob_repo(&FilesystemType::Apfs, opt_debug)?; + } else { + return Err(HttmError::new( + "httm could not find any valid datasets on the system.", + ) + .into()); + } + } + _ => {} + } // for a collection of btrfs mounts, indicates a common snapshot directory to ignore let opt_common_snap_dir = base_fs_info.common_snap_dir(); @@ -60,50 +104,6 @@ impl FilesystemInfo { None }; - let alias_values: Option> = match std::env::var_os("HTTM_MAP_ALIASES") { - Some(env_map_alias) => Some( - env_map_alias - .to_string_lossy() - .split_terminator(',') - .map(std::borrow::ToOwned::to_owned) - .collect(), - ), - None => opt_map_aliases.map(|cmd_map_aliases| { - cmd_map_aliases - .into_iter() - .map(|os_str| os_str.to_string_lossy().to_string()) - .collect() - }), - }; - - let raw_snap_dir = if let Some(value) = opt_remote_dir { - Some(value.to_os_string()) - } else if std::env::var_os("HTTM_REMOTE_DIR").is_some() { - std::env::var_os("HTTM_REMOTE_DIR") - } else { - // legacy env var name - std::env::var_os("HTTM_SNAP_POINT") - }; - - let opt_map_of_aliases = if raw_snap_dir.is_some() || alias_values.is_some() { - let env_local_dir = std::env::var_os("HTTM_LOCAL_DIR"); - - let raw_local_dir = if let Some(value) = opt_local_dir { - Some(value.to_os_string()) - } else { - env_local_dir - }; - - Some(MapOfAliases::new( - &raw_snap_dir, - &raw_local_dir, - pwd, - &alias_values, - )?) - } else { - None - }; - Ok(FilesystemInfo { map_of_datasets: base_fs_info.map_of_datasets, map_of_snaps: base_fs_info.map_of_snaps, @@ -111,6 +111,7 @@ impl FilesystemInfo { opt_map_of_alts, opt_common_snap_dir, opt_map_of_aliases, + opt_alt_store, }) } } diff --git a/src/data/paths.rs b/src/data/paths.rs index 603e18a1..df76b023 100644 --- a/src/data/paths.rs +++ b/src/data/paths.rs @@ -15,50 +15,195 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::{ListSnapsOfType, PrintMode}; +use super::selection::SelectionCandidate; +use crate::background::recursive::PathProvenance; +use crate::config::generate::PrintMode; +use crate::filesystem::mounts::{FilesystemType, IsFilterDir, MaxLen}; +use crate::library::file_ops::HashFileContents; use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::{date_string, display_human_size, DateFormat}; -use crate::parse::mounts::FilesystemType; -use crate::parse::mounts::MaxLen; -use crate::{GLOBAL_CONFIG, ZFS_SNAPSHOT_DIRECTORY}; -use once_cell::sync::{Lazy, OnceCell}; +use crate::library::utility::{date_string, display_human_size, DateFormat, HttmIsDir}; +use crate::{ + BTRFS_SNAPPER_HIDDEN_DIRECTORY, + GLOBAL_CONFIG, + ZFS_HIDDEN_DIRECTORY, + ZFS_SNAPSHOT_DIRECTORY, +}; use realpath_ext::{realpath, RealpathFlags}; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; use std::cmp::{Ord, Ordering, PartialOrd}; use std::ffi::OsStr; -use std::fs::{symlink_metadata, DirEntry, File, FileType, Metadata}; -use std::io::{BufRead, BufReader, ErrorKind}; +use std::fs::{symlink_metadata, DirEntry, FileType, Metadata}; +use std::hash::Hash; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, OnceLock}; use std::time::SystemTime; +static OPT_REQUESTED_DIR_DEV: LazyLock = LazyLock::new(|| { + GLOBAL_CONFIG + .opt_requested_dir + .as_ref() + .expect("opt_requested_dir should be Some value at this point in execution") + .symlink_metadata() + .expect("Cannot read metadata for directory requested for search.") + .dev() +}); + +static DATASET_MAX_LEN: LazyLock = + LazyLock::new(|| GLOBAL_CONFIG.dataset_collection.map_of_datasets.max_len()); + +static FILTER_DIRS_MAX_LEN: LazyLock = + LazyLock::new(|| GLOBAL_CONFIG.dataset_collection.filter_dirs.max_len()); + // only the most basic data from a DirEntry // for use to display in browse window and internally -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, Hash, PartialEq)] pub struct BasicDirEntryInfo { - pub path: PathBuf, - pub file_type: Option, + path: PathBuf, + opt_filetype: Option, } impl From<&DirEntry> for BasicDirEntryInfo { fn from(dir_entry: &DirEntry) -> Self { BasicDirEntryInfo { path: dir_entry.path(), - file_type: dir_entry.file_type().ok(), + opt_filetype: dir_entry.file_type().ok(), } } } +impl From for PathBuf { + fn from(entry: BasicDirEntryInfo) -> Self { + entry.path + } +} + impl BasicDirEntryInfo { + pub fn new(path: PathBuf, opt_filetype: Option) -> Self { + Self { path, opt_filetype } + } + pub fn filename(&self) -> &OsStr { self.path.file_name().unwrap_or_default() } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn opt_filetype(&self) -> &Option { + &self.opt_filetype + } + + pub fn to_path_buf(self) -> PathBuf { + self.path + } + + pub fn into_selection(self, is_phantom: &PathProvenance) -> SelectionCandidate { + let mut selection: SelectionCandidate = self.into(); + selection.set_phantom(is_phantom); + selection + } + + pub fn is_entry_dir(&self) -> bool { + // must do is_dir() look up on DirEntry file_type() as look up on Path will traverse links! + if GLOBAL_CONFIG.opt_no_traverse { + if let Ok(file_type) = self.filetype() { + return file_type.is_dir(); + } + } + + self.httm_is_dir() + } + + // this function creates dummy "live versions" values to match deleted files + // which have been found on snapshots, we return to the user "the path that + // once was" in their browse panel + pub fn into_pseudo_live_version(self, pseudo_live_dir: &Path) -> Option { + let path = pseudo_live_dir.join(self.path().file_name()?); + + let opt_filetype = *self.opt_filetype(); + + Some(Self { path, opt_filetype }) + } + + pub fn all_exclusions(&self) -> bool { + if GLOBAL_CONFIG.opt_no_filter { + return true; + } + + if GLOBAL_CONFIG.opt_no_hidden && self.filename().to_string_lossy().starts_with('.') { + return false; + } + + if GLOBAL_CONFIG.opt_one_filesystem { + match self.path().metadata() { + Ok(path_md) if *OPT_REQUESTED_DIR_DEV == path_md.dev() => {} + _ => { + // if we can't read the metadata for a path, + // we probably shouldn't show it either + return false; + } + } + } + + if let Ok(file_type) = self.filetype() { + if file_type.is_dir() { + return !self.is_path_excluded(); + } + } + + true + } + + fn is_path_excluded(&self) -> bool { + // FYI path is always a relative path, but no need to canonicalize as + // partial eq for paths is comparison of components iter + let path = self.path(); + + // never check the hidden snapshot directory for live files (duh) + // didn't think this was possible until I saw a SMB share return + // a .zfs dir entry + if path.ends_with(ZFS_HIDDEN_DIRECTORY) || path.ends_with(BTRFS_SNAPPER_HIDDEN_DIRECTORY) { + return true; + } + + // is a common btrfs snapshot dir? + if let Some(common_snap_dir) = &GLOBAL_CONFIG.dataset_collection.opt_common_snap_dir { + if path == common_snap_dir.as_ref() { + return true; + } + } + + // check whether user requested this dir specifically, then we will show + if let Some(user_requested_dir) = GLOBAL_CONFIG.opt_requested_dir.as_ref() { + if user_requested_dir.as_path() == path { + return false; + } + } + + // finally : is a non-supported dataset? + // bailout easily if path is larger than max_filter_dir len + if path.components().count() > *FILTER_DIRS_MAX_LEN { + return false; + } + + path.is_filter_dir() + } +} + +impl Into for BasicDirEntryInfo { + fn into(self) -> SelectionCandidate { + unsafe { std::mem::transmute(self) } + } } pub trait PathDeconstruction<'a> { fn alias(&self) -> Option; fn target(&self, proximate_dataset_mount: &Path) -> Option; fn source(&self, opt_proximate_dataset_mount: Option<&'a Path>) -> Option; + fn fs_type(&self, opt_proximate_dataset_mount: Option<&'a Path>) -> Option; fn relative_path(&'a self, proximate_dataset_mount: &'a Path) -> HttmResult<&'a Path>; fn proximate_dataset(&'a self) -> HttmResult<&'a Path>; fn live_path(&self) -> Option; @@ -67,21 +212,21 @@ pub trait PathDeconstruction<'a> { // detailed info required to differentiate and display file versions #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct PathData { - pub path_buf: PathBuf, - pub metadata: Option, + path_buf: PathBuf, + metadata: Option, } impl PartialOrd for PathData { #[inline] fn partial_cmp(&self, other: &PathData) -> Option { - Some(self.path_buf.cmp(&other.path_buf)) + Some(self.path().cmp(&other.path())) } } impl Ord for PathData { #[inline] fn cmp(&self, other: &PathData) -> Ordering { - self.path_buf.cmp(&other.path_buf) + self.path().cmp(&other.path()) } } @@ -122,16 +267,47 @@ impl PathData { } } + pub fn path<'a>(&'a self) -> &'a Path { + &self.path_buf + } + + pub fn opt_metadata<'a>(&'a self) -> &'a Option { + &self.metadata + } + #[inline(always)] - pub fn md_infallible(&self) -> PathMetadata { + pub fn metadata_infallible(&self) -> PathMetadata { self.metadata.unwrap_or_else(|| PHANTOM_PATH_METADATA) } + + pub fn is_same_file_contents(&self, other: &Self) -> bool { + let self_hash = HashFileContents::path_to_hash(self.path()); + let other_hash = HashFileContents::path_to_hash(other.path()); + + self_hash.cmp(&other_hash) == Ordering::Equal + } } impl<'a> PathDeconstruction<'a> for PathData { fn alias(&self) -> Option { - AliasedPath::new(&self.path_buf) + // find_map_first should return the first seq result with a par_iter + // but not with a par_bridge + GLOBAL_CONFIG + .dataset_collection + .opt_map_of_aliases + .as_ref() + .and_then(|map_of_aliases| { + self.path_buf.ancestors().find_map(|ancestor| { + map_of_aliases.get(ancestor).and_then(|metadata| { + Some(AliasedPath { + proximate_dataset: metadata.remote_dir.as_ref(), + relative_path: &self.path_buf.strip_prefix(ancestor).ok()?, + }) + }) + }) + }) } + fn live_path(&self) -> Option { Some(self.path_buf.clone()) } @@ -151,14 +327,14 @@ impl<'a> PathDeconstruction<'a> for PathData { } fn source(&self, opt_proximate_dataset_mount: Option<&'a Path>) -> Option { - let mount = + let mount: &Path = opt_proximate_dataset_mount.map_or_else(|| self.proximate_dataset().ok(), Some)?; GLOBAL_CONFIG .dataset_collection .map_of_datasets .get(mount) - .map(|md| md.source.clone()) + .map(|md| md.source.to_path_buf()) } #[inline(always)] @@ -166,9 +342,6 @@ impl<'a> PathDeconstruction<'a> for PathData { // for /usr/bin, we prefer the most proximate: /usr/bin to /usr and / // ancestors() iterates in this top-down order, when a value: dataset/fstype is available // we map to return the key, instead of the value - static DATASET_MAX_LEN: Lazy = - Lazy::new(|| GLOBAL_CONFIG.dataset_collection.map_of_datasets.max_len()); - self.path_buf .ancestors() .skip_while(|ancestor| ancestor.components().count() > *DATASET_MAX_LEN) @@ -186,6 +359,17 @@ impl<'a> PathDeconstruction<'a> for PathData { HttmError::new(&msg).into() }) } + + fn fs_type(&self, opt_proximate_dataset_mount: Option<&'a Path>) -> Option { + let proximate_dataset = + opt_proximate_dataset_mount.map_or_else(|| self.proximate_dataset().ok(), Some)?; + + GLOBAL_CONFIG + .dataset_collection + .map_of_datasets + .get(proximate_dataset) + .map(|md| md.fs_type.clone()) + } } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -194,30 +378,6 @@ pub struct AliasedPath<'a> { pub relative_path: &'a Path, } -impl<'a> AliasedPath<'a> { - #[inline(always)] - pub fn new(path: &'a Path) -> Option { - // find_map_first should return the first seq result with a par_iter - // but not with a par_bridge - - path.ancestors().find_map(|ancestor| { - GLOBAL_CONFIG - .dataset_collection - .opt_map_of_aliases - .as_ref() - .and_then(|map_of_aliases| { - let md = map_of_aliases.get(ancestor); - let relative_path = path.strip_prefix(ancestor).ok()?; - - md.map(|metadata| AliasedPath { - proximate_dataset: metadata.remote_dir.as_ref(), - relative_path, - }) - }) - }) - } -} - pub struct ZfsSnapPathGuard<'a> { inner: &'a PathData, } @@ -331,6 +491,10 @@ impl<'a> PathDeconstruction<'a> for ZfsSnapPathGuard<'_> { fn proximate_dataset(&'a self) -> HttmResult<&'a Path> { self.inner.proximate_dataset() } + + fn fs_type(&self, _opt_proximate_dataset_mount: Option<&'a Path>) -> Option { + Some(FilesystemType::Zfs) + } } impl Serialize for PathData { @@ -353,10 +517,7 @@ impl Serialize for PathMetadata { { let mut state = serializer.serialize_struct("PathData", 2)?; - if matches!( - GLOBAL_CONFIG.print_mode, - PrintMode::RawNewline | PrintMode::RawZero - ) { + if let PrintMode::Raw(_) = GLOBAL_CONFIG.print_mode { state.serialize_field("size", &self.size)?; state.serialize_field("modify_time", &self.modify_time)?; } else { @@ -377,8 +538,8 @@ impl Serialize for PathMetadata { #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub struct PathMetadata { - pub size: u64, - pub modify_time: SystemTime, + size: u64, + modify_time: SystemTime, } impl PathMetadata { @@ -386,7 +547,7 @@ impl PathMetadata { #[inline(always)] pub fn new(md: &Metadata) -> Option { // may fail on systems that don't collect a modify time - Self::modify_time(md).map(|time| PathMetadata { + md.modified().ok().map(|time| PathMetadata { size: md.len(), modify_time: time, }) @@ -395,12 +556,35 @@ impl PathMetadata { // using ctime instead of mtime might be more correct as mtime can be trivially changed from user space // but I think we want to use mtime here? People should be able to make a snapshot "unique" with only mtime? #[inline(always)] - fn modify_time(md: &Metadata) -> Option { - //#[cfg(not(unix))] - // return md.modified().unwrap_or(UNIX_EPOCH); - //#[cfg(unix)] - //return UNIX_EPOCH + time::Duration::new(md.ctime(), md.ctime_nsec() as i32); - md.modified().ok() + pub fn mtime(&self) -> SystemTime { + self.modify_time + } + + #[inline(always)] + pub fn size(&self) -> u64 { + self.size + } +} + +impl PartialOrd for PathMetadata { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PathMetadata { + #[inline(always)] + fn cmp(&self, other: &Self) -> Ordering { + let time_order: Ordering = self.mtime().cmp(&other.mtime()); + + if time_order.is_ne() { + return time_order; + } + + let size_order: Ordering = self.size().cmp(&other.size()); + + size_order } } @@ -412,131 +596,86 @@ pub const PHANTOM_PATH_METADATA: PathMetadata = PathMetadata { modify_time: PHANTOM_DATE, }; -#[derive(Eq, PartialEq)] -pub struct CompareVersionsContainer { +#[derive(Debug)] +pub struct CompareContentsContainer { pathdata: PathData, - opt_hash: Option>, + hash: OnceLock, } -impl From for PathData { - #[inline(always)] - fn from(container: CompareVersionsContainer) -> Self { - container.pathdata +impl Eq for CompareContentsContainer {} + +impl PartialEq for CompareContentsContainer { + fn eq(&self, other: &Self) -> bool { + self.cmp(&other).is_eq() } } -impl PartialOrd for CompareVersionsContainer { +impl PartialOrd for CompareContentsContainer { #[inline(always)] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for CompareVersionsContainer { +impl Ord for CompareContentsContainer { #[inline(always)] fn cmp(&self, other: &Self) -> Ordering { - let self_md = self.pathdata.md_infallible(); - let other_md = other.pathdata.md_infallible(); + let size_order: Ordering = self.size().cmp(&other.size()); - if self_md.modify_time == other_md.modify_time { - return self_md.size.cmp(&other_md.size); - } + if size_order.is_eq() { + let contents_order = self.cmp_file_contents(other); - // if files, differ re mtime, but have same size, we test by bytes whether the same - if self_md.size == other_md.size - && self.opt_hash.is_some() - // if above is true/false then "&& other.opt_hash.is_some()" is the same - && self.is_same_file(other) - { - return Ordering::Equal; + return contents_order; } - self_md.modify_time.cmp(&other_md.modify_time) + let time_order: Ordering = self.mtime().cmp(&other.mtime()); + + time_order } } -impl CompareVersionsContainer { +impl From for PathData { #[inline(always)] - pub fn new(pathdata: PathData, snaps_of_type: &ListSnapsOfType) -> Self { - let opt_hash = match snaps_of_type { - ListSnapsOfType::UniqueContents => Some(OnceCell::new()), - ListSnapsOfType::UniqueMetadata | ListSnapsOfType::All => None, - }; + fn from(value: CompareContentsContainer) -> Self { + value.pathdata + } +} - CompareVersionsContainer { pathdata, opt_hash } +impl From for CompareContentsContainer { + #[inline(always)] + fn from(pathdata: PathData) -> Self { + Self { + pathdata, + hash: OnceLock::new(), + } } +} - #[allow(unused_assignments)] - pub fn is_same_file(&self, other: &Self) -> bool { - // SAFETY: Unwrap will fail on opt_hash is None, here we've guarded this above - let self_hash_cell = self - .opt_hash - .as_ref() - .expect("opt_hash should be check prior to this point and must be Some"); - let other_hash_cell = other - .opt_hash - .as_ref() - .expect("opt_hash should be check prior to this point and must be Some"); +impl CompareContentsContainer { + #[inline(always)] + pub fn mtime(&self) -> SystemTime { + self.pathdata.metadata_infallible().modify_time + } - let (self_hash, other_hash): (HttmResult, HttmResult) = rayon::join( - || { - if let Some(hash_value) = self_hash_cell.get() { - return Ok(*hash_value); - } + #[inline(always)] + pub fn size(&self) -> u64 { + self.pathdata.metadata_infallible().size + } - self.hash().map(|hash| *self_hash_cell.get_or_init(|| hash)) + #[allow(unused_assignments)] + pub fn cmp_file_contents(&self, other: &Self) -> Ordering { + let (self_hash, other_hash): (&u64, &u64) = rayon::join( + || { + self.hash + .get_or_init(|| HashFileContents::path_to_hash(self.pathdata.path())) }, || { - if let Some(hash_value) = other_hash_cell.get() { - return Ok(*hash_value); - } - other - .hash() - .map(|hash| *other_hash_cell.get_or_init(|| hash)) + .hash + .get_or_init(|| HashFileContents::path_to_hash(other.pathdata.path())) }, ); - if let Ok(res_self) = self_hash { - if let Ok(res_other) = other_hash { - return res_self == res_other; - } - } - - false - } - - fn hash(&self) -> HttmResult { - use std::hash::Hasher; - - const IN_BUFFER_SIZE: usize = 131_072; - - let file = File::open(&self.pathdata.path_buf)?; - - let mut reader = BufReader::with_capacity(IN_BUFFER_SIZE, file); - - let mut hash = ahash::AHasher::default(); - - loop { - let consumed = match reader.fill_buf() { - Ok(buf) => { - if buf.is_empty() { - return Ok(hash.finish()); - } - - hash.write(buf); - buf.len() - } - Err(err) => match err.kind() { - ErrorKind::Interrupted => continue, - ErrorKind::UnexpectedEof => { - return Ok(hash.finish()); - } - _ => return Err(err.into()), - }, - }; - - reader.consume(consumed); - } + self_hash.cmp(&other_hash) } } diff --git a/src/data/selection.rs b/src/data/selection.rs index 50abfad6..4be90211 100644 --- a/src/data/selection.rs +++ b/src/data/selection.rs @@ -16,18 +16,18 @@ // that was distributed with this source code. use crate::background::recursive::PathProvenance; -use crate::config::generate::{ListSnapsOfType, PrintMode}; -use crate::data::paths::{BasicDirEntryInfo, PathData}; -use crate::display_versions::wrapper::VersionsDisplayWrapper; +use crate::config::generate::{DedupBy, FormattedMode, PrintMode}; +use crate::data::paths::PathData; +use crate::display::wrapper::DisplayWrapper; use crate::library::results::HttmResult; use crate::library::utility::paint_string; +use crate::lookup::versions::Versions; use crate::{Config, ExecMode, VersionsMap, GLOBAL_CONFIG}; use lscolors::Colorable; -use once_cell::sync::Lazy; use skim::prelude::*; use std::fs::FileType; -use std::path::Path; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; // these represent the items ready for selection and preview // contains everything one needs to request preview and paint with @@ -35,53 +35,63 @@ use std::path::PathBuf; // and impl Colorable for how we paint the path strings pub struct SelectionCandidate { path: PathBuf, - file_type: Option, + opt_filetype: Option, } impl SelectionCandidate { - pub fn new(basic_info: BasicDirEntryInfo, is_phantom: PathProvenance) -> Self { - // here save space of bool/padding instead of an "is_phantom: bool" - // - // issue: conflate not having a file_type as phantom - // for purposes of coloring the file_name/path only? - // - // std lib docs don't give much indication as to - // when file_type() fails? Doesn't seem to be a problem? - let file_type = match is_phantom { - PathProvenance::FromLiveDataset => basic_info.file_type, - PathProvenance::IsPhantom => None, - }; - - SelectionCandidate { - path: basic_info.path, - file_type, + pub fn new(path: PathBuf, opt_filetype: Option) -> Self { + Self { path, opt_filetype } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn opt_filetype(&self) -> &Option { + &self.opt_filetype + } + + pub fn set_phantom(&mut self, is_phantom: &PathProvenance) { + match is_phantom { + PathProvenance::FromLiveDataset => {} + PathProvenance::IsPhantom => self.opt_filetype = None, } } fn preview_view(&self) -> HttmResult { // generate a config for display let display_config: Config = Config::from(self); + let display_pathdata = PathData::from(&self.path); // finally run search on those paths - let versions_map = VersionsMap::new(&display_config, &display_config.paths)?; - let output_buf = VersionsDisplayWrapper::from(&display_config, versions_map).to_string(); + let all_snap_versions: VersionsMap = + [Versions::new(&display_pathdata, &display_config)?.into_inner()].into(); + + let output_buf = DisplayWrapper::from(&display_config, all_snap_versions).to_string(); Ok(output_buf) } fn display_name(&self) -> Cow { - static REQUESTED_DIR: Lazy<&Path> = Lazy::new(|| { + static REQUESTED_DIR: LazyLock<&Path> = LazyLock::new(|| { GLOBAL_CONFIG .opt_requested_dir .as_ref() .expect("requested_dir should never be None in Interactive Browse mode") }); + static REQUESTED_DIR_PARENT: LazyLock> = LazyLock::new(|| { + GLOBAL_CONFIG + .opt_requested_dir + .as_ref() + .and_then(|path| path.parent()) + }); + // this only works because we do not resolve symlinks when doing traversal match self.path.strip_prefix(*REQUESTED_DIR) { Ok(_) if self.path.as_path() == *REQUESTED_DIR => Cow::Borrowed("."), Ok(stripped) => stripped.to_string_lossy(), - Err(_) if Some(self.path.as_path()) == REQUESTED_DIR.parent() => Cow::Borrowed(".."), + Err(_) if Some(self.path.as_path()) == *REQUESTED_DIR_PARENT => Cow::Borrowed(".."), Err(_) => self.path.to_string_lossy(), } } @@ -95,7 +105,7 @@ impl Colorable for &SelectionCandidate { self.path.file_name().unwrap_or_default().to_os_string() } fn file_type(&self) -> Option { - self.file_type + *self.opt_filetype() } fn metadata(&self) -> Option { self.path.symlink_metadata().ok() @@ -104,13 +114,13 @@ impl Colorable for &SelectionCandidate { impl SkimItem for SelectionCandidate { fn text(&self) -> Cow { - self.path.to_string_lossy() + self.display_name() } fn display(&self, _context: DisplayContext<'_>) -> AnsiString { AnsiString::parse(&paint_string(self, &self.display_name())) } fn output(&self) -> Cow { - self.text() + self.path.to_string_lossy() } fn preview(&self, _: PreviewContext<'_>) -> skim::ItemPreview { let preview_output = self.preview_view().unwrap_or_default(); @@ -137,11 +147,11 @@ impl From> for Config { opt_last_snap: None, opt_preview: None, opt_deleted_mode: None, - uniqueness: ListSnapsOfType::UniqueMetadata, + dedup_by: DedupBy::Metadata, opt_omit_ditto: config.opt_omit_ditto, requested_utc_offset: config.requested_utc_offset, exec_mode: ExecMode::BasicDisplay, - print_mode: PrintMode::FormattedDefault, + print_mode: PrintMode::Formatted(FormattedMode::Default), dataset_collection: config.dataset_collection.clone(), pwd: config.pwd.clone(), opt_requested_dir: config.opt_requested_dir.clone(), diff --git a/src/display_map/format.rs b/src/display/maps.rs similarity index 70% rename from src/display_map/format.rs rename to src/display/maps.rs index 02edaf4a..f56b6891 100644 --- a/src/display_map/format.rs +++ b/src/display/maps.rs @@ -15,10 +15,9 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::PrintMode; -use crate::data::paths::PathData; -use crate::data::paths::ZfsSnapPathGuard; -use crate::display_versions::format::{NOT_SO_PRETTY_FIXED_WIDTH_PADDING, QUOTATION_MARKS_LEN}; +use crate::config::generate::{FormattedMode, PrintMode, RawMode}; +use crate::data::paths::{PathData, ZfsSnapPathGuard}; +use crate::display::versions::{NOT_SO_PRETTY_FIXED_WIDTH_PADDING, QUOTATION_MARKS_LEN}; use crate::library::utility::delimiter; use crate::{MountsForFiles, SnapNameMap, VersionsMap, GLOBAL_CONFIG}; use serde::ser::SerializeMap; @@ -77,7 +76,7 @@ impl<'a> From<&MountsForFiles<'a>> for PrintAsMap { .map(|path| path.to_string_lossy().to_string()) .collect(); - (pathdata.path_buf.to_string_lossy().to_string(), res) + (pathdata.path().to_string_lossy().to_string(), res) }) .collect(); Self { inner } @@ -91,9 +90,9 @@ impl From<&VersionsMap> for PrintAsMap { .map(|(key, values)| { let res = values .iter() - .map(|value| value.path_buf.to_string_lossy().to_string()) + .map(|value| value.path().to_string_lossy().to_string()) .collect(); - (key.path_buf.to_string_lossy().to_string(), res) + (key.path().to_string_lossy().to_string(), res) }) .collect(); Self { inner } @@ -104,7 +103,7 @@ impl From<&SnapNameMap> for PrintAsMap { fn from(map: &SnapNameMap) -> Self { let inner = map .iter() - .map(|(key, value)| (key.path_buf.to_string_lossy().to_string(), value.clone())) + .map(|(key, value)| (key.path().to_string_lossy().to_string(), value.clone())) .collect(); Self { inner } } @@ -116,18 +115,34 @@ impl std::string::ToString for PrintAsMap { return self.to_json(); } - let delimiter = delimiter(); - match &GLOBAL_CONFIG.print_mode { - PrintMode::RawNewline | PrintMode::RawZero => { - self.values() - .flatten() - .fold(String::new(), |mut buffer, value| { - buffer += format!("{value}{delimiter}").as_str(); + PrintMode::Raw(_) => { + let delimiter = if let PrintMode::Raw(RawMode::Csv) = GLOBAL_CONFIG.print_mode { + ',' + } else { + delimiter() + }; + + let last = self.values().len() - 1; + + self.values().flatten().enumerate().fold( + String::new(), + |mut buffer, (idx, value)| { + buffer.push_str(value); + + if let PrintMode::Raw(RawMode::Csv) = GLOBAL_CONFIG.print_mode { + if last == idx { + buffer.push('\n'); + return buffer; + } + } + + buffer.push(delimiter); buffer - }) + }, + ) } - PrintMode::FormattedDefault | PrintMode::FormattedNotPretty => self.format(), + PrintMode::Formatted(_) => self.format(), } } } @@ -142,10 +157,8 @@ impl PrintAsMap { pub fn to_json(&self) -> String { let res = match GLOBAL_CONFIG.print_mode { - PrintMode::FormattedNotPretty | PrintMode::RawNewline | PrintMode::RawZero => { - serde_json::to_string(&self) - } - PrintMode::FormattedDefault => serde_json::to_string_pretty(&self), + PrintMode::Formatted(FormattedMode::Default) => serde_json::to_string_pretty(&self), + _ => serde_json::to_string(&self), }; match res { @@ -173,18 +186,23 @@ impl PrintAsMap { } }) .map(|(key, values)| { - let display_path = - if matches!(&GLOBAL_CONFIG.print_mode, PrintMode::FormattedNotPretty) { - key.clone() - } else { - format!("\"{key}\"") - }; + let display_path = if matches!( + &GLOBAL_CONFIG.print_mode, + PrintMode::Formatted(FormattedMode::NotPretty) + ) { + key.clone() + } else { + format!("\"{key}\"") + }; let values_string: String = values .iter() .enumerate() .map(|(idx, value)| { - if matches!(&GLOBAL_CONFIG.print_mode, PrintMode::FormattedNotPretty) { + if matches!( + &GLOBAL_CONFIG.print_mode, + PrintMode::Formatted(FormattedMode::NotPretty) + ) { format!("{NOT_SO_PRETTY_FIXED_WIDTH_PADDING}{value}") } else if idx == 0 { format!( @@ -199,7 +217,10 @@ impl PrintAsMap { }) .collect::(); - if matches!(&GLOBAL_CONFIG.print_mode, PrintMode::FormattedNotPretty) { + if matches!( + &GLOBAL_CONFIG.print_mode, + PrintMode::Formatted(FormattedMode::NotPretty) + ) { format!("{display_path}:{values_string}\n") } else { values_string diff --git a/src/display_versions/num_versions.rs b/src/display/num_versions.rs similarity index 87% rename from src/display_versions/num_versions.rs rename to src/display/num_versions.rs index 98b24668..dad31681 100644 --- a/src/display_versions/num_versions.rs +++ b/src/display/num_versions.rs @@ -15,14 +15,14 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::{NumVersionsMode, PrintMode}; +use crate::config::generate::{FormattedMode, NumVersionsMode, PrintMode, RawMode}; use crate::data::paths::PathData; -use crate::display_map::format::PrintAsMap; +use crate::display::maps::PrintAsMap; use crate::library::utility::delimiter; use crate::lookup::versions::VersionsMap; -use crate::{VersionsDisplayWrapper, GLOBAL_CONFIG}; +use crate::{DisplayWrapper, GLOBAL_CONFIG}; -impl<'a> VersionsDisplayWrapper<'a> { +impl<'a> DisplayWrapper<'a> { pub fn format_as_num_versions(&self, num_versions_mode: &NumVersionsMode) -> String { // let delimiter = get_delimiter(config); let delimiter = delimiter(); @@ -78,7 +78,7 @@ impl<'a> VersionsDisplayWrapper<'a> { padding: usize, total_num_paths: usize, ) -> Option { - let display_path = live_version.path_buf.display(); + let display_path = live_version.path().display(); let mut num_versions = snaps.len(); @@ -89,14 +89,14 @@ impl<'a> VersionsDisplayWrapper<'a> { }; match print_mode { - PrintMode::FormattedDefault => Some(format!( + PrintMode::Formatted(FormattedMode::Default) => Some(format!( "{: { + _ => { unreachable!() } } @@ -107,17 +107,20 @@ impl<'a> VersionsDisplayWrapper<'a> { }; match print_mode { - PrintMode::FormattedDefault => Some(format!( + PrintMode::Formatted(FormattedMode::Default) => Some(format!( "{: { + PrintMode::Raw(RawMode::Csv) => { + Some(format!("{},{num_versions}{}", display_path, delimiter)) + } + PrintMode::Raw(_) if total_num_paths == 1 => { Some(format!("{num_versions}{}", delimiter)) } - PrintMode::FormattedNotPretty | PrintMode::RawNewline | PrintMode::RawZero => { + PrintMode::Formatted(FormattedMode::NotPretty) | _ => { Some(format!("{}\t{num_versions}{}", display_path, delimiter)) } } diff --git a/src/display_versions/format.rs b/src/display/versions.rs similarity index 60% rename from src/display_versions/format.rs rename to src/display/versions.rs index 0823a11a..0a127810 100644 --- a/src/display_versions/format.rs +++ b/src/display/versions.rs @@ -15,16 +15,23 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::{BulkExclusion, Config, PrintMode}; +use crate::config::generate::{BulkExclusion, Config, FormattedMode, PrintMode, RawMode}; use crate::data::paths::{PathData, PHANTOM_DATE, PHANTOM_SIZE}; +use crate::filesystem::mounts::IsFilterDir; use crate::library::utility::{ - date_string, delimiter, display_human_size, paint_string, path_is_filter_dir, DateFormat, + date_string, + delimiter, + display_human_size, + paint_string, + DateFormat, }; use crate::lookup::versions::ProximateDatasetAndOptAlts; -use crate::VersionsDisplayWrapper; +use crate::DisplayWrapper; use std::borrow::Cow; use std::ops::Deref; use terminal_size::{terminal_size, Height, Width}; +use time::UtcOffset; + // 2 space wide padding - used between date and size, and size and path pub const PRETTY_FIXED_WIDTH_PADDING: &str = " "; // our FIXED_WIDTH_PADDING is used twice @@ -34,22 +41,39 @@ pub const NOT_SO_PRETTY_FIXED_WIDTH_PADDING: &str = "\t"; // and we add 2 quotation marks to the path when we format pub const QUOTATION_MARKS_LEN: usize = 2; -impl<'a> VersionsDisplayWrapper<'a> { +impl<'a> DisplayWrapper<'a> { pub fn format(&self) -> String { - let keys: Vec<&PathData> = self.keys().collect(); - let values: Vec<&PathData> = self.values().flatten().collect(); + // if a single instance immediately return the global we already prepared + match &self.config.print_mode { + PrintMode::Formatted(_) => { + let keys: Vec<&PathData> = self.keys().collect(); + let values: Vec<&PathData> = self.values().flatten().collect(); - let global_display_set = DisplaySet::from((keys, values)); - let padding_collection = PaddingCollection::new(self.config, &global_display_set); + let global_display_set = DisplaySet::from((keys, values)); + let padding_collection = PaddingCollection::new(self.config, &global_display_set); - // if a single instance immediately return the global we already prepared - if matches!( - self.config.print_mode, - PrintMode::FormattedDefault | PrintMode::FormattedNotPretty - ) && self.len() == 1 - { - return global_display_set.format(self.config, &padding_collection); + if self.len() == 1 { + return global_display_set.format(self.config, &padding_collection); + } + + // else re compute for each instance and print per instance, now with uniform padding + self.iter() + .map(|(key, values)| { + let keys: Vec<&PathData> = vec![key]; + let values: Vec<&PathData> = values.iter().collect(); + + let display_set = DisplaySet::from((keys, values)); + + display_set.format(self.config, &padding_collection) + }) + .collect::() + } + PrintMode::Raw(raw_mode) => self.raw(&raw_mode), } + } + + fn raw(&self, raw_mode: &RawMode) -> String { + let delimiter = delimiter(); // else re compute for each instance and print per instance, now with uniform padding self.iter() @@ -57,31 +81,22 @@ impl<'a> VersionsDisplayWrapper<'a> { let keys: Vec<&PathData> = vec![key]; let values: Vec<&PathData> = values.iter().collect(); - let display_set = DisplaySet::from((keys, values)); - - match &self.config.print_mode { - PrintMode::FormattedDefault | PrintMode::FormattedNotPretty => { - display_set.format(self.config, &padding_collection) - } - PrintMode::RawNewline | PrintMode::RawZero => { - let delimiter = delimiter(); - - display_set - .iter() - .enumerate() - .map(|(idx, snap_or_live_set)| { - (DisplaySetType::from(idx), snap_or_live_set) - }) - .filter(|(display_set_type, _snap_or_live_set)| { - display_set_type.filter_bulk_exclusions(self.config) - }) - .flat_map(|(_idx, snap_or_live_set)| snap_or_live_set) - .fold(String::new(), |mut buffer, pathdata| { - buffer += &format!("{}{}", pathdata.path_buf.display(), delimiter); - buffer - }) - } - } + DisplaySet::from((keys, values)) + }) + .enumerate() + .map(|(idx, snap_or_live_set)| (DisplaySetType::from(idx), snap_or_live_set)) + .filter(|(display_set_type, _snap_or_live_set)| { + display_set_type.filter_bulk_exclusions(&self.config) + }) + .map(|(_display_set_type, display_set)| display_set) + .map(|display_set| { + display_set + .iter() + .flatten() + .map(|path_data| { + path_data.raw(raw_mode, delimiter, self.config.requested_utc_offset) + }) + .collect::() }) .collect::() } @@ -93,6 +108,7 @@ pub struct DisplaySet<'a> { } impl<'a> From<(Vec<&'a PathData>, Vec<&'a PathData>)> for DisplaySet<'a> { + #[inline(always)] fn from((keys, values): (Vec<&'a PathData>, Vec<&'a PathData>)) -> Self { Self { inner: [values, keys], @@ -126,7 +142,7 @@ impl From for DisplaySetType { } impl DisplaySetType { - #[inline] + #[inline(always)] fn filter_bulk_exclusions(&self, config: &Config) -> bool { match &self { DisplaySetType::IsLive @@ -145,6 +161,7 @@ impl DisplaySetType { } impl<'a> DisplaySet<'a> { + #[inline(always)] pub fn format(&self, config: &Config, padding_collection: &PaddingCollection) -> String { let mut border: String = padding_collection.fancy_border_string.to_string(); @@ -160,38 +177,47 @@ impl<'a> DisplaySet<'a> { |mut display_set_buffer, (display_set_type, snap_or_live_set)| { let mut component_buffer: String = snap_or_live_set .iter() - .map(|pathdata| { - pathdata.format(config, &display_set_type, padding_collection) + .map(|path_data| { + path_data.format(config, &display_set_type, padding_collection) }) .collect(); // add each buffer to the set - print fancy border string above, below and between sets - if matches!(config.print_mode, PrintMode::FormattedNotPretty) { + if matches!( + config.print_mode, + PrintMode::Formatted(FormattedMode::NotPretty) + ) { display_set_buffer += &component_buffer; - } else if matches!(display_set_type, DisplaySetType::IsSnap) { - if component_buffer.is_empty() { - let live_pathdata = self.inner[1][0]; - - let warning = live_pathdata.warning_underlying_snaps(config); - let warning_len = warning.chars().count(); - let border_len = border.chars().count(); - - if warning_len > border_len { - let diff = warning_len - border_len; - let mut new_border = border.trim_end().to_string(); - new_border += &format!("{:─ { + if component_buffer.is_empty() { + let live_path_data = self.inner[1][0]; + + let warning = live_path_data.warning_underlying_snaps(config); + let warning_len = warning.chars().count(); + let border_len = border.chars().count(); + + if warning_len > border_len { + let diff = warning_len - border_len; + let mut new_border = border.trim_end().to_string(); + new_border += &format!("{:─ { + display_set_buffer += &component_buffer; + display_set_buffer += &border; } - - display_set_buffer += &border; - display_set_buffer += &component_buffer; - display_set_buffer += &border; - } else { - display_set_buffer += &component_buffer; - display_set_buffer += &border; } display_set_buffer @@ -201,6 +227,7 @@ impl<'a> DisplaySet<'a> { } impl PathData { + #[inline(always)] pub fn format( &self, config: &Config, @@ -208,29 +235,29 @@ impl PathData { padding_collection: &PaddingCollection, ) -> String { // obtain metadata for timestamp and size - let metadata = self.md_infallible(); + let metadata = self.metadata_infallible(); // tab delimited if "no pretty", no border lines, and no colors let (display_size, display_path, display_padding) = match &config.print_mode { - PrintMode::FormattedNotPretty => { + PrintMode::Formatted(FormattedMode::NotPretty) => { // displays blanks for phantom values, equaling their dummy lens and dates. // // we use a dummy instead of a None value here. Basically, sometimes, we want // to print the request even if a live file does not exist - let size = if self.metadata.is_some() { - Cow::Owned(display_human_size(metadata.size)) + let size = if self.opt_metadata().is_some() { + Cow::Owned(display_human_size(metadata.size())) } else { Cow::Borrowed(&padding_collection.phantom_size_pad_str) }; - let path = self.path_buf.to_string_lossy(); + let path = self.path().to_string_lossy(); let padding = NOT_SO_PRETTY_FIXED_WIDTH_PADDING; (size, path, padding) } - _ => { + PrintMode::Formatted(FormattedMode::Default) => { // print with padding and pretty border lines and ls colors let size = { - let size = if self.metadata.is_some() { - Cow::Owned(display_human_size(metadata.size)) + let size = if self.opt_metadata().is_some() { + Cow::Owned(display_human_size(metadata.size())) } else { Cow::Borrowed(&padding_collection.phantom_size_pad_str) }; @@ -241,7 +268,7 @@ impl PathData { )) }; let path = { - let path_buf = &self.path_buf; + let path_buf = &self.path(); // paint the live strings with ls colors - idx == 1 is 2nd or live set let painted_path_str = match display_set_type { @@ -261,12 +288,13 @@ impl PathData { let padding = PRETTY_FIXED_WIDTH_PADDING; (size, path, padding) } + _ => unreachable!(), }; - let display_date = if self.metadata.is_some() { + let display_date = if self.opt_metadata().is_some() { Cow::Owned(date_string( config.requested_utc_offset, - &metadata.modify_time, + &metadata.mtime(), DateFormat::Display, )) } else { @@ -287,7 +315,7 @@ impl PathData { Some(_) if config.opt_omit_ditto => { "WARN: Omitting the only snapshot version available, which is identical to the live file.\n" } - Some(_) if path_is_filter_dir(&self.path_buf) => { + Some(_) if self.path().is_filter_dir() => { "WARN: Most proximate dataset for path is an unsupported filesystem.\n" } Some(_) => { @@ -295,6 +323,38 @@ impl PathData { } } } + + pub fn raw( + &self, + raw_mode: &RawMode, + delimiter: char, + requested_utc_offset: UtcOffset, + ) -> String { + match raw_mode { + RawMode::Csv => match self.opt_metadata() { + Some(md) => { + let date = + date_string(requested_utc_offset, &md.mtime(), DateFormat::Timestamp); + + let size = md.size(); + + format!( + "{},{},\"{}\"{}", + date, + size, + self.path().to_string_lossy(), + delimiter + ) + } + None => { + format!(",,\"{}\"{}", self.path().to_string_lossy(), delimiter) + } + }, + RawMode::Newline | RawMode::Zero => { + format!("{}{}", self.path().to_string_lossy(), delimiter) + } + } + } } pub struct PaddingCollection { @@ -305,30 +365,31 @@ pub struct PaddingCollection { } impl PaddingCollection { + #[inline(always)] pub fn new(config: &Config, display_set: &DisplaySet) -> PaddingCollection { // calculate padding and borders for display later let (size_padding_len, fancy_border_len) = display_set.iter().flatten().fold( (0usize, 0usize), - |(mut size_padding_len, mut fancy_border_len), pathdata| { - let metadata = pathdata.md_infallible(); + |(mut size_padding_len, mut fancy_border_len), path_data| { + let metadata = path_data.metadata_infallible(); let (display_date, display_size, display_path) = { let date = date_string( config.requested_utc_offset, - &metadata.modify_time, + &metadata.mtime(), DateFormat::Display, ); let size = format!( "{:>width$}", - display_human_size(metadata.size), + display_human_size(metadata.size()), width = size_padding_len ); - let path = pathdata.path_buf.to_string_lossy(); + let path = path_data.path().to_string_lossy(); (date, size, path) }; - let display_size_len = display_human_size(metadata.size).chars().count(); + let display_size_len = display_human_size(metadata.size()).chars().count(); let formatted_line_len = display_date.chars().count() + display_size.chars().count() + display_path.chars().count() @@ -368,26 +429,21 @@ impl PaddingCollection { } } + #[inline(always)] fn fancy_border_string(fancy_border_len: usize) -> String { - let get_max_sized_border = || { - // Active below is the most idiomatic Rust, but it maybe slower than the commented portion - // (0..fancy_border_len).map(|_| "─").collect() - format!("{:─ { - let width_as_usize = width as usize; + if let Some((Width(width), Height(_height))) = terminal_size() { + let width_as_usize = width as usize; - if width_as_usize < fancy_border_len { - // Active below is the most idiomatic Rust, but it maybe slower than the commented portion - // (0..width as usize).map(|_| "─").collect() - return format!("{:─ get_max_sized_border(), } + + // Active below is the most idiomatic Rust, but it maybe slower than the commented portion + // (0..fancy_border_len).map(|_| "─").collect() + // this is the max sized border + format!("{:─ { +pub struct DisplayWrapper<'a> { pub config: &'a Config, pub map: VersionsMap, } -impl<'a> std::string::ToString for VersionsDisplayWrapper<'a> { +impl<'a> std::string::ToString for DisplayWrapper<'a> { fn to_string(&self) -> String { match &self.config.exec_mode { ExecMode::NumVersions(num_versions_mode) => { @@ -52,7 +52,7 @@ impl<'a> std::string::ToString for VersionsDisplayWrapper<'a> { } } -impl<'a> Deref for VersionsDisplayWrapper<'a> { +impl<'a> Deref for DisplayWrapper<'a> { type Target = BTreeMap>; fn deref(&self) -> &Self::Target { @@ -60,17 +60,15 @@ impl<'a> Deref for VersionsDisplayWrapper<'a> { } } -impl<'a> VersionsDisplayWrapper<'a> { +impl<'a> DisplayWrapper<'a> { pub fn from(config: &'a Config, map: VersionsMap) -> Self { Self { config, map } } pub fn to_json(&self) -> String { let res = match self.config.print_mode { - PrintMode::FormattedNotPretty | PrintMode::RawNewline | PrintMode::RawZero => { - serde_json::to_string(self) - } - PrintMode::FormattedDefault => serde_json::to_string_pretty(self), + PrintMode::Formatted(FormattedMode::Default) => serde_json::to_string_pretty(self), + _ => serde_json::to_string(self), }; match res { @@ -86,7 +84,7 @@ impl<'a> VersionsDisplayWrapper<'a> { } } -impl<'a> Serialize for VersionsDisplayWrapper<'a> { +impl<'a> Serialize for DisplayWrapper<'a> { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -97,12 +95,12 @@ impl<'a> Serialize for VersionsDisplayWrapper<'a> { .clone() .into_iter() .map(|(key, values)| match &self.config.opt_bulk_exclusion { - Some(BulkExclusion::NoLive) => (key.path_buf.display().to_string(), values), - Some(BulkExclusion::NoSnap) => (key.path_buf.display().to_string(), vec![key]), + Some(BulkExclusion::NoLive) => (key.path().display().to_string(), values), + Some(BulkExclusion::NoSnap) => (key.path().display().to_string(), vec![key]), None => { let mut new_values = values; new_values.push(key.clone()); - (key.path_buf.display().to_string(), new_values) + (key.path().display().to_string(), new_values) } }) .collect(); diff --git a/src/filesystem/aliases.rs b/src/filesystem/aliases.rs new file mode 100644 index 00000000..e52e1dd6 --- /dev/null +++ b/src/filesystem/aliases.rs @@ -0,0 +1,165 @@ +// ___ ___ ___ ___ +// /\__\ /\ \ /\ \ /\__\ +// /:/ / \:\ \ \:\ \ /::| | +// /:/__/ \:\ \ \:\ \ /:|:| | +// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ +// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ +// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / +// \::/ / /:/ / /:/ / /:/ / +// /:/ / \/__/ \/__/ /:/ / +// /:/ / /:/ / +// \/__/ \/__/ +// +// Copyright (c) 2023, Robert Swinford gmail.com> +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +use crate::filesystem::mounts::{DatasetMetadata, FilesystemType}; +use crate::library::results::{HttmError, HttmResult}; +use std::collections::BTreeMap; +use std::ops::Deref; +use std::path::Path; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemotePathAndFsType { + pub remote_dir: Arc, + pub fs_type: FilesystemType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapOfAliases { + inner: BTreeMap, RemotePathAndFsType>, +} + +impl From, RemotePathAndFsType>> for MapOfAliases { + fn from(map: BTreeMap, RemotePathAndFsType>) -> Self { + Self { inner: map } + } +} + +impl Deref for MapOfAliases { + type Target = BTreeMap, RemotePathAndFsType>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl MapOfAliases { + pub fn new( + map_of_datasets: &BTreeMap, DatasetMetadata>, + opt_raw_aliases: Option>, + opt_remote_dir: Option<&String>, + opt_local_dir: Option<&String>, + pwd: &Path, + ) -> HttmResult> { + let alias_values: Option> = match std::env::var_os("HTTM_MAP_ALIASES") { + Some(env_map_alias) => Some( + env_map_alias + .to_string_lossy() + .split_terminator(',') + .map(|s| s.to_owned()) + .collect(), + ), + None => opt_raw_aliases, + }; + + let opt_snap_dir: Option> = if let Some(value) = opt_remote_dir { + Some(Box::from(Path::new(&value))) + } else if std::env::var_os("HTTM_REMOTE_DIR").is_some() { + std::env::var_os("HTTM_REMOTE_DIR").map(|s| Box::from(Path::new(&s))) + } else { + // legacy env var name + std::env::var_os("HTTM_SNAP_POINT").map(|s| Box::from(Path::new(&s))) + }; + + if alias_values.is_none() && opt_snap_dir.is_none() { + return Ok(None); + } + + let opt_local_dir: Option> = if let Some(value) = opt_local_dir { + Some(Box::from(Path::new(&value))) + } else { + std::env::var_os("HTTM_LOCAL_DIR").map(|s| Box::from(Path::new(&s))) + }; + + let mut aliases_iter: Vec<(Box, Box)> = match alias_values { + Some(input_aliases) => { + let res: Option, Box)>> = input_aliases + .iter() + .map(|alias| { + alias + .split_once(':') + .map(|(first, rest)| (Path::new(first).into(), Path::new(rest).into())) + }) + .collect(); + + res.ok_or_else(|| { + HttmError::new( + "Must use specified delimiter (':') between aliases for MAP_ALIASES.", + ) + })? + } + None => Vec::new(), + }; + + // user defined dir exists?: check that path contains the hidden snapshot directory + let snap_point = opt_snap_dir.map(|snap_dir| { + // local relative dir can be set at cmdline or as an env var, + // but defaults to current working directory if empty + let local_dir = opt_local_dir.unwrap_or_else(|| pwd.into()); + + (snap_dir, local_dir) + }); + + if let Some(value) = snap_point { + aliases_iter.push(value) + } + + let map_of_aliases: BTreeMap, RemotePathAndFsType> = aliases_iter + .into_iter() + .filter_map(|(local_dir, snap_dir)| { + // why get snap dir? because local dir is alias, snap dir must be a dataset + match map_of_datasets + .get_key_value(snap_dir.as_ref()) + .map(|(k, _v)| k.clone()) + { + Some(_snap_dir) if !local_dir.exists() => { + eprintln!( + "WARN: An alias path specified does not exist, or is not mounted: {:?}", + local_dir + ); + return None; + } + Some(snap_dir) => Some((local_dir, snap_dir)), + None => { + eprintln!( + "WARN: An alias path specified does not exist, or is not mounted: {:?}", + snap_dir + ); + None + } + } + }) + .filter_map(|(local_dir, remote_dir)| { + FilesystemType::new(&remote_dir).map(|fs_type| { + ( + local_dir, + RemotePathAndFsType { + remote_dir, + fs_type, + }, + ) + }) + }) + .collect(); + + if map_of_aliases.is_empty() { + return Ok(None); + } + + Ok(Some(map_of_aliases.into())) + } +} diff --git a/src/parse/alts.rs b/src/filesystem/alts.rs similarity index 67% rename from src/parse/alts.rs rename to src/filesystem/alts.rs index 5e279c68..bea7f774 100644 --- a/src/parse/alts.rs +++ b/src/filesystem/alts.rs @@ -15,32 +15,32 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +use crate::filesystem::mounts::MapOfDatasets; use crate::library::results::{HttmError, HttmResult}; -use crate::parse::mounts::MapOfDatasets; -use hashbrown::HashMap; use rayon::prelude::*; +use std::collections::BTreeMap; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::sync::Arc; #[derive(Debug, Clone, PartialEq, Eq)] pub struct MapOfAlts { - inner: HashMap, + inner: BTreeMap, AltMetadata>, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct AltMetadata { - pub proximate_dataset_mount: PathBuf, - pub opt_datasets_of_interest: Option>, + pub opt_datasets_of_interest: Option>>, } -impl From> for MapOfAlts { - fn from(map: HashMap) -> Self { +impl From, AltMetadata>> for MapOfAlts { + fn from(map: BTreeMap, AltMetadata>) -> Self { Self { inner: map } } } impl Deref for MapOfAlts { - type Target = HashMap; + type Target = BTreeMap, AltMetadata>; fn deref(&self) -> &Self::Target { &self.inner @@ -50,10 +50,12 @@ impl Deref for MapOfAlts { impl MapOfAlts { // instead of looking up, precompute possible alt replicated mounts before exec pub fn new(map_of_datasets: &MapOfDatasets) -> Self { - let res: HashMap = map_of_datasets + let res: BTreeMap, AltMetadata> = map_of_datasets .par_iter() .flat_map(|(mount, _dataset_info)| { - Self::from_mount(mount, map_of_datasets).map(|datasets| (mount.clone(), datasets)) + Self::from_mount(mount, map_of_datasets) + .ok() + .map(|datasets| (mount.clone(), datasets)) }) .collect(); @@ -64,8 +66,11 @@ impl MapOfAlts { proximate_dataset_mount: &Path, map_of_datasets: &MapOfDatasets, ) -> HttmResult { - let proximate_dataset_fs_name = match &map_of_datasets.get(proximate_dataset_mount) { - Some(dataset_info) => dataset_info.source.as_os_str(), + let fs_name = match map_of_datasets + .get(proximate_dataset_mount) + .map(|p| p.source.as_os_str()) + { + Some(name) => name, None => { return Err(HttmError::new("httm was unable to detect an alternate replicated mount point. Perhaps the replicated filesystem is not mounted?").into()); } @@ -74,15 +79,11 @@ impl MapOfAlts { // find a filesystem that ends with our most local filesystem name // but which has a prefix, like a different pool name: rpool might be // replicated to tank/rpool - let mut alt_replicated_mounts: Vec = map_of_datasets - .iter() - .map(|(mount, dataset_info)| (mount, Path::new(&dataset_info.source))) - .filter(|(_mount, source)| { - source.as_os_str() != proximate_dataset_fs_name - && source.ends_with(proximate_dataset_fs_name) - }) - .map(|(mount, _source)| mount) - .cloned() + let mut alt_replicated_mounts: Vec> = map_of_datasets + .par_iter() + .map(|(mount, dataset_info)| (mount, &dataset_info.source)) + .filter(|(_mount, source)| source.as_os_str() != fs_name && source.ends_with(fs_name)) + .map(|(mount, _source)| mount.as_ref().into()) .collect(); if alt_replicated_mounts.is_empty() { @@ -91,7 +92,6 @@ impl MapOfAlts { } else { alt_replicated_mounts.sort_unstable_by_key(|path| path.as_os_str().len()); Ok(AltMetadata { - proximate_dataset_mount: proximate_dataset_mount.to_path_buf(), opt_datasets_of_interest: Some(alt_replicated_mounts), }) } diff --git a/src/filesystem/mounts.rs b/src/filesystem/mounts.rs new file mode 100644 index 00000000..2918d199 --- /dev/null +++ b/src/filesystem/mounts.rs @@ -0,0 +1,556 @@ +// ___ ___ ___ ___ +// /\__\ /\ \ /\ \ /\__\ +// /:/ / \:\ \ \:\ \ /::| | +// /:/__/ \:\ \ \:\ \ /:|:| | +// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ +// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ +// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / +// \::/ / /:/ / /:/ / /:/ / +// /:/ / \/__/ \/__/ /:/ / +// /:/ / /:/ / +// \/__/ \/__/ +// +// Copyright (c) 2023, Robert Swinford gmail.com> +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed wth this source code. + +use crate::filesystem::snaps::MapOfSnaps; +use crate::library::results::{HttmError, HttmResult}; +use crate::library::utility::{find_common_path, get_mount_command}; +use crate::{ + BTRFS_SNAPPER_HIDDEN_DIRECTORY, + GLOBAL_CONFIG, + NILFS2_SNAPSHOT_ID_KEY, + RESTIC_LATEST_SNAPSHOT_DIRECTORY, + TM_DIR_LOCAL, + TM_DIR_REMOTE, + ZFS_HIDDEN_DIRECTORY, + ZFS_SNAPSHOT_DIRECTORY, +}; +use proc_mounts::MountIter; +use rayon::iter::Either; +use rayon::prelude::*; +use realpath_ext::{realpath, RealpathFlags}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Command as ExecProcess; +use std::sync::{Arc, LazyLock, OnceLock}; + +pub const ZFS_FSTYPE: &str = "zfs"; +pub const NILFS2_FSTYPE: &str = "nilfs2"; +pub const BTRFS_FSTYPE: &str = "btrfs"; +pub const SMB_FSTYPE: &str = "smbfs"; +pub const NFS_FSTYPE: &str = "nfs"; +pub const AFP_FSTYPE: &str = "afpfs"; +pub const RESTIC_FSTYPE: &str = "restic"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum LinkType { + Local, + Network, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BtrfsAdditionalData { + pub base_subvol: Box, + pub snap_names: OnceLock, Box>>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResticAdditionalData { + pub repos: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FilesystemType { + Zfs, + Btrfs(Option>), + Nilfs2, + Apfs, + Restic(Option>), +} + +impl FilesystemType { + pub fn new(dataset_mount: &Path) -> Option { + // set fstype, known by whether there is a ZFS hidden snapshot dir in the root dir + if dataset_mount + .join(ZFS_SNAPSHOT_DIRECTORY) + .symlink_metadata() + .is_ok() + { + Some(FilesystemType::Zfs) + } else if dataset_mount + .join(BTRFS_SNAPPER_HIDDEN_DIRECTORY) + .symlink_metadata() + .is_ok() + { + Some(FilesystemType::Btrfs(None)) + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DatasetMetadata { + pub source: Box, + pub fs_type: FilesystemType, + pub link_type: LinkType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FilterDirs { + inner: BTreeSet>, +} + +impl Deref for FilterDirs { + type Target = BTreeSet>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl FilterDirs { + pub fn is_filter_dir(&self, path: &Path) -> bool { + self.iter().any(|filter_dir| path == filter_dir.as_ref()) + } +} + +pub trait IsFilterDir { + fn is_filter_dir(&self) -> bool; +} + +impl> IsFilterDir for T +where + T: AsRef, +{ + fn is_filter_dir(self: &T) -> bool { + GLOBAL_CONFIG + .dataset_collection + .filter_dirs + .is_filter_dir(self.as_ref()) + } +} + +pub trait MaxLen { + fn max_len(&self) -> usize; +} + +impl MaxLen for FilterDirs { + fn max_len(&self) -> usize { + self.inner + .iter() + .map(|dir| dir.components().count()) + .max() + .unwrap_or(usize::MAX) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapOfDatasets { + inner: BTreeMap, DatasetMetadata>, +} + +impl Deref for MapOfDatasets { + type Target = BTreeMap, DatasetMetadata>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl From, DatasetMetadata>> for MapOfDatasets { + fn from(value: BTreeMap, DatasetMetadata>) -> Self { + Self { inner: value } + } +} + +impl MaxLen for MapOfDatasets { + fn max_len(&self) -> usize { + self.inner + .keys() + .map(|mount| mount.components().count()) + .max() + .unwrap_or(usize::MAX) + } +} + +pub static PROC_MOUNTS: LazyLock = LazyLock::new(|| PathBuf::from("/proc/mounts")); +pub static BTRFS_ROOT_SUBVOL: LazyLock = LazyLock::new(|| PathBuf::from("")); +pub static ROOT_PATH: LazyLock = LazyLock::new(|| PathBuf::from("/")); +static ETC_MNT_TAB: LazyLock = LazyLock::new(|| PathBuf::from("/etc/mnttab")); +pub static TM_DIR_REMOTE_PATH: LazyLock = LazyLock::new(|| PathBuf::from(TM_DIR_REMOTE)); +pub static TM_DIR_LOCAL_PATH: LazyLock = LazyLock::new(|| PathBuf::from(TM_DIR_LOCAL)); + +pub struct BaseFilesystemInfo { + pub map_of_datasets: MapOfDatasets, + pub map_of_snaps: MapOfSnaps, + pub filter_dirs: FilterDirs, +} + +impl BaseFilesystemInfo { + // divide by the type of system we are on + // Linux allows us the read proc mounts + pub fn new(opt_debug: bool, opt_alt_store: &Option) -> HttmResult { + let (mut raw_datasets, filter_dirs_set) = if PROC_MOUNTS.exists() { + Self::from_file(&PROC_MOUNTS, opt_alt_store)? + } else if ETC_MNT_TAB.exists() { + Self::from_file(&ETC_MNT_TAB, opt_alt_store)? + } else { + Self::from_mount_cmd(opt_alt_store)? + }; + + let map_of_snaps = MapOfSnaps::new(&mut raw_datasets, opt_debug)?; + + let map_of_datasets = { + MapOfDatasets { + inner: raw_datasets, + } + }; + + let filter_dirs = { + FilterDirs { + inner: filter_dirs_set, + } + }; + + Ok(BaseFilesystemInfo { + map_of_datasets, + map_of_snaps, + filter_dirs, + }) + } + + // parsing from proc mounts is both faster and necessary for certain btrfs features + // for instance, allows us to read subvolumes mounts, like "/@" or "/@home" + fn from_file( + path: &Path, + opt_alt_store: &Option, + ) -> HttmResult<(BTreeMap, DatasetMetadata>, BTreeSet>)> { + let mount_iter = MountIter::new_from_file(path)?; + + let (map_of_datasets, filter_dirs): ( + BTreeMap, DatasetMetadata>, + BTreeSet>, + ) = mount_iter + .par_bridge() + .flatten() + .filter(|mount_info| { + !mount_info + .dest + .to_string_lossy() + .contains(ZFS_HIDDEN_DIRECTORY) + }) + .filter(|mount_info| { + !mount_info + .options + .iter() + .any(|opt| opt.contains(NILFS2_SNAPSHOT_ID_KEY)) + }) + .map(|mount_info| { + let dest_path = Arc::from(Path::new(&mount_info.dest)); + (mount_info, dest_path) + }) + .partition_map(|(mount_info, dest_path)| match mount_info.fstype.as_str() { + ZFS_FSTYPE => Either::Left(( + dest_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Zfs, + link_type: LinkType::Local, + }, + )), + SMB_FSTYPE | AFP_FSTYPE | NFS_FSTYPE => match FilesystemType::new(&dest_path) { + Some(FilesystemType::Zfs) => Either::Left(( + dest_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Zfs, + link_type: LinkType::Network, + }, + )), + Some(FilesystemType::Btrfs(None)) => Either::Left(( + dest_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Btrfs(None), + link_type: LinkType::Network, + }, + )), + _ => Either::Right(dest_path), + }, + BTRFS_FSTYPE => { + let keyed_options: BTreeMap<&str, &str> = mount_info + .options + .iter() + .filter(|line| line.contains('=')) + .filter_map(|line| line.split_once('=')) + .collect(); + + let opt_additional_data = keyed_options + .get("subvol") + .map(|subvol| match keyed_options.get("subvolid") { + Some(id) if *id == "5" => BTRFS_ROOT_SUBVOL.as_path(), + _ => Path::new(subvol), + }) + .map(|base_subvol| { + Box::new(BtrfsAdditionalData { + base_subvol: base_subvol.into(), + snap_names: OnceLock::new(), + }) + }); + + Either::Left(( + dest_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Btrfs(opt_additional_data), + link_type: LinkType::Local, + }, + )) + } + NILFS2_FSTYPE => Either::Left(( + dest_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Nilfs2, + link_type: LinkType::Local, + }, + )), + _ if mount_info.source.to_string_lossy().contains(RESTIC_FSTYPE) => { + let base_path = if let Some(FilesystemType::Restic(_)) = opt_alt_store { + dest_path.to_path_buf() + } else { + dest_path.as_ref().join(RESTIC_LATEST_SNAPSHOT_DIRECTORY) + }; + + let canonical_path = realpath(&base_path, RealpathFlags::ALLOW_MISSING) + .unwrap_or_else(|_| base_path.to_path_buf()) + .into(); + + Either::Left(( + canonical_path, + DatasetMetadata { + source: mount_info.source.into_boxed_path(), + fs_type: FilesystemType::Restic(None), + link_type: LinkType::Local, + }, + )) + } + _ => Either::Right(dest_path), + }); + + Ok((map_of_datasets, filter_dirs)) + } + + // old fashioned parsing for non-Linux systems, nearly as fast, works everywhere with a mount command + // both methods are much faster than using zfs command + fn from_mount_cmd( + opt_alt_store: &Option, + ) -> HttmResult<(BTreeMap, DatasetMetadata>, BTreeSet>)> { + // do we have the necessary commands for search if user has not defined a snap point? + // if so run the mount search, if not print some errors + let mount_command = get_mount_command()?; + + let command_output = &ExecProcess::new(mount_command).output()?; + + let stderr_string = std::str::from_utf8(&command_output.stderr)?; + + if !stderr_string.is_empty() { + return Err(HttmError::new(stderr_string).into()); + } + + let stdout_string = std::str::from_utf8(&command_output.stdout)?; + + // parse "mount" for filesystems and mountpoints + let (map_of_datasets, filter_dirs): ( + BTreeMap, DatasetMetadata>, + BTreeSet>, + ) = stdout_string + .par_lines() + // but exclude snapshot mounts. we want the raw filesystem names. + .filter(|line| !line.contains(ZFS_HIDDEN_DIRECTORY)) + .filter(|line| !line.contains(TM_DIR_REMOTE)) + .filter(|line| !line.contains(TM_DIR_LOCAL)) + // mount cmd includes and " on " between src and rest + .filter_map(|line| line.split_once(" on ")) + // where to split, to just have the src and dest of mounts + .filter_map(|(filesystem, rest)| { + // GNU Linux mount output + if rest.contains("type") { + let opt_mount = rest.split_once(" type"); + opt_mount.map(|the_rest| (filesystem, the_rest.0, the_rest.1)) + // Busybox and BSD mount output + } else if rest.contains(" (") { + let opt_mount = rest.split_once(" ("); + opt_mount.map(|the_rest| (filesystem, the_rest.0, the_rest.1)) + } else { + None + } + }) + .map(|(filesystem, mount, the_rest)| { + let link_type = if the_rest.contains(SMB_FSTYPE) + || the_rest.contains(AFP_FSTYPE) + || the_rest.contains(NFS_FSTYPE) + { + LinkType::Network + } else { + LinkType::Local + }; + + ( + Box::from(Path::new(filesystem)), + Arc::from(Path::new(mount)), + link_type, + ) + }) + // sanity check: does the filesystem exist and have a ZFS hidden dir? if not, filter it out + // and flip around, mount should key of key/value + .partition_map( + |(source, mount, link_type)| match FilesystemType::new(&mount) { + Some(FilesystemType::Zfs) => Either::Left(( + mount, + DatasetMetadata { + source, + fs_type: FilesystemType::Zfs, + link_type, + }, + )), + Some(FilesystemType::Btrfs(_)) => Either::Left(( + mount, + DatasetMetadata { + source, + fs_type: FilesystemType::Btrfs(None), + link_type, + }, + )), + _ if source.to_string_lossy().contains(RESTIC_FSTYPE) => { + let base_path = if let Some(FilesystemType::Restic(_)) = opt_alt_store { + mount.to_path_buf() + } else { + mount.join(RESTIC_LATEST_SNAPSHOT_DIRECTORY) + }; + + let canonical_path = realpath(&base_path, RealpathFlags::ALLOW_MISSING) + .unwrap_or_else(|_| base_path.to_path_buf()) + .into(); + + Either::Left(( + canonical_path, + DatasetMetadata { + source, + fs_type: FilesystemType::Restic(None), + link_type, + }, + )) + } + _ => Either::Right(mount), + }, + ); + + Ok((map_of_datasets, filter_dirs)) + } + + pub fn from_blob_repo( + &mut self, + repo_type: &FilesystemType, + opt_debug: bool, + ) -> HttmResult<()> { + let metadata = match repo_type { + FilesystemType::Restic(_) => { + let retained_keys: Vec> = self + .map_of_datasets + .iter() + .filter(|(_k, v)| &v.fs_type == repo_type) + .map(|(k, _v)| k.as_ref().into()) + .collect(); + + if retained_keys.is_empty() { + return Err(HttmError::new( + "No supported Restic datasets were found on the system.", + ) + .into()); + } + + let repos: Vec> = retained_keys; + + DatasetMetadata { + source: Path::new(RESTIC_FSTYPE).into(), + fs_type: FilesystemType::Restic(Some(Box::new(ResticAdditionalData { repos }))), + link_type: LinkType::Local, + } + } + FilesystemType::Apfs => { + if !cfg!(target_os = "macos") { + return Err(HttmError::new( + "Time Machine is only supported on Mac OS. This appears to be an unsupported OS." + ) + .into()); + } + + if !TM_DIR_REMOTE_PATH.exists() && !TM_DIR_LOCAL_PATH.exists() { + return Err(HttmError::new( + "Neither a local nor a remote Time Machine path seems to exist for this system." + ) + .into()); + } + + DatasetMetadata { + source: Path::new("timemachine").into(), + fs_type: FilesystemType::Apfs, + link_type: LinkType::Local, + } + } + _ => { + return Err(HttmError::new( + "The file system type specified is not a supported alternative store.", + ) + .into()); + } + }; + + let datasets = BTreeMap::from([(Arc::from(ROOT_PATH.as_ref()), metadata)]); + + let snaps = MapOfSnaps::new(&datasets, opt_debug)?; + + *self = Self { + map_of_datasets: datasets.into(), + map_of_snaps: snaps, + filter_dirs: self.filter_dirs.clone(), + }; + + Ok(()) + } + + // if we have some btrfs mounts, we check to see if there is a snap directory in common + // so we can hide that common path from searches later + pub fn common_snap_dir(&self) -> Option> { + let map_of_datasets: &MapOfDatasets = &self.map_of_datasets; + let map_of_snaps: &MapOfSnaps = &self.map_of_snaps; + + if map_of_datasets + .par_iter() + .any(|(_mount, dataset_info)| !matches!(dataset_info.fs_type, FilesystemType::Zfs)) + { + let vec_snaps: Vec<&Box> = map_of_datasets + .par_iter() + .filter(|(_mount, dataset_info)| { + if matches!(dataset_info.fs_type, FilesystemType::Btrfs(_)) { + return true; + } + + false + }) + .filter_map(|(mount, _dataset_info)| map_of_snaps.get(mount)) + .flatten() + .collect(); + + return find_common_path(vec_snaps); + } + + None + } +} diff --git a/src/filesystem/snaps.rs b/src/filesystem/snaps.rs new file mode 100644 index 00000000..90844df5 --- /dev/null +++ b/src/filesystem/snaps.rs @@ -0,0 +1,452 @@ +// ___ ___ ___ ___ +// /\__\ /\ \ /\ \ /\__\ +// /:/ / \:\ \ \:\ \ /::| | +// /:/__/ \:\ \ \:\ \ /:|:| | +// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ +// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ +// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / +// \::/ / /:/ / /:/ / /:/ / +// /:/ / \/__/ \/__/ /:/ / +// /:/ / /:/ / +// \/__/ \/__/ +// +// Copyright (c) 2023, Robert Swinford gmail.com> +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +use super::mounts::ROOT_PATH; +use crate::filesystem::mounts::{DatasetMetadata, FilesystemType, BTRFS_ROOT_SUBVOL, PROC_MOUNTS}; +use crate::library::results::{HttmError, HttmResult}; +use crate::library::utility::{get_btrfs_command, user_has_effective_root}; +use crate::{ + BTRFS_SNAPPER_HIDDEN_DIRECTORY, + BTRFS_SNAPPER_SUFFIX, + RESTIC_SNAPSHOT_DIRECTORY, + TM_DIR_LOCAL, + TM_DIR_REMOTE, + ZFS_SNAPSHOT_DIRECTORY, +}; +use proc_mounts::MountIter; +use rayon::prelude::*; +use std::collections::BTreeMap; +use std::fs::read_dir; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Command as ExecProcess; +use std::sync::{Arc, Once}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapOfSnaps { + inner: BTreeMap, Vec>>, +} + +impl From, Vec>>> for MapOfSnaps { + fn from(map: BTreeMap, Vec>>) -> Self { + Self { inner: map } + } +} + +impl Deref for MapOfSnaps { + type Target = BTreeMap, Vec>>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl MapOfSnaps { + // fans out precompute of snap mounts to the appropriate function based on fstype + pub fn new( + map_of_datasets: &BTreeMap, DatasetMetadata>, + opt_debug: bool, + ) -> HttmResult { + let map_of_snaps: BTreeMap, Vec>> = map_of_datasets + .par_iter() + .map(|(mount, dataset_info)| { + let snap_mounts: Vec> = match &dataset_info.fs_type { + FilesystemType::Zfs | FilesystemType::Nilfs2 | FilesystemType::Apfs | FilesystemType::Restic(_) | FilesystemType::Btrfs(None) => { + Self::from_defined_mounts(mount, dataset_info) + } + // btrfs Some mounts are potential local mount + FilesystemType::Btrfs(Some(additional_data)) => { + let map = Self::from_btrfs_cmd( + mount, + dataset_info, + &additional_data.base_subvol, + map_of_datasets, + opt_debug, + ); + + if map.is_empty() { + static NOTICE_FALLBACK: Once = Once::new(); + + NOTICE_FALLBACK.call_once(|| { + eprintln!( + "NOTICE: Falling back to detection of btrfs snapshot mounts perhaps defined by Snapper re: mount: {:?}", mount + ); + }); + + Self::from_defined_mounts(mount, dataset_info) + } else { + additional_data.snap_names.get_or_init(|| { + map.clone() + }); + + map.into_keys().collect() + } + } + }; + + (mount.clone(), snap_mounts) + }) + .collect(); + + if map_of_snaps.iter().any(|(_mount, snaps)| snaps.is_empty()) { + if opt_debug { + eprintln!("WARN: httm relies on the user (and/or the filesystem's auto-mounter) to mount snapshots. Make certain any snapshots the user may want to view are mounted, or are able to be mounted, and/or the user has the correct permissions to view."); + } + + if map_of_snaps.values().count() == 0 { + return Err(HttmError::new( + "httm could not find any valid snapshots on the system. Quitting.", + ) + .into()); + } + + if opt_debug { + map_of_snaps.iter().for_each(|(mount, snaps)| { + if snaps.is_empty() { + eprintln!( + "WARN: Mount {:?} appears to have no snapshots available.", + mount + ) + } + }) + } + } + + Ok(map_of_snaps.into()) + } + + // build paths to all snap mounts + pub fn from_btrfs_cmd( + base_mount: &Path, + base_mount_metadata: &DatasetMetadata, + base_subvol: &Path, + map_of_datasets: &BTreeMap, DatasetMetadata>, + opt_debug: bool, + ) -> BTreeMap, Box> { + const BTRFS_COMMAND_REQUIRES_ROOT: &str = + "btrfs mounts detected. User must have super user permissions to determine the location of btrfs snapshots"; + + if let Err(_err) = user_has_effective_root(&BTRFS_COMMAND_REQUIRES_ROOT) { + static USER_HAS_ROOT_WARNING: Once = Once::new(); + + USER_HAS_ROOT_WARNING.call_once(|| { + eprintln!("WARN: {}", BTRFS_COMMAND_REQUIRES_ROOT); + }); + return BTreeMap::new(); + } + + let Ok(btrfs_command) = get_btrfs_command() else { + static BTRFS_COMMAND_AVAILABLE_WARNING: Once = Once::new(); + + BTRFS_COMMAND_AVAILABLE_WARNING.call_once(|| { + eprintln!( + "WARN: 'btrfs' command not found. Make sure the command 'btrfs' is in your path.", + ); + }); + + return BTreeMap::new(); + }; + + let exec_command = btrfs_command; + let arg_path = base_mount.to_string_lossy(); + let args = vec!["subvolume", "show", &arg_path]; + + // must exec for each mount, probably a better way by calling into a lib + let Some(command_output) = ExecProcess::new(exec_command) + .args(&args) + .output() + .ok() + .and_then(|output| { + std::str::from_utf8(&output.stdout) + .map(|string| string.trim().to_owned()) + .ok() + }) + else { + static COULD_NOT_OBTAIN_BTRFS_COMMAND_OUTPUT: Once = Once::new(); + + COULD_NOT_OBTAIN_BTRFS_COMMAND_OUTPUT.call_once(|| { + eprintln!("WARN: Could not obtain btrfs command output.",); + }); + return BTreeMap::new(); + }; + + match command_output + .split_once("Snapshot(s):\n") + .map(|(_first, last)| match last.rsplit_once("Quota group:") { + Some((snap_paths, _remainder)) => snap_paths, + None => last, + }) + .map(|snap_paths| { + snap_paths + .par_lines() + .map(|line| line.trim()) + .map(|line| Path::new(line)) + .filter(|line| !line.as_os_str().is_empty()) + .filter_map(|snap_name| { + let opt_snap_location = Self::parse_btrfs_relative_path( + base_mount, + &base_mount_metadata.source, + base_subvol, + snap_name, + map_of_datasets, + opt_debug, + ); + + opt_snap_location.map(|snap_location| { + (snap_location.into_boxed_path(), snap_name.into()) + }) + }) + .collect() + }) { + Some(map) => map, + None => { + //eprintln!("WARN: No snaps found for mount: {:?}", base_mount); + BTreeMap::new() + } + } + } + + fn parse_btrfs_relative_path( + base_mount: &Path, + base_mount_source: &Path, + base_subvol: &Path, + snap_relative: &Path, + map_of_datasets: &BTreeMap, DatasetMetadata>, + opt_debug: bool, + ) -> Option { + let mut path_iter = snap_relative.components(); + + let opt_first_snap_component = path_iter.next(); + + let the_rest = path_iter; + + if opt_debug { + eprintln!( + "DEBUG: Base mount: {:?}, Base subvol: {:?}, Snap Relative Path: {:?}", + base_mount, base_subvol, snap_relative + ); + } + + match opt_first_snap_component + .and_then(|first_snap_component| { + // btrfs subvols usually look like /@subvol in mounts info, but are listed elsewhere + // such as the first snap component, as @subvol, so here we remove the leading "/" + let potential_dataset = first_snap_component.as_os_str().to_string_lossy(); + let base_subvol_name = base_subvol.to_string_lossy(); + + // short circuit -- if subvol is same as dataset return base mount + if potential_dataset == base_subvol_name.trim_start_matches("/") { + return Some(base_mount); + } + + map_of_datasets.iter().find_map(|(mount, metadata)| { + // if the datasets do not match then can't be the same btrfs subvol + if metadata.source.as_ref() != base_mount_source { + return None; + } + + match &metadata.fs_type { + FilesystemType::Btrfs(Some(additional_data)) => { + let subvol_name = additional_data.base_subvol.to_string_lossy(); + + if potential_dataset == subvol_name.trim_start_matches("/") { + Some(mount.as_ref()) + } else { + None + } + } + _ => None, + } + }) + }) + .map(|mount| { + let joined = mount.join(the_rest); + + if opt_debug { + eprintln!("DEBUG: Joined path: {:?}", joined); + } + + joined + }) { + // here we check if the path actually exists because of course this is inexact! + Some(snap_mount) => { + if snap_mount.exists() { + Some(snap_mount) + } else { + eprintln!( + "WARN: Snapshot mount requested does not exist or perhaps is not mounted: {:?}", + snap_relative + ); + None + } + } + None => { + // btrfs root is different for each device, here, we check to see they have the same device + // and when we parse mounts we check to see that they have a subvolid of "5", then we replace + // whatever subvol name with a special id: + let btrfs_root = map_of_datasets + .iter() + .find(|(_mount, metadata)| match &metadata.fs_type { + FilesystemType::Btrfs(Some(additional_data)) => { + metadata.source.as_ref() == base_mount_source + && additional_data.base_subvol.as_ref() + == BTRFS_ROOT_SUBVOL.as_path() + } + _ => false, + }) + .map(|(mount, _metadata)| mount.to_owned()) + .unwrap_or_else(|| Arc::from(ROOT_PATH.as_ref())); + + let snap_mount = btrfs_root.join(snap_relative); + + if opt_debug { + eprintln!( + "DEBUG: Btrfs top level {:?}, Snap Mount: {:?}", + btrfs_root, snap_mount + ); + } + + // here we check if the path actually exists because of course this is inexact! + if snap_mount.exists() { + Some(snap_mount) + } else { + eprintln!( + "WARN: Snapshot mount requested does not exist or perhaps is not mounted: {:?}", + snap_relative + ); + None + } + } + } + } + + fn from_defined_mounts( + mount_point_path: &Path, + dataset_metadata: &DatasetMetadata, + ) -> Vec> { + fn inner( + mount_point_path: &Path, + dataset_metadata: &DatasetMetadata, + ) -> HttmResult>> { + let snaps: Vec> = match &dataset_metadata.fs_type { + FilesystemType::Btrfs(_) => { + match read_dir(mount_point_path.join(BTRFS_SNAPPER_HIDDEN_DIRECTORY)) { + Err(err) => { + if err.kind() == std::io::ErrorKind::PermissionDenied { + eprintln!("WARN: Permission denied to read snapshot locations from defined mount: {:?}", mount_point_path); + } + + Vec::new() + } + Ok(read_dir)=> { + read_dir + .flatten() + .par_bridge() + .map(|entry| entry.path().join(BTRFS_SNAPPER_SUFFIX)) + .map(|path| path.into_boxed_path()) + .collect() + } + } + } + FilesystemType::Restic(None) => { + // base is latest, parent is the snap path + let repos = mount_point_path.parent(); + + repos + .iter() + .flat_map(|repo| read_dir(repo)) + .flatten() + .flatten() + .map(|dir_entry| dir_entry.path()) + .map(|path| path.into_boxed_path()) + .filter(|path| !path.ends_with("latest")) + .collect() + } + FilesystemType::Restic(Some(additional_data)) => additional_data + .repos + .par_iter() + .flat_map(|repo| read_dir(repo.join(RESTIC_SNAPSHOT_DIRECTORY))) + .flatten_iter() + .flatten() + .map(|dir_entry| dir_entry.path()) + .map(|path| path.into_boxed_path()) + .filter(|path| !path.ends_with("latest")) + .collect(), + FilesystemType::Zfs => read_dir(mount_point_path.join(ZFS_SNAPSHOT_DIRECTORY))? + .flatten() + .par_bridge() + .map(|entry| entry.path()) + .map(|path| path.into_boxed_path()) + .collect(), + FilesystemType::Apfs => { + let mut res: Vec> = Vec::new(); + + if Path::new(&TM_DIR_LOCAL).exists() { + let local = read_dir(TM_DIR_LOCAL)? + .par_bridge() + .flatten() + .flat_map(|entry| read_dir(entry.path())) + .flatten_iter() + .flatten_iter() + .map(|entry| entry.path().join("Data")) + .map(|path| path.into_boxed_path()); + + res.par_extend(local); + } + + if Path::new(&TM_DIR_REMOTE).exists() { + let remote = read_dir(TM_DIR_REMOTE)? + .par_bridge() + .flatten() + .flat_map(|entry| read_dir(entry.path())) + .flatten_iter() + .flatten_iter() + .map(|entry| entry.path().join(entry.file_name()).join("Data")) + .map(|path| path.into_boxed_path()); + + res.par_extend(remote); + } + + res + } + FilesystemType::Nilfs2 => { + let source_path = dataset_metadata.source.as_ref(); + + let mount_iter = MountIter::new_from_file(&*PROC_MOUNTS)?; + + mount_iter + .par_bridge() + .flatten() + .filter(|mount_info| Path::new(&mount_info.source) == source_path) + .filter(|mount_info| { + mount_info.options.iter().any(|opt| opt.contains("cp=")) + }) + .map(|mount_info| PathBuf::from(mount_info.dest)) + .map(|path| path.into_boxed_path()) + .collect() + } + }; + + Ok(snaps) + } + + match inner(mount_point_path, dataset_metadata) { + Ok(res) => res, + Err(_err) => Vec::new(), + } + } +} diff --git a/src/interactive/browse.rs b/src/interactive/browse.rs index e0c11264..a61db41e 100644 --- a/src/interactive/browse.rs +++ b/src/interactive/browse.rs @@ -15,11 +15,15 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +use crate::background::recursive::RecursiveSearch; use crate::data::paths::PathData; use crate::interactive::view_mode::ViewMode; use crate::library::results::{HttmError, HttmResult}; use crate::GLOBAL_CONFIG; - +use crossbeam_channel::unbounded; +use skim::prelude::*; +use std::path::Path; +use std::sync::atomic::AtomicBool; use std::thread::JoinHandle; #[derive(Debug)] @@ -29,20 +33,20 @@ pub struct InteractiveBrowse { } impl InteractiveBrowse { - pub fn new() -> HttmResult { + pub fn new() -> HttmResult { let browse_result = match &GLOBAL_CONFIG.opt_requested_dir { // collect string paths from what we get from lookup_view Some(requested_dir) => { - let view_mode = ViewMode::Browse; - let browse_result = view_mode.browse(requested_dir)?; - if browse_result.selected_pathdata.is_empty() { + let res = Self::view(requested_dir)?; + + if res.selected_pathdata.is_empty() { return Err(HttmError::new( "None of the selected strings could be converted to paths.", ) .into()); } - browse_result + res } None => { // go to interactive_select early if user has already requested a file @@ -60,12 +64,98 @@ impl InteractiveBrowse { // Config::from should never allow us to have an instance where we don't // have at least one path to use None => unreachable!( - "GLOBAL_CONFIG.paths.get(0) should never be a None value in Interactive Mode" - ), + "GLOBAL_CONFIG.paths.get(0) should never be a None value in Interactive Mode" + ), } } }; Ok(browse_result) } + + #[allow(dead_code)] + #[cfg(feature = "malloc_trim")] + #[cfg(target_os = "linux")] + #[cfg(target_env = "gnu")] + fn malloc_trim() { + unsafe { + let _ = libc::malloc_trim(0usize); + } + } + + fn view(requested_dir: &Path) -> HttmResult { + // prep thread spawn + let started = Arc::new(AtomicBool::new(false)); + let hangup = Arc::new(AtomicBool::new(false)); + let hangup_clone = hangup.clone(); + let started_clone = started.clone(); + let requested_dir_clone = requested_dir.to_path_buf(); + let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); + + // thread spawn fn enumerate_directory - permits recursion into dirs without blocking + let background_handle = std::thread::spawn(move || { + // no way to propagate error from closure so exit and explain error here + RecursiveSearch::new( + &requested_dir_clone, + tx_item.clone(), + hangup.clone(), + started, + ) + .exec(); + + #[cfg(feature = "malloc_trim")] + #[cfg(target_os = "linux")] + #[cfg(target_env = "gnu")] + Self::malloc_trim(); + }); + + let header: String = ViewMode::Browse.print_header(); + + let opt_multi = GLOBAL_CONFIG.opt_preview.is_none(); + + // create the skim component for previews + let skim_opts = SkimOptionsBuilder::default() + .preview_window(Some("up:50%")) + .preview(Some("")) + .nosort(true) + .exact(GLOBAL_CONFIG.opt_exact) + .header(Some(&header)) + .multi(opt_multi) + .regex(false) + .tiebreak(Some("score,index".to_string())) + .algorithm(FuzzyAlgorithm::Simple) + .build() + .expect("Could not initialized skim options for browse_view"); + + while !started_clone.load(Ordering::SeqCst) {} + + // run_with() reads and shows items from the thread stream created above + match skim::Skim::run_with(&skim_opts, Some(rx_item)) { + Some(output) if output.is_abort => { + eprintln!("httm interactive file browse session was aborted. Quitting."); + std::process::exit(0) + } + Some(output) => { + // hangup the channel so the background recursive search can gracefully cleanup and exit + hangup_clone.store(true, Ordering::Relaxed); + + #[cfg(feature = "malloc_trim")] + #[cfg(target_os = "linux")] + #[cfg(target_env = "gnu")] + Self::malloc_trim(); + + let selected_pathdata: Vec = output + .selected_items + .iter() + .map(|item| PathData::from(Path::new(item.output().as_ref()))) + .collect(); + + Ok(Self { + selected_pathdata, + opt_background_handle: Some(background_handle), + }) + } + None => Err(HttmError::new("httm interactive file browse session failed.").into()), + } + } } diff --git a/src/interactive/exec.rs b/src/interactive/exec.rs deleted file mode 100644 index 97337259..00000000 --- a/src/interactive/exec.rs +++ /dev/null @@ -1,42 +0,0 @@ -// ___ ___ ___ ___ -// /\__\ /\ \ /\ \ /\__\ -// /:/ / \:\ \ \:\ \ /::| | -// /:/__/ \:\ \ \:\ \ /:|:| | -// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ -// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ -// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / -// \::/ / /:/ / /:/ / /:/ / -// /:/ / \/__/ \/__/ /:/ / -// /:/ / /:/ / -// \/__/ \/__/ -// -// Copyright (c) 2023, Robert Swinford gmail.com> -// -// For the full copyright and license information, please view the LICENSE file -// that was distributed with this source code. - -use crate::config::generate::InteractiveMode; -use crate::data::paths::PathData; -use crate::interactive::browse::InteractiveBrowse; -use crate::interactive::select::InteractiveSelect; -use crate::library::results::HttmResult; - -#[derive(Debug)] -pub struct InteractiveExec {} - -impl InteractiveExec { - pub fn exec(interactive_mode: &InteractiveMode) -> HttmResult> { - let browse_result = InteractiveBrowse::new()?; - - // do we return back to our main exec function to print, - // or continue down the interactive rabbit hole? - match interactive_mode { - InteractiveMode::Restore(_) | InteractiveMode::Select(_) => { - InteractiveSelect::exec(browse_result, interactive_mode)?; - unreachable!() - } - // InteractiveMode::Browse executes back through fn exec() in main.rs - InteractiveMode::Browse => Ok(browse_result.selected_pathdata), - } - } -} diff --git a/src/interactive/prune.rs b/src/interactive/prune.rs index 336c9336..ea3a275e 100644 --- a/src/interactive/prune.rs +++ b/src/interactive/prune.rs @@ -16,12 +16,11 @@ // that was distributed with this source code. use crate::config::generate::ListSnapsFilters; -use crate::interactive::view_mode::MultiSelect; -use crate::interactive::view_mode::ViewMode; +use crate::interactive::view_mode::{MultiSelect, ViewMode}; use crate::library::results::{HttmError, HttmResult}; use crate::lookup::snap_names::SnapNameMap; use crate::lookup::versions::VersionsMap; -use std::process::Command as ExecProcess; +use crate::zfs::run_command::RunZFSCommand; pub struct PruneSnaps; @@ -38,42 +37,14 @@ impl PruneSnaps { false }; - InteractivePrune::new(&snap_name_map, select_mode)?; - - std::process::exit(0) + InteractivePrune::new(&snap_name_map, select_mode) } fn prune(snap_name_map: &SnapNameMap) -> HttmResult<()> { - let zfs_command = which::which("zfs").map_err(|_err| { - HttmError::new("'zfs' command not found. Make sure the command 'zfs' is in your path.") - })?; - - snap_name_map - .values() - .flatten() - .try_for_each(|snapshot_name| { - let process_args = vec!["destroy".to_owned(), snapshot_name.clone()]; - - let process_output = ExecProcess::new(&zfs_command) - .args(&process_args) - .output()?; - let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); - - // stderr_string is a string not an error, so here we build an err or output - if !stderr_string.is_empty() { - let msg = if stderr_string.contains("cannot destroy snapshots: permission denied") { - "httm must have root privileges to destroy a snapshot filesystem".to_owned() - } else { - "httm was unable to destroy snapshots. The 'zfs' command issued the following error: " - .to_owned() - + stderr_string - }; - - Err(HttmError::new(&msg).into()) - } else { - Ok(()) - } - }) + let snapshot_names: Vec = snap_name_map.values().flatten().cloned().collect(); + + let run_zfs = RunZFSCommand::new()?; + run_zfs.prune(&snapshot_names) } } @@ -83,7 +54,7 @@ impl InteractivePrune { fn new(snap_name_map: &SnapNameMap, select_mode: bool) -> HttmResult<()> { let file_names_string: String = snap_name_map.keys().fold(String::new(), |mut buffer, key| { - buffer += format!("{:?}\n", key.path_buf).as_str(); + buffer += format!("{:?}\n", key.path()).as_str(); buffer }); @@ -93,8 +64,8 @@ impl InteractivePrune { .flatten() .map(|name| format!("{name}\n")) .collect(); - let view_mode = &ViewMode::Select(None); - view_mode.select(&buffer, MultiSelect::On)? + let view_mode = ViewMode::Select(None); + view_mode.view_buffer(&buffer, MultiSelect::On)? } else { snap_name_map .values() @@ -108,7 +79,7 @@ impl InteractivePrune { .map(|name| format!("{name}\n")) .collect(); - let preview_buffer = format!( + let prune_buffer = format!( "User has requested snapshots related to the following file/s be pruned:\n\n{}\n\ httm will destroy the following snapshot/s:\n\n{}\n\ Before httm destroys these snapshot/s, it would like your consent. Continue? (YES/NO)\n\ @@ -122,7 +93,7 @@ impl InteractivePrune { loop { let view_mode = ViewMode::Prune; - let selection = view_mode.select(&preview_buffer, MultiSelect::Off)?; + let selection = view_mode.view_buffer(&prune_buffer, MultiSelect::Off)?; let user_consent = selection .get(0) diff --git a/src/interactive/restore.rs b/src/interactive/restore.rs index 63179529..5b5af3f8 100644 --- a/src/interactive/restore.rs +++ b/src/interactive/restore.rs @@ -16,45 +16,38 @@ // that was distributed with this source code. use crate::config::generate::{ExecMode, InteractiveMode, RestoreMode, RestoreSnapGuard}; -use crate::data::paths::PathData; +use crate::data::paths::{PathData, PathDeconstruction, ZfsSnapPathGuard}; use crate::interactive::select::InteractiveSelect; -use crate::interactive::view_mode::MultiSelect; -use crate::interactive::view_mode::ViewMode; +use crate::interactive::view_mode::{MultiSelect, ViewMode}; use crate::library::file_ops::Copy; use crate::library::results::{HttmError, HttmResult}; -use crate::library::snap_guard::SnapGuard; use crate::library::utility::{date_string, DateFormat}; +use crate::zfs::snap_guard::SnapGuard; use crate::GLOBAL_CONFIG; - use nu_ansi_term::Color::LightYellow; -use terminal_size::Height; -use terminal_size::Width; - use std::path::{Path, PathBuf}; +use terminal_size::{Height, Width}; pub struct InteractiveRestore { - select_result: InteractiveSelect, + pub _view_mode: ViewMode, + pub snap_path_strings: Vec, + pub opt_live_version: Option, } impl From for InteractiveRestore { - fn from(value: InteractiveSelect) -> Self { - Self { - select_result: value, - } + fn from(interactive_select: InteractiveSelect) -> Self { + unsafe { std::mem::transmute(interactive_select) } } } impl InteractiveRestore { - pub fn exec(&self) -> HttmResult<()> { - self.select_result - .snap_path_strings + pub fn restore(&self) -> HttmResult<()> { + self.snap_path_strings .iter() - .try_for_each(|snap_path_string| self.restore(snap_path_string))?; - - std::process::exit(0) + .try_for_each(|snap_path_string| self.restore_per_path(snap_path_string)) } - fn restore(&self, snap_path_string: &str) -> HttmResult<()> { + fn restore_per_path(&self, snap_path_string: &str) -> HttmResult<()> { // build pathdata from selection buffer parsed string // // request is also sanity check for snap path exists below when we check @@ -67,7 +60,7 @@ impl InteractiveRestore { let should_preserve = Self::should_preserve_attributes(); // tell the user what we're up to, and get consent - let preview_buffer = format!( + let restore_buffer = format!( "httm will perform a copy from snapshot:\n\n\ \tsource:\t{:?}\n\ \ttarget:\t{new_file_path_buf:?}\n\n\ @@ -75,14 +68,14 @@ impl InteractiveRestore { ─────────────────────────────────────────────────────────────────────────────────────────\n\ YES\n\ NO", - snap_pathdata.path_buf + snap_pathdata.path() ); // loop until user consents or doesn't loop { - let view_mode = &ViewMode::Restore; + let view_mode = ViewMode::Restore; - let selection = view_mode.select(&preview_buffer, MultiSelect::Off)?; + let selection = view_mode.view_buffer(&restore_buffer, MultiSelect::Off)?; let user_consent = selection .get(0) @@ -90,43 +83,45 @@ impl InteractiveRestore { match user_consent.to_ascii_uppercase().as_ref() { "YES" | "Y" => { - if matches!( - GLOBAL_CONFIG.exec_mode, - ExecMode::Interactive(InteractiveMode::Restore(RestoreMode::Overwrite( - RestoreSnapGuard::Guarded - ))) - ) { - let snap_guard: SnapGuard = - SnapGuard::try_from(new_file_path_buf.as_path())?; - - if let Err(err) = Copy::recursive( - &snap_pathdata.path_buf, - &new_file_path_buf, - should_preserve, - ) { - let msg = format!( - "httm restore failed for the following reason: {}.\n\ + match GLOBAL_CONFIG.exec_mode { + ExecMode::Interactive(InteractiveMode::Restore( + RestoreMode::Overwrite(RestoreSnapGuard::Guarded), + )) => { + let snap_guard: SnapGuard = + SnapGuard::try_from(new_file_path_buf.as_path())?; + + if let Err(err) = Copy::recursive( + &snap_pathdata.path(), + &new_file_path_buf, + should_preserve, + ) { + let msg = format!( + "httm restore failed for the following reason: {}.\n\ Attempting roll back to precautionary pre-execution snapshot.", - err - ); + err + ); - eprintln!("{}", msg); + eprintln!("{}", msg); - snap_guard - .rollback() - .map(|_| println!("Rollback succeeded."))?; + snap_guard + .rollback() + .map(|_| println!("Rollback succeeded."))?; - std::process::exit(1); + std::process::exit(1); + } } - } else { - if let Err(err) = Copy::recursive( - &snap_pathdata.path_buf, - &new_file_path_buf, - should_preserve, - ) { - let msg = - format!("httm restore failed for the following reason: {}.", err); - return Err(HttmError::new(&msg).into()); + _ => { + if let Err(err) = Copy::recursive( + &snap_pathdata.path(), + &new_file_path_buf, + should_preserve, + ) { + let msg = format!( + "httm restore failed for the following reason: {}.", + err + ); + return Err(HttmError::new(&msg).into()); + } } } @@ -135,7 +130,7 @@ impl InteractiveRestore { \tsource:\t{:?}\n\ \ttarget:\t{new_file_path_buf:?}\n\n\ Restore completed successfully.", - snap_pathdata.path_buf + snap_pathdata.path() ); let summary_string = LightYellow.paint(Self::summary_string()); @@ -143,7 +138,7 @@ impl InteractiveRestore { break println!("{summary_string}{result_buffer}"); } "NO" | "N" => { - break println!("User declined restore of: {:?}", snap_pathdata.path_buf) + break println!("User declined restore of: {:?}", snap_pathdata.path()) } // if not yes or no, then noop and continue to the next iter of loop _ => {} @@ -171,6 +166,16 @@ impl InteractiveRestore { ) } + pub fn opt_live_version(&self, snap_pathdata: &PathData) -> HttmResult { + match &self.opt_live_version { + Some(live_version) => Some(PathBuf::from(live_version)), + None => { + ZfsSnapPathGuard::new(snap_pathdata).and_then(|snap_guard| snap_guard.live_path()) + } + } + .ok_or_else(|| HttmError::new("Could not determine a possible live version.").into()) + } + fn build_new_file_path(&self, snap_pathdata: &PathData) -> HttmResult { // build new place to send file if matches!( @@ -182,20 +187,20 @@ impl InteractiveRestore { // so, if you were in /etc and wanted to restore /etc/samba/smb.conf, httm will make certain to overwrite // at /etc/samba/smb.conf - return self.select_result.opt_live_version(snap_pathdata); + return self.opt_live_version(snap_pathdata); } let snap_filename = snap_pathdata - .path_buf + .path() .file_name() .expect("Could not obtain a file name for the snap file version of path given") .to_string_lossy() .into_owned(); - let Some(snap_metadata) = snap_pathdata.metadata else { + let Some(snap_metadata) = snap_pathdata.opt_metadata() else { let msg = format!( "Source location: {:?} does not exist on disk Quitting.", - snap_pathdata.path_buf + snap_pathdata.path() ); return Err(HttmError::new(&msg).into()); }; @@ -208,7 +213,7 @@ impl InteractiveRestore { + ".httm_restored." + &date_string( GLOBAL_CONFIG.requested_utc_offset, - &snap_metadata.modify_time, + &snap_metadata.mtime(), DateFormat::Timestamp, ); let new_file_dir = GLOBAL_CONFIG.pwd.as_path(); @@ -217,7 +222,7 @@ impl InteractiveRestore { // don't let the user rewrite one restore over another in non-overwrite mode if new_file_path_buf.exists() { Err( - HttmError::new("httm will not restore to that file, as a file with the same path name already exists. Quitting.").into(), + HttmError::new("httm will not restore to that file location, as a file with the same path name already exists. Quitting.").into(), ) } else { Ok(new_file_path_buf) diff --git a/src/interactive/select.rs b/src/interactive/select.rs index 335a936e..a79064ff 100644 --- a/src/interactive/select.rs +++ b/src/interactive/select.rs @@ -15,61 +15,38 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::{InteractiveMode, PrintMode, SelectMode}; -use crate::data::paths::PathDeconstruction; -use crate::data::paths::{PathData, ZfsSnapPathGuard}; -use crate::display_versions::wrapper::VersionsDisplayWrapper; -use crate::interactive::browse::InteractiveBrowse; +use super::browse::InteractiveBrowse; +use crate::config::generate::{PrintMode, SelectMode}; +use crate::display::wrapper::DisplayWrapper; use crate::interactive::preview::PreviewSelection; -use crate::interactive::restore::InteractiveRestore; -use crate::interactive::view_mode::MultiSelect; -use crate::interactive::view_mode::ViewMode; +use crate::interactive::view_mode::{MultiSelect, ViewMode}; use crate::library::results::{HttmError, HttmResult}; use crate::library::utility::{delimiter, print_output_buf}; use crate::lookup::versions::VersionsMap; use crate::{Config, GLOBAL_CONFIG}; - use std::io::Read; use std::path::{Path, PathBuf}; use std::process::Command as ExecProcess; +#[allow(dead_code)] pub struct InteractiveSelect { + pub view_mode: ViewMode, pub snap_path_strings: Vec, pub opt_live_version: Option, } -impl InteractiveSelect { - pub fn exec( - browse_result: InteractiveBrowse, - interactive_mode: &InteractiveMode, - ) -> HttmResult<()> { - // continue to interactive_restore or print and exit here? - let select_result = Self::new(browse_result)?; - - match interactive_mode { - // one only allow one to select one path string during select - // but we retain paths_selected_in_browse because we may need - // it later during restore if opt_overwrite is selected - InteractiveMode::Restore(_) => { - let interactive_restore = InteractiveRestore::from(select_result); - interactive_restore.exec()?; - } - InteractiveMode::Select(select_mode) => select_result.print_selections(select_mode)?, - InteractiveMode::Browse => unreachable!(), - } - - std::process::exit(0); - } +impl TryFrom<&mut InteractiveBrowse> for InteractiveSelect { + type Error = Box; - fn new(browse_result: InteractiveBrowse) -> HttmResult { - let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &browse_result.selected_pathdata)?; + fn try_from(interactive_browse: &mut InteractiveBrowse) -> HttmResult { + let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &interactive_browse.selected_pathdata)?; // snap and live set has no snaps if versions_map.is_empty() { - let paths: Vec = browse_result + let paths: Vec = interactive_browse .selected_pathdata .iter() - .map(|path| path.path_buf.to_string_lossy().to_string()) + .map(|path| path.path().to_string_lossy().to_string()) .collect(); let msg = format!( "{}{:?}", @@ -79,30 +56,30 @@ impl InteractiveSelect { return Err(HttmError::new(&msg).into()); } - let opt_live_version: Option = if browse_result.selected_pathdata.len() > 1 { + let opt_live_version: Option = if interactive_browse.selected_pathdata.len() > 1 { None } else { - browse_result + interactive_browse .selected_pathdata .get(0) - .map(|pathdata| pathdata.path_buf.to_string_lossy().into_owned()) + .map(|pathdata| pathdata.path().to_string_lossy().into_owned()) }; + let view_mode = ViewMode::Select(opt_live_version.clone()); + let snap_path_strings = if GLOBAL_CONFIG.opt_last_snap.is_some() { Self::last_snap(&versions_map) } else { // same stuff we do at fn exec, snooze... - let display_config = Config::from(browse_result.selected_pathdata.clone()); + let display_config = Config::from(interactive_browse.selected_pathdata.clone()); - let display_map = VersionsDisplayWrapper::from(&display_config, versions_map); + let display_map = DisplayWrapper::from(&display_config, versions_map); let selection_buffer = display_map.to_string(); - let view_mode = ViewMode::Select(opt_live_version.clone()); - display_map.map.iter().try_for_each(|(live, snaps)| { if snaps.is_empty() { - let msg = format!("WARN: Path {:?} has no snapshots available.", live.path_buf); + let msg = format!("Path {:?} has no snapshots available.", live.path()); return Err(HttmError::new(&msg)); } @@ -112,7 +89,11 @@ impl InteractiveSelect { // loop until user selects a valid snapshot version loop { // get the file name - let selected_line = view_mode.select(&selection_buffer, MultiSelect::On)?; + let selected_line = view_mode.view_buffer(&selection_buffer, MultiSelect::On)?; + + if let Some(background_handle) = interactive_browse.opt_background_handle.take() { + let _ = background_handle.join(); + } let requested_file_names = selected_line .iter() @@ -127,7 +108,7 @@ impl InteractiveSelect { // and cannot select a 'live' version or other invalid value. display_map .keys() - .all(|key| key.path_buf.as_path() != Path::new(selection_buffer)) + .all(|key| key.path() != Path::new(selection_buffer)) }) .map(|selection_buffer| selection_buffer.to_string()) .collect::>(); @@ -140,23 +121,22 @@ impl InteractiveSelect { } }; - if let Some(handle) = browse_result.opt_background_handle { - let _ = handle.join(); - } - Ok(Self { + view_mode, snap_path_strings, opt_live_version, }) } +} +impl InteractiveSelect { fn last_snap(map: &VersionsMap) -> Vec { map.iter() .filter_map(|(key, values)| { if values.is_empty() { eprintln!( "WARN: No last snap of {:?} is available for selection. Perhaps you omitted identical files.", - key.path_buf + key.path() ); None } else { @@ -164,17 +144,15 @@ impl InteractiveSelect { } }) .flatten() - .map(|pathdata| pathdata.path_buf.to_string_lossy().to_string()) + .map(|pathdata| pathdata.path().to_string_lossy().to_string()) .collect() } - fn print_selections(&self, select_mode: &SelectMode) -> HttmResult<()> { + pub fn print_selections(&self, select_mode: &SelectMode) -> HttmResult<()> { self.snap_path_strings .iter() .map(Path::new) - .try_for_each(|snap_path| self.print_snap_path(snap_path, select_mode))?; - - Ok(()) + .try_for_each(|snap_path| self.print_snap_path(snap_path, select_mode)) } fn print_snap_path(&self, snap_path: &Path, select_mode: &SelectMode) -> HttmResult<()> { @@ -182,24 +160,22 @@ impl InteractiveSelect { SelectMode::Path => { let delimiter = delimiter(); let output_buf = match GLOBAL_CONFIG.print_mode { - PrintMode::RawNewline | PrintMode::RawZero => { + PrintMode::Raw(_) => { format!("{}{delimiter}", snap_path.to_string_lossy()) } - PrintMode::FormattedDefault | PrintMode::FormattedNotPretty => { + PrintMode::Formatted(_) => { format!("\"{}\"{delimiter}", snap_path.to_string_lossy()) } }; - print_output_buf(&output_buf)?; - - Ok(()) + print_output_buf(&output_buf) } SelectMode::Contents => { if !snap_path.is_file() { let msg = format!("Path is not a file: {:?}", snap_path); return Err(HttmError::new(&msg).into()); } - let mut f = std::fs::File::open(snap_path)?; + let mut f = std::fs::OpenOptions::new().read(true).open(snap_path)?; let mut contents = Vec::new(); f.read_to_end(&mut contents)?; @@ -207,12 +183,10 @@ impl InteractiveSelect { // This is the same as simply `cat`-ing the file. let output_buf = unsafe { std::str::from_utf8_unchecked(&contents) }; - print_output_buf(output_buf)?; - - Ok(()) + print_output_buf(output_buf) } SelectMode::Preview => { - let view_mode = ViewMode::Select(self.opt_live_version.clone()); + let view_mode = &self.view_mode; let preview_selection = PreviewSelection::new(&view_mode)?; @@ -260,14 +234,4 @@ impl InteractiveSelect { } } } - - pub fn opt_live_version(&self, snap_pathdata: &PathData) -> HttmResult { - match &self.opt_live_version { - Some(live_version) => Some(PathBuf::from(live_version)), - None => { - ZfsSnapPathGuard::new(snap_pathdata).and_then(|snap_guard| snap_guard.live_path()) - } - } - .ok_or_else(|| HttmError::new("Could not determine a possible live version.").into()) - } } diff --git a/src/interactive/view_mode.rs b/src/interactive/view_mode.rs index 60effa13..4303ae4d 100644 --- a/src/interactive/view_mode.rs +++ b/src/interactive/view_mode.rs @@ -15,18 +15,11 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::background::recursive::RecursiveSearch; -use crate::data::paths::PathData; -use crate::interactive::browse::InteractiveBrowse; use crate::interactive::preview::PreviewSelection; -use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::Never; -use crate::GLOBAL_CONFIG; -use crossbeam_channel::unbounded; +use crate::library::results::HttmError; +use crate::{HttmResult, GLOBAL_CONFIG}; use skim::prelude::*; use std::io::Cursor; -use std::path::Path; -use std::thread; pub enum ViewMode { Browse, @@ -41,7 +34,7 @@ pub enum MultiSelect { } impl ViewMode { - fn print_header(&self) -> String { + pub fn print_header(&self) -> String { format!( "PREVIEW UP: shift+up | PREVIEW DOWN: shift+down | {}\n\ PAGE UP: page up | PAGE DOWN: page down \n\ @@ -60,92 +53,8 @@ impl ViewMode { } } - pub fn browse(&self, requested_dir: &Path) -> HttmResult { - // prep thread spawn - let requested_dir_clone = requested_dir.to_path_buf(); - let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); - let (hangup_tx, hangup_rx): (Sender, Receiver) = bounded(0); - - // thread spawn fn enumerate_directory - permits recursion into dirs without blocking - let background_handle = thread::spawn(move || { - // no way to propagate error from closure so exit and explain error here - RecursiveSearch::exec(&requested_dir_clone, tx_item.clone(), hangup_rx.clone()); - }); - - let header: String = self.print_header(); - - let display_handle = thread::spawn(move || { - #[cfg(feature = "setpriority")] - #[cfg(target_os = "linux")] - #[cfg(target_env = "gnu")] - { - use crate::library::utility::ThreadPriorityType; - let tid = std::process::id(); - let _ = ThreadPriorityType::Process.nice_thread(Some(tid), -3i32); - } - - let opt_multi = GLOBAL_CONFIG.opt_preview.is_none(); - - // create the skim component for previews - let skim_opts = SkimOptionsBuilder::default() - .preview_window(Some("up:50%")) - .preview(Some("")) - .nosort(true) - .exact(GLOBAL_CONFIG.opt_exact) - .header(Some(&header)) - .multi(opt_multi) - .regex(false) - .build() - .expect("Could not initialized skim options for browse_view"); - - // run_with() reads and shows items from the thread stream created above - let res = match skim::Skim::run_with(&skim_opts, Some(rx_item)) { - Some(output) if output.is_abort => { - eprintln!("httm interactive file browse session was aborted. Quitting."); - std::process::exit(0) - } - Some(output) => { - // hangup the channel so the background recursive search can gracefully cleanup and exit - drop(hangup_tx); - - output - .selected_items - .iter() - .map(|i| PathData::from(Path::new(&i.output().to_string()))) - .collect() - } - None => { - return Err(HttmError::new( - "httm interactive file browse session failed.", - )); - } - }; - - #[cfg(feature = "malloc_trim")] - #[cfg(target_os = "linux")] - #[cfg(target_env = "gnu")] - { - use crate::library::utility::malloc_trim; - malloc_trim(); - } - - Ok(res) - }); - - match display_handle.join() { - Ok(selected_pathdata) => { - let res = InteractiveBrowse { - selected_pathdata: selected_pathdata?, - opt_background_handle: Some(background_handle), - }; - Ok(res) - } - Err(_) => Err(HttmError::new("Interactive browse thread panicked.").into()), - } - } - - pub fn select(&self, preview_buffer: &str, opt_multi: MultiSelect) -> HttmResult> { - let preview_selection = PreviewSelection::new(self)?; + pub fn view_buffer(&self, buffer: &str, opt_multi: MultiSelect) -> HttmResult> { + let preview_selection = PreviewSelection::new(&self)?; let header = self.print_header(); @@ -165,7 +74,7 @@ impl ViewMode { .exact(true) .multi(opt_multi) .regex(false) - .tiebreak(Some("length,index".to_string())) + .tiebreak(Some("score,index".to_string())) .header(Some(&header)) .build() .expect("Could not initialized skim options for select_restore_view"); @@ -174,7 +83,7 @@ impl ViewMode { let item_reader = SkimItemReader::new(item_reader_opts); let (items, opt_ingest_handle) = - item_reader.of_bufread(Box::new(Cursor::new(preview_buffer.trim().to_owned()))); + item_reader.of_bufread(Box::new(Cursor::new(buffer.trim().to_owned()))); // run_with() reads and shows items from the thread stream created above let res = match skim::Skim::run_with(&skim_opts, Some(items)) { diff --git a/src/library/diff_copy.rs b/src/library/diff_copy.rs index bc082ced..b8ed5e43 100644 --- a/src/library/diff_copy.rs +++ b/src/library/diff_copy.rs @@ -43,24 +43,21 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -use crate::config::generate::ListSnapsOfType; -use crate::data::paths::{CompareVersionsContainer, PathData}; -use crate::library::results::HttmError; -use crate::library::results::HttmResult; -use crate::GLOBAL_CONFIG; -use once_cell::sync::Lazy; +use crate::data::paths::PathData; +use crate::library::results::{HttmError, HttmResult}; +use crate::zfs::run_command::RunZFSCommand; +use crate::{ExecMode, GLOBAL_CONFIG, IN_BUFFER_SIZE}; use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Seek, SeekFrom, Write}; use std::os::fd::{AsFd, BorrowedFd}; use std::path::Path; use std::process::Command as ExecProcess; use std::sync::atomic::AtomicBool; +use std::sync::LazyLock; -const CHUNK_SIZE: usize = 65_536; - -static IS_CLONE_COMPATIBLE: Lazy = Lazy::new(|| { - if let Ok(zfs_command) = which::which("zfs") { - let Ok(process_output) = ExecProcess::new(zfs_command).arg("-V").output() else { +static IS_CLONE_COMPATIBLE: LazyLock = LazyLock::new(|| { + if let Ok(run_zfs) = RunZFSCommand::new() { + let Ok(process_output) = ExecProcess::new(&run_zfs.zfs_command).arg("-V").output() else { return AtomicBool::new(false); }; @@ -81,6 +78,10 @@ static IS_CLONE_COMPATIBLE: Lazy = Lazy::new(|| { { return AtomicBool::new(false); } + + if let ExecMode::RollForward(_) = GLOBAL_CONFIG.exec_mode { + return AtomicBool::new(false); + } } AtomicBool::new(true) @@ -106,85 +107,82 @@ pub struct HttmCopy; impl HttmCopy { pub fn new(src: &Path, dst: &Path) -> HttmResult<()> { // create source file reader - let src_file = File::open(src)?; + let src_file = std::fs::OpenOptions::new().read(true).open(src)?; let src_len = src_file.metadata()?.len(); - let dst_file = OpenOptions::new() + let mut dst_file = OpenOptions::new() .write(true) .read(true) .create(true) .open(dst)?; dst_file.set_len(src_len)?; - let amt_written = DiffCopy::new(&src_file, &dst_file)?; - - if amt_written != src_len as usize { - let msg = format!( - "Amount written (\"{}\") != Source length (\"{}\"). Quitting.", - amt_written, src_len - ); - return Err(HttmError::new(&msg).into()); - } - - if GLOBAL_CONFIG.opt_debug { - DiffCopy::confirm(src, dst)? + match DiffCopy::new(&src_file, &mut dst_file) { + Ok(_) if GLOBAL_CONFIG.opt_debug => { + eprintln!("DEBUG: Write to file completed. Confirmation initiated."); + DiffCopy::confirm(src, dst) + } + Ok(_) => Ok(()), + Err(err) => Err(err), } - - Ok(()) } } struct DiffCopy; impl DiffCopy { - fn new(src_file: &File, dst_file: &File) -> HttmResult { + fn new(src_file: &File, dst_file: &mut File) -> HttmResult<()> { + let src_len = src_file.metadata()?.len(); + if !GLOBAL_CONFIG.opt_no_clones && IS_CLONE_COMPATIBLE.load(std::sync::atomic::Ordering::Relaxed) { let src_fd = src_file.as_fd(); let dst_fd = dst_file.as_fd(); - let src_len = src_file.metadata()?.len(); match Self::copy_file_range(src_fd, dst_fd, src_len as usize) { - Ok(amt_written) if amt_written as u64 == src_len => { + Ok(_) => { if GLOBAL_CONFIG.opt_debug { eprintln!("DEBUG: copy_file_range call successful."); } - return Ok(amt_written); + return Ok(()); } - _ => { + Err(err) => { IS_CLONE_COMPATIBLE.store(false, std::sync::atomic::Ordering::Relaxed); if GLOBAL_CONFIG.opt_debug { eprintln!( - "DEBUG: copy_file_range call unsuccessful. \ - IS_CLONE_COMPATIBLE variable has been modified to: \"{:?}\".", - IS_CLONE_COMPATIBLE.load(std::sync::atomic::Ordering::Relaxed) + "DEBUG: copy_file_range call unsuccessful for the following reason: \"{:?}\".\n + DEBUG: Retrying a conventional diff copy.", + err ); } } } } - Self::write_no_cow(&src_file, &dst_file) + Self::write_no_cow(&src_file, &dst_file)?; + + // re docs, both a flush and a sync seem to be required re consistency + dst_file.flush()?; + dst_file.sync_data()?; + + Ok(()) } #[inline] - fn write_no_cow(src_file: &File, dst_file: &File) -> HttmResult { + fn write_no_cow(src_file: &File, dst_file: &File) -> HttmResult<()> { // create destination file writer and maybe reader // only include dst file reader if the dst file exists // otherwise we just write to that location - let mut src_reader = BufReader::with_capacity(CHUNK_SIZE, src_file); - let mut dst_reader = BufReader::with_capacity(CHUNK_SIZE, dst_file); - let mut dst_writer = BufWriter::with_capacity(CHUNK_SIZE, dst_file); + let mut src_reader = BufReader::with_capacity(IN_BUFFER_SIZE, src_file); + let mut dst_reader = BufReader::with_capacity(IN_BUFFER_SIZE, dst_file); + let mut dst_writer = BufWriter::with_capacity(IN_BUFFER_SIZE, dst_file); let dst_exists = DstFileState::exists(dst_file); // cur pos - byte offset in file, let mut cur_pos = 0u64; - // return value - let mut bytes_processed = 0usize; - loop { match src_reader.fill_buf() { Ok(src_read) => { @@ -196,25 +194,15 @@ impl DiffCopy { } match dst_exists { - DstFileState::DoesNotExist => Self::write_to_offset( - &mut dst_writer, - src_read, - cur_pos, - &mut bytes_processed, - )?, + DstFileState::DoesNotExist => { + Self::write_to_offset(&mut dst_writer, src_read, cur_pos)?; + } DstFileState::Exists => { // read same amt from dst file, if it exists, to compare match dst_reader.fill_buf() { Ok(dst_read) => { - if Self::is_same_bytes(src_read, dst_read) { - bytes_processed += src_amt_read; - } else { - Self::write_to_offset( - &mut dst_writer, - src_read, - cur_pos, - &mut bytes_processed, - )? + if !Self::is_same_bytes(src_read, dst_read) { + Self::write_to_offset(&mut dst_writer, src_read, cur_pos)? } let dst_amt_read = dst_read.len(); @@ -245,10 +233,7 @@ impl DiffCopy { }; } - // re docs, both a flush and a sync seem to be required re consistency - dst_file.sync_data()?; - - Ok(bytes_processed) + Ok(()) } #[inline] @@ -261,9 +246,11 @@ impl DiffCopy { #[inline] fn hash(bytes: &[u8]) -> u64 { - use std::hash::Hasher; + use foldhash::fast::FixedState; + use std::hash::{BuildHasher, Hasher}; - let mut hash = ahash::AHasher::default(); + let s = LazyLock::new(|| FixedState::default()); + let mut hash = s.build_hasher(); hash.write(bytes); hash.finish() @@ -273,17 +260,10 @@ impl DiffCopy { dst_writer: &mut BufWriter<&File>, src_read: &[u8], cur_pos: u64, - bytes_processed: &mut usize, ) -> HttmResult<()> { // seek to current byte offset in dst writer - let seek_pos = dst_writer.seek(SeekFrom::Start(cur_pos))?; - - if seek_pos != cur_pos { - let msg = format!("Could not seek to offset in destination file: {}", cur_pos); - return Err(HttmError::new(&msg).into()); - } - - *bytes_processed += dst_writer.write(src_read)?; + dst_writer.seek(SeekFrom::Start(cur_pos))?; + dst_writer.write_all(src_read)?; Ok(()) } @@ -293,11 +273,41 @@ impl DiffCopy { src_file_fd: BorrowedFd, dst_file_fd: BorrowedFd, len: usize, - ) -> HttmResult { + ) -> HttmResult<()> { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { - match nix::fcntl::copy_file_range(src_file_fd, None, dst_file_fd, None, len) { - Ok(bytes_written) => return Ok(bytes_written), + let mut amt_written = 0u64; + + // copy_file_range needs to be run in a loop as it is interruptible + match nix::fcntl::copy_file_range( + src_file_fd, + Some(&mut (amt_written as i64)), + dst_file_fd, + Some(&mut (amt_written as i64)), + len, + ) { + // However, a return of zero for a non-zero len argument + // indicates that the offset for infd is at or beyond EOF. + Ok(bytes_written) if bytes_written == 0usize && len != 0usize => { + return Err(HttmError::new("Amount written == 0 for a file len > 0. This may indicate that the offset for source file is at or beyond EOF.").into()); + } + Ok(bytes_written) => { + amt_written += bytes_written as u64; + + if amt_written == len as u64 { + return Ok(()); + } + + if amt_written < len as u64 { + // Upon successful completion, copy_file_range() will return the number of bytes copied + // between files. This could be less than the length originally requested. + return Ok(()); + } + + if amt_written > len as u64 { + return Err(HttmError::new("Amount written larger than file len.").into()); + } + } Err(err) => match err { nix::errno::Errno::ENOSYS => { return Err(HttmError::new( @@ -317,12 +327,10 @@ impl DiffCopy { } fn confirm(src: &Path, dst: &Path) -> HttmResult<()> { - let src_test = - CompareVersionsContainer::new(PathData::from(src), &ListSnapsOfType::UniqueContents); - let dst_test = - CompareVersionsContainer::new(PathData::from(dst), &ListSnapsOfType::UniqueContents); + let src_test = PathData::from(src); + let dst_test = PathData::from(dst); - if src_test.is_same_file(&dst_test) { + if src_test.is_same_file_contents(&dst_test) { eprintln!( "DEBUG: Copy successful. File contents of {} and {} are the same.", src.display(), diff --git a/src/library/file_ops.rs b/src/library/file_ops.rs index 0919dc2f..77e1c7c9 100644 --- a/src/library/file_ops.rs +++ b/src/library/file_ops.rs @@ -15,20 +15,20 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::data::paths::PathData; -use crate::data::paths::PathDeconstruction; +use crate::data::paths::{PathData, PathDeconstruction}; use crate::library::diff_copy::HttmCopy; use crate::library::results::{HttmError, HttmResult}; +use crate::{GLOBAL_CONFIG, IN_BUFFER_SIZE}; use nix::sys::stat::SFlag; use nu_ansi_term::Color::{Blue, Red}; -use std::fs::{self, File, FileTimes}; -use std::os::unix::fs::chown; -use std::os::unix::fs::FileTypeExt; -use std::os::unix::fs::MetadataExt; - use std::fs::{create_dir_all, read_dir, set_permissions}; use std::iter::Iterator; +use std::os::unix::fs::{chown, FileTypeExt, MetadataExt}; use std::path::Path; +use std::sync::LazyLock; + +const CHAR_KIND: SFlag = nix::sys::stat::SFlag::S_IFCHR; +const BLK_KIND: SFlag = nix::sys::stat::SFlag::S_IFBLK; pub struct Copy; @@ -79,9 +79,6 @@ impl Copy { } fn special_file(src: &Path, dst: &Path) -> HttmResult<()> { - const CHAR_KIND: SFlag = SFlag::from_bits_truncate(libc::S_IFCHR); - const BLK_KIND: SFlag = SFlag::from_bits_truncate(libc::S_IFBLK); - let src_metadata = src.metadata()?; let src_file_type = src_metadata.file_type(); let src_mode_bits = src_metadata.mode(); @@ -149,7 +146,20 @@ impl Copy { } if should_preserve { - Preserve::recursive(src, dst)? + // macos likes to fail on the metadata copy + match Preserve::recursive(src, dst) { + Ok(_) => {} + Err(err) => { + if is_metadata_same(src, dst).is_ok() { + if GLOBAL_CONFIG.opt_debug { + eprintln!("WARN: The OS reports an error that it was unable to copy file metadata for the following reason: {}", err.to_string().trim_end()); + eprintln!("NOTICE: This is most likely because such feature is unsupported by this OS. httm confirms basic file metadata (size and mtime) are the same for transfer: {:?} -> {:?}.", src, dst) + } + } else { + return Err(err); + } + } + } } Ok(()) @@ -161,6 +171,11 @@ pub struct Preserve; impl Preserve { pub fn direct(src: &Path, dst: &Path) -> HttmResult<()> { let src_metadata = src.symlink_metadata()?; + let dst_file = std::fs::File::options() + .create(false) + .read(true) + .write(false) + .open(&dst)?; // Mode { @@ -198,14 +213,15 @@ impl Preserve { // Timestamps { - let src = fs::metadata(src)?; - let dst_file = File::options().write(true).open(dst)?; - let times = FileTimes::new() - .set_accessed(src.accessed()?) - .set_modified(src.modified()?); - dst_file.set_times(times)?; + let src_times = std::fs::FileTimes::new() + .set_accessed(src_metadata.accessed()?) + .set_modified(src_metadata.modified()?); + + dst_file.set_times(src_times)?; } + dst_file.sync_all()?; + Ok(()) } @@ -269,3 +285,67 @@ impl Remove { Ok(()) } } + +use super::utility::is_metadata_same; +use std::hash::{Hash, Hasher}; +use std::io::{BufRead, BufReader, ErrorKind}; + +pub struct HashFileContents<'a> { + inner: &'a Path, +} + +impl<'a> HashFileContents<'a> { + pub fn path_to_hash(path: &Path) -> u64 { + use foldhash::quality::FixedState; + use std::hash::{BuildHasher, Hasher}; + + let s = LazyLock::new(|| FixedState::default()); + let mut hash = s.build_hasher(); + + HashFileContents::from(path).hash(&mut hash); + + hash.finish() + } +} + +impl<'a> From<&'a Path> for HashFileContents<'a> { + fn from(path: &'a Path) -> Self { + Self { inner: path } + } +} + +impl<'a> Hash for HashFileContents<'a> { + fn hash(&self, state: &mut H) { + let Some(self_file) = std::fs::OpenOptions::new() + .read(true) + .open(&self.inner) + .ok() + else { + return; + }; + + let mut reader = BufReader::with_capacity(IN_BUFFER_SIZE, self_file); + + loop { + let consumed = match reader.fill_buf() { + Ok(buf) => { + if buf.is_empty() { + return; + } + + state.write(buf); + buf.len() + } + Err(err) => match err.kind() { + ErrorKind::Interrupted => continue, + ErrorKind::UnexpectedEof => { + return; + } + _ => return, + }, + }; + + reader.consume(consumed); + } + } +} diff --git a/src/library/iter_extensions.rs b/src/library/iter_extensions.rs index f1d87b8c..c5671186 100644 --- a/src/library/iter_extensions.rs +++ b/src/library/iter_extensions.rs @@ -56,6 +56,7 @@ use std::hash::Hash; use std::iter::Iterator; pub trait HttmIter: Iterator { + #[allow(dead_code)] fn into_group_map(self) -> HashMap> where Self: Iterator + Sized, @@ -93,7 +94,9 @@ pub mod group_map { vec_val.push(val); } None => { - lookup.insert_unique_unchecked(key, [val].into()); + unsafe { + lookup.insert_unique_unchecked(key, [val].into()); + }; } }); diff --git a/src/library/results.rs b/src/library/results.rs index 9bf579d0..ac9aafdd 100644 --- a/src/library/results.rs +++ b/src/library/results.rs @@ -28,6 +28,14 @@ pub struct HttmError { pub details: String, } +impl From> for HttmError { + fn from(value: Box) -> Self { + Self { + details: value.to_string(), + } + } +} + impl HttmError { pub fn new(msg: &str) -> Self { HttmError { diff --git a/src/library/utility.rs b/src/library/utility.rs index a0297548..1ab39d73 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -15,66 +15,38 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::PrintMode; -use crate::data::paths::{BasicDirEntryInfo, PathData, PathMetadata, PHANTOM_DATE}; +use crate::config::generate::{PrintMode, RawMode}; +use crate::data::paths::{BasicDirEntryInfo, PathData, PathMetadata}; use crate::data::selection::SelectionCandidate; use crate::library::results::{HttmError, HttmResult}; - -use crate::parse::mounts::FilesystemType; -use crate::{BTRFS_SNAPPER_HIDDEN_DIRECTORY, GLOBAL_CONFIG, ZFS_SNAPSHOT_DIRECTORY}; -use crossbeam_channel::{Receiver, TryRecvError}; +use crate::GLOBAL_CONFIG; use lscolors::{Colorable, LsColors, Style}; use nu_ansi_term::Style as AnsiTermStyle; use number_prefix::NumberPrefix; -use once_cell::sync::Lazy; use std::borrow::Cow; use std::fs::FileType; use std::io::Write; use std::iter::Iterator; -use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; use std::time::SystemTime; use time::{format_description, OffsetDateTime, UtcOffset}; +use which::which; -#[cfg(feature = "setpriority")] -#[cfg(target_os = "linux")] -#[cfg(target_env = "gnu")] -#[allow(dead_code)] -pub enum ThreadPriorityType { - Process = 0, - PGroup = 1, - User = 2, -} - -#[cfg(feature = "setpriority")] -#[cfg(target_os = "linux")] -#[cfg(target_env = "gnu")] -impl ThreadPriorityType { - // nice calling thread to a specified level - pub fn nice_thread(self, opt_tid: Option, priority_level: i32) -> HttmResult<()> { - let tid = opt_tid.unwrap_or_else(|| std::process::id()); - - // #[cfg(any(target_os = "macos", target_os = "freebsd"))] - // unsafe { - // let _ = libc::setpriority(priority_type as i32, tid, priority_level); - // }; - - unsafe { - let priority_type = self as u32; - let _ = libc::setpriority(priority_type, tid, priority_level); - } - - Ok(()) - } +pub fn get_mount_command() -> HttmResult { + which("mount").map_err(|_err| { + HttmError::new( + "'mount' command not be found. Make sure the command 'mount' is in your path.", + ) + .into() + }) } -#[cfg(feature = "malloc_trim")] -#[cfg(target_os = "linux")] -#[cfg(target_env = "gnu")] -pub fn malloc_trim() { - unsafe { - let _ = libc::malloc_trim(0); - } +pub fn get_btrfs_command() -> HttmResult { + which("btrfs").map_err(|_err| { + HttmError::new("'btrfs' command not found. Make sure the command 'btrfs' is in your path.") + .into() + }) } pub fn user_has_effective_root(msg: &str) -> HttmResult<()> { @@ -87,22 +59,22 @@ pub fn user_has_effective_root(msg: &str) -> HttmResult<()> { } pub fn delimiter() -> char { - if matches!(GLOBAL_CONFIG.print_mode, PrintMode::RawZero) { - '\0' - } else { - '\n' + if let PrintMode::Raw(RawMode::Zero) = GLOBAL_CONFIG.print_mode { + return '\0'; } + + '\n' } -pub enum Never {} +// pub enum Never {} -pub fn is_channel_closed(chan: &Receiver) -> bool { - match chan.try_recv() { - Ok(never) => match never {}, - Err(TryRecvError::Disconnected) => true, - Err(TryRecvError::Empty) => false, - } -} +// pub fn is_channel_closed(chan: &Receiver) -> bool { +// match chan.try_recv() { +// Ok(never) => match never {}, +// Err(TryRecvError::Disconnected) => true, +// Err(TryRecvError::Empty) => false, +// } +// } const TMP_SUFFIX: &str = ".tmp"; @@ -112,7 +84,7 @@ pub fn make_tmp_path(path: &Path) -> PathBuf { PathBuf::from(res) } -pub fn find_common_path(paths: I) -> Option +pub fn find_common_path(paths: I) -> Option> where I: IntoIterator, P: AsRef, @@ -120,7 +92,9 @@ where let mut path_iter = paths.into_iter(); let initial_value = path_iter.next()?.as_ref().to_path_buf(); - path_iter.try_fold(initial_value, |acc, path| cmp_path(acc, path)) + path_iter + .try_fold(initial_value, |acc, path| cmp_path(acc, path)) + .map(|res| res.into_boxed_path()) } fn cmp_path, B: AsRef>(a: A, b: B) -> Option { @@ -164,7 +138,9 @@ where // canonicalize will read_link/resolve the link for us match path.canonicalize() { Ok(link_target) if !link_target.is_dir() => false, - Ok(link_target) => path.ancestors().all(|ancestor| ancestor != link_target), + Ok(link_target) => { + find_common_path([link_target, path.to_path_buf()].into_iter()).is_none() + } // we get an error? still pass the path on, as we get a good path from the dir entry _ => false, } @@ -198,10 +174,10 @@ impl<'a> HttmIsDir<'a> for PathData { httm_is_dir(self) } fn filetype(&self) -> Result { - Ok(self.path_buf.symlink_metadata()?.file_type()) + Ok(self.path().symlink_metadata()?.file_type()) } fn path(&'a self) -> &'a Path { - &self.path_buf + &self.path() } } @@ -213,20 +189,18 @@ impl<'a> HttmIsDir<'a> for BasicDirEntryInfo { // of course, this is a placeholder error, we just need an error to report back // why not store the error in the struct instead? because it's more complex. it might // make it harder to copy around etc - self.file_type + self.opt_filetype() .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound)) } fn path(&'a self) -> &'a Path { - &self.path + &self.path() } } -static ENV_LS_COLORS: Lazy = Lazy::new(|| LsColors::from_env().unwrap_or_default()); -static PHANTOM_STYLE: Lazy = Lazy::new(|| { - Style::to_nu_ansi_term_style( - &Style::from_ansi_sequence("38;2;250;200;200;1;0").unwrap_or_default(), - ) -}); +static ENV_LS_COLORS: LazyLock = + LazyLock::new(|| LsColors::from_env().unwrap_or_default()); +static PHANTOM_STYLE: LazyLock = + LazyLock::new(|| nu_ansi_term::Style::default().dimmed()); pub fn paint_string(path: T, display_name: &str) -> Cow where @@ -254,10 +228,10 @@ pub trait PaintString { impl PaintString for &PathData { fn ls_style(&self) -> Option<&lscolors::style::Style> { - ENV_LS_COLORS.style_for_path(&self.path_buf) + ENV_LS_COLORS.style_for_path(&self.path()) } fn is_phantom(&self) -> bool { - self.metadata.is_none() + self.opt_metadata().is_none() } } @@ -270,25 +244,6 @@ impl PaintString for &SelectionCandidate { } } -pub fn fs_type_from_hidden_dir(dataset_mount: &Path) -> Option { - // set fstype, known by whether there is a ZFS hidden snapshot dir in the root dir - if dataset_mount - .join(ZFS_SNAPSHOT_DIRECTORY) - .symlink_metadata() - .is_ok() - { - Some(FilesystemType::Zfs) - } else if dataset_mount - .join(BTRFS_SNAPPER_HIDDEN_DIRECTORY) - .symlink_metadata() - .is_ok() - { - Some(FilesystemType::Btrfs) - } else { - None - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum DateFormat { Display, @@ -345,21 +300,17 @@ where T: ComparePathMetadata, { if src.opt_metadata().is_none() { - let msg = format!("WARN: Metadata not found: {:?}", src.path()); + let msg = format!("Metadata not found: {:?}", src.path()); return Err(HttmError::new(&msg).into()); } if src.path().is_symlink() && (src.path().read_link().ok() != dst.path().read_link().ok()) { - let msg = format!("WARN: Symlink do not match: {:?}", src.path()); + let msg = format!("Symlink do not match: {:?}", src.path()); return Err(HttmError::new(&msg).into()); } if src.opt_metadata() != dst.opt_metadata() { - let msg = format!( - "WARN: Metadata mismatch: {:?} !-> {:?}", - src.path(), - dst.path() - ); + let msg = format!("Metadata mismatch: {:?} !-> {:?}", src.path(), dst.path()); return Err(HttmError::new(&msg).into()); } @@ -374,12 +325,10 @@ pub trait ComparePathMetadata { impl> ComparePathMetadata for T { fn opt_metadata(&self) -> Option { // never follow symlinks for comparison - let opt_md = self.as_ref().symlink_metadata().ok(); - - opt_md.map(|md| PathMetadata { - size: md.len(), - modify_time: md.modified().unwrap_or(PHANTOM_DATE), - }) + self.as_ref() + .symlink_metadata() + .ok() + .and_then(|md| PathMetadata::new(&md)) } fn path(&self) -> &Path { @@ -387,22 +336,13 @@ impl> ComparePathMetadata for T { } } -pub fn path_is_filter_dir(path: &Path) -> bool { - GLOBAL_CONFIG - .dataset_collection - .filter_dirs - .deref() - .iter() - .any(|filter_dir| path == filter_dir) -} - pub fn pwd() -> HttmResult { - if let Ok(pwd) = std::env::current_dir() { - Ok(pwd) - } else { - Err(HttmError::new( + let Ok(pwd) = std::env::current_dir() else { + return Err(HttmError::new( "Working directory does not exist or your do not have permissions to access it.", ) - .into()) - } + .into()); + }; + + Ok(pwd) } diff --git a/src/lookup/deleted.rs b/src/lookup/deleted.rs index 8feff64d..ae8f1ce0 100644 --- a/src/lookup/deleted.rs +++ b/src/lookup/deleted.rs @@ -21,8 +21,7 @@ use crate::lookup::versions::{ProximateDatasetAndOptAlts, RelativePathAndSnapMou use hashbrown::{HashMap, HashSet}; use std::ffi::OsString; use std::fs::read_dir; -use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; #[derive(Debug, Clone, Eq, PartialEq)] pub struct DeletedFiles { @@ -34,38 +33,6 @@ pub struct DeletedFiles { // this, believe it or not, will be faster impl DeletedFiles { pub fn new(requested_dir: &Path) -> HttmResult { - // we always need a requesting dir because we are comparing the files in the - // requesting dir to those of their relative dirs on snapshots - let requested_dir_pathdata = PathData::from(requested_dir); - - // create vec of all local and replicated backups at once - // - // we need to make certain that what we return from possibly multiple datasets are unique - // as these will be the filenames that populate our interactive views, so deduplicate - // by filename and latest file version here - let basic_info_map: HashMap = - ProximateDatasetAndOptAlts::new(&requested_dir_pathdata)? - .into_search_bundles() - .flat_map(|search_bundle| { - Self::unique_deleted_for_dir(&requested_dir_pathdata.path_buf, &search_bundle) - }) - .flatten() - .map(|basic_info| (basic_info.filename().to_os_string(), basic_info)) - .collect(); - - Ok(Self { - inner: basic_info_map.into_values().collect(), - }) - } - - pub fn into_inner(self) -> Vec { - self.inner - } - - fn unique_deleted_for_dir( - requested_dir: &Path, - search_bundle: &RelativePathAndSnapMounts, - ) -> HttmResult> { // get all local entries we need to compare against these to know // what is a deleted file // @@ -75,68 +42,53 @@ impl DeletedFiles { .map(|dir_entry| dir_entry.file_name()) .collect(); - let unique_snap_filenames: HashMap = - Self::unique_snap_filenames(search_bundle.snap_mounts, search_bundle.relative_path); + let inner = Self::unique_deleted_for_dir(requested_dir, &local_filenames_set)?; - // compare local filenames to all unique snap filenames - none values are unique, here - let all_deleted_versions = unique_snap_filenames - .into_iter() - .filter(|(file_name, _basic_info)| !local_filenames_set.contains(file_name)) - .map(|(_file_name, basic_info)| basic_info) - .collect(); - - Ok(all_deleted_versions) + Ok(Self { inner }) } - fn unique_snap_filenames( - mounts: &[PathBuf], - relative_path: &Path, - ) -> HashMap { - mounts - .iter() - .map(|path| path.join(relative_path)) - .flat_map(read_dir) - .flatten() - .flatten() - .map(|dir_entry| (dir_entry.file_name(), BasicDirEntryInfo::from(&dir_entry))) - .collect::>() + #[inline(always)] + pub fn into_inner(self) -> Vec { + self.inner } -} -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct LastInTimeSet { - inner: Vec, -} + #[inline(always)] + fn unique_deleted_for_dir<'a>( + requested_dir: &'a Path, + local_filenames_set: &'a HashSet, + ) -> HttmResult> { + // we always need a requesting dir because we are comparing the files in the + // requesting dir to those of their relative dirs on snapshots + let path_data = PathData::from(requested_dir); -impl Deref for LastInTimeSet { - type Target = Vec; + // create vec of all local and replicated backups at once + // + // we need to make certain that what we return from possibly multiple datasets are unique + let unique_deleted_for_dir: HashMap = + ProximateDatasetAndOptAlts::new(&path_data)? + .into_search_bundles() + .flat_map(|search_bundle| { + Self::deleted_files_for_dataset(search_bundle, &local_filenames_set) + }) + .collect(); - fn deref(&self) -> &Self::Target { - &self.inner + Ok(unique_deleted_for_dir.into_values().collect()) } -} - -impl LastInTimeSet { - // this is very similar to VersionsMap, but of course returns only last in time - // for directory paths during deleted searches. it's important to have a policy, here, - // last in time, for which directory we return during deleted searches, because - // different snapshot-ed dirs may contain different files. - // this fn is also missing parallel iter fns, to make the searches more responsive - // by leaving parallel search for the interactive views - pub fn new(path_set: Vec) -> HttmResult { - let res = path_set + #[inline(always)] + fn deleted_files_for_dataset<'a>( + search_bundle: RelativePathAndSnapMounts<'a>, + local_filenames_set: &'a HashSet, + ) -> impl Iterator + 'a { + // compare local filenames to all unique snap filenames - none values are unique, here + search_bundle + .snap_mounts .iter() - .flat_map(ProximateDatasetAndOptAlts::new) - .filter_map(|prox_opt_alts| { - prox_opt_alts - .into_search_bundles() - .filter_map(|search_bundle| search_bundle.last_version()) - .max_by_key(|pathdata| pathdata.md_infallible().modify_time) - .map(|pathdata| pathdata.path_buf) - }) - .collect(); - - Ok(Self { inner: res }) + .map(|path| path.join(search_bundle.relative_path.as_os_str())) + .flat_map(std::fs::read_dir) + .flatten() + .flatten() + .filter(|dir_entry| !local_filenames_set.contains(&dir_entry.file_name())) + .map(|dir_entry| (dir_entry.file_name(), BasicDirEntryInfo::from(&dir_entry))) } } diff --git a/src/lookup/file_mounts.rs b/src/lookup/file_mounts.rs index 66c81d2d..899f681e 100644 --- a/src/lookup/file_mounts.rs +++ b/src/lookup/file_mounts.rs @@ -15,14 +15,11 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::data::paths::PathData; -use crate::data::paths::PathDeconstruction; +use crate::data::paths::{PathData, PathDeconstruction}; use crate::library::results::{HttmError, HttmResult}; use crate::lookup::versions::ProximateDatasetAndOptAlts; -use crate::ExecMode; -use crate::GLOBAL_CONFIG; +use crate::{ExecMode, GLOBAL_CONFIG}; use rayon::prelude::*; -use std::collections::BTreeSet; use std::ops::Deref; use std::path::PathBuf; @@ -39,10 +36,10 @@ impl MountDisplay { T: PathDeconstruction<'a> + ?Sized, { match self { - MountDisplay::Target => path.target(&mount.path_buf), - MountDisplay::Source => path.source(Some(&mount.path_buf)), + MountDisplay::Target => path.target(&mount.path()), + MountDisplay::Source => path.source(Some(&mount.path())), MountDisplay::RelativePath => path - .relative_path(&mount.path_buf) + .relative_path(&mount.path()) .ok() .map(|path| path.to_path_buf()), } @@ -51,12 +48,12 @@ impl MountDisplay { #[derive(Debug)] pub struct MountsForFiles<'a> { - inner: BTreeSet>, + inner: Vec>, mount_display: &'a MountDisplay, } impl<'a> Deref for MountsForFiles<'a> { - type Target = BTreeSet>; + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.inner @@ -73,29 +70,26 @@ impl<'a> MountsForFiles<'a> { // we only check for phantom files in "mount for file" mode because // people should be able to search for deleted files in other modes - let set: BTreeSet = GLOBAL_CONFIG + let set: Vec = GLOBAL_CONFIG .paths .par_iter() .filter_map(|pd| match ProximateDatasetAndOptAlts::new(pd) { Ok(prox_opt_alts) => Some(prox_opt_alts), - Err(_) => { + Err(err) => { if !is_interactive_mode { - eprintln!( - "WARN: Filesystem upon which the path resides is not supported: {:?}", - pd.path_buf - ) + eprintln!("WARN: {:?}", err.to_string()) } None } }) .map(|prox_opt_alts| { if !is_interactive_mode - && prox_opt_alts.pathdata.metadata.is_none() + && prox_opt_alts.pathdata.opt_metadata().is_none() && prox_opt_alts.datasets_of_interest().count() == 0 { eprintln!( "WARN: Input file may have never existed: {:?}", - prox_opt_alts.pathdata.path_buf + prox_opt_alts.pathdata.path() ); } @@ -109,7 +103,9 @@ impl<'a> MountsForFiles<'a> { if set .iter() .all(|prox| prox.datasets_of_interest().count() == 0) - || set.iter().all(|prox| prox.pathdata.metadata.is_none()) + || set + .iter() + .all(|prox| prox.pathdata.opt_metadata().is_none()) { return Err(HttmError::new( "httm could either not find any mounts for the path/s specified, or all the path do not exist, so, umm, 🤷? Please try another path.", diff --git a/src/lookup/snap_names.rs b/src/lookup/snap_names.rs index 8428dc4c..78cfb955 100644 --- a/src/lookup/snap_names.rs +++ b/src/lookup/snap_names.rs @@ -16,13 +16,14 @@ // that was distributed with this source code. use crate::config::generate::ListSnapsFilters; -use crate::data::paths::PathDeconstruction; -use crate::data::paths::{PathData, ZfsSnapPathGuard}; +use crate::data::paths::{PathData, PathDeconstruction, ZfsSnapPathGuard}; +use crate::filesystem::mounts::FilesystemType; use crate::library::results::{HttmError, HttmResult}; use crate::lookup::versions::VersionsMap; use rayon::prelude::*; use std::collections::BTreeMap; use std::ops::Deref; +use std::path::PathBuf; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapNameMap { @@ -49,38 +50,61 @@ impl SnapNameMap { opt_filters: &Option, ) -> HttmResult { let inner: BTreeMap> = versions_map - .par_iter() + .iter() .filter(|(pathdata, snaps)| { if snaps.is_empty() { let msg = format!( "httm could not find any snapshots for the file specified: {:?}", - pathdata.path_buf + pathdata.path() ); - eprintln!("WARNING: {msg}"); + eprintln!("WARN: {msg}"); return false; } true }) - .map(|(pathdata, vec_snaps)| { - // use par iter here because no one else is using the global rayon threadpool any more - let snap_names: Vec = vec_snaps - .par_iter() - .filter_map(|pd| { - ZfsSnapPathGuard::new(pd).and_then(|spd| spd.source(None)) - }) - .filter(|snap| { - if let Some(filters) = opt_filters { - if let Some(names) = &filters.name_filters { - return names.iter().any(|pattern| snap.to_string_lossy().contains(pattern)); - } - } - true - }) - .map(|path| path.to_string_lossy().to_string()) - .collect(); + .filter_map(|(pathdata, snaps)| { + let opt_proximate_dataset = pathdata.proximate_dataset().ok(); + + match pathdata.fs_type(opt_proximate_dataset) { + Some(FilesystemType::Zfs) => { + // use par iter here because no one else is using the global rayon threadpool any more + let snap_names: Vec = snaps + .par_iter() + .filter_map(|snap_pd| { + ZfsSnapPathGuard::new(snap_pd).and_then(|spd| spd.source(opt_proximate_dataset)) + }) + .collect(); - (pathdata, snap_names) + Some((pathdata, snap_names)) + } + Some(FilesystemType::Btrfs(opt_additional_btrfs_data)) => { + if let Some(additional_btrfs_data) = opt_additional_btrfs_data { + if let Some(new_map) = additional_btrfs_data.snap_names.get() { + let values: Vec = new_map.values().cloned().map(|k| k.into_path_buf()).collect(); + return Some((pathdata, values)) + } + } + + None + }, + _ => { + eprintln!("ERROR: LIST_SNAPS is a ZFS and btrfs only option. Path does not appear to be on a supported dataset: {:?}", pathdata.path()); + None + } + } + }) + .map(|(mount, snaps)| { + let vec_snaps: Vec<_> = snaps.iter().map(|p| p.to_string_lossy().to_string()).collect(); + (mount, vec_snaps) + }) + .filter(|(_pathdata, snaps)| { + if let Some(filters) = opt_filters { + if let Some(names) = &filters.name_filters { + return names.iter().any(|pattern| snaps.iter().any(|snap| snap.contains(pattern))); + } + } + true }) .filter_map(|(pathdata, mut vec_snaps)| { if let Some(mode_filter) = opt_filters { diff --git a/src/lookup/versions.rs b/src/lookup/versions.rs index 8ff311d5..82f298ad 100644 --- a/src/lookup/versions.rs +++ b/src/lookup/versions.rs @@ -15,17 +15,18 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::config::generate::{Config, ExecMode, LastSnapMode, ListSnapsOfType}; -use crate::data::paths::PathDeconstruction; -use crate::data::paths::PathMetadata; -use crate::data::paths::{CompareVersionsContainer, PathData}; +use crate::config::generate::{Config, DedupBy, ExecMode, LastSnapMode}; +use crate::data::paths::{CompareContentsContainer, PathData, PathDeconstruction}; +use crate::filesystem::mounts::LinkType; use crate::library::results::{HttmError, HttmResult}; use crate::GLOBAL_CONFIG; +use hashbrown::HashSet; use rayon::prelude::*; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::io::ErrorKind; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, RwLock}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct VersionsMap { @@ -38,6 +39,14 @@ impl From>> for VersionsMap { } } +impl From<[(PathData, Vec); 1]> for VersionsMap { + fn from(slice: [(PathData, Vec); 1]) -> Self { + Self { + inner: slice.into(), + } + } +} + impl Deref for VersionsMap { type Target = BTreeMap>; @@ -60,28 +69,25 @@ impl VersionsMap { .par_iter() .filter_map(|pathdata| match Versions::new(pathdata, config) { Ok(versions) => Some(versions), - Err(_err) => { + Err(err) => { if !is_interactive_mode { - eprintln!( - "WARN: Filesystem upon which the path resides is not supported: {:?}\n", - pathdata.path_buf - ) + eprintln!("WARN: {}", err.to_string()) } None } }) .map(|versions| { if !is_interactive_mode - && versions.live_path.metadata.is_none() + && versions.live_path.opt_metadata().is_none() && versions.snap_versions.is_empty() { eprintln!( "WARN: Input file may have never existed: {:?}", - versions.live_path.path_buf + versions.live_path.path() ); } - versions.destructure() + versions.into_inner() }) .collect(); @@ -92,7 +98,7 @@ impl VersionsMap { if versions_map.values().all(std::vec::Vec::is_empty) && versions_map .keys() - .all(|pathdata| pathdata.metadata.is_none()) + .all(|pathdata| pathdata.opt_metadata().is_none()) { return Err(HttmError::new( "httm could find neither a live version, nor any snapshot version for all the specified paths, so, umm, 🤷? Please try another file.", @@ -114,7 +120,7 @@ impl VersionsMap { pub fn is_live_version_redundant(live_pathdata: &PathData, snaps: &[PathData]) -> bool { if let Some(last_snap) = snaps.last() { - return last_snap.metadata == live_pathdata.metadata; + return last_snap.opt_metadata() == live_pathdata.opt_metadata(); } false @@ -135,11 +141,11 @@ impl VersionsMap { // if last() is some, then should be able to unwrap pop() Some(last) => match last_snap_mode { LastSnapMode::Any => vec![last.to_owned()], - LastSnapMode::DittoOnly if pathdata.metadata == last.metadata => { + LastSnapMode::DittoOnly if pathdata.opt_metadata() == last.opt_metadata() => { vec![last.to_owned()] } LastSnapMode::NoDittoExclusive | LastSnapMode::NoDittoInclusive - if pathdata.metadata != last.metadata => + if pathdata.opt_metadata() != last.opt_metadata() => { vec![last.to_owned()] } @@ -163,14 +169,13 @@ pub struct Versions { impl Versions { #[inline(always)] - fn new(pathdata: &PathData, config: &Config) -> HttmResult { + pub fn new(pathdata: &PathData, config: &Config) -> HttmResult { let prox_opt_alts = ProximateDatasetAndOptAlts::new(pathdata)?; let live_path = prox_opt_alts.pathdata.clone(); let snap_versions: Vec = prox_opt_alts .into_search_bundles() - .par_bridge() .flat_map(|relative_path_snap_mounts| { - relative_path_snap_mounts.versions_processed(&config.uniqueness) + relative_path_snap_mounts.versions_processed(&config.dedup_by) }) .collect(); @@ -179,8 +184,9 @@ impl Versions { snap_versions, }) } + #[inline(always)] - fn destructure(self) -> (PathData, Vec) { + pub fn into_inner(self) -> (PathData, Vec) { (self.live_path, self.snap_versions) } } @@ -190,7 +196,7 @@ pub struct ProximateDatasetAndOptAlts<'a> { pub pathdata: &'a PathData, pub proximate_dataset: &'a Path, pub relative_path: &'a Path, - pub opt_alts: Option<&'a Vec>, + pub opt_alts: Option<&'a Vec>>, } impl<'a> Ord for ProximateDatasetAndOptAlts<'a> { @@ -251,19 +257,16 @@ impl<'a> ProximateDatasetAndOptAlts<'a> { opt_alts, }) } + #[inline(always)] pub fn datasets_of_interest(&'a self) -> impl Iterator { - let alts = self - .opt_alts - .as_deref() - .into_iter() - .flatten() - .map(PathBuf::as_path); + let alts = self.opt_alts.into_iter().flatten().map(|p| p.as_ref()); - let base = [self.proximate_dataset].into_iter(); + let base = Some(self.proximate_dataset).into_iter(); alts.chain(base) } + #[inline(always)] pub fn into_search_bundles(&'a self) -> impl Iterator> { self.datasets_of_interest().flat_map(|dataset_of_interest| { @@ -275,57 +278,63 @@ impl<'a> ProximateDatasetAndOptAlts<'a> { #[derive(Debug, Clone)] pub struct RelativePathAndSnapMounts<'a> { pub relative_path: &'a Path, - pub snap_mounts: &'a [PathBuf], + pub snap_mounts: &'a [Box], + pub dataset_of_interest: &'a Path, } impl<'a> RelativePathAndSnapMounts<'a> { #[inline(always)] - fn new(relative_path: &'a Path, dataset_of_interest: &Path) -> Option { + pub fn new(relative_path: &'a Path, dataset_of_interest: &'a Path) -> Option { // building our relative path by removing parent below the snap dir // // for native searches the prefix is are the dirs below the most proximate dataset // for user specified dirs/aliases these are specified by the user - let snap_mounts = GLOBAL_CONFIG + GLOBAL_CONFIG .dataset_collection .map_of_snaps - .get(dataset_of_interest)?; - - Some(Self { - relative_path, - snap_mounts, - }) + .get(dataset_of_interest) + .map(|snap_mounts| Self { + relative_path, + snap_mounts, + dataset_of_interest, + }) } + #[inline(always)] - pub fn versions_processed(&'a self, uniqueness: &ListSnapsOfType) -> Vec { - let all_versions = self.versions_unprocessed(); + pub fn versions_processed(&'a self, dedup_by: &DedupBy) -> Vec { + loop { + let all_versions = self.all_versions_unprocessed(); - Self::sort_dedup_versions(all_versions, uniqueness) - } + let res = Self::sort_dedup_versions(all_versions, dedup_by); - pub fn last_version(&self) -> Option { - let mut sorted_versions = self.versions_processed(&ListSnapsOfType::All); + if res.is_empty() { + // opendir and readdir iter on the snap path are necessary to mount snapshots over SMB + match NetworkAutoMount::new(&self) { + NetworkAutoMount::Break => break res, + NetworkAutoMount::Continue => continue, + } + } - sorted_versions.pop() + break res; + } } + #[inline(always)] - fn versions_unprocessed(&'a self) -> impl ParallelIterator + 'a { + fn all_versions_unprocessed(&'a self) -> impl Iterator + 'a { // get the DirEntry for our snapshot path which will have all our possible // snapshots, like so: .zfs/snapshots// self .snap_mounts - .par_iter() - .map(|path| path.join(self.relative_path)) + .iter() + .map(|snap_path| { + snap_path.join(self.relative_path) + }) .filter_map(|joined_path| { match joined_path.symlink_metadata() { Ok(md) => { // why not PathData::new()? because symlinks will resolve! // symlinks from a snap will end up looking just like the link target, so this is very confusing... - let path_metadata = PathMetadata::new(&md); - - Some(PathData { - path_buf: joined_path, - metadata: path_metadata, - }) + Some(PathData::new(&joined_path, Some(md))) }, Err(err) => { match err.kind() { @@ -346,25 +355,86 @@ impl<'a> RelativePathAndSnapMounts<'a> { }) } - // remove duplicates with the same system modify time and size/file len (or contents! See --uniqueness) - #[allow(clippy::mutable_key_type)] + // remove duplicates with the same system modify time and size/file len (or contents! See --DEDUP_BY) #[inline(always)] fn sort_dedup_versions( - iter: impl ParallelIterator, - uniqueness: &ListSnapsOfType, + iter: impl Iterator, + dedup_by: &DedupBy, ) -> Vec { - match uniqueness { - ListSnapsOfType::All => { + match dedup_by { + DedupBy::Disable => { let mut vec: Vec = iter.collect(); - vec.sort_unstable(); + vec.sort_unstable_by_key(|pathdata| pathdata.metadata_infallible().mtime()); + vec + } + DedupBy::Metadata => { + let mut vec: Vec = iter.collect(); + + vec.sort_unstable_by_key(|pathdata| pathdata.metadata_infallible()); + vec.dedup_by_key(|a| a.metadata_infallible()); + vec } - ListSnapsOfType::UniqueContents | ListSnapsOfType::UniqueMetadata => { - let sorted_and_deduped: BTreeSet = iter - .map(|pd| CompareVersionsContainer::new(pd, uniqueness)) + DedupBy::Contents => { + let mut vec: Vec = iter + .map(|pathdata| CompareContentsContainer::from(pathdata)) .collect(); - sorted_and_deduped.into_iter().map(PathData::from).collect() + + vec.sort_unstable(); + vec.dedup(); + + vec.into_iter().map(|container| container.into()).collect() } } } } + +enum NetworkAutoMount { + Break, + Continue, +} + +impl NetworkAutoMount { + #[inline(always)] + fn new(bundle: &RelativePathAndSnapMounts) -> NetworkAutoMount { + if GLOBAL_CONFIG + .dataset_collection + .map_of_datasets + .get(bundle.dataset_of_interest) + .map(|md| matches!(md.link_type, LinkType::Local)) + .unwrap_or_else(|| true) + { + return NetworkAutoMount::Break; + } + + static CACHE_RESULT: LazyLock>> = + LazyLock::new(|| RwLock::new(HashSet::new())); + + if CACHE_RESULT + .try_read() + .ok() + .map(|cached_result| cached_result.contains(bundle.dataset_of_interest)) + .unwrap_or_else(|| true) + { + return NetworkAutoMount::Break; + } + + if let Ok(mut cached_result) = CACHE_RESULT.try_write() { + unsafe { + cached_result.insert_unique_unchecked(bundle.dataset_of_interest.to_path_buf()); + }; + + bundle.snap_mounts.iter().for_each(|snap_path| { + let _ = std::fs::read_dir(snap_path) + .into_iter() + .flatten() + .flatten() + .next(); + }); + + return NetworkAutoMount::Continue; + } + + NetworkAutoMount::Break + } +} diff --git a/src/main.rs b/src/main.rs index b5ffb95a..6d9ffb10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,12 +20,10 @@ mod data { pub mod paths; pub mod selection; } -mod display_map { - pub mod format; -} -mod display_versions { - pub mod format; +mod display { + pub mod maps; pub mod num_versions; + pub mod versions; pub mod wrapper; } mod background { @@ -34,7 +32,6 @@ mod background { } mod interactive { pub mod browse; - pub mod exec; pub mod preview; pub mod prune; pub mod restore; @@ -55,8 +52,6 @@ mod library { pub mod file_ops; pub mod iter_extensions; pub mod results; - pub mod snap_guard; - pub mod snap_mounts; pub mod utility; } mod lookup { @@ -65,27 +60,35 @@ mod lookup { pub mod snap_names; pub mod versions; } -mod parse { +mod filesystem { pub mod aliases; pub mod alts; pub mod mounts; pub mod snaps; } +mod zfs { + pub mod run_command; + pub mod snap_guard; + pub mod snap_mounts; +} +use crate::config::generate::InteractiveMode; +use crate::interactive::browse::InteractiveBrowse; +use crate::interactive::select::InteractiveSelect; use background::recursive::NonInteractiveRecursiveWrapper; use config::generate::{Config, ExecMode}; -use display_map::format::PrintAsMap; -use display_versions::wrapper::VersionsDisplayWrapper; -use interactive::exec::InteractiveExec; +use display::maps::PrintAsMap; +use display::wrapper::DisplayWrapper; use interactive::prune::PruneSnaps; +use interactive::restore::InteractiveRestore; use library::results::HttmResult; -use library::snap_mounts::SnapshotMounts; use library::utility::print_output_buf; use lookup::file_mounts::MountsForFiles; use lookup::snap_names::SnapNameMap; use lookup::versions::VersionsMap; -use once_cell::sync::Lazy; use roll_forward::exec::RollForward; +use std::sync::LazyLock; +use zfs::snap_mounts::SnapshotMounts; pub const ZFS_HIDDEN_DIRECTORY: &str = ".zfs"; pub const ZFS_SNAPSHOT_DIRECTORY: &str = ".zfs/snapshot"; @@ -93,8 +96,10 @@ pub const BTRFS_SNAPPER_HIDDEN_DIRECTORY: &str = ".snapshots"; pub const TM_DIR_REMOTE: &str = "/Volumes/.timemachine"; pub const TM_DIR_LOCAL: &str = "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb"; pub const BTRFS_SNAPPER_SUFFIX: &str = "snapshot"; -pub const ROOT_DIRECTORY: &str = "/"; pub const NILFS2_SNAPSHOT_ID_KEY: &str = "cp="; +pub const RESTIC_SNAPSHOT_DIRECTORY: &str = "snapshots"; +pub const RESTIC_LATEST_SNAPSHOT_DIRECTORY: &str = "snapshots/latest"; +pub const IN_BUFFER_SIZE: usize = 131_072; fn main() { match exec() { @@ -108,7 +113,7 @@ fn main() { // get our program args and generate a config for use // everywhere else -static GLOBAL_CONFIG: Lazy = Lazy::new(|| { +static GLOBAL_CONFIG: LazyLock = LazyLock::new(|| { Config::new() .map_err(|error| { eprintln!("Error: {error}"); @@ -122,16 +127,36 @@ fn exec() -> HttmResult<()> { match &GLOBAL_CONFIG.exec_mode { // ExecMode::Interactive *may* return back to this function to be printed ExecMode::Interactive(interactive_mode) => { - let pathdata_set = InteractiveExec::exec(interactive_mode)?; - let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &pathdata_set)?; - let output_buf = VersionsDisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); + let mut browse_result = InteractiveBrowse::new()?; - print_output_buf(&output_buf) + match interactive_mode { + InteractiveMode::Restore(_) => { + let interactive_select = InteractiveSelect::try_from(&mut browse_result)?; + + let interactive_restore = InteractiveRestore::from(interactive_select); + + interactive_restore.restore() + } + InteractiveMode::Select(select_mode) => { + let interactive_select = InteractiveSelect::try_from(&mut browse_result)?; + + interactive_select.print_selections(&select_mode) + } + // InteractiveMode::Browse executes back through fn exec() in main.rs + InteractiveMode::Browse => { + let versions_map = + VersionsMap::new(&GLOBAL_CONFIG, &browse_result.selected_pathdata)?; + + let output_buf = DisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); + + print_output_buf(&output_buf) + } + } } // ExecMode::BasicDisplay will be just printed, we already know the paths ExecMode::BasicDisplay | ExecMode::NumVersions(_) => { let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &GLOBAL_CONFIG.paths)?; - let output_buf = VersionsDisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); + let output_buf = DisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string(); print_output_buf(&output_buf) } diff --git a/src/parse/aliases.rs b/src/parse/aliases.rs deleted file mode 100644 index 2a0ba681..00000000 --- a/src/parse/aliases.rs +++ /dev/null @@ -1,129 +0,0 @@ -// ___ ___ ___ ___ -// /\__\ /\ \ /\ \ /\__\ -// /:/ / \:\ \ \:\ \ /::| | -// /:/__/ \:\ \ \:\ \ /:|:| | -// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ -// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ -// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / -// \::/ / /:/ / /:/ / /:/ / -// /:/ / \/__/ \/__/ /:/ / -// /:/ / /:/ / -// \/__/ \/__/ -// -// Copyright (c) 2023, Robert Swinford gmail.com> -// -// For the full copyright and license information, please view the LICENSE file -// that was distributed with this source code. - -use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::fs_type_from_hidden_dir; -use crate::parse::mounts::FilesystemType; -use hashbrown::HashMap; -use std::ffi::OsString; -use std::ops::Deref; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RemotePathAndFsType { - pub remote_dir: PathBuf, - pub fs_type: FilesystemType, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MapOfAliases { - inner: HashMap, -} - -impl From> for MapOfAliases { - fn from(map: HashMap) -> Self { - Self { inner: map } - } -} - -impl Deref for MapOfAliases { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl MapOfAliases { - pub fn new( - raw_local_dir: &Option, - raw_snap_dir: &Option, - pwd: &Path, - opt_input_aliases: &Option>, - ) -> HttmResult { - // user defined dir exists?: check that path contains the hidden snapshot directory - let snap_point = raw_snap_dir.as_ref().map(|value| { - let snap_dir = PathBuf::from(value); - - // local relative dir can be set at cmdline or as an env var, - // but defaults to current working directory if empty - let local_dir = match raw_local_dir { - Some(value) => PathBuf::from(value), - None => pwd.to_path_buf(), - }; - - (snap_dir, local_dir) - }); - - let mut aliases_iter: Vec<(PathBuf, PathBuf)> = match opt_input_aliases { - Some(input_aliases) => { - let res: Option> = input_aliases - .iter() - .map(|alias| { - alias - .split_once(':') - .map(|(first, rest)| (PathBuf::from(first), PathBuf::from(rest))) - }) - .collect(); - - res.ok_or_else(|| { - HttmError::new( - "Must use specified delimiter (':') between aliases for MAP_ALIASES.", - ) - })? - } - None => Vec::new(), - }; - - if let Some(value) = snap_point { - aliases_iter.push(value) - } - - let map_of_aliases: HashMap = aliases_iter - .into_iter() - .filter_map(|(local_dir, snap_dir)| { - if !local_dir.exists() || !snap_dir.exists() { - [local_dir, snap_dir] - .into_iter() - .filter(|dir| !dir.exists()) - .for_each(|dir| { - eprintln!( - "WARN: An alias path specified does not exist, or is not mounted: {:?}", - dir - ) - }); - return None; - } - - Some((local_dir, snap_dir)) - }) - .filter_map(|(local_dir, remote_dir)| { - fs_type_from_hidden_dir(&remote_dir).map(|fs_type| { - ( - local_dir, - RemotePathAndFsType { - remote_dir, - fs_type, - }, - ) - }) - }) - .collect(); - - Ok(map_of_aliases.into()) - } -} diff --git a/src/parse/mounts.rs b/src/parse/mounts.rs deleted file mode 100644 index ef38c14c..00000000 --- a/src/parse/mounts.rs +++ /dev/null @@ -1,386 +0,0 @@ -// ___ ___ ___ ___ -// /\__\ /\ \ /\ \ /\__\ -// /:/ / \:\ \ \:\ \ /::| | -// /:/__/ \:\ \ \:\ \ /:|:| | -// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ -// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ -// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / -// \::/ / /:/ / /:/ / /:/ / -// /:/ / \/__/ \/__/ /:/ / -// /:/ / /:/ / -// \/__/ \/__/ -// -// Copyright (c) 2023, Robert Swinford gmail.com> -// -// For the full copyright and license information, please view the LICENSE file -// that was distributed with this source code. - -use crate::library::results::{HttmError, HttmResult}; -use crate::library::utility::{find_common_path, fs_type_from_hidden_dir}; -use crate::parse::snaps::MapOfSnaps; -use crate::{ - NILFS2_SNAPSHOT_ID_KEY, ROOT_DIRECTORY, TM_DIR_LOCAL, TM_DIR_REMOTE, ZFS_HIDDEN_DIRECTORY, -}; -use hashbrown::{HashMap, HashSet}; -use once_cell::sync::Lazy; -use proc_mounts::MountIter; -use rayon::iter::Either; -use rayon::prelude::*; -use std::collections::BTreeMap; -use std::ops::Deref; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command as ExecProcess; -use which::which; - -pub const ZFS_FSTYPE: &str = "zfs"; -pub const NILFS2_FSTYPE: &str = "nilfs2"; -pub const BTRFS_FSTYPE: &str = "btrfs"; -pub const SMB_FSTYPE: &str = "smbfs"; -pub const NFS_FSTYPE: &str = "nfs"; -pub const AFP_FSTYPE: &str = "afpfs"; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum FilesystemType { - Zfs, - Btrfs, - Nilfs2, - Apfs, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum MountType { - Local, - Network, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DatasetMetadata { - pub source: PathBuf, - pub fs_type: FilesystemType, - pub mount_type: MountType, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FilterDirs { - inner: HashSet, -} - -impl Deref for FilterDirs { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub trait MaxLen { - fn max_len(&self) -> usize; -} - -impl MaxLen for FilterDirs { - fn max_len(&self) -> usize { - self.inner - .iter() - .map(|dir| dir.components().count()) - .max() - .unwrap_or(usize::MAX) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MapOfDatasets { - inner: HashMap, -} - -impl Deref for MapOfDatasets { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl MaxLen for MapOfDatasets { - fn max_len(&self) -> usize { - self.inner - .keys() - .map(|mount| mount.components().count()) - .max() - .unwrap_or(usize::MAX) - } -} - -pub static PROC_MOUNTS: Lazy = Lazy::new(|| PathBuf::from("/proc/mounts")); - -static ETC_MNTTAB: Lazy = Lazy::new(|| PathBuf::from("/proc/mounts")); - -pub struct BaseFilesystemInfo { - pub map_of_datasets: MapOfDatasets, - pub map_of_snaps: MapOfSnaps, - pub filter_dirs: FilterDirs, -} - -impl BaseFilesystemInfo { - // divide by the type of system we are on - // Linux allows us the read proc mounts - pub fn new() -> HttmResult { - let (raw_datasets, filter_dirs_set) = if PROC_MOUNTS.exists() { - Self::from_file(&PROC_MOUNTS)? - } else if ETC_MNTTAB.exists() { - Self::from_file(&ETC_MNTTAB)? - } else { - Self::from_mount_cmd()? - }; - - let map_of_snaps = MapOfSnaps::new(&raw_datasets)?; - - let map_of_datasets = { - MapOfDatasets { - inner: raw_datasets, - } - }; - - let filter_dirs = { - FilterDirs { - inner: filter_dirs_set, - } - }; - - Ok(BaseFilesystemInfo { - map_of_datasets, - map_of_snaps, - filter_dirs, - }) - } - - // parsing from proc mounts is both faster and necessary for certain btrfs features - // for instance, allows us to read subvolumes mounts, like "/@" or "/@home" - fn from_file(path: &Path) -> HttmResult<(HashMap, HashSet)> { - let mount_iter = MountIter::new_from_file(path)?; - - let (map_of_datasets, filter_dirs): (HashMap, HashSet) = - mount_iter - .par_bridge() - .flatten() - .filter(|mount_info| { - !mount_info - .dest - .to_string_lossy() - .contains(ZFS_HIDDEN_DIRECTORY) - }) - .filter(|mount_info| { - !mount_info - .options - .iter() - .any(|opt| opt.contains(NILFS2_SNAPSHOT_ID_KEY)) - }) - .map(|mount_info| { - let dest_path = PathBuf::from(&mount_info.dest); - (mount_info, dest_path) - }) - .partition_map(|(mount_info, dest_path)| match mount_info.fstype.as_str() { - ZFS_FSTYPE => Either::Left(( - dest_path, - DatasetMetadata { - source: PathBuf::from(mount_info.source), - fs_type: FilesystemType::Zfs, - mount_type: MountType::Local, - }, - )), - SMB_FSTYPE | AFP_FSTYPE | NFS_FSTYPE => { - match fs_type_from_hidden_dir(&dest_path) { - Some(FilesystemType::Zfs) => Either::Left(( - dest_path, - DatasetMetadata { - source: PathBuf::from(mount_info.source), - fs_type: FilesystemType::Zfs, - mount_type: MountType::Network, - }, - )), - Some(FilesystemType::Btrfs) => Either::Left(( - dest_path, - DatasetMetadata { - source: PathBuf::from(mount_info.source), - fs_type: FilesystemType::Btrfs, - mount_type: MountType::Network, - }, - )), - _ => Either::Right(dest_path), - } - } - BTRFS_FSTYPE => { - let keyed_options: BTreeMap<&str, &str> = mount_info - .options - .iter() - .filter(|line| line.contains('=')) - .filter_map(|line| line.split_once('=')) - .collect(); - - let source = match keyed_options.get("subvol") { - Some(subvol) => PathBuf::from(subvol), - None => PathBuf::from(mount_info.source), - }; - - Either::Left(( - dest_path, - DatasetMetadata { - source, - fs_type: FilesystemType::Btrfs, - mount_type: MountType::Local, - }, - )) - } - NILFS2_FSTYPE => Either::Left(( - dest_path, - DatasetMetadata { - source: PathBuf::from(mount_info.source), - fs_type: FilesystemType::Nilfs2, - mount_type: MountType::Local, - }, - )), - _ => Either::Right(dest_path), - }); - - if map_of_datasets.is_empty() { - Err(HttmError::new("httm could not find any valid datasets on the system.").into()) - } else { - Ok((map_of_datasets, filter_dirs)) - } - } - - // old fashioned parsing for non-Linux systems, nearly as fast, works everywhere with a mount command - // both methods are much faster than using zfs command - fn from_mount_cmd() -> HttmResult<(HashMap, HashSet)> { - // do we have the necessary commands for search if user has not defined a snap point? - // if so run the mount search, if not print some errors - let mount_command = which("mount").map_err(|_err| { - HttmError::new( - "'mount' command not be found. Make sure the command 'mount' is in your path.", - ) - })?; - - let command_output = &ExecProcess::new(mount_command).output()?; - - let stderr_string = std::str::from_utf8(&command_output.stderr)?; - - if !stderr_string.is_empty() { - return Err(HttmError::new(stderr_string).into()); - } - - let stdout_string = std::str::from_utf8(&command_output.stdout)?; - - // parse "mount" for filesystems and mountpoints - let (mut map_of_datasets, filter_dirs): ( - HashMap, - HashSet, - ) = stdout_string - .par_lines() - // but exclude snapshot mounts. we want the raw filesystem names. - .filter(|line| !line.contains(ZFS_HIDDEN_DIRECTORY)) - .filter(|line| !line.contains(TM_DIR_REMOTE)) - .filter(|line| !line.contains(TM_DIR_LOCAL)) - // mount cmd includes and " on " between src and rest - .filter_map(|line| line.split_once(" on ")) - // where to split, to just have the src and dest of mounts - .filter_map(|(filesystem, rest)| { - // GNU Linux mount output - if rest.contains("type") { - let opt_mount = rest.split_once(" type"); - opt_mount.map(|mount| (filesystem, mount.0)) - // Busybox and BSD mount output - } else if rest.contains(" (") { - let opt_mount = rest.split_once(" ("); - opt_mount.map(|mount| (filesystem, mount.0)) - } else { - None - } - }) - .map(|(filesystem, mount)| (PathBuf::from(filesystem), PathBuf::from(mount))) - // sanity check: does the filesystem exist and have a ZFS hidden dir? if not, filter it out - // and flip around, mount should key of key/value - .partition_map(|(source, mount)| match fs_type_from_hidden_dir(&mount) { - Some(FilesystemType::Zfs) => Either::Left(( - mount, - DatasetMetadata { - source, - fs_type: FilesystemType::Zfs, - mount_type: MountType::Local, - }, - )), - Some(FilesystemType::Btrfs) => Either::Left(( - mount, - DatasetMetadata { - source, - fs_type: FilesystemType::Btrfs, - mount_type: MountType::Local, - }, - )), - _ => Either::Right(mount), - }); - - Self::from_tm_dir(&mut map_of_datasets); - - if map_of_datasets.is_empty() { - Err(HttmError::new("httm could not find any valid datasets on the system.").into()) - } else { - Ok((map_of_datasets, filter_dirs)) - } - } - - fn from_tm_dir(map_of_datasets: &mut HashMap) { - if cfg!(target_os = "macos") { - let tm_dir_remote_path = std::path::Path::new(TM_DIR_REMOTE); - let tm_dir_local_path = std::path::Path::new(TM_DIR_LOCAL); - - if tm_dir_remote_path.exists() || tm_dir_local_path.exists() { - let root_dir = PathBuf::from(ROOT_DIRECTORY); - - match map_of_datasets.get(&root_dir) { - Some(md) => { - eprintln!("WARN: httm has prioritized a discovered root directory mount over any potential Time Machine mounts: {:?}", md.source); - } - None => { - let metadata = DatasetMetadata { - source: PathBuf::from("timemachine"), - fs_type: FilesystemType::Apfs, - mount_type: MountType::Local, - }; - - // SAFETY: Check no entry is here just above - map_of_datasets.insert_unique_unchecked(root_dir, metadata); - } - } - } - } - } - - // if we have some btrfs mounts, we check to see if there is a snap directory in common - // so we can hide that common path from searches later - pub fn common_snap_dir(&self) -> Option { - let map_of_datasets: &MapOfDatasets = &self.map_of_datasets; - let map_of_snaps: &MapOfSnaps = &self.map_of_snaps; - - if map_of_datasets - .par_iter() - .any(|(_mount, dataset_info)| dataset_info.fs_type == FilesystemType::Btrfs) - { - let vec_snaps: Vec<&PathBuf> = map_of_datasets - .par_iter() - .filter(|(_mount, dataset_info)| { - if dataset_info.fs_type == FilesystemType::Btrfs { - return true; - } - - false - }) - .filter_map(|(mount, _dataset_info)| map_of_snaps.get(mount)) - .flatten() - .collect(); - - return find_common_path(vec_snaps); - } - - None - } -} diff --git a/src/parse/snaps.rs b/src/parse/snaps.rs deleted file mode 100644 index 5477dccc..00000000 --- a/src/parse/snaps.rs +++ /dev/null @@ -1,190 +0,0 @@ -// ___ ___ ___ ___ -// /\__\ /\ \ /\ \ /\__\ -// /:/ / \:\ \ \:\ \ /::| | -// /:/__/ \:\ \ \:\ \ /:|:| | -// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ -// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ -// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / -// \::/ / /:/ / /:/ / /:/ / -// /:/ / \/__/ \/__/ /:/ / -// /:/ / /:/ / -// \/__/ \/__/ -// -// Copyright (c) 2023, Robert Swinford gmail.com> -// -// For the full copyright and license information, please view the LICENSE file -// that was distributed with this source code. - -use crate::library::results::{HttmError, HttmResult}; -use crate::parse::mounts::PROC_MOUNTS; -use crate::parse::mounts::{DatasetMetadata, FilesystemType, MountType}; -use crate::{ - BTRFS_SNAPPER_HIDDEN_DIRECTORY, BTRFS_SNAPPER_SUFFIX, TM_DIR_LOCAL, TM_DIR_REMOTE, - ZFS_SNAPSHOT_DIRECTORY, -}; -use hashbrown::HashMap; -use proc_mounts::MountIter; -use rayon::prelude::*; -use std::fs::read_dir; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use std::process::Command as ExecProcess; -use which::which; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MapOfSnaps { - inner: HashMap>, -} - -impl From>> for MapOfSnaps { - fn from(map: HashMap>) -> Self { - Self { inner: map } - } -} - -impl Deref for MapOfSnaps { - type Target = HashMap>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl MapOfSnaps { - // fans out precompute of snap mounts to the appropriate function based on fstype - pub fn new(map_of_datasets: &HashMap) -> HttmResult { - let map_of_snaps: HashMap> = map_of_datasets - .par_iter() - .flat_map(|(mount, dataset_info)| { - let snap_mounts: HttmResult> = match dataset_info.fs_type { - FilesystemType::Zfs | FilesystemType::Nilfs2 | FilesystemType::Apfs => { - Self::from_defined_mounts(mount, dataset_info) - } - FilesystemType::Btrfs => match dataset_info.mount_type { - MountType::Local => Self::from_btrfs_cmd(mount), - MountType::Network => Self::from_defined_mounts(mount, dataset_info), - }, - }; - - snap_mounts.map(|snap_mounts| (mount.clone(), snap_mounts)) - }) - .collect(); - - if map_of_snaps.is_empty() { - Err(HttmError::new("httm could not find any valid datasets on the system.").into()) - } else { - Ok(map_of_snaps.into()) - } - } - - // build paths to all snap mounts - fn from_btrfs_cmd(mount: &Path) -> HttmResult> { - let btrfs_command = which("btrfs").map_err(|_err| { - HttmError::new( - "'btrfs' command not found. Make sure the command 'btrfs' is in your path.", - ) - })?; - - let exec_command = btrfs_command; - let arg_path = mount.to_string_lossy(); - let args = vec!["subvolume", "show", &arg_path]; - - // must exec for each mount, probably a better way by calling into a lib - let command_output = - std::str::from_utf8(&ExecProcess::new(exec_command).args(&args).output()?.stdout)? - .to_owned(); - - let snaps = command_output - .split_once("Snapshot(s):\n") - .map(|(pre, snap_paths)| { - snap_paths - .lines() - .map(|line| line.trim()) - .map(|relative| { - if pre.contains("") { - // "/" should be the root path - mount.join(relative) - } else { - // btrfs sub list -a -s output includes the sub name (eg @home) - // when that sub could be mounted anywhere, so we remove here - let snap_path_parsed: PathBuf = - Path::new(relative).components().skip(1).collect(); - - mount.join(snap_path_parsed) - } - }) - .filter(|snap| snap.exists()) - .collect() - }) - .ok_or_else(|| { - let msg = format!("No snaps found for mount: {:?}", mount); - HttmError::new(&msg) - })?; - - Ok(snaps) - } - - fn from_defined_mounts( - mount_point_path: &Path, - dataset_metadata: &DatasetMetadata, - ) -> HttmResult> { - let snaps = match dataset_metadata.fs_type { - FilesystemType::Btrfs => { - read_dir(mount_point_path.join(BTRFS_SNAPPER_HIDDEN_DIRECTORY))? - .flatten() - .par_bridge() - .map(|entry| entry.path().join(BTRFS_SNAPPER_SUFFIX)) - .collect() - } - FilesystemType::Zfs => read_dir(mount_point_path.join(ZFS_SNAPSHOT_DIRECTORY))? - .flatten() - .par_bridge() - .map(|entry| entry.path()) - .collect(), - FilesystemType::Apfs => { - let mut res: Vec = Vec::new(); - - if PathBuf::from(&TM_DIR_LOCAL).exists() { - let local = read_dir(TM_DIR_LOCAL)? - .par_bridge() - .flatten() - .flat_map(|entry| read_dir(entry.path())) - .flatten_iter() - .flatten_iter() - .map(|entry| entry.path().join("Data")); - - res.par_extend(local); - } - - if PathBuf::from(&TM_DIR_REMOTE).exists() { - let remote = read_dir(TM_DIR_REMOTE)? - .par_bridge() - .flatten() - .flat_map(|entry| read_dir(entry.path())) - .flatten_iter() - .flatten_iter() - .map(|entry| entry.path().join(entry.file_name()).join("Data")); - - res.par_extend(remote); - } - - res - } - FilesystemType::Nilfs2 => { - let source_path = Path::new(&dataset_metadata.source); - - let mount_iter = MountIter::new_from_file(&*PROC_MOUNTS)?; - - mount_iter - .par_bridge() - .flatten() - .filter(|mount_info| Path::new(&mount_info.source) == source_path) - .filter(|mount_info| mount_info.options.iter().any(|opt| opt.contains("cp="))) - .map(|mount_info| PathBuf::from(mount_info.dest)) - .collect() - } - }; - - Ok(snaps) - } -} diff --git a/src/roll_forward/exec.rs b/src/roll_forward/exec.rs index 5b04c968..ecfe907e 100644 --- a/src/roll_forward/exec.rs +++ b/src/roll_forward/exec.rs @@ -15,38 +15,30 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::data::paths::PathData; -use crate::data::paths::PathDeconstruction; -use crate::library::file_ops::Copy; -use crate::library::file_ops::Preserve; -use crate::library::file_ops::Remove; +use crate::data::paths::{PathData, PathDeconstruction}; +use crate::library::file_ops::{Copy, Preserve, Remove}; +use crate::library::iter_extensions::HttmIter; use crate::library::results::{HttmError, HttmResult}; -use crate::library::snap_guard::{PrecautionarySnapType, SnapGuard}; -use crate::library::utility::is_metadata_same; -use crate::library::utility::user_has_effective_root; -use crate::roll_forward::preserve_hard_links::{HardLinkMap, PreserveHardLinks}; +use crate::library::utility::{is_metadata_same, user_has_effective_root}; +use crate::roll_forward::diff_events::{DiffEvent, DiffType}; +use crate::roll_forward::preserve_hard_links::{PreserveHardLinks, SpawnPreserveLinks}; +use crate::zfs::run_command::RunZFSCommand; +use crate::zfs::snap_guard::{PrecautionarySnapType, SnapGuard}; use crate::{GLOBAL_CONFIG, ZFS_SNAPSHOT_DIRECTORY}; - -use crate::library::iter_extensions::HttmIter; -use crate::roll_forward::diff_events::DiffEvent; -use crate::roll_forward::diff_events::DiffType; - use indicatif::ProgressBar; use nu_ansi_term::Color::{Blue, Red}; use rayon::prelude::*; -use which::which; - use std::fs::read_dir; use std::io::{BufRead, Read}; use std::path::{Path, PathBuf}; -use std::process::{Child, ChildStderr, ChildStdout, Command as ExecProcess, Stdio}; -use std::thread::JoinHandle; +use std::process::{ChildStderr, ChildStdout}; +use std::sync::Arc; pub struct RollForward { dataset: String, snap: String, progress_bar: ProgressBar, - proximate_dataset_mount: PathBuf, + pub proximate_dataset_mount: Arc, } impl RollForward { @@ -54,16 +46,18 @@ impl RollForward { let (dataset, snap) = if let Some(res) = full_snap_name.split_once('@') { res } else { - let msg = format!("{} is not a valid data set name. A valid ZFS snapshot name requires a '@' separating dataset name and snapshot name.", &full_snap_name); + let msg = format!("\"{}\" is not a valid data set name. A valid ZFS snapshot name requires a '@' separating dataset name and snapshot name.", &full_snap_name); return Err(HttmError::new(&msg).into()); }; + let dataset_path = Path::new(&dataset); + let proximate_dataset_mount = GLOBAL_CONFIG .dataset_collection .map_of_datasets .iter() - .find(|(_mount, md)| md.source == PathBuf::from(&dataset)) - .map(|(mount, _)| mount.to_owned()) + .find(|(_mount, md)| md.source.as_ref() == dataset_path) + .map(|(mount, _)| mount.clone()) .ok_or_else(|| HttmError::new("Could not determine proximate dataset mount"))?; let progress_bar: ProgressBar = indicatif::ProgressBar::new_spinner(); @@ -76,11 +70,13 @@ impl RollForward { }) } - fn full_name(&self) -> String { + pub fn full_name(&self) -> String { format!("{}@{}", self.dataset, self.snap) } pub fn exec(&self) -> HttmResult<()> { + // ZFS allow is not sufficient so a ZFSAllowPriv guard isn't here either + // we need root, so we do a raw SnapGuard after checking that we have root user_has_effective_root("Roll forward to a snapshot.")?; let snap_guard: SnapGuard = @@ -109,8 +105,9 @@ impl RollForward { SnapGuard::new( &self.dataset, PrecautionarySnapType::PostRollForward(self.snap.to_owned()), - ) - .map(|_res| ()) + )?; + + Ok(()) } fn zfs_diff_std_err(opt_stderr: Option) -> HttmResult { @@ -124,9 +121,13 @@ impl RollForward { } fn roll_forward(&self) -> HttmResult<()> { - let (snap_handle, live_handle) = self.spawn_preserve_links(); + let spawn_res = SpawnPreserveLinks::new(self); + + let (snap_handle, live_handle) = (spawn_res.snap_handle, spawn_res.live_handle); - let mut process_handle = self.zfs_diff_cmd()?; + let run_zfs = RunZFSCommand::new()?; + + let mut process_handle = run_zfs.diff(&self)?; let opt_stderr = process_handle.stderr.take(); let mut opt_stdout = process_handle.stdout.take(); @@ -189,10 +190,12 @@ impl RollForward { .par_iter() .filter(|(key, _values)| !exclusions.contains(key.as_path())) .flat_map(|(_key, values)| values.iter().max_by_key(|event| event.time)) - .try_for_each(|event| match &event.diff_type { - DiffType::Renamed(new_file) if exclusions.contains(new_file) => Ok(()), - _ => self.diff_action(event), - })?; + .for_each(|event| match &event.diff_type { + DiffType::Renamed(new_file) if exclusions.contains(new_file) => (), + _ => { + let _ = self.diff_action(event); + } + }); self.verify() } @@ -200,61 +203,73 @@ impl RollForward { fn verify(&self) -> HttmResult<()> { let snap_dataset = self.snap_dataset(); - let mut first_pass: Vec = vec![snap_dataset.clone()]; - let mut second_pass = Vec::new(); + let mut directory_list: Vec = vec![snap_dataset.clone()]; + let mut file_list: Vec = Vec::new(); - eprint!("Verifying files and symlinks: "); - while let Some(item) = first_pass.pop() { - let (vec_dirs, vec_files): (Vec, Vec) = read_dir(&item)? + eprint!("Building file and directory list: "); + while let Some(item) = directory_list.pop() { + let (mut vec_dirs, mut vec_files): (Vec, Vec) = read_dir(&item)? .flatten() .map(|dir_entry| dir_entry.path()) .partition(|path| path.is_dir()); - // change attrs on dir when at the top of a dir tree, so not over written from above - if vec_dirs.is_empty() { - let live_path = self - .live_path(&item) - .ok_or_else(|| HttmError::new("Could not generate live path"))?; + directory_list.append(&mut vec_dirs); + file_list.append(&mut vec_files); + } + eprintln!("OK"); - Preserve::recursive(&item, &live_path)? - } + eprint!("Verifying files and symlinks: "); + // first pass only verify non-directories + file_list.sort_by_key(|path| path.components().count()); - first_pass.extend(vec_dirs.clone()); - second_pass.extend(vec_dirs); + file_list.reverse(); - // first pass only verify non-directories - vec_files.into_iter().try_for_each(|path| { + file_list + .into_iter() + .filter_map(|snap_path| { + self.live_path(&snap_path) + .map(|live_path| (snap_path, live_path)) + }) + .try_for_each(|(snap_path, live_path)| { self.progress_bar.tick(); - let live_path = self - .live_path(&path) - .ok_or_else(|| HttmError::new("Could not generate live path"))?; - is_metadata_same(&path, &live_path) + is_metadata_same(&snap_path, &live_path) })?; - } + self.progress_bar.finish_and_clear(); eprintln!("OK"); eprint!("Verifying directories: "); + // 2nd pass checks dirs - why? we don't check dirs on first pass, + // because copying of data may have changed dir size/mtime + directory_list.sort_by_key(|path| path.components().count()); + + directory_list.reverse(); + + directory_list + .into_iter() + .filter_map(|snap_path| { + self.live_path(&snap_path) + .map(|live_path| (snap_path, live_path)) + }) + .try_for_each(|(snap_path, live_path)| { + self.progress_bar.tick(); + + Preserve::direct(&snap_path, &live_path)?; + + is_metadata_same(&snap_path, &live_path) + })?; + // copy attributes for base dataset, our recursive attr copy does stops // before including the base dataset let live_dataset = self .live_path(&snap_dataset) .ok_or_else(|| HttmError::new("Could not generate live path"))?; - Preserve::direct(&snap_dataset, &live_dataset)?; + let _ = Preserve::direct(&snap_dataset, &live_dataset); - // 2nd pass checks dirs - why? we don't check dirs on first pass, - // because copying of data may have changed dir size/mtime - second_pass.into_iter().try_for_each(|path| { - self.progress_bar.tick(); - let live_path = self - .live_path(&path) - .ok_or_else(|| HttmError::new("Could not generate live path"))?; - - is_metadata_same(&path, &live_path) - })?; self.progress_bar.finish_and_clear(); + eprintln!("OK"); Ok(()) @@ -267,30 +282,12 @@ impl RollForward { .and_then(|path| path.strip_prefix(ZFS_SNAPSHOT_DIRECTORY).ok()) .and_then(|path| path.strip_prefix(&self.snap).ok()) .map(|relative_path| { - [self.proximate_dataset_mount.as_path(), relative_path] + [self.proximate_dataset_mount.as_ref(), relative_path] .into_iter() .collect() }) } - fn zfs_diff_cmd(&self) -> HttmResult { - let zfs_command = which("zfs").map_err(|_err| { - HttmError::new("'zfs' command not found. Make sure the command 'zfs' is in your path.") - })?; - - // -H: tab separated, -t: Specify time, -h: Normalize paths (don't use escape codes) - let full_name = self.full_name(); - let process_args = vec!["diff", "-H", "-t", "-h", &full_name]; - - let process_handle = ExecProcess::new(zfs_command) - .args(&process_args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - Ok(process_handle) - } - fn ingest( output: &mut Option, ) -> HttmResult> + '_> { @@ -343,29 +340,13 @@ impl RollForward { } } - fn spawn_preserve_links( - &self, - ) -> ( - JoinHandle>, - JoinHandle>, - ) { - let snap_dataset = self.snap_dataset(); - - let proximate_dataset_mount = self.proximate_dataset_mount.clone(); - - let snap_handle = std::thread::spawn(move || HardLinkMap::new(&snap_dataset)); - let live_handle = std::thread::spawn(move || HardLinkMap::new(&proximate_dataset_mount)); - - (snap_handle, live_handle) - } - pub fn snap_path(&self, path: &Path) -> Option { PathData::from(path) .relative_path(&self.proximate_dataset_mount) .ok() .map(|relative_path| { let snap_file_path: PathBuf = [ - self.proximate_dataset_mount.as_path(), + self.proximate_dataset_mount.as_ref(), Path::new(ZFS_SNAPSHOT_DIRECTORY), Path::new(&self.snap), relative_path, @@ -415,13 +396,15 @@ impl RollForward { return Err(HttmError::new(&msg).into()); } + Preserve::direct(src, dst)?; + eprintln!("{}: {:?} -> {:?}", Blue.paint("Restored "), src, dst); Ok(()) } - fn snap_dataset(&self) -> PathBuf { + pub fn snap_dataset(&self) -> PathBuf { [ - self.proximate_dataset_mount.as_path(), + self.proximate_dataset_mount.as_ref(), Path::new(ZFS_SNAPSHOT_DIRECTORY), Path::new(&self.snap), ] diff --git a/src/roll_forward/preserve_hard_links.rs b/src/roll_forward/preserve_hard_links.rs index 287bfe69..b40acc2c 100644 --- a/src/roll_forward/preserve_hard_links.rs +++ b/src/roll_forward/preserve_hard_links.rs @@ -16,9 +16,7 @@ // that was distributed with this source code. use crate::data::paths::BasicDirEntryInfo; -use crate::library::file_ops::Copy; -use crate::library::file_ops::Preserve; -use crate::library::file_ops::Remove; +use crate::library::file_ops::{Copy, Preserve, Remove}; use crate::library::results::{HttmError, HttmResult}; use crate::RollForward; use hashbrown::{HashMap, HashSet}; @@ -28,6 +26,28 @@ use std::fs::read_dir; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicBool; +use std::thread::JoinHandle; + +pub struct SpawnPreserveLinks { + pub snap_handle: JoinHandle>, + pub live_handle: JoinHandle>, +} + +impl SpawnPreserveLinks { + pub fn new(roll_forward: &RollForward) -> Self { + let snap_dataset = roll_forward.snap_dataset(); + + let proximate_dataset_mount = roll_forward.proximate_dataset_mount.clone(); + + let snap_handle = std::thread::spawn(move || HardLinkMap::new(&snap_dataset)); + let live_handle = std::thread::spawn(move || HardLinkMap::new(&proximate_dataset_mount)); + + Self { + snap_handle, + live_handle, + } + } +} // key: inode, values: Paths pub struct HardLinkMap { @@ -37,10 +57,7 @@ pub struct HardLinkMap { impl HardLinkMap { pub fn new(requested_path: &Path) -> HttmResult { - let constructed = BasicDirEntryInfo { - path: requested_path.to_path_buf(), - file_type: None, - }; + let constructed = BasicDirEntryInfo::new(requested_path.to_path_buf(), None); let mut queue: Vec = vec![constructed]; let mut tmp: HashMap> = HashMap::new(); @@ -48,28 +65,28 @@ impl HardLinkMap { while let Some(item) = queue.pop() { // no errors will be propagated in recursive mode // far too likely to run into a dir we don't have permissions to view - let (vec_dirs, vec_files): (Vec, Vec) = - read_dir(item.path)? + let (mut vec_dirs, vec_files): (Vec, Vec) = + read_dir(item.path())? .flatten() // checking file_type on dir entries is always preferable // as it is much faster than a metadata call on the path .map(|dir_entry| BasicDirEntryInfo::from(&dir_entry)) - .partition(|dir_entry| dir_entry.path.is_dir()); + .partition(|dir_entry| dir_entry.path().is_dir()); let mut combined = vec_files; - combined.extend_from_slice(&vec_dirs); - queue.extend_from_slice(&vec_dirs); + combined.append(&mut vec_dirs); + queue.append(&mut vec_dirs); combined .into_iter() .filter(|entry| { - if let Some(ft) = entry.file_type { + if let Some(ft) = entry.opt_filetype() { return ft.is_file(); } false }) - .filter_map(|entry| entry.path.metadata().ok().map(|md| (md.ino(), entry))) + .filter_map(|entry| entry.path().metadata().ok().map(|md| (md.ino(), entry))) .for_each(|(ino, entry)| match tmp.get_mut(&ino) { Some(values) => values.push(entry), None => { @@ -86,7 +103,7 @@ impl HardLinkMap { let remainder = remain_tmp .into_values() .flatten() - .map(|entry| entry.path) + .map(|entry| entry.to_path_buf()) .collect(); Ok(Self { @@ -133,7 +150,7 @@ impl<'a> PreserveHardLinks<'a> { .clone() .into_values() .flatten() - .map(|entry| entry.path), + .map(|entry| entry.to_path_buf()), ); eprintln!("Preserving necessary links from the snapshot dataset."); @@ -144,14 +161,14 @@ impl<'a> PreserveHardLinks<'a> { .clone() .into_values() .flatten() - .map(|entry| entry.path), + .map(|entry| entry.to_path_buf()), ); Ok(exclusions) } fn remove_live_links(&self) -> HttmResult<()> { - let none_removed = AtomicBool::new(true); + static NONE_REMOVED: AtomicBool = AtomicBool::new(true); self.live_map .link_map @@ -160,19 +177,19 @@ impl<'a> PreserveHardLinks<'a> { values.iter().try_for_each(|live_path| { let snap_path = self .roll_forward - .snap_path(&live_path.path) + .snap_path(live_path.path()) .ok_or_else(|| HttmError::new("Could obtain live path for snap path"))?; if !snap_path.exists() { - none_removed.store(false, std::sync::atomic::Ordering::Relaxed); - return Self::rm_hard_link(&live_path.path); + NONE_REMOVED.store(false, std::sync::atomic::Ordering::Relaxed); + return Self::rm_hard_link(live_path.path()); } Ok(()) }) })?; - if none_removed.load(std::sync::atomic::Ordering::Relaxed) { + if NONE_REMOVED.load(std::sync::atomic::Ordering::Relaxed) { eprintln!("No hard links found which require removal."); return Ok(()); } @@ -181,23 +198,25 @@ impl<'a> PreserveHardLinks<'a> { } fn preserve_snap_links(&self) -> HttmResult<()> { - let none_preserved = AtomicBool::new(true); + static NONE_PRESERVED: AtomicBool = AtomicBool::new(true); self.snap_map .link_map .par_iter() .try_for_each(|(_key, values)| { - let complemented_paths: Vec<(PathBuf, &PathBuf)> = values + let complemented_paths: Vec<(PathBuf, PathBuf)> = values .iter() .map(|snap_path| { let live_path = - self.roll_forward.live_path(&snap_path.path).ok_or_else(|| { - HttmError::new("Could obtain live path for snap path").into() - }); + self.roll_forward + .live_path(&snap_path.path()) + .ok_or_else(|| { + HttmError::new("Could obtain live path for snap path").into() + }); - live_path.map(|live| (live, &snap_path.path)) + live_path.map(|live| (live, snap_path.path().to_path_buf())) }) - .collect::>>()?; + .collect::>>()?; let mut opt_original = complemented_paths .iter() @@ -208,7 +227,7 @@ impl<'a> PreserveHardLinks<'a> { .iter() .filter(|(_live_path, snap_path)| snap_path.exists()) .try_for_each(|(live_path, snap_path)| { - none_preserved.store(false, std::sync::atomic::Ordering::Relaxed); + NONE_PRESERVED.store(false, std::sync::atomic::Ordering::Relaxed); match opt_original { Some(original) if original == live_path => { @@ -223,7 +242,7 @@ impl<'a> PreserveHardLinks<'a> { }) })?; - if none_preserved.load(std::sync::atomic::Ordering::Relaxed) { + if NONE_PRESERVED.load(std::sync::atomic::Ordering::Relaxed) { println!("No hard links found which require preservation."); return Ok(()); } @@ -252,7 +271,7 @@ impl<'a> PreserveHardLinks<'a> { .flat_map(|(_key, values)| values) .map(|snap_entry| { self.roll_forward - .live_path(&snap_entry.path) + .live_path(&snap_entry.path().to_owned()) .ok_or_else(|| HttmError::new("Could obtain live path for snap path").into()) }) .collect::>>() @@ -291,7 +310,7 @@ impl<'a> PreserveHardLinks<'a> { .clone() .into_values() .flatten() - .map(|entry| entry.path) + .map(|entry| entry.to_path_buf()) .collect(); let orphans_intersection = live_map_as_set.intersection(&snaps_to_live_map); diff --git a/src/library/snap_guard.rs b/src/zfs/run_command.rs similarity index 51% rename from src/library/snap_guard.rs rename to src/zfs/run_command.rs index 5b6953ba..43a75b42 100644 --- a/src/library/snap_guard.rs +++ b/src/zfs/run_command.rs @@ -15,90 +15,37 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -use crate::data::paths::PathData; -use crate::data::paths::PathDeconstruction; +use crate::data::paths::{PathData, PathDeconstruction}; +use crate::filesystem::mounts::FilesystemType; use crate::library::results::{HttmError, HttmResult}; use crate::library::utility::user_has_effective_root; -use crate::library::utility::{date_string, DateFormat}; -use crate::{print_output_buf, GLOBAL_CONFIG}; -use std::path::Path; -use std::process::Command as ExecProcess; -use std::time::SystemTime; +use crate::roll_forward::exec::RollForward; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command as ExecProcess, Stdio}; use which::which; -pub enum PrecautionarySnapType { - PreRollForward, - PostRollForward(String), - PreRestore, +pub struct RunZFSCommand { + pub zfs_command: PathBuf, } -impl TryFrom<&Path> for SnapGuard { - type Error = Box; +impl RunZFSCommand { + pub fn new() -> HttmResult { + let zfs_command = which("zfs").map_err(|_err| { + HttmError::new("'zfs' command not found. Make sure the command 'zfs' is in your path.") + })?; - fn try_from(path: &Path) -> HttmResult { - ZfsAllowPriv::Snapshot.from_path(&path)?; - - let pathdata = PathData::from(path); - - let dataset_name = match pathdata.source(None) { - Some(source) => source, - None => { - return Err(HttmError::new("Could not obtain source dataset for mount: ").into()) - } - }; - - SnapGuard::new( - &dataset_name.to_string_lossy(), - PrecautionarySnapType::PreRestore, - ) + Ok(Self { zfs_command }) } -} - -pub struct SnapGuard { - new_snap_name: String, - dataset_name: String, -} - -impl SnapGuard { - pub fn new(dataset_name: &str, snap_type: PrecautionarySnapType) -> HttmResult { - let zfs_command = which("zfs")?; - - let timestamp = date_string( - GLOBAL_CONFIG.requested_utc_offset, - &SystemTime::now(), - DateFormat::Timestamp, - ); - - let new_snap_name = match &snap_type { - PrecautionarySnapType::PreRollForward => { - // all snapshots should have the same timestamp - let new_snap_name = format!( - "{}@snap_pre_{}_httmSnapRollForward", - dataset_name, timestamp - ); - - new_snap_name - } - PrecautionarySnapType::PostRollForward(additional_snap_info_str) => { - let new_snap_name = format!( - "{}@snap_post_{}_:{}:_httmSnapRollForward", - dataset_name, timestamp, additional_snap_info_str - ); - new_snap_name - } - PrecautionarySnapType::PreRestore => { - // all snapshots should have the same timestamp - let new_snap_name = - format!("{}@snap_pre_{}_httmSnapRestore", dataset_name, timestamp); + pub fn snapshot(&self, snapshot_names: &[String]) -> HttmResult<()> { + let mut process_args = vec!["snapshot".to_owned()]; - new_snap_name - } - }; + process_args.extend_from_slice(snapshot_names); - let process_args = vec!["snapshot".to_owned(), new_snap_name.clone()]; + let process_output = ExecProcess::new(&self.zfs_command) + .args(&process_args) + .output()?; - let process_output = ExecProcess::new(zfs_command).args(&process_args).output()?; let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); // stderr_string is a string not an error, so here we build an err or output @@ -111,39 +58,20 @@ impl SnapGuard { + stderr_string }; - Err(HttmError::new(&msg).into()) - } else { - let output_buf = match &snap_type { - PrecautionarySnapType::PreRollForward | PrecautionarySnapType::PreRestore => { - format!( - "httm took a pre-execution snapshot named: {}\n", - &new_snap_name - ) - } - PrecautionarySnapType::PostRollForward(_) => { - format!( - "httm took a post-execution snapshot named: {}\n", - &new_snap_name - ) - } - }; - - print_output_buf(&output_buf)?; - - Ok(SnapGuard { - new_snap_name, - dataset_name: dataset_name.to_string(), - }) + return Err(HttmError::new(&msg).into()); } + + Ok(()) } - pub fn rollback(&self) -> HttmResult<()> { - ZfsAllowPriv::Rollback.from_fs_name(&self.dataset_name)?; + pub fn rollback(&self, snapshot_names: &[String]) -> HttmResult<()> { + let mut process_args = vec!["rollback".to_owned(), "-r".to_owned()]; - let zfs_command = which("zfs")?; - let process_args = vec!["rollback", "-r", &self.new_snap_name]; + process_args.extend_from_slice(snapshot_names); - let process_output = ExecProcess::new(zfs_command).args(&process_args).output()?; + let process_output = ExecProcess::new(&self.zfs_command) + .args(&process_args) + .output()?; let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); // stderr_string is a string not an error, so here we build an err or output @@ -159,6 +87,78 @@ impl SnapGuard { Ok(()) } + + pub fn prune(&self, snapshot_names: &[String]) -> HttmResult<()> { + let mut process_args = vec!["destroy".to_owned(), "-r".to_owned()]; + + process_args.extend_from_slice(snapshot_names); + + let process_output = ExecProcess::new(&self.zfs_command) + .args(&process_args) + .output()?; + let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); + + // stderr_string is a string not an error, so here we build an err or output + if !stderr_string.is_empty() { + let msg = if stderr_string.contains("cannot destroy snapshots: permission denied") { + "httm must have root privileges to destroy a snapshot filesystem".to_owned() + } else { + "httm was unable to destroy snapshots. The 'zfs' command issued the following error: " + .to_owned() + + stderr_string + }; + + return Err(HttmError::new(&msg).into()); + } + + Ok(()) + } + + pub fn allow(&self, fs_name: &str, allow_type: &ZfsAllowPriv) -> HttmResult<()> { + let process_args = vec!["allow", fs_name]; + + let process_output = ExecProcess::new(&self.zfs_command) + .args(&process_args) + .output()?; + let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); + let stdout_string: &str = std::str::from_utf8(&process_output.stdout)?.trim(); + + // stderr_string is a string not an error, so here we build an err or output + if !stderr_string.is_empty() { + let msg = "httm was unable to determine 'zfs allow' for the path given. The 'zfs' command issued the following error: ".to_owned() + stderr_string; + + return Err(HttmError::new(&msg).into()); + } + + let user_name = std::env::var("USER")?; + + if !stdout_string.contains(&user_name) + || !allow_type + .as_zfs_cmd_strings() + .iter() + .all(|p| stdout_string.contains(p)) + { + let msg = "User does not have 'zfs allow' privileges for the path given."; + + return Err(HttmError::new(msg).into()); + } + + Ok(()) + } + + pub fn diff(&self, roll_forward: &RollForward) -> HttmResult { + // -H: tab separated, -t: Specify time, -h: Normalize paths (don't use escape codes) + let full_name = roll_forward.full_name(); + let process_args = vec!["diff", "-H", "-t", "-h", &full_name]; + + let process_handle = ExecProcess::new(&self.zfs_command) + .args(&process_args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + Ok(process_handle) + } } pub enum ZfsAllowPriv { @@ -167,18 +167,39 @@ pub enum ZfsAllowPriv { } impl ZfsAllowPriv { - pub fn from_path(&self, new_file_path: &Path) -> HttmResult<()> { - let pathdata = PathData::from(new_file_path); + pub fn from_path(&self, path: &Path) -> HttmResult { + let pathdata = PathData::from(path); - let Some(fs_name) = pathdata.source(None) else { + ZfsAllowPriv::from_opt_proximate_dataset(&self, &pathdata, None) + } + + pub fn from_opt_proximate_dataset( + &self, + pathdata: &PathData, + opt_proximate_dataset: Option<&Path>, + ) -> HttmResult { + let Some(fs_name) = pathdata.source(opt_proximate_dataset) else { let msg = format!( "Could not determine dataset name from path given: {:?}", - new_file_path + pathdata.path() ); return Err(HttmError::new(&msg).into()); }; - Self::from_fs_name(&self, &fs_name.to_string_lossy()) + match pathdata.fs_type(opt_proximate_dataset) { + Some(FilesystemType::Zfs) => {} + _ => { + let msg = format!( + "httm only supports snapshot guards for ZFS paths. Path is not located on a ZFS dataset: {:?}", + pathdata.path() + ); + return Err(HttmError::new(&msg).into()); + } + } + + Self::from_fs_name(&self, &fs_name.to_string_lossy())?; + + Ok(fs_name) } pub fn from_fs_name(&self, fs_name: &str) -> HttmResult<()> { @@ -204,34 +225,7 @@ impl ZfsAllowPriv { } fn user_has_zfs_allow_priv(&self, fs_name: &str) -> HttmResult<()> { - let zfs_command = which("zfs")?; - - let process_args = vec!["allow", fs_name]; - - let process_output = ExecProcess::new(zfs_command).args(&process_args).output()?; - let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); - let stdout_string = std::str::from_utf8(&process_output.stdout)?.trim(); - - // stderr_string is a string not an error, so here we build an err or output - if !stderr_string.is_empty() { - let msg = "httm was unable to determine 'zfs allow' for the path given. The 'zfs' command issued the following error: ".to_owned() + stderr_string; - - return Err(HttmError::new(&msg).into()); - } - - let user_name = std::env::var("USER")?; - - if !stdout_string.contains(&user_name) - || !self - .as_zfs_cmd_strings() - .iter() - .all(|p| stdout_string.contains(p)) - { - let msg = "User does not have 'zfs allow' privileges for the path given."; - - return Err(HttmError::new(msg).into()); - } - - Ok(()) + let run_zfs = RunZFSCommand::new()?; + run_zfs.allow(fs_name, self) } } diff --git a/src/zfs/snap_guard.rs b/src/zfs/snap_guard.rs new file mode 100644 index 00000000..0f038600 --- /dev/null +++ b/src/zfs/snap_guard.rs @@ -0,0 +1,121 @@ +// ___ ___ ___ ___ +// /\__\ /\ \ /\ \ /\__\ +// /:/ / \:\ \ \:\ \ /::| | +// /:/__/ \:\ \ \:\ \ /:|:| | +// /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ +// /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ +// \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / +// \::/ / /:/ / /:/ / /:/ / +// /:/ / \/__/ \/__/ /:/ / +// /:/ / /:/ / +// \/__/ \/__/ +// +// Copyright (c) 2023, Robert Swinford gmail.com> +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +use super::run_command::RunZFSCommand; +use crate::library::results::HttmResult; +use crate::library::utility::{date_string, DateFormat}; +use crate::zfs::run_command::ZfsAllowPriv; +use crate::{print_output_buf, GLOBAL_CONFIG}; +use std::path::Path; +use std::time::SystemTime; + +pub enum PrecautionarySnapType { + PreRollForward, + PostRollForward(String), + PreRestore, +} + +impl TryFrom<&Path> for SnapGuard { + type Error = Box; + + fn try_from(path: &Path) -> HttmResult { + // guards the ZFS action, returns source dataset + let allowed_source = ZfsAllowPriv::Snapshot.from_path(&path)?; + + SnapGuard::new( + &allowed_source.to_string_lossy(), + PrecautionarySnapType::PreRestore, + ) + } +} + +pub struct SnapGuard { + new_snap_name: String, + dataset_name: String, +} + +impl SnapGuard { + pub fn new(dataset_name: &str, snap_type: PrecautionarySnapType) -> HttmResult { + let timestamp = date_string( + GLOBAL_CONFIG.requested_utc_offset, + &SystemTime::now(), + DateFormat::Timestamp, + ); + + let new_snap_name = match &snap_type { + PrecautionarySnapType::PreRollForward => { + // all snapshots should have the same timestamp + let new_snap_name = format!( + "{}@snap_pre_{}_httmSnapRollForward", + dataset_name, timestamp + ); + + new_snap_name + } + PrecautionarySnapType::PostRollForward(additional_snap_info_str) => { + let new_snap_name = format!( + "{}@snap_post_{}_:{}:_httmSnapRollForward", + dataset_name, timestamp, additional_snap_info_str + ); + + new_snap_name + } + PrecautionarySnapType::PreRestore => { + // all snapshots should have the same timestamp + let new_snap_name = + format!("{}@snap_pre_{}_httmSnapRestore", dataset_name, timestamp); + + new_snap_name + } + }; + + let run_zfs = RunZFSCommand::new()?; + + run_zfs.snapshot(&[new_snap_name.clone()])?; + + let output_buf = match &snap_type { + PrecautionarySnapType::PreRollForward | PrecautionarySnapType::PreRestore => { + format!( + "httm took a pre-execution snapshot named: {}\n", + &new_snap_name + ) + } + PrecautionarySnapType::PostRollForward(_) => { + format!( + "httm took a post-execution snapshot named: {}\n", + &new_snap_name + ) + } + }; + + print_output_buf(&output_buf)?; + + Ok(SnapGuard { + new_snap_name, + dataset_name: dataset_name.to_string(), + }) + } + + pub fn rollback(&self) -> HttmResult<()> { + ZfsAllowPriv::Rollback.from_fs_name(&self.dataset_name)?; + + let run_zfs = RunZFSCommand::new()?; + run_zfs.rollback(&[self.new_snap_name.to_owned()])?; + + Ok(()) + } +} diff --git a/src/library/snap_mounts.rs b/src/zfs/snap_mounts.rs similarity index 50% rename from src/library/snap_mounts.rs rename to src/zfs/snap_mounts.rs index 01a9bfa6..93daaf23 100644 --- a/src/library/snap_mounts.rs +++ b/src/zfs/snap_mounts.rs @@ -15,16 +15,14 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +use super::run_command::{RunZFSCommand, ZfsAllowPriv}; use crate::config::generate::PrintMode; use crate::library::iter_extensions::HttmIter; use crate::library::results::{HttmError, HttmResult}; use crate::library::utility::{date_string, delimiter, print_output_buf, DateFormat}; -use crate::lookup::file_mounts::MountDisplay; -use crate::lookup::file_mounts::MountsForFiles; -use crate::parse::mounts::FilesystemType; +use crate::lookup::file_mounts::{MountDisplay, MountsForFiles}; use crate::GLOBAL_CONFIG; use std::collections::BTreeMap; -use std::process::Command as ExecProcess; use std::time::SystemTime; pub struct SnapshotMounts; @@ -33,60 +31,46 @@ impl SnapshotMounts { pub fn exec(requested_snapshot_suffix: &str) -> HttmResult<()> { let mounts_for_files: MountsForFiles = MountsForFiles::new(&MountDisplay::Target)?; - Self::snapshot_mounts(&mounts_for_files, requested_snapshot_suffix) - } + let map_snapshot_names = + Self::snapshot_names(&mounts_for_files, requested_snapshot_suffix)?; - fn snapshot_mounts( - mounts_for_files: &MountsForFiles, - requested_snapshot_suffix: &str, - ) -> HttmResult<()> { - let zfs_command = which::which("zfs").map_err(|_err| { - HttmError::new("'zfs' command not found. Make sure the command 'zfs' is in your path.") - })?; - let map_snapshot_names = Self::snapshot_names(mounts_for_files, requested_snapshot_suffix)?; - - map_snapshot_names.iter().try_for_each(|(_pool_name, snapshot_names)| { - let mut process_args = vec!["snapshot".to_owned()]; - process_args.extend_from_slice(snapshot_names); - - let process_output = ExecProcess::new(&zfs_command) - .args(&process_args) - .output()?; - let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim(); - - // stderr_string is a string not an error, so here we build an err or output - if !stderr_string.is_empty() { - let msg = if stderr_string.contains("cannot create snapshots : permission denied") { - "httm must have root privileges to snapshot a filesystem".to_owned() - } else { - "httm was unable to take snapshots. The 'zfs' command issued the following error: " - .to_owned() - + stderr_string - }; + let run_zfs = RunZFSCommand::new()?; - Err(HttmError::new(&msg).into()) - } else { - let output_buf: String = snapshot_names - .iter() - .map(|snap_name| { - if matches!( - GLOBAL_CONFIG.print_mode, - PrintMode::RawNewline | PrintMode::RawZero - ) { + map_snapshot_names.values().try_for_each(|snapshot_names| { + run_zfs.snapshot(snapshot_names)?; + + let output_buf: String = snapshot_names + .iter() + .map(|snap_name| { + if let PrintMode::Raw(_) = GLOBAL_CONFIG.print_mode { let delimiter = delimiter(); format!("{}{delimiter}", &snap_name) } else { format!("httm took a snapshot named: {}\n", &snap_name) } - }) - .collect(); - print_output_buf(&output_buf) - } + }) + .collect(); + + print_output_buf(&output_buf) })?; Ok(()) } + pub fn pool_from_snap_name(snapshot_name: &str) -> HttmResult { + // split on "/" why? because a snap looks like: rpool/kimono@snap... + // splits according to pool name, then the rest of the snap name + match snapshot_name.split_once('/') { + Some((pool_name, _snap_name)) => Ok(pool_name.into()), + None => { + let msg = format!( + "Could not determine pool name from the constructed snapshot name: {snapshot_name}" + ); + Err(HttmError::new(&msg).into()) + } + } + } + fn snapshot_names( mounts_for_files: &MountsForFiles, requested_snapshot_suffix: &str, @@ -100,39 +84,18 @@ impl SnapshotMounts { let vec_snapshot_names: Vec = mounts_for_files .iter() - .flat_map(|prox| prox.datasets_of_interest()) - .map(|mount| { - let dataset = match &GLOBAL_CONFIG.dataset_collection.opt_map_of_aliases { - None => { - match GLOBAL_CONFIG - .dataset_collection - .map_of_datasets - .get(mount) - { - Some(dataset_info) => { - if let FilesystemType::Zfs = dataset_info.fs_type { - Ok(dataset_info.source.to_string_lossy()) - } else { - Err(HttmError::new( - "httm does not currently support snapshot-ing non-ZFS filesystems.", - )) - } - } - None => { - return Err(HttmError::new( - "httm was unable to parse dataset from mount!", - )) - } - } - } - Some(_) => return Err(HttmError::new( - "httm does not currently support snapshot-ing user defined mount points.", - )), - }?; + .map(|prox| { + let pathdata = prox.pathdata; + + let fs_name = ZfsAllowPriv::Snapshot + .from_opt_proximate_dataset(&pathdata, Some(prox.proximate_dataset)) + .map_err(|err| HttmError::from(err))?; let snapshot_name = format!( "{}@snap_{}_{}", - dataset, timestamp, requested_snapshot_suffix, + fs_name.to_string_lossy(), + timestamp, + requested_snapshot_suffix, ); Ok(snapshot_name) @@ -153,7 +116,7 @@ impl SnapshotMounts { .into_iter() .into_group_map_by(|snapshot_name| { Self::pool_from_snap_name(snapshot_name).unwrap_or_else(|err| { - eprintln!("{}", err); + eprintln!("ERROR: {:?}", err); std::process::exit(1) }) }) @@ -171,21 +134,4 @@ impl SnapshotMounts { Ok(map_snapshot_names) } - - fn pool_from_snap_name(snapshot_name: &str) -> HttmResult { - // split on "/" why? because a snap looks like: rpool/kimono@snap... - // splits according to pool name, then the rest of the snap name - match snapshot_name.split_once('@') { - Some((dataset_name, _snap_name)) => match dataset_name.split_once('/') { - Some((pool_name, _the_rest)) => Ok(pool_name.into()), - None => Ok(dataset_name.into()), - }, - None => { - let msg = format!( - "Could not determine pool name from the constructed snapshot name: {snapshot_name}" - ); - Err(HttmError::new(&msg).into()) - } - } - } } diff --git a/third_party/LICENSES_THIRD_PARTY.html b/third_party/LICENSES_THIRD_PARTY.html index 9641d59a..bd3bbfd6 100644 --- a/third_party/LICENSES_THIRD_PARTY.html +++ b/third_party/LICENSES_THIRD_PARTY.html @@ -46,10 +46,11 @@

Third Party Licenses

Overview of licenses:

All license text:

@@ -454,7 +455,6 @@

Apache License 2.0

Used by:

@@ -665,7 +665,7 @@ 

Used by:

Apache License 2.0

Used by:

                                  Apache License
@@ -856,7 +856,7 @@ 

Used by:

same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Jacob Pratt et al. + Copyright 2023 Jacob Pratt Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -875,9 +875,10 @@

Used by:

Apache License 2.0

Used by:

-
                                 Apache License
+                
+                                 Apache License
                            Version 2.0, January 2004
                         http://www.apache.org/licenses/
 
@@ -1065,7 +1066,7 @@ 

Used by:

same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 The Fuchsia Authors + Copyright 2023 Jacob Pratt et al. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1078,19 +1079,16 @@

Used by:

WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -
  • Apache License 2.0

    Used by:

    -
                                     Apache License
    +                
    +                                 Apache License
                                Version 2.0, January 2004
                             http://www.apache.org/licenses/
     
    @@ -1278,7 +1276,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2024 Jacob Pratt et al. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1297,12 +1295,10 @@

    Used by:

    Apache License 2.0

    Used by:

    -
                                     Apache License
    +                
    +                                 Apache License
                                Version 2.0, January 2004
                             http://www.apache.org/licenses/
     
    @@ -1482,7 +1478,7 @@ 

    Used by:

    APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -1490,7 +1486,7 @@

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1503,637 +1499,881 @@

    Used by:

    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -
  • Apache License 2.0

    Used by:

    -
                                  Apache License
    -                        Version 2.0, January 2004
    -                     http://www.apache.org/licenses/
    +                
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
     
    -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    -1. Definitions.
    +   1. Definitions.
     
    -   "License" shall mean the terms and conditions for use, reproduction,
    -   and distribution as defined by Sections 1 through 9 of this document.
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
     
    -   "Licensor" shall mean the copyright owner or entity authorized by
    -   the copyright owner that is granting the License.
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
     
    -   "Legal Entity" shall mean the union of the acting entity and all
    -   other entities that control, are controlled by, or are under common
    -   control with that entity. For the purposes of this definition,
    -   "control" means (i) the power, direct or indirect, to cause the
    -   direction or management of such entity, whether by contract or
    -   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    -   outstanding shares, or (iii) beneficial ownership of such entity.
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
     
    -   "You" (or "Your") shall mean an individual or Legal Entity
    -   exercising permissions granted by this License.
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
     
    -   "Source" form shall mean the preferred form for making modifications,
    -   including but not limited to software source code, documentation
    -   source, and configuration files.
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
     
    -   "Object" form shall mean any form resulting from mechanical
    -   transformation or translation of a Source form, including but
    -   not limited to compiled object code, generated documentation,
    -   and conversions to other media types.
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
     
    -   "Work" shall mean the work of authorship, whether in Source or
    -   Object form, made available under the License, as indicated by a
    -   copyright notice that is included in or attached to the work
    -   (an example is provided in the Appendix below).
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
     
    -   "Derivative Works" shall mean any work, whether in Source or Object
    -   form, that is based on (or derived from) the Work and for which the
    -   editorial revisions, annotations, elaborations, or other modifications
    -   represent, as a whole, an original work of authorship. For the purposes
    -   of this License, Derivative Works shall not include works that remain
    -   separable from, or merely link (or bind by name) to the interfaces of,
    -   the Work and Derivative Works thereof.
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
     
    -   "Contribution" shall mean any work of authorship, including
    -   the original version of the Work and any modifications or additions
    -   to that Work or Derivative Works thereof, that is intentionally
    -   submitted to Licensor for inclusion in the Work by the copyright owner
    -   or by an individual or Legal Entity authorized to submit on behalf of
    -   the copyright owner. For the purposes of this definition, "submitted"
    -   means any form of electronic, verbal, or written communication sent
    -   to the Licensor or its representatives, including but not limited to
    -   communication on electronic mailing lists, source code control systems,
    -   and issue tracking systems that are managed by, or on behalf of, the
    -   Licensor for the purpose of discussing and improving the Work, but
    -   excluding communication that is conspicuously marked or otherwise
    -   designated in writing by the copyright owner as "Not a Contribution."
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
     
    -   "Contributor" shall mean Licensor and any individual or Legal Entity
    -   on behalf of whom a Contribution has been received by Licensor and
    -   subsequently incorporated within the Work.
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
     
    -2. Grant of Copyright License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   copyright license to reproduce, prepare Derivative Works of,
    -   publicly display, publicly perform, sublicense, and distribute the
    -   Work and such Derivative Works in Source or Object form.
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
     
    -3. Grant of Patent License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   (except as stated in this section) patent license to make, have made,
    -   use, offer to sell, sell, import, and otherwise transfer the Work,
    -   where such license applies only to those patent claims licensable
    -   by such Contributor that are necessarily infringed by their
    -   Contribution(s) alone or by combination of their Contribution(s)
    -   with the Work to which such Contribution(s) was submitted. If You
    -   institute patent litigation against any entity (including a
    -   cross-claim or counterclaim in a lawsuit) alleging that the Work
    -   or a Contribution incorporated within the Work constitutes direct
    -   or contributory patent infringement, then any patent licenses
    -   granted to You under this License for that Work shall terminate
    -   as of the date such litigation is filed.
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
     
    -4. Redistribution. You may reproduce and distribute copies of the
    -   Work or Derivative Works thereof in any medium, with or without
    -   modifications, and in Source or Object form, provided that You
    -   meet the following conditions:
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
     
    -   (a) You must give any other recipients of the Work or
    -       Derivative Works a copy of this License; and
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
     
    -   (b) You must cause any modified files to carry prominent notices
    -       stating that You changed the files; and
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
     
    -   (c) You must retain, in the Source form of any Derivative Works
    -       that You distribute, all copyright, patent, trademark, and
    -       attribution notices from the Source form of the Work,
    -       excluding those notices that do not pertain to any part of
    -       the Derivative Works; and
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
     
    -   (d) If the Work includes a "NOTICE" text file as part of its
    -       distribution, then any Derivative Works that You distribute must
    -       include a readable copy of the attribution notices contained
    -       within such NOTICE file, excluding those notices that do not
    -       pertain to any part of the Derivative Works, in at least one
    -       of the following places: within a NOTICE text file distributed
    -       as part of the Derivative Works; within the Source form or
    -       documentation, if provided along with the Derivative Works; or,
    -       within a display generated by the Derivative Works, if and
    -       wherever such third-party notices normally appear. The contents
    -       of the NOTICE file are for informational purposes only and
    -       do not modify the License. You may add Your own attribution
    -       notices within Derivative Works that You distribute, alongside
    -       or as an addendum to the NOTICE text from the Work, provided
    -       that such additional attribution notices cannot be construed
    -       as modifying the License.
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
     
    -   You may add Your own copyright statement to Your modifications and
    -   may provide additional or different license terms and conditions
    -   for use, reproduction, or distribution of Your modifications, or
    -   for any such Derivative Works as a whole, provided Your use,
    -   reproduction, and distribution of the Work otherwise complies with
    -   the conditions stated in this License.
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
     
    -5. Submission of Contributions. Unless You explicitly state otherwise,
    -   any Contribution intentionally submitted for inclusion in the Work
    -   by You to the Licensor shall be under the terms and conditions of
    -   this License, without any additional terms or conditions.
    -   Notwithstanding the above, nothing herein shall supersede or modify
    -   the terms of any separate license agreement you may have executed
    -   with Licensor regarding such Contributions.
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
     
    -6. Trademarks. This License does not grant permission to use the trade
    -   names, trademarks, service marks, or product names of the Licensor,
    -   except as required for reasonable and customary use in describing the
    -   origin of the Work and reproducing the content of the NOTICE file.
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
     
    -7. Disclaimer of Warranty. Unless required by applicable law or
    -   agreed to in writing, Licensor provides the Work (and each
    -   Contributor provides its Contributions) on an "AS IS" BASIS,
    -   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    -   implied, including, without limitation, any warranties or conditions
    -   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    -   PARTICULAR PURPOSE. You are solely responsible for determining the
    -   appropriateness of using or redistributing the Work and assume any
    -   risks associated with Your exercise of permissions under this License.
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
     
    -8. Limitation of Liability. In no event and under no legal theory,
    -   whether in tort (including negligence), contract, or otherwise,
    -   unless required by applicable law (such as deliberate and grossly
    -   negligent acts) or agreed to in writing, shall any Contributor be
    -   liable to You for damages, including any direct, indirect, special,
    -   incidental, or consequential damages of any character arising as a
    -   result of this License or out of the use or inability to use the
    -   Work (including but not limited to damages for loss of goodwill,
    -   work stoppage, computer failure or malfunction, or any and all
    -   other commercial damages or losses), even if such Contributor
    -   has been advised of the possibility of such damages.
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
     
    -9. Accepting Warranty or Additional Liability. While redistributing
    -   the Work or Derivative Works thereof, You may choose to offer,
    -   and charge a fee for, acceptance of support, warranty, indemnity,
    -   or other liability obligations and/or rights consistent with this
    -   License. However, in accepting such obligations, You may act only
    -   on Your own behalf and on Your sole responsibility, not on behalf
    -   of any other Contributor, and only if You agree to indemnify,
    -   defend, and hold each Contributor harmless for any liability
    -   incurred by, or claims asserted against, such Contributor by reason
    -   of your accepting any such warranty or additional liability.
    -
    -
  • -
  • -

    Apache License 2.0

    -

    Used by:

    - -
                                  Apache License
    -                        Version 2.0, January 2004
    -                     http://www.apache.org/licenses/
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
     
    -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +   END OF TERMS AND CONDITIONS
     
    -1. Definitions.
    +   APPENDIX: How to apply the Apache License to your work.
     
    -   "License" shall mean the terms and conditions for use, reproduction,
    -   and distribution as defined by Sections 1 through 9 of this document.
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
     
    -   "Licensor" shall mean the copyright owner or entity authorized by
    -   the copyright owner that is granting the License.
    +   Copyright [yyyy] [name of copyright owner]
     
    -   "Legal Entity" shall mean the union of the acting entity and all
    -   other entities that control, are controlled by, or are under common
    -   control with that entity. For the purposes of this definition,
    -   "control" means (i) the power, direct or indirect, to cause the
    -   direction or management of such entity, whether by contract or
    -   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    -   outstanding shares, or (iii) beneficial ownership of such entity.
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
     
    -   "You" (or "Your") shall mean an individual or Legal Entity
    -   exercising permissions granted by this License.
    +       http://www.apache.org/licenses/LICENSE-2.0
     
    -   "Source" form shall mean the preferred form for making modifications,
    -   including but not limited to software source code, documentation
    -   source, and configuration files.
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
     
    -   "Object" form shall mean any form resulting from mechanical
    -   transformation or translation of a Source form, including but
    -   not limited to compiled object code, generated documentation,
    -   and conversions to other media types.
     
    -   "Work" shall mean the work of authorship, whether in Source or
    -   Object form, made available under the License, as indicated by a
    -   copyright notice that is included in or attached to the work
    -   (an example is provided in the Appendix below).
    +--- LLVM Exceptions to the Apache 2.0 License ----
     
    -   "Derivative Works" shall mean any work, whether in Source or Object
    -   form, that is based on (or derived from) the Work and for which the
    -   editorial revisions, annotations, elaborations, or other modifications
    -   represent, as a whole, an original work of authorship. For the purposes
    -   of this License, Derivative Works shall not include works that remain
    -   separable from, or merely link (or bind by name) to the interfaces of,
    -   the Work and Derivative Works thereof.
    +As an exception, if, as a result of your compiling your source code, portions
    +of this Software are embedded into an Object form of such source code, you
    +may redistribute such embedded portions in such Object form without complying
    +with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
     
    -   "Contribution" shall mean any work of authorship, including
    -   the original version of the Work and any modifications or additions
    -   to that Work or Derivative Works thereof, that is intentionally
    -   submitted to Licensor for inclusion in the Work by the copyright owner
    -   or by an individual or Legal Entity authorized to submit on behalf of
    -   the copyright owner. For the purposes of this definition, "submitted"
    -   means any form of electronic, verbal, or written communication sent
    -   to the Licensor or its representatives, including but not limited to
    -   communication on electronic mailing lists, source code control systems,
    -   and issue tracking systems that are managed by, or on behalf of, the
    -   Licensor for the purpose of discussing and improving the Work, but
    -   excluding communication that is conspicuously marked or otherwise
    -   designated in writing by the copyright owner as "Not a Contribution."
    +In addition, if you combine or link compiled forms of this Software with
    +software that is licensed under the GPLv2 ("Combined Software") and if a
    +court of competent jurisdiction determines that the patent provision (Section
    +3), the indemnity provision (Section 9) or other Section of the License
    +conflicts with the conditions of the GPLv2, you may retroactively and
    +prospectively choose to deem waived or otherwise exclude such Section(s) of
    +the License, but only in their entirety and only with respect to the Combined
    +Software.
     
    -   "Contributor" shall mean Licensor and any individual or Legal Entity
    -   on behalf of whom a Contribution has been received by Licensor and
    -   subsequently incorporated within the Work.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
     
    -2. Grant of Copyright License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   copyright license to reproduce, prepare Derivative Works of,
    -   publicly display, publicly perform, sublicense, and distribute the
    -   Work and such Derivative Works in Source or Object form.
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    -3. Grant of Patent License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   (except as stated in this section) patent license to make, have made,
    -   use, offer to sell, sell, import, and otherwise transfer the Work,
    -   where such license applies only to those patent claims licensable
    -   by such Contributor that are necessarily infringed by their
    -   Contribution(s) alone or by combination of their Contribution(s)
    -   with the Work to which such Contribution(s) was submitted. If You
    -   institute patent litigation against any entity (including a
    -   cross-claim or counterclaim in a lawsuit) alleging that the Work
    -   or a Contribution incorporated within the Work constitutes direct
    -   or contributory patent infringement, then any patent licenses
    -   granted to You under this License for that Work shall terminate
    -   as of the date such litigation is filed.
    +   1. Definitions.
     
    -4. Redistribution. You may reproduce and distribute copies of the
    -   Work or Derivative Works thereof in any medium, with or without
    -   modifications, and in Source or Object form, provided that You
    -   meet the following conditions:
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
     
    -   (a) You must give any other recipients of the Work or
    -       Derivative Works a copy of this License; and
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
     
    -   (b) You must cause any modified files to carry prominent notices
    -       stating that You changed the files; and
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
     
    -   (c) You must retain, in the Source form of any Derivative Works
    -       that You distribute, all copyright, patent, trademark, and
    -       attribution notices from the Source form of the Work,
    -       excluding those notices that do not pertain to any part of
    -       the Derivative Works; and
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
     
    -   (d) If the Work includes a "NOTICE" text file as part of its
    -       distribution, then any Derivative Works that You distribute must
    -       include a readable copy of the attribution notices contained
    -       within such NOTICE file, excluding those notices that do not
    -       pertain to any part of the Derivative Works, in at least one
    -       of the following places: within a NOTICE text file distributed
    -       as part of the Derivative Works; within the Source form or
    -       documentation, if provided along with the Derivative Works; or,
    -       within a display generated by the Derivative Works, if and
    -       wherever such third-party notices normally appear. The contents
    -       of the NOTICE file are for informational purposes only and
    -       do not modify the License. You may add Your own attribution
    -       notices within Derivative Works that You distribute, alongside
    -       or as an addendum to the NOTICE text from the Work, provided
    -       that such additional attribution notices cannot be construed
    -       as modifying the License.
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
     
    -   You may add Your own copyright statement to Your modifications and
    -   may provide additional or different license terms and conditions
    -   for use, reproduction, or distribution of Your modifications, or
    -   for any such Derivative Works as a whole, provided Your use,
    -   reproduction, and distribution of the Work otherwise complies with
    -   the conditions stated in this License.
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
     
    -5. Submission of Contributions. Unless You explicitly state otherwise,
    -   any Contribution intentionally submitted for inclusion in the Work
    -   by You to the Licensor shall be under the terms and conditions of
    -   this License, without any additional terms or conditions.
    -   Notwithstanding the above, nothing herein shall supersede or modify
    -   the terms of any separate license agreement you may have executed
    -   with Licensor regarding such Contributions.
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
     
    -6. Trademarks. This License does not grant permission to use the trade
    -   names, trademarks, service marks, or product names of the Licensor,
    -   except as required for reasonable and customary use in describing the
    -   origin of the Work and reproducing the content of the NOTICE file.
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
     
    -7. Disclaimer of Warranty. Unless required by applicable law or
    -   agreed to in writing, Licensor provides the Work (and each
    -   Contributor provides its Contributions) on an "AS IS" BASIS,
    -   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    -   implied, including, without limitation, any warranties or conditions
    -   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    -   PARTICULAR PURPOSE. You are solely responsible for determining the
    -   appropriateness of using or redistributing the Work and assume any
    -   risks associated with Your exercise of permissions under this License.
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
     
    -8. Limitation of Liability. In no event and under no legal theory,
    -   whether in tort (including negligence), contract, or otherwise,
    -   unless required by applicable law (such as deliberate and grossly
    -   negligent acts) or agreed to in writing, shall any Contributor be
    -   liable to You for damages, including any direct, indirect, special,
    -   incidental, or consequential damages of any character arising as a
    -   result of this License or out of the use or inability to use the
    -   Work (including but not limited to damages for loss of goodwill,
    -   work stoppage, computer failure or malfunction, or any and all
    -   other commercial damages or losses), even if such Contributor
    -   has been advised of the possibility of such damages.
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
     
    -9. Accepting Warranty or Additional Liability. While redistributing
    -   the Work or Derivative Works thereof, You may choose to offer,
    -   and charge a fee for, acceptance of support, warranty, indemnity,
    -   or other liability obligations and/or rights consistent with this
    -   License. However, in accepting such obligations, You may act only
    -   on Your own behalf and on Your sole responsibility, not on behalf
    -   of any other Contributor, and only if You agree to indemnify,
    -   defend, and hold each Contributor harmless for any liability
    -   incurred by, or claims asserted against, such Contributor by reason
    -   of your accepting any such warranty or additional liability.
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
     
    -END OF TERMS AND CONDITIONS
    -
    -
  • -
  • -

    Apache License 2.0

    -

    Used by:

    - -
                                  Apache License
    -                        Version 2.0, January 2004
    -                     http://www.apache.org/licenses/
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
     
    -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
     
    -1. Definitions.
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
     
    -   "License" shall mean the terms and conditions for use, reproduction,
    -   and distribution as defined by Sections 1 through 9 of this document.
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
     
    -   "Licensor" shall mean the copyright owner or entity authorized by
    -   the copyright owner that is granting the License.
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
     
    -   "Legal Entity" shall mean the union of the acting entity and all
    -   other entities that control, are controlled by, or are under common
    -   control with that entity. For the purposes of this definition,
    -   "control" means (i) the power, direct or indirect, to cause the
    -   direction or management of such entity, whether by contract or
    -   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    -   outstanding shares, or (iii) beneficial ownership of such entity.
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
     
    -   "You" (or "Your") shall mean an individual or Legal Entity
    -   exercising permissions granted by this License.
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
     
    -   "Source" form shall mean the preferred form for making modifications,
    -   including but not limited to software source code, documentation
    -   source, and configuration files.
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
     
    -   "Object" form shall mean any form resulting from mechanical
    -   transformation or translation of a Source form, including but
    -   not limited to compiled object code, generated documentation,
    -   and conversions to other media types.
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
     
    -   "Work" shall mean the work of authorship, whether in Source or
    -   Object form, made available under the License, as indicated by a
    -   copyright notice that is included in or attached to the work
    -   (an example is provided in the Appendix below).
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
     
    -   "Derivative Works" shall mean any work, whether in Source or Object
    -   form, that is based on (or derived from) the Work and for which the
    -   editorial revisions, annotations, elaborations, or other modifications
    -   represent, as a whole, an original work of authorship. For the purposes
    -   of this License, Derivative Works shall not include works that remain
    -   separable from, or merely link (or bind by name) to the interfaces of,
    -   the Work and Derivative Works thereof.
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
     
    -   "Contribution" shall mean any work of authorship, including
    -   the original version of the Work and any modifications or additions
    -   to that Work or Derivative Works thereof, that is intentionally
    -   submitted to Licensor for inclusion in the Work by the copyright owner
    -   or by an individual or Legal Entity authorized to submit on behalf of
    -   the copyright owner. For the purposes of this definition, "submitted"
    -   means any form of electronic, verbal, or written communication sent
    -   to the Licensor or its representatives, including but not limited to
    -   communication on electronic mailing lists, source code control systems,
    -   and issue tracking systems that are managed by, or on behalf of, the
    -   Licensor for the purpose of discussing and improving the Work, but
    -   excluding communication that is conspicuously marked or otherwise
    -   designated in writing by the copyright owner as "Not a Contribution."
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
     
    -   "Contributor" shall mean Licensor and any individual or Legal Entity
    -   on behalf of whom a Contribution has been received by Licensor and
    -   subsequently incorporated within the Work.
    +   END OF TERMS AND CONDITIONS
     
    -2. Grant of Copyright License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   copyright license to reproduce, prepare Derivative Works of,
    -   publicly display, publicly perform, sublicense, and distribute the
    -   Work and such Derivative Works in Source or Object form.
    +   APPENDIX: How to apply the Apache License to your work.
     
    -3. Grant of Patent License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   (except as stated in this section) patent license to make, have made,
    -   use, offer to sell, sell, import, and otherwise transfer the Work,
    -   where such license applies only to those patent claims licensable
    -   by such Contributor that are necessarily infringed by their
    -   Contribution(s) alone or by combination of their Contribution(s)
    -   with the Work to which such Contribution(s) was submitted. If You
    -   institute patent litigation against any entity (including a
    -   cross-claim or counterclaim in a lawsuit) alleging that the Work
    -   or a Contribution incorporated within the Work constitutes direct
    -   or contributory patent infringement, then any patent licenses
    -   granted to You under this License for that Work shall terminate
    -   as of the date such litigation is filed.
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
     
    -4. Redistribution. You may reproduce and distribute copies of the
    -   Work or Derivative Works thereof in any medium, with or without
    -   modifications, and in Source or Object form, provided that You
    -   meet the following conditions:
    +   Copyright [yyyy] [name of copyright owner]
     
    -   (a) You must give any other recipients of the Work or
    -       Derivative Works a copy of this License; and
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
     
    -   (b) You must cause any modified files to carry prominent notices
    -       stating that You changed the files; and
    +       http://www.apache.org/licenses/LICENSE-2.0
     
    -   (c) You must retain, in the Source form of any Derivative Works
    -       that You distribute, all copyright, patent, trademark, and
    -       attribution notices from the Source form of the Work,
    -       excluding those notices that do not pertain to any part of
    -       the Derivative Works; and
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
     
    -   (d) If the Work includes a "NOTICE" text file as part of its
    -       distribution, then any Derivative Works that You distribute must
    -       include a readable copy of the attribution notices contained
    -       within such NOTICE file, excluding those notices that do not
    -       pertain to any part of the Derivative Works, in at least one
    -       of the following places: within a NOTICE text file distributed
    -       as part of the Derivative Works; within the Source form or
    -       documentation, if provided along with the Derivative Works; or,
    -       within a display generated by the Derivative Works, if and
    -       wherever such third-party notices normally appear. The contents
    -       of the NOTICE file are for informational purposes only and
    -       do not modify the License. You may add Your own attribution
    -       notices within Derivative Works that You distribute, alongside
    -       or as an addendum to the NOTICE text from the Work, provided
    -       that such additional attribution notices cannot be construed
    -       as modifying the License.
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    -   You may add Your own copyright statement to Your modifications and
    -   may provide additional or different license terms and conditions
    -   for use, reproduction, or distribution of Your modifications, or
    -   for any such Derivative Works as a whole, provided Your use,
    -   reproduction, and distribution of the Work otherwise complies with
    -   the conditions stated in this License.
    +   1. Definitions.
     
    -5. Submission of Contributions. Unless You explicitly state otherwise,
    -   any Contribution intentionally submitted for inclusion in the Work
    -   by You to the Licensor shall be under the terms and conditions of
    -   this License, without any additional terms or conditions.
    -   Notwithstanding the above, nothing herein shall supersede or modify
    -   the terms of any separate license agreement you may have executed
    -   with Licensor regarding such Contributions.
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
     
    -6. Trademarks. This License does not grant permission to use the trade
    -   names, trademarks, service marks, or product names of the Licensor,
    -   except as required for reasonable and customary use in describing the
    -   origin of the Work and reproducing the content of the NOTICE file.
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
     
    -7. Disclaimer of Warranty. Unless required by applicable law or
    -   agreed to in writing, Licensor provides the Work (and each
    -   Contributor provides its Contributions) on an "AS IS" BASIS,
    -   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    -   implied, including, without limitation, any warranties or conditions
    -   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    -   PARTICULAR PURPOSE. You are solely responsible for determining the
    -   appropriateness of using or redistributing the Work and assume any
    -   risks associated with Your exercise of permissions under this License.
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
     
    -8. Limitation of Liability. In no event and under no legal theory,
    -   whether in tort (including negligence), contract, or otherwise,
    -   unless required by applicable law (such as deliberate and grossly
    -   negligent acts) or agreed to in writing, shall any Contributor be
    -   liable to You for damages, including any direct, indirect, special,
    -   incidental, or consequential damages of any character arising as a
    -   result of this License or out of the use or inability to use the
    -   Work (including but not limited to damages for loss of goodwill,
    -   work stoppage, computer failure or malfunction, or any and all
    -   other commercial damages or losses), even if such Contributor
    -   has been advised of the possibility of such damages.
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "{}"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright {yyyy} {name of copyright owner}
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
     
    -9. Accepting Warranty or Additional Liability. While redistributing
    -   the Work or Derivative Works thereof, You may choose to offer,
    -   and charge a fee for, acceptance of support, warranty, indemnity,
    -   or other liability obligations and/or rights consistent with this
    -   License. However, in accepting such obligations, You may act only
    -   on Your own behalf and on Your sole responsibility, not on behalf
    -   of any other Contributor, and only if You agree to indemnify,
    -   defend, and hold each Contributor harmless for any liability
    -   incurred by, or claims asserted against, such Contributor by reason
    -   of your accepting any such warranty or additional liability.
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
     
    -END OF TERMS AND CONDITIONS
    +   END OF TERMS AND CONDITIONS
     
    -APPENDIX: How to apply the Apache License to your work.
    +   APPENDIX: How to apply the Apache License to your work.
     
    -   To apply the Apache License to your work, attach the following
    -   boilerplate notice, with the fields enclosed by brackets "[]"
    -   replaced with your own identifying information. (Don't include
    -   the brackets!)  The text should be enclosed in the appropriate
    -   comment syntax for the file format. We also recommend that a
    -   file or class name and description of purpose be included on the
    -   same "printed page" as the copyright notice for easier
    -   identification within third-party archives.
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "{}"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
     
    -Copyright 2020 Andrew Straw
    +   Copyright {yyyy} {name of copyright owner}
     
    -Licensed under the Apache License, Version 2.0 (the "License");
    -you may not use this file except in compliance with the License.
    -You may obtain a copy of the License at
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
     
    -	http://www.apache.org/licenses/LICENSE-2.0
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
     
    -Unless required by applicable law or agreed to in writing, software
    -distributed under the License is distributed on an "AS IS" BASIS,
    -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    -See the License for the specific language governing permissions and
    -limitations under the License.
     
  • Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -2309,44 +2549,29 @@ 

    Used by:

    defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.
  • Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    -                     https://www.apache.org/licenses/LICENSE-2.0
    +                     http://www.apache.org/licenses/
     
     TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    @@ -2469,225 +2694,97 @@ 

    Used by:

    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -
    -
  • -
  • -

    Apache License 2.0

    -

    Used by:

    - -
    Apache License
    -Version 2.0, January 2004
    -http://www.apache.org/licenses/
    -
    -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    -
    -1. Definitions.
    -
    -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
    -
    -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
    -
    -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
    -
    -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
    -
    -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
    -
    -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
    -
    -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
    -
    -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
    -
    -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
    -
    -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
    -
    -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
    -
    -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
    -
    -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
    -
    -     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
    -
    -     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
    -
    -     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
    -
    -     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
    -
    -     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
    -
    -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
    -
    -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
    -
    -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
    -
    -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
    -
    -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
    -
    -END OF TERMS AND CONDITIONS
    +   for any such Derivative Works as a whole, provided Your use,
    +   reproduction, and distribution of the Work otherwise complies with
    +   the conditions stated in this License.
     
    -APPENDIX: How to apply the Apache License to your work.
    +5. Submission of Contributions. Unless You explicitly state otherwise,
    +   any Contribution intentionally submitted for inclusion in the Work
    +   by You to the Licensor shall be under the terms and conditions of
    +   this License, without any additional terms or conditions.
    +   Notwithstanding the above, nothing herein shall supersede or modify
    +   the terms of any separate license agreement you may have executed
    +   with Licensor regarding such Contributions.
     
    -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
    +6. Trademarks. This License does not grant permission to use the trade
    +   names, trademarks, service marks, or product names of the Licensor,
    +   except as required for reasonable and customary use in describing the
    +   origin of the Work and reproducing the content of the NOTICE file.
     
    -Copyright [yyyy] [name of copyright owner]
    +7. Disclaimer of Warranty. Unless required by applicable law or
    +   agreed to in writing, Licensor provides the Work (and each
    +   Contributor provides its Contributions) on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +   implied, including, without limitation, any warranties or conditions
    +   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +   PARTICULAR PURPOSE. You are solely responsible for determining the
    +   appropriateness of using or redistributing the Work and assume any
    +   risks associated with Your exercise of permissions under this License.
     
    -Licensed under the Apache License, Version 2.0 (the "License");
    -you may not use this file except in compliance with the License.
    -You may obtain a copy of the License at
    +8. Limitation of Liability. In no event and under no legal theory,
    +   whether in tort (including negligence), contract, or otherwise,
    +   unless required by applicable law (such as deliberate and grossly
    +   negligent acts) or agreed to in writing, shall any Contributor be
    +   liable to You for damages, including any direct, indirect, special,
    +   incidental, or consequential damages of any character arising as a
    +   result of this License or out of the use or inability to use the
    +   Work (including but not limited to damages for loss of goodwill,
    +   work stoppage, computer failure or malfunction, or any and all
    +   other commercial damages or losses), even if such Contributor
    +   has been advised of the possibility of such damages.
     
    -http://www.apache.org/licenses/LICENSE-2.0
    +9. Accepting Warranty or Additional Liability. While redistributing
    +   the Work or Derivative Works thereof, You may choose to offer,
    +   and charge a fee for, acceptance of support, warranty, indemnity,
    +   or other liability obligations and/or rights consistent with this
    +   License. However, in accepting such obligations, You may act only
    +   on Your own behalf and on Your sole responsibility, not on behalf
    +   of any other Contributor, and only if You agree to indemnify,
    +   defend, and hold each Contributor harmless for any liability
    +   incurred by, or claims asserted against, such Contributor by reason
    +   of your accepting any such warranty or additional liability.
     
    -Unless required by applicable law or agreed to in writing, software
    -distributed under the License is distributed on an "AS IS" BASIS,
    -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    -See the License for the specific language governing permissions and
    -limitations under the License.
    -
    -
  • -
  • -

    Apache License 2.0

    -

    Used by:

    - -
    Licensed under the Apache License, Version 2.0
    -<LICENSE-APACHE or
    -http://www.apache.org/licenses/LICENSE-2.0> or the MIT
    -license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
    -at your option. All files in the project carrying such
    -notice may not be copied, modified, or distributed except
    -according to those terms.
    +END OF TERMS AND CONDITIONS
     
  • Apache License 2.0

    Used by:

    -
    Rust-chrono is dual-licensed under The MIT License [1] and
    -Apache 2.0 License [2]. Copyright (c) 2014--2017, Kang Seonghoon and
    -contributors.
    -
    -Nota Bene: This is same as the Rust Project's own license.
    -
    -
    -[1]: <http://opensource.org/licenses/MIT>, which is reproduced below:
    -
    -~~~~
    -The MIT License (MIT)
    -
    -Copyright (c) 2014, Kang Seonghoon.
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in
    -all copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    -THE SOFTWARE.
    -~~~~
    -
    -
    -[2]: <http://www.apache.org/licenses/LICENSE-2.0>, which is reproduced below:
    -
    -~~~~
    -                              Apache License
    +                
                                  Apache License
                             Version 2.0, January 2004
                          http://www.apache.org/licenses/
     
    @@ -2835,489 +2932,193 @@ 

    Used by:

    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -~~~~ - -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    Copyright (c) 2015 fangyuanziti
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in
    -all copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    -THE SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    Copyright (c) 2017 Gilad Naaman
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    -
    -Copyright (c) 2016 Martin Geisler
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    +   PARTICULAR PURPOSE. You are solely responsible for determining the
    +   appropriateness of using or redistributing the Work and assume any
    +   risks associated with Your exercise of permissions under this License.
     
    -Copyright (c) 2017 Ted Driggs
    +8. Limitation of Liability. In no event and under no legal theory,
    +   whether in tort (including negligence), contract, or otherwise,
    +   unless required by applicable law (such as deliberate and grossly
    +   negligent acts) or agreed to in writing, shall any Contributor be
    +   liable to You for damages, including any direct, indirect, special,
    +   incidental, or consequential damages of any character arising as a
    +   result of this License or out of the use or inability to use the
    +   Work (including but not limited to damages for loss of goodwill,
    +   work stoppage, computer failure or malfunction, or any and all
    +   other commercial damages or losses), even if such Contributor
    +   has been advised of the possibility of such damages.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +9. Accepting Warranty or Additional Liability. While redistributing
    +   the Work or Derivative Works thereof, You may choose to offer,
    +   and charge a fee for, acceptance of support, warranty, indemnity,
    +   or other liability obligations and/or rights consistent with this
    +   License. However, in accepting such obligations, You may act only
    +   on Your own behalf and on Your sole responsibility, not on behalf
    +   of any other Contributor, and only if You agree to indemnify,
    +   defend, and hold each Contributor harmless for any liability
    +   incurred by, or claims asserted against, such Contributor by reason
    +   of your accepting any such warranty or additional liability.
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +END OF TERMS AND CONDITIONS
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    +APPENDIX: How to apply the Apache License to your work.
     
    -Copyright (c) 2018 System76
    +   To apply the Apache License to your work, attach the following
    +   boilerplate notice, with the fields enclosed by brackets "[]"
    +   replaced with your own identifying information. (Don't include
    +   the brackets!)  The text should be enclosed in the appropriate
    +   comment syntax for the file format. We also recommend that a
    +   file or class name and description of purpose be included on the
    +   same "printed page" as the copyright notice for easier
    +   identification within third-party archives.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +Copyright [yyyy] [name of copyright owner]
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
     
  • -

    MIT License

    +

    Apache License 2.0

    Used by:

    -
    MIT License
    +                
    Apache License
    +Version 2.0, January 2004
    +http://www.apache.org/licenses/
     
    -Copyright (c) 2019 Jinzhou Zhang
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +1. Definitions.
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
     
    -Copyright (c) 2020 Katharos Technology
    +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
     
    -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
     
    -Copyright (c) 2020 cptpcrd
    +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    MIT License
    +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
     
    -Copyright (c) <year> <copyright holders>
    +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
     
    -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    The MIT License (MIT)
    +     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
     
    -Copyright (c) 2014 Benjamin Sago
    -Copyright (c) 2021-2022 The Nushell Project Developers
    +     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    The MIT License (MIT)
    +     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
     
    -Copyright (c) 2015 Andrew Gallant
    +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
     
    -The above copyright notice and this permission notice shall be included in
    -all copies or substantial portions of the Software.
    +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    -THE SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    The MIT License (MIT)
    +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
     
    -Copyright (c) 2015 Carl Lerche + nix-rust Authors
    +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +END OF TERMS AND CONDITIONS
     
    -The above copyright notice and this permission notice shall be included in
    -all copies or substantial portions of the Software.
    +APPENDIX: How to apply the Apache License to your work.
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    -THE SOFTWARE.
    -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    The MIT License (MIT)
    +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
     
    -Copyright (c) 2015 Danny Guo
    -Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
    -Copyright (c) 2018 Akash Kurdekar
    +Copyright [yyyy] [name of copyright owner]
     
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
     
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    +http://www.apache.org/licenses/LICENSE-2.0
     
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
     
  • -

    MIT License

    +

    Apache License 2.0

    Used by:

    -
    The MIT License (MIT)
    -
    -Copyright (c) 2016 Jinzhou Zhang
    -Copyright (c) 2023 Robert Swinford
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    +                
    Licensed under the Apache License, Version 2.0
    +<LICENSE-APACHE or
    +http://www.apache.org/licenses/LICENSE-2.0> or the MIT
    +license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
    +at your option. All files in the project carrying such
    +notice may not be copied, modified, or distributed except
    +according to those terms.
     
  • MIT License

    Used by:

    -
    The MIT License (MIT)
    -
    -Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
    -
    -Permission is hereby granted, free of charge, to any person obtaining a copy
    -of this software and associated documentation files (the "Software"), to deal
    -in the Software without restriction, including without limitation the rights
    -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -copies of the Software, and to permit persons to whom the Software is
    -furnished to do so, subject to the following conditions:
    -
    -The above copyright notice and this permission notice shall be included in all
    -copies or substantial portions of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -SOFTWARE.
    -
    +                
    Copyright (c) 2015 fangyuanziti
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in
    +all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +THE SOFTWARE.
     
  • MIT License

    Used by:

    -
    The MIT License (MIT)
    -
    -Copyright (c) 2019 Jinzhou Zhang
    +                
    Copyright (c) 2017 Gilad Naaman
     
     Permission is hereby granted, free of charge, to any person obtaining a copy
     of this software and associated documentation files (the "Software"), to deal
    @@ -3335,408 +3136,390 @@ 

    Used by:

    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -
    +SOFTWARE.
  • MIT License

    Used by:

    -
    This project is dual-licensed under the Unlicense and MIT licenses.
    -
    -You may use this code under the terms of either license.
    -
    -
  • -
  • -

    Mozilla Public License 2.0

    -

    Used by:

    - -
    Mozilla Public License Version 2.0
    -==================================
    -
    -1. Definitions
    ---------------
    -
    -1.1. "Contributor"
    -    means each individual or legal entity that creates, contributes to
    -    the creation of, or owns Covered Software.
    -
    -1.2. "Contributor Version"
    -    means the combination of the Contributions of others (if any) used
    -    by a Contributor and that particular Contributor's Contribution.
    -
    -1.3. "Contribution"
    -    means Covered Software of a particular Contributor.
    -
    -1.4. "Covered Software"
    -    means Source Code Form to which the initial Contributor has attached
    -    the notice in Exhibit A, the Executable Form of such Source Code
    -    Form, and Modifications of such Source Code Form, in each case
    -    including portions thereof.
    -
    -1.5. "Incompatible With Secondary Licenses"
    -    means
    -
    -    (a) that the initial Contributor has attached the notice described
    -        in Exhibit B to the Covered Software; or
    -
    -    (b) that the Covered Software was made available under the terms of
    -        version 1.1 or earlier of the License, but not also under the
    -        terms of a Secondary License.
    -
    -1.6. "Executable Form"
    -    means any form of the work other than Source Code Form.
    -
    -1.7. "Larger Work"
    -    means a work that combines Covered Software with other material, in
    -    a separate file or files, that is not Covered Software.
    -
    -1.8. "License"
    -    means this document.
    -
    -1.9. "Licensable"
    -    means having the right to grant, to the maximum extent possible,
    -    whether at the time of the initial grant or subsequently, any and
    -    all of the rights conveyed by this License.
    -
    -1.10. "Modifications"
    -    means any of the following:
    -
    -    (a) any file in Source Code Form that results from an addition to,
    -        deletion from, or modification of the contents of Covered
    -        Software; or
    -
    -    (b) any new file in Source Code Form that contains any Covered
    -        Software.
    -
    -1.11. "Patent Claims" of a Contributor
    -    means any patent claim(s), including without limitation, method,
    -    process, and apparatus claims, in any patent Licensable by such
    -    Contributor that would be infringed, but for the grant of the
    -    License, by the making, using, selling, offering for sale, having
    -    made, import, or transfer of either its Contributions or its
    -    Contributor Version.
    -
    -1.12. "Secondary License"
    -    means either the GNU General Public License, Version 2.0, the GNU
    -    Lesser General Public License, Version 2.1, the GNU Affero General
    -    Public License, Version 3.0, or any later versions of those
    -    licenses.
    -
    -1.13. "Source Code Form"
    -    means the form of the work preferred for making modifications.
    -
    -1.14. "You" (or "Your")
    -    means an individual or a legal entity exercising rights under this
    -    License. For legal entities, "You" includes any entity that
    -    controls, is controlled by, or is under common control with You. For
    -    purposes of this definition, "control" means (a) the power, direct
    -    or indirect, to cause the direction or management of such entity,
    -    whether by contract or otherwise, or (b) ownership of more than
    -    fifty percent (50%) of the outstanding shares or beneficial
    -    ownership of such entity.
    -
    -2. License Grants and Conditions
    ---------------------------------
    -
    -2.1. Grants
    -
    -Each Contributor hereby grants You a world-wide, royalty-free,
    -non-exclusive license:
    -
    -(a) under intellectual property rights (other than patent or trademark)
    -    Licensable by such Contributor to use, reproduce, make available,
    -    modify, display, perform, distribute, and otherwise exploit its
    -    Contributions, either on an unmodified basis, with Modifications, or
    -    as part of a Larger Work; and
    -
    -(b) under Patent Claims of such Contributor to make, use, sell, offer
    -    for sale, have made, import, and otherwise transfer either its
    -    Contributions or its Contributor Version.
    -
    -2.2. Effective Date
    -
    -The licenses granted in Section 2.1 with respect to any Contribution
    -become effective for each Contribution on the date the Contributor first
    -distributes such Contribution.
    -
    -2.3. Limitations on Grant Scope
    +                
    MIT License
     
    -The licenses granted in this Section 2 are the only rights granted under
    -this License. No additional rights or licenses will be implied from the
    -distribution or licensing of Covered Software under this License.
    -Notwithstanding Section 2.1(b) above, no patent license is granted by a
    -Contributor:
    +Copyright (c) 2017 Ted Driggs
     
    -(a) for any code that a Contributor has removed from Covered Software;
    -    or
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -(b) for infringements caused by: (i) Your and any other third party's
    -    modifications of Covered Software, or (ii) the combination of its
    -    Contributions with other software (except as part of its Contributor
    -    Version); or
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -(c) under Patent Claims infringed by Covered Software in the absence of
    -    its Contributions.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
     
    -This License does not grant any rights in the trademarks, service marks,
    -or logos of any Contributor (except as may be necessary to comply with
    -the notice requirements in Section 3.4).
    +Copyright (c) 2018 System76
     
    -2.4. Subsequent Licenses
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -No Contributor makes additional grants as a result of Your choice to
    -distribute the Covered Software under a subsequent version of this
    -License (see Section 10.2) or under the terms of a Secondary License (if
    -permitted under the terms of Section 3.3).
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -2.5. Representation
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
     
    -Each Contributor represents that the Contributor believes its
    -Contributions are its original creation(s) or it has sufficient rights
    -to grant the rights to its Contributions conveyed by this License.
    +Copyright (c) 2019 Jinzhou Zhang
     
    -2.6. Fair Use
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -This License is not intended to limit any rights You have under
    -applicable copyright doctrines of fair use, fair dealing, or other
    -equivalents.
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -2.7. Conditions
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
     
    -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
    -in Section 2.1.
    +Copyright (c) 2020 Katharos Technology
     
    -3. Responsibilities
    --------------------
    +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
     
    -3.1. Distribution of Source Form
    +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
     
    -All distribution of Covered Software in Source Code Form, including any
    -Modifications that You create or to which You contribute, must be under
    -the terms of this License. You must inform recipients that the Source
    -Code Form of the Covered Software is governed by the terms of this
    -License, and how they can obtain a copy of this License. You may not
    -attempt to alter or restrict the recipients' rights in the Source Code
    -Form.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
     
    -3.2. Distribution of Executable Form
    +Copyright (c) 2020 cptpcrd
     
    -If You distribute Covered Software in Executable Form then:
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -(a) such Covered Software must also be made available in Source Code
    -    Form, as described in Section 3.1, and You must inform recipients of
    -    the Executable Form how they can obtain a copy of such Source Code
    -    Form by reasonable means in a timely manner, at a charge no more
    -    than the cost of distribution to the recipient; and
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -(b) You may distribute such Executable Form under the terms of this
    -    License, or sublicense it under different terms, provided that the
    -    license for the Executable Form does not attempt to limit or alter
    -    the recipients' rights in the Source Code Form under this License.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
     
    -3.3. Distribution of a Larger Work
    +Copyright (c) <year> <copyright holders>
     
    -You may create and distribute a Larger Work under terms of Your choice,
    -provided that You also comply with the requirements of this License for
    -the Covered Software. If the Larger Work is a combination of Covered
    -Software with a work governed by one or more Secondary Licenses, and the
    -Covered Software is not Incompatible With Secondary Licenses, this
    -License permits You to additionally distribute such Covered Software
    -under the terms of such Secondary License(s), so that the recipient of
    -the Larger Work may, at their option, further distribute the Covered
    -Software under the terms of either this License or such Secondary
    -License(s).
    +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
     
    -3.4. Notices
    +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
     
    -You may not remove or alter the substance of any license notices
    -(including copyright notices, patent notices, disclaimers of warranty,
    -or limitations of liability) contained within the Source Code Form of
    -the Covered Software, except that You may alter any license notices to
    -the extent required to remedy known factual inaccuracies.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -3.5. Application of Additional Terms
    +Copyright (c) 2014 Benjamin Sago
    +Copyright (c) 2021-2022 The Nushell Project Developers
     
    -You may choose to offer, and to charge a fee for, warranty, support,
    -indemnity or liability obligations to one or more recipients of Covered
    -Software. However, You may do so only on Your own behalf, and not on
    -behalf of any Contributor. You must make it absolutely clear that any
    -such warranty, support, indemnity, or liability obligation is offered by
    -You alone, and You hereby agree to indemnify every Contributor for any
    -liability incurred by such Contributor as a result of warranty, support,
    -indemnity or liability terms You offer. You may include additional
    -disclaimers of warranty and limitations of liability specific to any
    -jurisdiction.
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -4. Inability to Comply Due to Statute or Regulation
    ----------------------------------------------------
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -If it is impossible for You to comply with any of the terms of this
    -License with respect to some or all of the Covered Software due to
    -statute, judicial order, or regulation then You must: (a) comply with
    -the terms of this License to the maximum extent possible; and (b)
    -describe the limitations and the code they affect. Such description must
    -be placed in a text file included with all distributions of the Covered
    -Software under this License. Except to the extent prohibited by statute
    -or regulation, such description must be sufficiently detailed for a
    -recipient of ordinary skill to be able to understand it.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -5. Termination
    ---------------
    +Copyright (c) 2015 Andrew Gallant
     
    -5.1. The rights granted under this License will terminate automatically
    -if You fail to comply with any of its terms. However, if You become
    -compliant, then the rights granted under this License from a particular
    -Contributor are reinstated (a) provisionally, unless and until such
    -Contributor explicitly and finally terminates Your grants, and (b) on an
    -ongoing basis, if such Contributor fails to notify You of the
    -non-compliance by some reasonable means prior to 60 days after You have
    -come back into compliance. Moreover, Your grants from a particular
    -Contributor are reinstated on an ongoing basis if such Contributor
    -notifies You of the non-compliance by some reasonable means, this is the
    -first time You have received notice of non-compliance with this License
    -from such Contributor, and You become compliant prior to 30 days after
    -Your receipt of the notice.
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -5.2. If You initiate litigation against any entity by asserting a patent
    -infringement claim (excluding declaratory judgment actions,
    -counter-claims, and cross-claims) alleging that a Contributor Version
    -directly or indirectly infringes any patent, then the rights granted to
    -You by any and all Contributors for the Covered Software under Section
    -2.1 of this License shall terminate.
    +The above copyright notice and this permission notice shall be included in
    +all copies or substantial portions of the Software.
     
    -5.3. In the event of termination under Sections 5.1 or 5.2 above, all
    -end user license agreements (excluding distributors and resellers) which
    -have been validly granted by You or Your distributors under this License
    -prior to termination shall survive termination.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +THE SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -************************************************************************
    -*                                                                      *
    -*  6. Disclaimer of Warranty                                           *
    -*  -------------------------                                           *
    -*                                                                      *
    -*  Covered Software is provided under this License on an "as is"       *
    -*  basis, without warranty of any kind, either expressed, implied, or  *
    -*  statutory, including, without limitation, warranties that the       *
    -*  Covered Software is free of defects, merchantable, fit for a        *
    -*  particular purpose or non-infringing. The entire risk as to the     *
    -*  quality and performance of the Covered Software is with You.        *
    -*  Should any Covered Software prove defective in any respect, You     *
    -*  (not any Contributor) assume the cost of any necessary servicing,   *
    -*  repair, or correction. This disclaimer of warranty constitutes an   *
    -*  essential part of this License. No use of any Covered Software is   *
    -*  authorized under this License except under this disclaimer.         *
    -*                                                                      *
    -************************************************************************
    +Copyright (c) 2015 Carl Lerche + nix-rust Authors
     
    -************************************************************************
    -*                                                                      *
    -*  7. Limitation of Liability                                          *
    -*  --------------------------                                          *
    -*                                                                      *
    -*  Under no circumstances and under no legal theory, whether tort      *
    -*  (including negligence), contract, or otherwise, shall any           *
    -*  Contributor, or anyone who distributes Covered Software as          *
    -*  permitted above, be liable to You for any direct, indirect,         *
    -*  special, incidental, or consequential damages of any character      *
    -*  including, without limitation, damages for lost profits, loss of    *
    -*  goodwill, work stoppage, computer failure or malfunction, or any    *
    -*  and all other commercial damages or losses, even if such party      *
    -*  shall have been informed of the possibility of such damages. This   *
    -*  limitation of liability shall not apply to liability for death or   *
    -*  personal injury resulting from such party's negligence to the       *
    -*  extent applicable law prohibits such limitation. Some               *
    -*  jurisdictions do not allow the exclusion or limitation of           *
    -*  incidental or consequential damages, so this exclusion and          *
    -*  limitation may not apply to You.                                    *
    -*                                                                      *
    -************************************************************************
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -8. Litigation
    --------------
    +The above copyright notice and this permission notice shall be included in
    +all copies or substantial portions of the Software.
     
    -Any litigation relating to this License may be brought only in the
    -courts of a jurisdiction where the defendant maintains its principal
    -place of business and such litigation shall be governed by laws of that
    -jurisdiction, without reference to its conflict-of-law provisions.
    -Nothing in this Section shall prevent a party's ability to bring
    -cross-claims or counter-claims.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +THE SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -9. Miscellaneous
    -----------------
    +Copyright (c) 2015 Danny Guo
    +Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
    +Copyright (c) 2018 Akash Kurdekar
     
    -This License represents the complete agreement concerning the subject
    -matter hereof. If any provision of this License is held to be
    -unenforceable, such provision shall be reformed only to the extent
    -necessary to make it enforceable. Any law or regulation which provides
    -that the language of a contract shall be construed against the drafter
    -shall not be used to construe this License against a Contributor.
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -10. Versions of the License
    ----------------------------
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -10.1. New Versions
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -Mozilla Foundation is the license steward. Except as provided in Section
    -10.3, no one other than the license steward has the right to modify or
    -publish new versions of this License. Each version will be given a
    -distinguishing version number.
    +Copyright (c) 2016 Jinzhou Zhang
    +Copyright (c) 2023 Robert Swinford
     
    -10.2. Effect of New Versions
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -You may distribute the Covered Software under the terms of the version
    -of the License under which You originally received the Covered Software,
    -or under the terms of any subsequent version published by the license
    -steward.
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -10.3. Modified Versions
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -If you create software not governed by this License, and you want to
    -create a new license for such software, you may create and use a
    -modified version of this License if you rename the license and remove
    -any references to the name of the license steward (except to note that
    -such modified license differs from this License).
    +Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
     
    -10.4. Distributing Source Code Form that is Incompatible With Secondary
    -Licenses
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -If You choose to distribute Source Code Form that is Incompatible With
    -Secondary Licenses under the terms of this version of the License, the
    -notice described in Exhibit B of this License must be attached.
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -Exhibit A - Source Code Form License Notice
    --------------------------------------------
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
     
    -  This Source Code Form is subject to the terms of the Mozilla Public
    -  License, v. 2.0. If a copy of the MPL was not distributed with this
    -  file, You can obtain one at http://mozilla.org/MPL/2.0/.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
     
    -If it is not possible or desirable to put the notice in a particular
    -file, then You may include the notice in a location (such as a LICENSE
    -file in a relevant directory) where a recipient would be likely to look
    -for such a notice.
    +Copyright (c) 2019 Jinzhou Zhang
    +Copyright (c) 2024 Robert Swinford
     
    -You may add additional accurate notices of copyright ownership.
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
     
    -Exhibit B - "Incompatible With Secondary Licenses" Notice
    ----------------------------------------------------------
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
     
    -  This Source Code Form is "Incompatible With Secondary Licenses", as
    -  defined by the Mozilla Public License, v. 2.0.
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    This project is dual-licensed under the Unlicense and MIT licenses.
    +
    +You may use this code under the terms of either license.
     
  • Mozilla Public License 2.0

    Used by:

    Mozilla Public License Version 2.0
     ==================================
    @@ -4142,6 +3925,32 @@ 

    Used by:

    Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.
  • +
  • +

    zlib License

    +

    Used by:

    + +
    Copyright (c) 2024 Orson Peters
    +
    +This software is provided 'as-is', without any express or implied warranty. In
    +no event will the authors be held liable for any damages arising from the use of
    +this software.
    +
    +Permission is granted to anyone to use this software for any purpose, including
    +commercial applications, and to alter it and redistribute it freely, subject to
    +the following restrictions:
    +
    +1. The origin of this software must not be misrepresented; you must not claim
    +    that you wrote the original software. If you use this software in a product,
    +    an acknowledgment in the product documentation would be appreciated but is
    +    not required.
    +
    +2. Altered source versions must be plainly marked as such, and must not be
    +    misrepresented as being the original software.
    +
    +3. This notice may not be removed or altered from any source distribution.
    +