diff --git a/Cargo.lock b/Cargo.lock index 8b5665a..cd75f2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,71 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.11.3" @@ -39,6 +98,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "console" version = "0.15.10" @@ -49,7 +120,7 @@ dependencies = [ "libc", "once_cell", "unicode-width", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -77,13 +148,61 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "dock_sprout" version = "1.0.3" dependencies = [ + "ctrlc", + "dirs", + "env_logger", "gumdrop", "ignore", "indicatif", + "log", "tempfile", "walkdir", ] @@ -94,6 +213,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "errno" version = "0.3.10" @@ -101,7 +243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -110,6 +252,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.1" @@ -118,8 +271,8 @@ checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", - "wasi", - "windows-targets", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -184,6 +337,36 @@ dependencies = [ "web-time", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -196,9 +379,19 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] [[package]] name = "linux-raw-sys" @@ -218,24 +411,72 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "portable-atomic" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -254,6 +495,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -281,7 +545,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -295,22 +559,31 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.115", ] [[package]] @@ -326,9 +599,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -343,10 +616,30 @@ checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", ] [[package]] @@ -361,6 +654,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" @@ -371,6 +670,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" @@ -401,7 +706,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.115", "wasm-bindgen-shared", ] @@ -423,7 +728,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.115", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -453,7 +758,22 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.5", ] [[package]] @@ -462,7 +782,31 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -471,28 +815,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -505,24 +867,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index f5ccff3..708a6f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ gumdrop = "0.8.1" ignore = "0.4.23" indicatif = "0.17.11" walkdir = "2.4.0" +log = "0.4" +env_logger = "0.11" +ctrlc = "3.4" +dirs = "5.0" [dev-dependencies] tempfile = "3.16.0" diff --git a/README.md b/README.md index 169c2e1..0b28956 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# DockSprout 🌱🐳 + +# DockSprout **A simple CLI tool to bring up Docker containers from multiple `docker-compose.yml` files in subdirectories.** --- -## **📌 Features** +## **Features** - Scans a **central directory** for `docker-compose.yml` files. - Recursively searches **subdirectories** for Docker Compose projects. @@ -17,12 +18,12 @@ 1. Install Rust toolchain 2. Run `cargo install dock_sprout` -### **🔹 Homebrew** +### **Homebrew** ```bash brew install Thompson-Jason/tap/dock_sprout ``` -### **🔹 Build from Source** +### **Build from Source** ```bash git clone https://github.com/Thompson-Jason/DockSprout.git cd DockSprout @@ -30,12 +31,40 @@ cargo build --release ``` --- -## **🚀 Usage** +## **Usage** + + +### **Basic Usage** +``` +sprout +``` + +### **Options** + +- `--concurrent` : Run all docker compose commands in parallel (faster for many projects) +- `--verbose` : Output docker compose logs to stdout/stderr + +## **Examples** + +**Bring up all containers, sequentially:** +``` +sprout ~/my-docker-projects up +``` -`sprout ` +**Bring up all containers concurrently:** +``` +sprout --concurrent ~/my-docker-projects up +``` -## 🔹 **Example**: -`sprout ~/my-docker-projects up` +**Bring up all containers and show all output:** +``` +sprout --verbose ~/my-docker-projects up +``` + +**Bring down all containers concurrently and show output:** +``` +sprout --concurrent --verbose ~/my-docker-projects down +``` ``` ─── my-docker-projects diff --git a/src/lib.rs b/src/lib.rs index 016dbc2..efd2049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,127 @@ +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt; +#[cfg(windows)] +use std::os::windows::process::ExitStatusExt; pub mod walker; -pub fn run_docker_compose(files: Vec, direction_args: Vec, verbose: bool, mut command_runner: F) +use log::error; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::AtomicBool; + +pub fn run_docker_compose(files: Vec, direction_args: &[String], verbose: bool, mut command_runner: F) where - F: FnMut(&str, &Vec, bool) -> std::io::Result, + F: FnMut(&str, &[String], bool) -> std::io::Result, { for file_path in files.iter() { + match command_runner(file_path, direction_args, verbose) { + Ok(_status) => {}, + Err(e) => error!("Failed to run docker compose for {}: {}", file_path, e), + } + } +} - command_runner(file_path, &direction_args, verbose).unwrap(); +pub fn run_docker_compose_collect(files: Vec, direction_args: &[String], verbose: bool, mut command_runner: F) -> Vec<(String, String)> +where + F: FnMut(&str, &[String], bool) -> std::io::Result, +{ + let mut errors = Vec::new(); + for file_path in files.iter() { + match command_runner(file_path, direction_args, verbose) { + Ok(status) => { + if !status.success() { + errors.push((file_path.clone(), format!("Exited with status: {}", status))); + } + }, + Err(e) => errors.push((file_path.clone(), e.to_string())), + } } + errors } -pub fn run_docker_compose_concurrent(files: Vec, direction_args: Vec, verbose: bool, mut command_runner: F) +pub fn run_docker_compose_concurrent(files: Vec, direction_args: &[String], verbose: bool, mut command_runner: F) where - F: FnMut(&str, &Vec, bool) -> std::io::Result, + F: FnMut(&str, &[String], bool) -> std::io::Result, { for file_path in files.iter() { + match command_runner(file_path, direction_args, verbose) { + Ok(_child) => {}, + Err(e) => error!("Failed to run docker compose for {}: {}", file_path, e), + } + } +} - command_runner(file_path, &direction_args, verbose).unwrap(); +pub fn run_docker_compose_concurrent_collect( + files: Vec, + direction_args: &[String], + verbose: bool, + command_runner: F, + children: Arc>>, + shutdown_flag: Arc, +) -> Vec<(String, String)> +where + F: FnMut(&str, &[String], bool) -> std::io::Result + Send + Clone + 'static, +{ + use std::thread; + let mut handles = Vec::new(); + let errors = Arc::new(Mutex::new(Vec::new())); + for file_path in files.iter() { + let file = file_path.clone(); + let args = direction_args.to_vec(); + let mut runner = command_runner.clone(); + let errors = Arc::clone(&errors); + let children = Arc::clone(&children); + let shutdown_flag = Arc::clone(&shutdown_flag); + let handle = std::thread::spawn(move || { + if shutdown_flag.load(std::sync::atomic::Ordering::SeqCst) { + return; + } + match runner(&file, &args, verbose) { + Ok(child) => { + let child_id = child.id(); + { + let mut ch = children.lock().unwrap(); + ch.push(child); + } + let status = { + let mut ch = children.lock().unwrap(); + let idx = ch.iter().position(|c| c.id() == child_id); + // Remove the child from the list before waiting + let child_opt = idx.map(|i| ch.remove(i)); + let status = if let Some(mut child) = child_opt { + child.wait() + } else { + // Should not happen, but fallback + #[cfg(unix)] + { Ok(std::process::ExitStatus::from_raw(1)) } + #[cfg(windows)] + { Ok(std::process::ExitStatus::from_raw(1)) } + }; + status + }; + match status { + Ok(status) => { + if !status.success() { + let mut errs = errors.lock().unwrap(); + errs.push((file.clone(), format!("Exited with status: {}", status))); + } + }, + Err(e) => { + let mut errs = errors.lock().unwrap(); + errs.push((file.clone(), e.to_string())); + } + } + }, + Err(e) => { + let mut errs = errors.lock().unwrap(); + errs.push((file.clone(), e.to_string())); + } + } + }); + handles.push(handle); + } + for handle in handles { + let _ = handle.join(); } + let errors = Arc::try_unwrap(errors).map(|m| m.into_inner().unwrap()).unwrap_or_else(|arc| (*arc.lock().unwrap()).clone()); + errors } diff --git a/src/main.rs b/src/main.rs index e4d21aa..65f8657 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod walker; +mod signal; use gumdrop::Options; use indicatif::{ProgressBar, ProgressStyle}; @@ -6,7 +7,10 @@ use std::{env}; use std::time::Duration; use std::path::PathBuf; use std::process::{Command, Stdio, ExitStatus, Child}; -use dock_sprout::{run_docker_compose, run_docker_compose_concurrent}; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::AtomicBool; +use log::error; +// ...existing code... @@ -34,8 +38,11 @@ struct Opts { } -fn real_docker_runner(file_path: &str, direction_args: &Vec, verbose: bool) -> std::io::Result { +fn real_docker_runner(file_path: &str, direction_args: &[String], verbose: bool) -> std::io::Result { + if file_path.contains(';') || file_path.contains('&') || file_path.contains('|') || file_path.contains('`') || file_path.contains('$') { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid characters in file path")); + } if verbose { Command::new("docker") .arg("compose") @@ -45,7 +52,7 @@ fn real_docker_runner(file_path: &str, direction_args: &Vec, verbose: bo .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .status() - }else{ + } else { Command::new("docker") .arg("compose") .arg("-f") @@ -57,7 +64,11 @@ fn real_docker_runner(file_path: &str, direction_args: &Vec, verbose: bo } } -fn real_docker_runner_concurrent(file_path: &str, direction_args: &Vec, verbose: bool) -> std::io::Result { +fn real_docker_runner_concurrent(file_path: &str, direction_args: &[String], verbose: bool) -> std::io::Result { + // Security: Validate file_path to prevent command injection + if file_path.contains(';') || file_path.contains('&') || file_path.contains('|') || file_path.contains('`') || file_path.contains('$') { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid characters in file path")); + } if verbose { Command::new("docker") .arg("compose") @@ -67,7 +78,7 @@ fn real_docker_runner_concurrent(file_path: &str, direction_args: &Vec, .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn() - }else{ + } else { Command::new("docker") .arg("compose") .arg("-f") @@ -80,42 +91,71 @@ fn real_docker_runner_concurrent(file_path: &str, direction_args: &Vec, } fn main() { + env_logger::init(); if env::args().any(|arg| arg == "-v" || arg == "--version") { println!("DockSprout {VERSION}"); std::process::exit(0); } - let args = Opts::parse_args_default_or_exit(); - let root = args.source.unwrap(); + let root = match args.source { + Some(r) => r, + None => { + error!("No source directory provided."); + std::process::exit(1); + } + }; let option = args.option.to_lowercase(); - let mut direction_args = vec![]; + let direction_args = if option != "up" && option != "down" && option != "pull" { + error!("Docker Compose direction has to be one of the following (up|down|pull). Argument given = {}", option); + std::process::exit(1); + } else if option == "up" { + vec![option, "-d".to_string()] + } else { + vec![option] + }; + let files = walker::get_compose_filepaths(&root); - if option != "up" && option != "down" && option != "pull" { - eprintln!("Docker Compose direction has to be one of the following (up|down|pull). Argument given = {}", option); + if files.is_empty() { + error!("No docker-compose.yml files found in {:?}", root); std::process::exit(1); - }else if option == "up" { - direction_args = vec![option, "-d".to_string()]; - }else if option == "down" || option == "pull"{ - direction_args = vec![option]; } - let files = walker::get_compose_filepaths(&root); - + // Spinner is used for visual feedback, but does not track per-file progress let spinner = ProgressBar::new_spinner(); spinner.set_style( - ProgressStyle::default_spinner() - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) - .template("{spinner:.blue} Running {msg}...") // Custom styling - .unwrap(), + ProgressStyle::default_spinner() + .tick_strings(&["\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}", "\u{2826}", "\u{2827}", "\u{2807}", "\u{280f}"]) + .template("{spinner:.blue} Running {msg}...") + .unwrap(), ); spinner.enable_steady_tick(Duration::from_millis(100)); if args.concurrent { - run_docker_compose_concurrent(files, direction_args, args.verbose, real_docker_runner_concurrent); - }else{ - run_docker_compose(files, direction_args, args.verbose, real_docker_runner); + // ...existing code... + let children = Arc::new(Mutex::new(Vec::new())); + let shutdown_flag = Arc::new(AtomicBool::new(false)); + signal::setup_signal_handler(Arc::clone(&children), Arc::clone(&shutdown_flag)); + let errors = dock_sprout::run_docker_compose_concurrent_collect(files, &direction_args, args.verbose, real_docker_runner_concurrent, Arc::clone(&children), Arc::clone(&shutdown_flag)); + spinner.finish_and_clear(); + if !errors.is_empty() { + error!("Some docker compose commands failed:"); + for (file, err) in errors { + error!(" {}: {}", file, err); + } + std::process::exit(2); + } + } else { + let errors = dock_sprout::run_docker_compose_collect(files, &direction_args, args.verbose, real_docker_runner); + spinner.finish_and_clear(); + if !errors.is_empty() { + error!("Some docker compose commands failed:"); + for (file, err) in errors { + error!(" {}: {}", file, err); + } + std::process::exit(2); + } } } diff --git a/src/signal.rs b/src/signal.rs new file mode 100644 index 0000000..b8f7031 --- /dev/null +++ b/src/signal.rs @@ -0,0 +1,21 @@ +// Logging and signal handling utilities for DockSprout +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::process::Child; +use std::sync::Mutex; +use log::warn; + +/// Sets up Ctrl+C handler to terminate all running children gracefully. +pub fn setup_signal_handler(children: Arc>>, shutdown_flag: Arc) { + let children = Arc::clone(&children); + let shutdown_flag = Arc::clone(&shutdown_flag); + ctrlc::set_handler(move || { + warn!("Received Ctrl+C! Attempting to terminate all child processes..."); + shutdown_flag.store(true, Ordering::SeqCst); + let mut children = children.lock().unwrap(); + for child in children.iter_mut() { + let _ = child.kill(); + } + std::process::exit(130); // 130 = 128 + SIGINT + }).expect("Error setting Ctrl+C handler"); +} diff --git a/src/walker.rs b/src/walker.rs index 7134f3d..a453a4a 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -1,36 +1,63 @@ use std::path::{Path}; +fn is_allowed_root(path: &Path) -> bool { + // Allow override for testing or admin use + if std::env::var("SPROUT_ALLOW_ANY_ROOT").map(|v| v == "1" || v.to_lowercase() == "true").unwrap_or(false) { + return true; + } + if let Some(home) = dirs::home_dir() { + if path.starts_with(&home) { + return true; + } + } + if let Some(tmp) = std::env::temp_dir().to_str() { + if path.starts_with(tmp) { + return true; + } + } + false +} use ignore::{Walk, WalkBuilder}; +use std::env; +use log::{info, error, warn}; - fn build_walker(path: &Path) -> Walk { - let mut walker = WalkBuilder::new(path); - walker - .standard_filters(false) - // .parents(true) - .hidden(false) - .require_git(true) - .git_ignore(true) - .git_global(true) - .git_exclude(true) - .add_custom_ignore_filename(".sprout-ignore"); - - walker.build() - } +fn build_walker(path: &Path) -> Walk { + let require_git = env::var("SPROUT_REQUIRE_GIT").map(|v| v == "1" || v.to_lowercase() == "true").unwrap_or(false); + let mut walker = WalkBuilder::new(path); + walker + .standard_filters(false) + // .parents(true) + .hidden(false) + .require_git(require_git) + .git_ignore(true) + .git_global(true) + .git_exclude(true) + // Uses .sprout-ignore for custom ignore patterns + .add_custom_ignore_filename(".sprout-ignore"); + + walker.build() +} pub fn get_compose_filepaths(root: &Path) -> Vec { + if !is_allowed_root(root) { + error!("Directory traversal denied: {:?} is not under $HOME or /tmp. Set SPROUT_ALLOW_ANY_ROOT=1 to override.", root); + return Vec::new(); + } let mut paths = Vec::new(); let walker = build_walker(root); for entry in walker { - if let Err(_e) = entry { - continue; - } - let entry = entry.unwrap(); - let path = entry.path(); - - if let Some(file_stem) = path.file_stem() { - if file_stem == "docker-compose" && (path.extension().map_or(false, |ext| ext == "yaml") || path.extension().map_or(false, |ext| ext == "yml")) { - println!("Path: {}", path.display()); - paths.push(path.to_string_lossy().to_string()); + match entry { + Ok(entry) => { + let path = entry.path(); + if let Some(file_stem) = path.file_stem() { + if file_stem == "docker-compose" && (path.extension().map_or(false, |ext| ext == "yaml") || path.extension().map_or(false, |ext| ext == "yml")) { + info!("Found compose file: {}", path.display()); + paths.push(path.to_string_lossy().to_string()); + } + } + }, + Err(e) => { + warn!("Walker error: {}", e); } } } diff --git a/tests/libTests.rs b/tests/libTests.rs index 03659f6..8fc7f47 100644 --- a/tests/libTests.rs +++ b/tests/libTests.rs @@ -1,7 +1,10 @@ #[cfg(test)] mod tests { use std::process::{Command, ExitStatus, Child, Stdio}; + #[cfg(unix)] use std::os::unix::process::ExitStatusExt; // For creating fake exit statuses + #[cfg(windows)] + use std::os::windows::process::ExitStatusExt; use dock_sprout::{run_docker_compose, run_docker_compose_concurrent}; #[test] @@ -14,12 +17,15 @@ mod tests { let direction_args = vec!["up".to_string(), "-d".to_string()]; // Mock function to replace the real Docker command - let mock_runner = |file: &str, _direction_args: &Vec, _verbose: bool| -> std::io::Result { + let mock_runner = |file: &str, _direction_args: &[String], _verbose: bool| -> std::io::Result { println!("Mocked execution: docker compose -f {} up -d", file); - Ok(ExitStatus::from_raw(0)) + #[cfg(unix)] + { Ok(ExitStatus::from_raw(0)) } + #[cfg(windows)] + { Ok(ExitStatus::from_raw(0)) } }; - run_docker_compose(test_files, direction_args, true, mock_runner); + run_docker_compose(test_files, &direction_args, true, mock_runner); } @@ -33,16 +39,29 @@ mod tests { let direction_args = vec!["up".to_string(), "-d".to_string()]; // Mock function to replace the real Docker command - let mock_runner = |file: &str, _direction_args: &Vec, _verbose: bool| -> std::io::Result { - println!("Mocked execution: docker compose -f {} up -d", file); - Command::new("sleep") - .arg("1") // A short-lived command that immediately exits - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() // Returns a Child process + let mock_runner = |file: &str, _direction_args: &[String], _verbose: bool| -> std::io::Result { + println!("Mocked execution: docker compose -f {} up -d", file); + // Use a cross-platform short-lived command + #[cfg(unix)] + { + Command::new("sleep") + .arg("1") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + } + #[cfg(windows)] + { + Command::new("cmd") + .arg("/C") + .arg("timeout /T 1 > NUL") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + } }; - run_docker_compose_concurrent(test_files, direction_args, true, mock_runner); + run_docker_compose_concurrent(test_files, &direction_args, true, mock_runner); } } diff --git a/tests/walkerTests.rs b/tests/walkerTests.rs index 32f0786..fbb52e8 100644 --- a/tests/walkerTests.rs +++ b/tests/walkerTests.rs @@ -1,5 +1,98 @@ #[cfg(test)] mod tests { + + #[test] + fn test_empty_directory() { + let temp_dir = tempdir().unwrap(); + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(found_files.is_empty(), "No files should be found in an empty directory"); + } + + #[test] + fn test_symlinked_compose_file() { + use std::os::unix::fs::symlink; + let temp_dir = tempdir().unwrap(); + let real_dir = temp_dir.path().join("real"); + let link_dir = temp_dir.path().join("link"); + create_dir_all(&real_dir).unwrap(); + create_dir_all(&link_dir).unwrap(); + let compose_file_path = real_dir.join("docker-compose.yml"); + File::create(&compose_file_path).unwrap(); + let symlink_path = link_dir.join("docker-compose.yml"); + symlink(&compose_file_path, &symlink_path).unwrap(); + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(found_files.iter().any(|f| f.ends_with("docker-compose.yml")), "Should find symlinked compose file"); + } + + + #[test] + fn test_sprout_ignore_complex_patterns() { + let temp_dir = tempdir().unwrap(); + let docker_dir1 = temp_dir.path().join("project1"); + let docker_dir2 = temp_dir.path().join("project2"); + let docker_dir3 = temp_dir.path().join("project3"); + create_dir_all(&docker_dir1).unwrap(); + create_dir_all(&docker_dir2).unwrap(); + create_dir_all(&docker_dir3).unwrap(); + let compose_file_path1 = docker_dir1.join("docker-compose.yml"); + let compose_file_path2 = docker_dir2.join("docker-compose.yml"); + let compose_file_path3 = docker_dir3.join("docker-compose.yml"); + File::create(&compose_file_path1).unwrap(); + File::create(&compose_file_path2).unwrap(); + File::create(&compose_file_path3).unwrap(); + let ignore_file_path = temp_dir.path().join(".sprout-ignore"); + // Ignore project2 and any directory starting with 'project3' + let _ = File::create(&ignore_file_path).unwrap().write_all(b"project2/*\nproject3*"); + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(found_files.contains(&compose_file_path1.to_string_lossy().to_string()), "project1 should not be ignored"); + assert!(!found_files.contains(&compose_file_path2.to_string_lossy().to_string()), "project2 should be ignored"); + assert!(!found_files.contains(&compose_file_path3.to_string_lossy().to_string()), "project3 should be ignored by pattern"); + } + use std::os::unix::fs::PermissionsExt; + + #[test] + fn test_permission_error() { + let temp_dir = tempdir().unwrap(); + let restricted_dir = temp_dir.path().join("restricted"); + create_dir_all(&restricted_dir).unwrap(); + let compose_file_path = restricted_dir.join("docker-compose.yml"); + File::create(&compose_file_path).unwrap(); + // Remove read permissions from the directory + let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions(); + perms.set_mode(0o000); + std::fs::set_permissions(&restricted_dir, perms.clone()).unwrap(); + // Should not panic, should not find the file + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(!found_files.contains(&compose_file_path.to_string_lossy().to_string()), "Should not find file in unreadable directory"); + // Restore permissions for cleanup + perms.set_mode(0o700); + std::fs::set_permissions(&restricted_dir, perms).unwrap(); + } + + #[test] + fn test_deeply_nested_directories() { + let temp_dir = tempdir().unwrap(); + let mut deep_dir = temp_dir.path().to_path_buf(); + for i in 0..10 { + deep_dir = deep_dir.join(format!("level{}", i)); + create_dir_all(&deep_dir).unwrap(); + } + let compose_file_path = deep_dir.join("docker-compose.yml"); + File::create(&compose_file_path).unwrap(); + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(found_files.contains(&compose_file_path.to_string_lossy().to_string()), "Should find file in deeply nested directory"); + } + + #[test] + fn test_invalid_compose_file_name() { + let temp_dir = tempdir().unwrap(); + let docker_dir = temp_dir.path().join("project1"); + create_dir_all(&docker_dir).unwrap(); + let invalid_file_path = docker_dir.join("docker-compose.txt"); + File::create(&invalid_file_path).unwrap(); + let found_files = get_compose_filepaths(temp_dir.path()); + assert!(!found_files.contains(&invalid_file_path.to_string_lossy().to_string()), "Should not detect non-yml compose file"); + } use std::fs::{create_dir_all, File}; use tempfile::tempdir; use dock_sprout::walker::get_compose_filepaths;