commit c48880d0ca15659f3d3eb1b4e988613586afe988 Author: lucalise Date: Mon Feb 2 00:22:51 2026 -0800 feat: initial commit, add fs revision storage and reporter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fa373f9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,811 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +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", +] + +[[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", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indicatif" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "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 = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minecraft-sync" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "flate2", + "indicatif", + "tar", + "tempfile", + "thiserror", + "thiserror-ext", + "tracing", + "tracing-subscriber", + "walkdir", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[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 = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7e61141f4141832ca9aad63c3c90023843f944a1975460abdacc64d03f534" +dependencies = [ + "thiserror", + "thiserror-ext-derive", +] + +[[package]] +name = "thiserror-ext-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5042dd3b562d1d57711be902006a0003fa2781b81d5b2bec07416be31586ff" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "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 = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[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 = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..da0d087 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "minecraft-sync" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.56", features = ["derive"] } +dirs = "6.0.0" +flate2 = "1.1.8" +indicatif = "0.18.3" +tar = "0.4.44" +tempfile = "3.24.0" +thiserror = "2.0.18" +thiserror-ext = "0.3.0" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +walkdir = "2.5.0" + +[profile.dev-fast] +inherits = "dev" +opt-level = 2 diff --git a/src/archive.rs b/src/archive.rs new file mode 100644 index 0000000..70f0b67 --- /dev/null +++ b/src/archive.rs @@ -0,0 +1,107 @@ +use std::{ + fs::{self, DirEntry}, + io::BufWriter, + path::Path, +}; + +use flate2::{Compression, write::GzEncoder}; +use tar::Builder; +use tempfile::NamedTempFile; +use walkdir::WalkDir; + +use crate::{ + Data, + error::{Error, Result}, + reporter::Reporter, + storage::{IGNORE_FILES, Storage, base_path_prism}, +}; + +pub struct ArchiveResult { + pub temp_file: NamedTempFile, +} + +pub fn create_archive(app_state: Data, reporter: &Reporter) -> Result { + let entries = get_sync_entries()?; + + let temp_file = match app_state.storage { + Storage::FS(_) => { + let target = crate::storage::fs::base_path(); + tempfile::Builder::new() + .prefix(".revision_") + .suffix(".tar.gz.tmp") + .tempfile_in(&target)? + } + }; + let file = temp_file.reopen()?; + let buffered = BufWriter::with_capacity(64 * 1024, file); + let encoder = GzEncoder::new(buffered, Compression::default()); + let mut archive = Builder::new(encoder); + let total_files: u64 = entries + .iter() + .map(|entry| { + let path = entry.path(); + if path.is_dir() { + WalkDir::new(&path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .count() as u64 + } else { + 1 + } + }) + .sum(); + + reporter.start_archive_progress(total_files); + reporter.set_message("creating revision"); + for entry in entries { + let path = entry.path(); + let base_name = entry.file_name(); + if path.is_dir() { + for walk_entry in WalkDir::new(&path) { + let walk_entry = walk_entry.map_err(|e| Error::create_archive(e, &path))?; + let full_path = walk_entry.path(); + let relative = Path::new(&base_name).join(full_path.strip_prefix(&path).unwrap()); + + if walk_entry.file_type().is_file() { + reporter.set_subtle(relative.display().to_string()); + archive + .append_path_with_name(full_path, &relative) + .map_err(|e| Error::create_archive(e, &path))?; + reporter.inc_archive_progress(); + } else { + archive + .append_dir(&relative, full_path) + .map_err(|e| Error::create_archive(e, &path))?; + } + } + } else { + archive + .append_path_with_name(&path, &base_name) + .map_err(|e| Error::create_archive(e, &path))?; + reporter.inc_archive_progress(); + } + } + + archive + .finish() + .map_err(|e| Error::create_archive(e, &temp_file.path().file_name().unwrap()))?; + reporter.finish_archive_progress("created revision"); + + Ok(ArchiveResult { temp_file }) +} + +fn get_sync_entries() -> Result> { + let base = base_path_prism(); + let entries = fs::read_dir(&base)? + .filter_map(|e| e.ok()) + .filter(|entry| { + entry + .file_name() + .to_str() + .map(|name| !IGNORE_FILES.contains(&name)) + .unwrap_or(false) + }) + .collect(); + Ok(entries) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..cf6aab9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,33 @@ +use std::path::PathBuf; + +use thiserror::Error; +use thiserror_ext::{Box, Construct}; + +#[derive(Error, Box, Debug, Construct)] +#[thiserror_ext(newtype(name = Error))] +pub enum ErrorKind { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("error parsing revision lockfile")] + RevisionLock, + #[error("error reading revision lockfile at {path}")] + ReadRevision { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("error creating archive at {path}")] + CreateArchive { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("error persisting archive to {path}")] + PersistError { + path: PathBuf, + #[source] + source: tempfile::PersistError, + }, +} + +pub type Result = core::result::Result; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..70c114a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,95 @@ +mod archive; +mod error; +mod reporter; +mod storage; + +use std::{ops::Deref, sync::Arc, time::Duration}; + +use clap::{CommandFactory, Parser, Subcommand}; +use indicatif::MultiProgress; +use thiserror_ext::AsReport; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{ + EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, +}; + +use crate::{ + reporter::Reporter, + storage::{Storage, StorageImpl, get_storage_from_env}, +}; + +#[derive(Parser, Debug)] +#[command(version, about = "minecraft-sync", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// push current state to storage + Push, +} + +struct AppState { + multi: MultiProgress, + storage: Storage, +} + +impl AppState { + pub fn new() -> Self { + Self { + multi: MultiProgress::new(), + storage: get_storage_from_env(), + } + } +} + +#[derive(Clone)] +struct Data(Arc); + +impl Deref for Data { + type Target = AppState; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Data { + pub fn new(inner: AppState) -> Self { + Self(Arc::new(inner)) + } +} + +fn run() -> crate::error::Result<()> { + let cli = Cli::parse(); + let app_state = Data::new(AppState::new()); + + match cli.command { + Some(Commands::Push) => { + app_state.storage.store_revision(app_state.clone())?; + } + _ => Cli::command().print_long_help()?, + } + + Ok(()) +} + +fn main() -> std::io::Result<()> { + let tracing_env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + tracing_subscriber::registry() + .with(tracing_env_filter) + .with( + tracing_subscriber::fmt::layer() + .compact() + .with_span_events(FmtSpan::CLOSE), + ) + .init(); + if let Err(e) = run() { + eprintln!("Error: {}", e.as_report()); + } + + Ok(()) +} diff --git a/src/reporter.rs b/src/reporter.rs new file mode 100644 index 0000000..dd61351 --- /dev/null +++ b/src/reporter.rs @@ -0,0 +1,57 @@ +use std::{borrow::Cow, time::Duration}; + +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; + +use crate::Data; + +pub struct Reporter { + spinner: ProgressBar, +} + +pub const TICK_CHARS: &str = "⣷⣯⣟⡿⢿⣻⣽⣾"; + +impl Reporter { + pub fn new(app_state: Data) -> Self { + let spinner = app_state.multi.add(ProgressBar::new_spinner()); + spinner.set_style( + ProgressStyle::with_template("{msg:>8.214/yellow} {spinner} [{elapsed_precise}]") + .unwrap() + .tick_chars(TICK_CHARS), + ); + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + spinner.set_draw_target(ProgressDrawTarget::stderr_with_hz(20)); + Self { spinner } + } + + pub fn start_archive_progress(&self, total: u64) { + self.spinner.set_length(total); + self.spinner.set_position(0); + self.spinner.set_style( + ProgressStyle::with_template( + "{msg:>12.214/yellow} {spinner} [{elapsed_precise}] [{bar:30.yellow/blue}] {pos}/{len}\n └─ {prefix:.dim}", + ) + .unwrap() + .tick_chars(TICK_CHARS) + .progress_chars("█▉▊▋▌▍▎▏ "), + ); + } + + pub fn inc_archive_progress(&self) { + self.spinner.inc(1); + } + + pub fn set_message(&self, msg: impl Into>) { + self.spinner.set_message(msg); + } + + pub fn set_subtle(&self, msg: impl Into>) { + self.spinner.set_prefix(msg); + } + + pub fn finish_archive_progress(&self, msg: impl Into>) { + self.spinner.set_style( + ProgressStyle::with_template("{msg:>12.green} ✓ [{elapsed_precise}]").unwrap(), + ); + self.spinner.finish_with_message(msg); + } +} diff --git a/src/storage/fs.rs b/src/storage/fs.rs new file mode 100644 index 0000000..b5f0d3a --- /dev/null +++ b/src/storage/fs.rs @@ -0,0 +1,63 @@ +use std::fs::DirEntry; +use std::io::BufWriter; +use std::path::{self, Path}; +use std::{fs, path::PathBuf}; + +use flate2::{Compression, write::GzEncoder}; +use tar::Builder; +use walkdir::WalkDir; + +use crate::Data; +use crate::archive::create_archive; +use crate::error::{Error, Result}; +use crate::reporter::Reporter; +use crate::storage::{IGNORE_FILES, StorageImpl, base_path_prism}; + +pub struct FSStorage; + +const LOCKFILE_NAME: &str = "revision.lock"; + +impl StorageImpl for FSStorage { + fn get_revision(&self) -> Result { + let path = base_path().join(LOCKFILE_NAME); + let revision = match fs::read_to_string(&path) { + Ok(rev) => rev.parse::().map_err(|_| Error::revision_lock())?, + Err(_) => { + tracing::debug!("creating directory {:?}", path.parent().unwrap()); + std::fs::create_dir_all(path.parent().unwrap())?; + fs::write(&path, "0").map_err(|e| Error::read_revision(e, &path))?; + 0 + } + }; + + Ok(revision) + } + + fn store_revision(&self, app_state: Data) -> Result<()> { + let revision = self.get_revision()?; + let new_revision = revision + 1; + let reporter = Reporter::new(app_state.clone()); + let result = create_archive(app_state, &reporter)?; + let archive_path = base_path().join(&format!("revision_{new_revision}.tar.gz")); + + result + .temp_file + .persist(&archive_path) + .map_err(|e| Error::persist_error(e, &archive_path))?; + let lockfile_path = base_path().join(LOCKFILE_NAME); + fs::write(&lockfile_path, new_revision.to_string())?; + + tracing::info!( + revision = new_revision, + path = ?archive_path, + "stored revision" + ); + Ok(()) + } +} + +pub fn base_path() -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("minecraft-sync") +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..3346af6 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,40 @@ +pub mod fs; + +use std::path::PathBuf; + +use crate::{Data, error::Result}; + +pub trait StorageImpl { + fn get_revision(&self) -> Result; + fn store_revision(&self, app_state: Data) -> Result<()>; +} + +pub enum Storage { + FS(fs::FSStorage), +} + +pub const IGNORE_FILES: [&str; 6] = ["assets", "cache", "catpacks", "logs", "meta", "metacache"]; + +impl StorageImpl for Storage { + fn get_revision(&self) -> Result { + match self { + Self::FS(storage) => storage.get_revision(), + } + } + + fn store_revision(&self, app_state: Data) -> Result<()> { + match self { + Self::FS(storage) => storage.store_revision(app_state), + } + } +} + +pub fn get_storage_from_env() -> Storage { + Storage::FS(fs::FSStorage) +} + +pub fn base_path_prism() -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("PrismLauncher") +}