From 15e5b45e46f9cb39ada516c196336834054600f5 Mon Sep 17 00:00:00 2001 From: Max Struebing Date: Sat, 26 Nov 2022 11:23:16 +0100 Subject: [PATCH] feat: initial commit --- .github/workflows/ci.yaml | 61 ++++ .github/workflows/commitlint.yaml | 11 + .github/workflows/generate-changelog.yaml | 29 ++ .github/workflows/release.yaml | 65 ++++ .gitignore | 1 + Cargo.lock | 399 ++++++++++++++++++++++ Cargo.toml | 18 + Makefile | 3 + README.md | 52 +++ src/archive.rs | 27 ++ src/config.rs | 55 +++ src/files.rs | 93 +++++ src/main.rs | 152 +++++++++ src/remotes.rs | 69 ++++ 14 files changed, 1035 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/commitlint.yaml create mode 100644 .github/workflows/generate-changelog.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/archive.rs create mode 100644 src/config.rs create mode 100644 src/files.rs create mode 100644 src/main.rs create mode 100644 src/remotes.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..5b11817 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,61 @@ +name: CI +on: + push: + branches: + - main + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - uses: actions-rs/toolchain@v1 + id: toolchain + with: + profile: minimal + toolchain: stable + components: clippy, rustfmt + + - name: Check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features -- -D warnings + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --no-fail-fast diff --git a/.github/workflows/commitlint.yaml b/.github/workflows/commitlint.yaml new file mode 100644 index 0000000..ee53d28 --- /dev/null +++ b/.github/workflows/commitlint.yaml @@ -0,0 +1,11 @@ +name: Lint Commit Messages +on: [pull_request] + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v3 diff --git a/.github/workflows/generate-changelog.yaml b/.github/workflows/generate-changelog.yaml new file mode 100644 index 0000000..06aed99 --- /dev/null +++ b/.github/workflows/generate-changelog.yaml @@ -0,0 +1,29 @@ +name: CI - Changelog + +on: + push: + branches: [main] + +jobs: + changelog_prerelease: + name: Update Changelog For Prerelease + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: main + - name: Update Changelog + uses: heinrichreimer/github-changelog-generator-action@v2.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issues: true + issuesWoLabels: true + pullRequests: true + prWoLabels: true + unreleased: true + addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}' + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Update Changelog for PR + file_pattern: CHANGELOG.md diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..844ed34 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,65 @@ +name: Release +on: + push: + tags: + - "*.*.*" + +env: + CARGO_TERM_COLOR: always + +jobs: + release-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build + run: | + cargo build --release --target x86_64-unknown-linux-gnu + mv target/x86_64-unknown-linux-gnu/release/sback ./sback + chmod +x sback + tar -czf sback-linux-x86_64.tar.gz sback + rm sback + rustup target add x86_64-unknown-linux-musl + cargo build --release --target x86_64-unknown-linux-musl + mv target/x86_64-unknown-linux-musl/release/sback ./sback + chmod +x sback + tar -czf sback-alpine-x86_64.tar.gz sback + - name: Publish release + uses: softprops/action-gh-release@v1 + with: + files: sback-* + draft: true + body_path: CHANGELOG.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-darwin: + runs-on: macos-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Build + run: | + cargo build --release --target x86_64-apple-darwin + mv target/x86_64-apple-darwin/release/sback ./sback + chmod +x sback + tar -czf sback-darwin-x86_64.tar.gz sback + - name: Publish release + uses: softprops/action-gh-release@v1 + with: + files: sback-* + draft: true + body_path: CHANGELOG.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-crate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build and publish to crates.io + run: | + cargo login ${{ secrets.CRATES_TOKEN }} + cargo publish 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..7415869 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,399 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "sback" +version = "0.1.0" +dependencies = [ + "clap", + "flate2", + "tar", + "xdg", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[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 = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.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 = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e019dc0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sback" +version = "0.1.0" +edition = "2021" +authors = ["Max StrĂ¼bing "] +description = "A CLI tool to manage and run your backups." +repository = "https://github.com/mstruebing/backup.rs" +homepage = "https://github.com/mstruebing/backup.rs" +license = "MIT" +categories = ["command-line-utilities", "backup"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "3.2.17" +xdg = "2.4.1" +flate2 = "1.0.24" +tar = "0.4.38" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b5534bc --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +clean: + rm -rf ~/.config/sback + rm -rf ~/.cache/sback diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a8652c --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# backup.rs + +A CLI tool to manage and run your backups. + +# How it works + +Most of the backup solutions out there are way to complex, this is why I've created this simple tool. +It has a configuration file which contains all files or directories you want to back up and a different configuration file +which contains the destinations where the backup should be placed in an rsync compatible format. +When you start the backup there is a gzipped tarball created which then gets transferred to all remotes one by one. +And that's it. No magic, no surprises. + +The configuration files are placed in `$XDG_CONFIG_HOME` and the tarball is placed in `$XDG_CACHE_HOME` + +Here is a table showing the different commands + +| command | subcommand | description | example | +| ------- | ---------- | -------------------------------------------------------------------- | ------------------------------------------------------ | +| files | list | list all files | `sback files list` | +| files | add | add a file | `sback files add ./README.md` | +| files | remove | remove a file | `sback files remove $PWD/README.md` | +| files | clean | sorts the file list and removes duplicates in case of manual editing | `sback files clean` | +| remotes | list | list all remote | `sback remotes list` | +| remotes | add | add a remote | `sback remote add backup-user@12.98.34.76:~/backup` | +| remotes | remove | remove a remote | `sback remote remove backup-user@12.98.34.76:~/backup` | +| run | - | executes the backup process | `sback run` | + +# Dependencies + +This tool uses rsync to transfer these files so you need rsync installed. + +# Installation + +## Crates.io + +`cargo install sback` + +## Raw + +Clone the repository and run `cargo build --release` and you should find the binary in `./target/release/sback`. + +## Release Page + +Or grab a binary from the [release page](https://github.com/mstruebing/backup.rs/releases) + +# Contribution + +- Fork this project +- Create a branch +- Provide a pull request + +The CI will lint your commit message with [commitlint](https://commitlint.js.org/#/). diff --git a/src/archive.rs b/src/archive.rs new file mode 100644 index 0000000..a9bd093 --- /dev/null +++ b/src/archive.rs @@ -0,0 +1,27 @@ +use flate2::write::GzEncoder; +use flate2::Compression; +use std::fs::File; +use std::path::PathBuf; + +// Creates a gzipped tar file of given files in a given destination +pub fn create(files: Vec, destination: PathBuf) -> Result<(), ()> { + let tar_gz = File::create(destination).unwrap(); + let enc = GzEncoder::new(tar_gz, Compression::best()); + let mut tar = tar::Builder::new(enc); + + // prevent from panic in case of broken symlinks + tar.follow_symlinks(false); + + for file in files { + let filename_for_tar = file.strip_prefix("/").unwrap(); + + if file.is_dir() { + tar.append_dir_all(filename_for_tar, file.clone()).unwrap() + } else if file.is_file() { + tar.append_path_with_name(file.clone(), filename_for_tar) + .unwrap(); + } + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f95e385 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +pub struct Config { + pub files: PathBuf, + pub cache: PathBuf, + pub remotes: PathBuf, +} + +impl Config { + pub fn new(prefix: &str) -> Config { + let xdg_dirs = xdg::BaseDirectories::with_prefix(prefix).unwrap(); + + let files = xdg_dirs + .place_config_file("files") + .expect("cannot create configuration directory"); + + let remotes = xdg_dirs + .place_config_file("remotes") + .expect("cannot create configuration directory"); + + let cache = xdg_dirs + .place_cache_file("archive.tar.gz") + .expect("cannot create cache directory"); + + create_file(files.to_owned()).ok(); + create_file(remotes.to_owned()).ok(); + create_file(cache.to_owned()).ok(); + + Config { + files, + remotes, + cache, + } + } +} + +fn create_file(path: PathBuf) -> Result<(), ()> { + let directory = path.parent(); + + match directory { + Some(d) => { + if !d.is_dir() { + std::fs::create_dir_all(d).unwrap(); + } + } + None => return Err(()), + }; + + // Create files file if it doesn't exist + if !path.is_file() { + std::fs::File::create(path).unwrap(); + } + + Ok(()) +} diff --git a/src/files.rs b/src/files.rs new file mode 100644 index 0000000..361217e --- /dev/null +++ b/src/files.rs @@ -0,0 +1,93 @@ +use std::fs; +use std::fs::File; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + +pub struct Files { + pub config_path: PathBuf, +} + +impl Files { + pub fn list(&self) -> Result<(), ()> { + let file_list = self.get()?; + for file in file_list { + println!("{}", file.display()); + } + + Ok(()) + } + + pub fn get(&self) -> Result, ()> { + let config_string = fs::read_to_string(&self.config_path).unwrap_or_else(|_| { + panic!( + "Can not read config file: {}", + self.config_path.to_str().unwrap() + ) + }); + + let files = config_string.lines().map(PathBuf::from).collect(); + + Ok(files) + } + + // Should be used when creating the backup archive + pub fn get_only_existing(&self) -> Result, ()> { + let files = self.get()?; + let mut existing_files = files + .into_iter() + .filter_map(|maybe_file| { + if Path::new(&maybe_file).exists() { + Some(PathBuf::from(&maybe_file)) + } else { + None + } + }) + .collect::>(); + + existing_files.sort(); + existing_files.dedup(); + Ok(existing_files) + } + + pub fn add(&self, path: &Path) -> Result<(), ()> { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .open(&self.config_path) + .unwrap(); + + writeln!(file, "{}", std::fs::canonicalize(path).unwrap().display()).unwrap(); + + Ok(()) + } + + pub fn remove(&self, path: &PathBuf) -> Result<(), ()> { + let files = self.get()?; + let files_without: Vec = files + .into_iter() + .filter(|f| f != &std::fs::canonicalize(path).unwrap()) + .collect(); + self.write_file(files_without) + } + + pub fn clean(&self) -> Result<(), ()> { + // We do not use get_only_existing here to preserve files which might + // get created later + let mut files = self.get()?; + files.sort(); + files.dedup(); + self.write_file(files) + } + + fn write_file(&self, files: Vec) -> Result<(), ()> { + let mut file = File::create(&self.config_path).unwrap(); + + for f in files { + writeln!(file, "{}", f.display()).unwrap(); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9689d92 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,152 @@ +use clap::{arg, Command}; +use config::Config; +use std::path::PathBuf; + +mod archive; +mod config; +mod files; +mod remotes; + +extern crate xdg; + +fn cli() -> Command<'static> { + Command::new("backup") + .about("A backup CLI") + .subcommand_required(true) + .arg_required_else_help(true) + .allow_external_subcommands(true) + .subcommand(Command::new("run").about("Executes the backup")) + .subcommand( + Command::new("files") + .args_conflicts_with_subcommands(true) + .about("Subcommands for files") + .subcommand( + Command::new("add") + .about("Add files") + .arg_required_else_help(true) + .arg( + arg!( "Path of the file to add") + .value_parser(clap::value_parser!(PathBuf)), + ), + ) + .subcommand( + Command::new("remove") + .about("Remove files") + .arg_required_else_help(true) + .arg( + arg!( "Path of the file to remove") + .value_parser(clap::value_parser!(PathBuf)), + ), + ) + .subcommand(Command::new("list").about("Lists all files")) + .subcommand(Command::new("clean").about("Sort files and removes duplicates")), + ) + .subcommand( + Command::new("remotes") + .args_conflicts_with_subcommands(true) + .about("Subcommands for remotes") + .subcommand( + Command::new("add") + .about( + "Add a remote in an rsync compatible format i.e. `@:`", + ) + .arg_required_else_help(true) + .arg( + arg!( "Stuff to add") + .value_parser(clap::value_parser!(String)), + ), + ) + .subcommand( + Command::new("remove") + .about("Remove a remote") + .arg_required_else_help(true) + .arg(arg!( "The remote to target")), + ) + .subcommand(Command::new("list").about("List remotes")), + ) +} + +fn main() -> Result<(), ()> { + let config = Config::new(env!("CARGO_PKG_NAME")); + + let files = files::Files { + config_path: config.files, + }; + let remotes = remotes::Remotes { + config_path: config.remotes, + }; + + let matches = cli().get_matches(); + + match matches.subcommand() { + Some(("run", _)) => { + println!("Run backup"); + archive::create(files.get_only_existing().unwrap(), config.cache.clone())?; + remotes.transfer(config.cache)? + } + Some(("files", sub_matches)) => { + let files_command = sub_matches.subcommand().unwrap_or(("list", sub_matches)); + match files_command { + ("list", _sub_matches) => { + files.list()?; + } + ("add", sub_matches) => { + let paths = sub_matches + .get_many::("PATH") + .into_iter() + .flatten() + .collect::>(); + + for path in paths { + files.add(path)?; + println!("File {} added", path.display()) + } + + files.clean()?; + } + ("remove", sub_matches) => { + let paths = sub_matches + .get_many::("PATH") + .into_iter() + .flatten() + .collect::>(); + + for path in paths { + files.remove(path)?; + println!("File {} removed", path.display()) + } + } + ("clean", _sub_matches) => { + files.clean()?; + } + (name, _) => { + unreachable!("Unsupported subcommand `{}`", name) + } + } + } + Some(("remote", sub_matches)) => { + let remote_command = sub_matches.subcommand().unwrap_or(("list", sub_matches)); + match remote_command { + ("list", _sub_matches) => { + remotes.list(); + } + ("add", sub_matches) => { + let r = sub_matches.get_one::("String").unwrap(); + + println!("Adding remote {:?}", r); + remotes.add(r.to_owned()).unwrap(); + } + ("remove", sub_matches) => { + let remote = sub_matches.get_one::("REMOTE").expect("required"); + println!("Removing remote {:?}", remote); + } + (name, _) => { + unreachable!("Unsupported subcommand `{}`", name) + } + } + } + _ => unreachable!(), + } + + Ok(()) +} diff --git a/src/remotes.rs b/src/remotes.rs new file mode 100644 index 0000000..010cc1b --- /dev/null +++ b/src/remotes.rs @@ -0,0 +1,69 @@ +use std::fs; +use std::fs::OpenOptions; +use std::process::Command; + +use std::io::Write; +use std::path::PathBuf; + +pub struct Remotes { + pub config_path: PathBuf, +} + +impl Remotes { + pub fn add(&self, remote_string: String) -> Result<(), ()> { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .open(&self.config_path) + .unwrap(); + + writeln!(file, "{}", remote_string).unwrap(); + + Ok(()) + } + + pub fn list(&self) { + let remotes = self.get_remotes(); + for remote in remotes { + println!("{}", remote); + } + } + + pub fn transfer(&self, file: PathBuf) -> Result<(), ()> { + let remotes = self.get_remotes(); + let string_file = &file.into_os_string().into_string().unwrap(); + + for remote in remotes { + println!("Copying to remote: {}", remote); + let cmd = Command::new("rsync") + .args(["-azvhP", string_file, &remote]) + .spawn() + .unwrap(); + + let output = cmd.wait_with_output(); + + match output { + Ok(output) => { + println!("output: {:?}", output); + } + Err(error) => { + println!("error: {:?}", error); + } + } + } + + Ok(()) + } + + fn get_remotes(&self) -> Vec { + let config_string = fs::read_to_string(&self.config_path).unwrap_or_else(|_| { + panic!( + "Can not read config file: {}", + self.config_path.to_str().unwrap() + ) + }); + + let remotes: Vec = config_string.lines().map(String::from).collect(); + remotes + } +}