From 7d5812e192f47a75b285f9952aecae13d46b46b2 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Feb 2023 17:56:57 +1100 Subject: [PATCH] first commit --- .gitignore | 1 + Cargo.lock | 630 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 ++ README.md | 23 ++ install.sh | 16 ++ src/main.rs | 308 +++++++++++++++++++++++++ 6 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100755 install.sh create mode 100644 src/main.rs 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..7e83bfe --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,630 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cxx" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "expanduser" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e0b79235da57db6b6c2beed9af6e5de867d63a973ae3e91910ddc33ba40bc0" +dependencies = [ + "dirs", + "lazy_static", + "pwd", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pwd" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" +dependencies = [ + "libc", + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + +[[package]] +name = "soyuz-cli" +version = "1.0.0" +dependencies = [ + "chrono", + "expanduser", + "regex", + "serde", + "toml", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "toml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +dependencies = [ + "indexmap", + "nom8", + "serde", + "serde_spanned", + "toml_datetime", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ae625f8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "soyuz-cli" +version = "1.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.23" +expanduser = "1.2.2" +regex = "1.7.1" +serde = { version = "1.0.152", features = ["derive"] } +toml = { version = "0.7.2", features = ["parse"] } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4f5db2 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# soyuz-cli + +This is the sister application to `soyuz-web`. + +`soyuz-cli` can be used alone or on combination with `soyuz-web` to publish and syncronise a gemlog between your local machine and your gemini server. + +It is essentially a wrapper around `rsync`. Additionally it will maintain an archive of posts for each year, and the latest 5 posts on your gemlog homepage. + +## Assumptions + +1. You are using MacOS locally and a unix-like OS on the server (it probably works on n*x locally but I haven't tested it) +2. If you have text _under_ the list of latest posts on your homepage, you have at least 5 posts listed already (I could have put in some logic to deal with this but I am lazy) +3. You have `rsync` installed on your local machine +4. You have read and write permission for your gemlog files on the server, with the same username as your local machine (otherwise file permissions get weird) +5. You have permission to install files at `/usr/local/bin` on your local machine + +## Installation + +The easiest way to install `soyuz-cli` is using the install script: + +```sh +curl https://hugh.run/install-soyuz | bash +``` \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..395a6f2 --- /dev/null +++ b/install.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# This script installs soyuz-cli +# Download latest binary and save in /usr/local/bin +curl -L https://github.com/hughrun/soyuz-cli/releases/download/latest/MacOS > /usr/local/bin/soyuz +# make it executable +chmod +x /usr/local/bin/yawp_test + +cat 1>&2 << 'EOM' + +🚀 soyuz-cli is now installed! + + 🔧 Get set up with 'soyuz settings' + ℹ️ For help try 'soyuz help' + +EOM \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d3e0c89 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,308 @@ +#![feature(file_create_new)] +#![feature(fs_try_exists)] +#![allow(dead_code)] + +use chrono::prelude::*; +use std::env; +use expanduser::expanduser; +use std::path::PathBuf; +use regex::Regex; +use serde::Deserialize; +use std::fs; +use std::process::Command; +use toml; + +#[derive(Debug, Deserialize)] +struct Config { + local_dir: String, + remote_dir: String, + editor: String, + index_heading: String +} + +fn create_or_update_homepage(string: String) -> Result<(), std::io::Error> { + let config = read_config().expect("cannot read config"); + let filepath = format!("{}{}", config.local_dir, "index.gmi"); + match fs::try_exists(&filepath) { + Ok(true) => { + let homepage = fs::read_to_string(&filepath)?; + match homepage.find(&string) { + Some(_s) => { + // if it's already listed, do nothing and move on... + }, + None => { + let mut lines: Vec<&str> = homepage.lines().collect(); + if lines.contains(&config.index_heading.as_str()) { + let i = lines.iter().position(|x| x == &config.index_heading).unwrap(); // ERROR here if the file exists but is empty + let first_entry = format!("\n{}", string); + lines[i + 1] = &first_entry; + lines.remove(i + 6); + fs::write(filepath, lines.join("\n"))?; + } else { + // if the file exists but is empty (or can't find the header) just write out the new entry + // Note: this will overwrite the homepage if it doesn't contain the header string exactly as + // per the config + let contents = format!("{}\n\n{}", config.index_heading, string); + fs::write(&filepath, contents)?; + } + } + } + }, + Ok(false) => { + // there's no homepage, create a new one + let contents = format!("{}\n\n{}", config.index_heading, string); + fs::write(&filepath, contents)?; + }, + Err(err) => eprintln!("{}", err) + }; + Ok(()) +} + +fn help() { + println!("Союз (Soyuz) - v{}", option_env!("CARGO_PKG_VERSION").unwrap()); + println!("A command line program for publishing Gemini posts.\n"); + println!(" soyuz ( help | settings | write | publish | sync [ --overwrite | --delete ] )\n"); + println!(" help"); + println!(" - shows this help screen\n"); + println!(" settings"); + println!(" - create or edit the settings file\n"); + println!(" write"); + println!(" - create or edit today's gempost using the editor specified in the settings file\n"); + println!(" publish"); + println!(" - update the homepage and year archive lists of posts, and publish to server."); + println!(" Syncs new or changed files from the server before updating indexes and syncing up.\n"); + println!(" sync"); + println!(" - synchronise files between local machine and server."); + println!(" This is a wrapper around rsync, the default being 'rsync -rtOq'"); + println!(" Without arguments sync defaults to 'sync up' which is the equivalent of 'publish'\n"); + println!(" sync up"); + println!(" - syncronise new or changed files from local machine to server.\n"); + println!(" sync down"); + println!(" - syncronise new or changed files from server to local machine.\n"); + println!(" sync (down | up) --overwrite"); + println!(" - sync, and overwrite all files at the destination regardless of last edit date."); + println!(" Without this flag, sync will ignore files at the destination edited more recently than the source.\n"); + println!(" sync (down | up) --delete"); + println!(" - sync, and delete any files at the destination that do not exist at the source.\n"); +} + +fn open_file(filepath: std::path::PathBuf) { + let editor = match read_config() { + Ok(config) => config.editor, + Err(_err) => String::from("nano") + }; + + let mut open = Command::new(&editor) + .arg(&filepath) + .spawn() + .expect(format!("Failed to run {} {:?} command", editor, filepath.into_os_string()).as_str()); + let _result = open.wait().expect("problem waiting for editor"); +} + +fn publish() -> Result<(), Box> { + let config = read_config()?; + // sync down from server (only files that are newer on the server than local) + let mut sync = Command::new("rsync") + .args(["-rtOq", "--update", &config.remote_dir, &config.local_dir]) + .spawn() + .expect("syncing from server failed"); + let _result = sync.wait().expect("problem waiting for rsync"); + // check for this year's directory + let dt = Local::now(); + let year = &dt.format("%Y").to_string(); + let dirpath = format!("{}{}", &config.local_dir, year); + let indexfilepath = format!("{}/{}", &dirpath, "index.gmi"); + if let Ok(entries) = fs::read_dir(&dirpath) { + let mut most_recent: (i32, String) = (0,String::new()); + for entry in entries { + if let Ok(entry) = entry { + // if filename format yyyy-mm-dd.gmi + let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").expect("Error creating regex"); + let file_string = entry.file_name().into_string().expect("Cannot parse filename"); + let ent = match &file_string.strip_suffix(".gmi") { + Some(stripped) => stripped, + None => "" + }; + if re.is_match(&ent) { + let allnums = ent.replace("-", ""); + let name_as_int: i32 = allnums.parse().unwrap(); + if most_recent.0 < name_as_int { + most_recent = (name_as_int, file_string); + } + } + }; + }; + if most_recent.0 > 0 { + let filename = most_recent.1; + let sliced = &filename.strip_suffix(".gmi").expect("file is not a gemini file!"); + let post = fs::read_to_string(format!("{}/{}", dirpath, &filename))?; + let post_lines: Vec<&str> = post.lines().collect(); + let title = post_lines[0].strip_prefix("# "); + let entry_string = format!("=> {} {} ({})", filename, sliced, title.unwrap()); + let home_entry_string = format!("=> /{}/{} {} ({})", &year, filename, sliced, title.unwrap()); + if let Ok(index) = fs::read_to_string(&indexfilepath) { + // is the file listed in the archive? + match index.find(&filename) { + Some(_i) => { + // the file is already listed in the year index + // check that it's listed on the homepage too + create_or_update_homepage(home_entry_string)?; + }, + None => { + let mut lines: Vec<&str> = index.lines().collect(); + if lines.len() > 2 { + // there's an archive page, update it + let first_value = format!("{}\n{}", &entry_string, lines[2]); + lines[2] = &first_value; + let new_string = lines.join("\n"); + fs::write(indexfilepath, &new_string)?; + } else { + // file is blank, overwrite it + let contents = format!("# {}\n\n{}", &year, &entry_string); + fs::write(indexfilepath, contents)?; + } + // update or create the homepage + create_or_update_homepage(home_entry_string)?; + } + } + } else { + // year archive file doesn't exist, just create it + let contents = format!("# {}\n\n{}", &year, &entry_string); + fs::write(indexfilepath, contents)?; + create_or_update_homepage(home_entry_string)?; + } + + } else { + // dir has no files for some reason in + // in this case there's nothing to do + // except sync up any changes from other dirs + } + } else { + // create new directory then try again + // this will create an empty archive if used + // before there are any new posts for the year + fs::create_dir_all(dirpath)?; + publish()?; + } + + // rsync to server (no del) + let mut sync_up = Command::new("rsync") + .args(["-rtOq", &config.local_dir, &config.remote_dir]) + .spawn() + .expect("syncing to server failed"); + let _sync_up_result = sync_up.wait().expect("problem waiting for rsync up"); + println!("🎉 published!"); + Ok(()) +} + + +fn read_config() -> Result> { + let config_path: std::path::PathBuf = expanduser("~/.config/soyuz/config.toml")?; + let vals = fs::read_to_string(config_path)?; + + let mut config: Config = toml::from_str(&vals)?; + let local: PathBuf = expanduser(config.local_dir).unwrap(); + let remote: PathBuf = expanduser(config.remote_dir).unwrap(); + let l_trimmed = local.to_str().unwrap().trim_end_matches("/"); + let r_trimmed = remote.to_str().unwrap().trim_end_matches("/"); + config.local_dir = format!("{}/", l_trimmed); + config.remote_dir = format!("{}/", r_trimmed); + Ok(config) +} + + +fn settings() -> Result<(), Box> { + let config_dir: std::path::PathBuf = expanduser("~/.config/soyuz")?; + let config_path: std::path::PathBuf = expanduser("~/.config/soyuz/config.toml")?; + fs::create_dir_all(config_dir)?; + let _file = match fs::File::create_new(&config_path) { + Ok(_file) => { + fs::write(&config_path, "local_dir = \nremote_dir = \neditor = \"nano\"\nindex_heading =\"## Latest notes\"\n") + }, + Err(_err) => Ok(()) // do nothing, this means the file exists already + }; + Ok(open_file(config_path)) +} + +fn sync(args: &Vec) -> Result<(), Box> { + let config = read_config()?; + let rsync_args = match args.len() { + 2 => ["-rtOq", "--update", &config.remote_dir, &config.local_dir], + 3 | 4 => { + match args[2].as_str() { + "down" => { + if args.len() == 3 { + ["-rtOq", "--update", &config.remote_dir, &config.local_dir] + } else { + match args[3].as_str() { + "--overwrite" => { + ["-rtO", "--quiet", &config.remote_dir, &config.local_dir] + }, + "--delete" => { + ["-rtOq", "--delete", &config.remote_dir, &config.local_dir] + }, + _ => ["", "", "", ""], + } + } + }, + "up" => { + if args.len() == 3 { + ["-rtOq", "--update", &config.local_dir, &config.remote_dir] + } else { + match args[3].as_str() { + "--overwrite" => { + ["-rtO", "--quiet", &config.local_dir, &config.remote_dir] + }, + "--delete" => { + ["-rtOq", "--delete", &config.local_dir, &config.remote_dir] + }, + _ => ["", "", "", ""], + } + } + }, + _ => ["","","",""], + } + }, + _ => ["","","",""], + }; + if rsync_args[0] == "" { + Ok(help()) + } else { + let mut sync = Command::new("rsync") + .args(rsync_args) + .spawn() + .expect("syncing failed"); + let _sync_res = sync.wait().expect("problem waiting for rsync"); + Ok(println!("Sync complete.")) + } +} + +fn write() -> Result<(), Box> { + let config = read_config()?; + let dt = Local::now(); + let year = &dt.format("%Y").to_string(); + let local_dir: std::path::PathBuf = expanduser(config.local_dir)?; + fs::create_dir_all(format!("{}{}", &local_dir.display(), &year))?; + let filepath = format!("{}{}/{}.gmi", local_dir.display(), year, dt.format("%Y-%m-%d")); + Ok(open_file(filepath.into())) +} + +fn match_single_arg(args: &Vec) -> Result<(), Box> { + match args[1].as_str() { + "write" => write(), + "settings" => settings(), + "publish" => publish(), + "sync" => sync(args), + _ => Ok(help()) + } +} + +fn main() -> Result<(), Box>{ + let args: Vec = env::args().collect(); + match &args.len() { + 1 => Ok(help()), + 2 => match_single_arg(&args), + 3 | 4 => sync(&args), + _ => Ok(help()) + } +}