Compare commits
10 Commits
0104e69657
...
1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
64a874252a
|
|||
|
c6b3934ca5
|
|||
|
3af70f3647
|
|||
|
3ef29ba7bc
|
|||
|
4d866f9608
|
|||
|
37498f5b33
|
|||
|
905452e91b
|
|||
|
0e583377e9
|
|||
|
eb42576823
|
|||
|
f866c8dd72
|
209
Cargo.lock
generated
209
Cargo.lock
generated
@ -13,9 +13,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.44"
|
version = "1.0.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
@ -30,9 +30,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
@ -42,7 +42,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "butterup"
|
name = "butterup"
|
||||||
version = "0.1.0"
|
version = "1.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -54,15 +54,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.70"
|
version = "1.0.73"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@ -85,27 +79,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.0.0-beta.4"
|
version = "3.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406"
|
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
"clap_lex",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"os_str_bytes",
|
|
||||||
"strsim",
|
"strsim",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"vec_map",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "3.0.0-beta.4"
|
version = "3.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac"
|
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
@ -115,12 +108,12 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloudabi"
|
name = "clap_lex"
|
||||||
version = "0.0.3"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -144,12 +137,9 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.3.3"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||||
dependencies = [
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
@ -171,14 +161,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.7.0"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -187,15 +186,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.103"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libssh2-sys"
|
name = "libssh2-sys"
|
||||||
version = "0.2.21"
|
version = "0.2.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee"
|
checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@ -207,9 +206,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libz-sys"
|
name = "libz-sys"
|
||||||
version = "1.1.3"
|
version = "1.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
|
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@ -219,33 +218,34 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.3.4"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
|
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.14"
|
version = "0.4.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.44"
|
version = "0.1.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -253,50 +253,61 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.14"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-src"
|
||||||
version = "0.9.67"
|
version = "111.20.0+1.1.1o"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
|
checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_str_bytes"
|
name = "os_str_bytes"
|
||||||
version = "3.1.0"
|
version = "6.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
|
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.10.2"
|
version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
|
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"instant",
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.7.2"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
|
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if",
|
||||||
"cloudabi",
|
"instant",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -305,9 +316,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.20"
|
version = "0.3.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pretty_env_logger"
|
name = "pretty_env_logger"
|
||||||
@ -345,11 +356,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.29"
|
version = "1.0.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -360,24 +371,27 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.9"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.1.57"
|
version = "0.2.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.5.4"
|
version = "1.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -386,9 +400,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.25"
|
version = "0.6.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
@ -398,15 +412,15 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ssh2"
|
name = "ssh2"
|
||||||
version = "0.9.1"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7"
|
checksum = "269343e64430067a14937ae0e3c4ec604c178fb896dde0964b1acd22b3e2eeb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"libc",
|
"libc",
|
||||||
@ -422,32 +436,29 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.77"
|
version = "1.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
|
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"unicode-xid",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.14.2"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||||
dependencies = [
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
@ -461,22 +472,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-ident"
|
||||||
version = "1.8.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
@ -484,17 +483,11 @@ version = "0.2.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vec_map"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "butterup"
|
name = "butterup"
|
||||||
description = "Backup btrfs snapshots over SSH"
|
description = "Backup btrfs snapshots over SSH"
|
||||||
version = "0.1.0"
|
version = "1.1.1"
|
||||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@ -9,7 +9,13 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
clap = "3.0.0-beta.4"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
ssh2 = "0.9.1"
|
|
||||||
|
[dependencies.clap]
|
||||||
|
version = "3.1.0"
|
||||||
|
features = ["derive", "cargo"]
|
||||||
|
|
||||||
|
[dependencies.ssh2]
|
||||||
|
version = "0.9.1"
|
||||||
|
features = ["vendored-openssl"]
|
||||||
|
|||||||
10
README.md
Normal file
10
README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# butterup
|
||||||
|
Backup btrfs snapshots over ssh.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Point butterup at a folder containing btrfs subvolumes named according to RFC 3339, it will upload them to a remote host using ssh.
|
||||||
|
|
||||||
|
For detailed usage:
|
||||||
|
```sh
|
||||||
|
butterup help
|
||||||
|
```
|
||||||
@ -1,17 +1,17 @@
|
|||||||
use crate::{local, planner, remote, Opt};
|
use crate::{local, planner, remote, Opt};
|
||||||
|
|
||||||
pub fn run(opt: &Opt) -> anyhow::Result<()> {
|
pub fn run(opt: &Opt, include_all: bool) -> anyhow::Result<()> {
|
||||||
info!("showing backup plan");
|
info!("showing backup plan");
|
||||||
|
|
||||||
let local_list = local::file_list(opt)?;
|
let local_list = local::file_list(opt)?;
|
||||||
let session = remote::connect(opt)?;
|
let session = remote::connect(opt)?;
|
||||||
let remote_list = remote::file_list(opt, &session)?;
|
let remote_list = remote::file_list(opt, &session)?;
|
||||||
|
|
||||||
let plan = planner::plan(&local_list, &remote_list);
|
let plan = planner::plan(&local_list, &remote_list, include_all);
|
||||||
let presence = planner::presence(&local_list, &remote_list);
|
let presence = planner::presence(&local_list, &remote_list);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"found {} out of {} folders that need backup",
|
"found that {} out of {} folders need backup",
|
||||||
plan.transfers.len(),
|
plan.transfers.len(),
|
||||||
presence.len(),
|
presence.len(),
|
||||||
);
|
);
|
||||||
@ -19,7 +19,7 @@ pub fn run(opt: &Opt) -> anyhow::Result<()> {
|
|||||||
if !plan.transfers.is_empty() {
|
if !plan.transfers.is_empty() {
|
||||||
println!("plan:");
|
println!("plan:");
|
||||||
for (item, item_plan) in plan.transfers {
|
for (item, item_plan) in plan.transfers {
|
||||||
println!("- {}: {:?}", item, item_plan);
|
println!("- {:?}: {:?}", item, item_plan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
use crate::local;
|
use crate::local;
|
||||||
use crate::planner::{self, TransferKind};
|
use crate::planner::{self, TransferKind};
|
||||||
use crate::remote;
|
use crate::remote;
|
||||||
|
use crate::util::{format_duration, path_as_utf8};
|
||||||
use crate::Opt;
|
use crate::Opt;
|
||||||
use ssh2::Session;
|
use ssh2::Session;
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read, Write};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
pub fn run(opt: &Opt, sync_all: bool) -> anyhow::Result<()> {
|
const TMP_FOLDER: &str = ".tmp";
|
||||||
// TODO: currently we only sync the latest local files
|
|
||||||
// --all will force a sync of ALL files on local which does not exist on remote
|
|
||||||
if sync_all {
|
|
||||||
error!("backup --all is not yet implemented");
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
pub fn run(opt: &Opt, include_all: bool) -> anyhow::Result<()> {
|
||||||
info!("generating backup plan");
|
info!("generating backup plan");
|
||||||
let local_list = local::file_list(opt)?;
|
let local_list = local::file_list(opt)?;
|
||||||
let session = remote::connect(opt)?;
|
let session = remote::connect(opt)?;
|
||||||
let remote_list = remote::file_list(opt, &session)?;
|
let remote_list = remote::file_list(opt, &session)?;
|
||||||
|
|
||||||
let plan = planner::plan(&local_list, &remote_list);
|
let plan = planner::plan(&local_list, &remote_list, include_all);
|
||||||
|
|
||||||
if plan.transfers.is_empty() {
|
if plan.transfers.is_empty() {
|
||||||
info!("nothing to do");
|
info!("nothing to do");
|
||||||
@ -44,13 +43,81 @@ pub fn run(opt: &Opt, sync_all: bool) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CmdOutput {
|
||||||
|
exit_status: i32,
|
||||||
|
stdout: String,
|
||||||
|
stderr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_cmd(session: &Session, cmd: &str) -> anyhow::Result<CmdOutput> {
|
||||||
|
let mut ch = session.channel_session()?;
|
||||||
|
|
||||||
|
ch.exec(cmd)?;
|
||||||
|
ch.send_eof()?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
|
ch.stderr().read_to_string(&mut stderr)?;
|
||||||
|
ch.read_to_string(&mut stdout)?;
|
||||||
|
|
||||||
|
ch.close()?;
|
||||||
|
ch.wait_close()?;
|
||||||
|
let exit_status = ch.exit_status()?;
|
||||||
|
|
||||||
|
Ok(CmdOutput {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
exit_status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_tmp_dir(opt: &Opt, session: &Session) -> anyhow::Result<bool> {
|
||||||
|
let tmp_path = opt.remote.path.join(TMP_FOLDER);
|
||||||
|
let tmp_path = path_as_utf8(&tmp_path)?;
|
||||||
|
let cmd = format!(r#"rm -r "{}""#, tmp_path);
|
||||||
|
let result = do_cmd(session, &cmd)?;
|
||||||
|
|
||||||
|
let success = result.exit_status == 0;
|
||||||
|
|
||||||
|
Ok(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tmp_dir(opt: &Opt, session: &Session) -> anyhow::Result<()> {
|
||||||
|
let tmp_path = opt.remote.path.join(TMP_FOLDER);
|
||||||
|
let tmp_path = path_as_utf8(&tmp_path)?;
|
||||||
|
let cmd = format!(r#"mkdir "{}""#, tmp_path);
|
||||||
|
let result = do_cmd(session, &cmd)?;
|
||||||
|
|
||||||
|
if result.exit_status != 0 {
|
||||||
|
anyhow::bail!("failed to create {} dir on remote", TMP_FOLDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn send_snapshot(
|
fn send_snapshot(
|
||||||
opt: &Opt,
|
opt: &Opt,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
snapshot: &str,
|
snapshot: &str,
|
||||||
parent: Option<&str>,
|
parent: Option<&str>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
info!("[{}] transmitting delta", snapshot);
|
if parent.is_none() {
|
||||||
|
info!("[{}] sending full snapshot data", snapshot);
|
||||||
|
} else {
|
||||||
|
info!("[{}] sending snapshot delta", snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if clear_tmp_dir(opt, session)? {
|
||||||
|
warn!(
|
||||||
|
"[{}] {} dir did already exist, it is likely that a previous upload failed.",
|
||||||
|
snapshot, TMP_FOLDER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_tmp_dir(opt, session)?;
|
||||||
|
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
// spawn btrfs send
|
// spawn btrfs send
|
||||||
let mut send = Command::new("btrfs")
|
let mut send = Command::new("btrfs")
|
||||||
@ -59,7 +126,7 @@ fn send_snapshot(
|
|||||||
.arg(snapshot)
|
.arg(snapshot)
|
||||||
.current_dir(&opt.path)
|
.current_dir(&opt.path)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
@ -69,35 +136,83 @@ fn send_snapshot(
|
|||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| anyhow::format_err!("failed to take stdout"))?;
|
.ok_or_else(|| anyhow::format_err!("failed to take stdout"))?;
|
||||||
|
|
||||||
// start btrfs receive
|
// #### UPLOAD SNAPSHOT FILE ####
|
||||||
let remote_path = opt
|
const CHUNK_SIZE: usize = 1024 * 1024 * 100; // 100MiB
|
||||||
.remote
|
|
||||||
.path
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| anyhow::format_err!("path not utf-8"))?;
|
|
||||||
let mut receive = session.channel_session()?;
|
|
||||||
receive.exec(&format!(r#"btrfs receive "{}""#, remote_path,))?;
|
|
||||||
|
|
||||||
// pipe send to receive
|
let (data_tx, data_rx) = mpsc::sync_channel(10);
|
||||||
let num_bytes = io::copy(&mut send_stdout, &mut receive)?;
|
let tmp_path = opt.remote.path.join(TMP_FOLDER);
|
||||||
info!("[{}] sent {} bytes", snapshot, num_bytes);
|
|
||||||
|
|
||||||
// wait for send to complete
|
// spawn a thread to stream data from `btrfs send` in chunks
|
||||||
let local_out = send.wait_with_output()?;
|
thread::spawn(move || -> io::Result<()> {
|
||||||
if !local_out.status.success() {
|
'outer: for _chunk in 0.. {
|
||||||
let stderr = std::str::from_utf8(&local_out.stderr)
|
let mut buf: Vec<u8> = vec![0u8; CHUNK_SIZE];
|
||||||
.unwrap_or("failed to parse stderr, not valid utf8");
|
let mut len = 0;
|
||||||
anyhow::bail!("btrfs send failed\nstderr:\n{}", stderr);
|
loop {
|
||||||
|
let free = &mut buf[len..];
|
||||||
|
let n = send_stdout.read(free)?;
|
||||||
|
len += n;
|
||||||
|
|
||||||
|
if n == 0 || n == free.len() {
|
||||||
|
buf.truncate(len);
|
||||||
|
if data_tx.send(buf).is_err() {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we reached EOF
|
||||||
|
if n == 0 {
|
||||||
|
break 'outer;
|
||||||
|
} else {
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut byte_count = 0;
|
||||||
|
let mut i = 0;
|
||||||
|
while let Ok(data) = data_rx.recv() {
|
||||||
|
byte_count += data.len();
|
||||||
|
info!("[{}] uploading {} bytes...", snapshot, byte_count);
|
||||||
|
let snapshot_file = tmp_path.join(format!("{:016}", i));
|
||||||
|
let mut ch = session.scp_send(&snapshot_file, 0o600, data.len() as u64, None)?;
|
||||||
|
ch.write_all(&data)?;
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for receive to complete
|
info!(
|
||||||
receive.send_eof()?;
|
"[{}] re-creating snapshot (this can take a while)",
|
||||||
let mut remote_err = String::new();
|
snapshot
|
||||||
receive.stderr().read_to_string(&mut remote_err)?;
|
);
|
||||||
receive.wait_close()?;
|
let remote_path = path_as_utf8(&opt.remote.path)?;
|
||||||
let status = receive.exit_status()?;
|
let tmp_path = path_as_utf8(&tmp_path)?;
|
||||||
if status != 0 {
|
let cmd = format!(
|
||||||
anyhow::bail!("btrfs receive failed\nstderr:\n{}", remote_err);
|
r#"cat "{}"/* | btrfs receive -e "{}""#,
|
||||||
|
tmp_path, remote_path
|
||||||
|
);
|
||||||
|
let out = do_cmd(session, &cmd)?;
|
||||||
|
|
||||||
|
let time_elapsed = start_time.elapsed();
|
||||||
|
|
||||||
|
if out.exit_status != 0 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"btrfs receive failed\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
out.stdout,
|
||||||
|
out.stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[{}] snapshot was {} bytes, time taken was {}",
|
||||||
|
snapshot,
|
||||||
|
byte_count,
|
||||||
|
format_duration(time_elapsed)
|
||||||
|
);
|
||||||
|
|
||||||
|
if !clear_tmp_dir(opt, session)? {
|
||||||
|
anyhow::bail!("failed to remove {} dir", TMP_FOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -16,7 +16,7 @@ pub fn file_list(opt: &Opt) -> anyhow::Result<FileList> {
|
|||||||
|
|
||||||
let name = match entry.file_name().into_string() {
|
let name = match entry.file_name().into_string() {
|
||||||
Ok(name) => name,
|
Ok(name) => name,
|
||||||
Err(_) => continue,
|
Err(_) => continue, // ignore names that aren't valid utf-8
|
||||||
};
|
};
|
||||||
|
|
||||||
let date = DateTime::parse_from_rfc3339(&name)?;
|
let date = DateTime::parse_from_rfc3339(&name)?;
|
||||||
|
|||||||
39
src/main.rs
39
src/main.rs
@ -5,11 +5,12 @@ mod actions;
|
|||||||
mod local;
|
mod local;
|
||||||
mod planner;
|
mod planner;
|
||||||
mod remote;
|
mod remote;
|
||||||
mod snapshot;
|
mod util;
|
||||||
|
|
||||||
use actions::{list, show_plan, sync};
|
use actions::{list, show_plan, sync};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use clap::{AppSettings, Clap};
|
use clap::{crate_version, Parser};
|
||||||
|
use log::LevelFilter;
|
||||||
use remote::Remote;
|
use remote::Remote;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -18,9 +19,17 @@ pub type TimeStamp = DateTime<FixedOffset>;
|
|||||||
pub type FileList = BTreeMap<TimeStamp, String>;
|
pub type FileList = BTreeMap<TimeStamp, String>;
|
||||||
|
|
||||||
/// Backup btrfs snapshots over SSH
|
/// Backup btrfs snapshots over SSH
|
||||||
#[derive(Clap)]
|
#[derive(Parser)]
|
||||||
#[clap(setting = AppSettings::ColoredHelp)]
|
#[clap(version = crate_version!())]
|
||||||
pub struct Opt {
|
pub struct Opt {
|
||||||
|
/// Log more stuff
|
||||||
|
#[clap(long, short, parse(from_occurrences))]
|
||||||
|
verbose: u8,
|
||||||
|
|
||||||
|
/// Do not output anything but errors.
|
||||||
|
#[clap(long, short)]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
/// The path of the backup directory on the local filesystem
|
/// The path of the backup directory on the local filesystem
|
||||||
#[clap(short = 'l', long)]
|
#[clap(short = 'l', long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@ -41,16 +50,21 @@ pub struct Opt {
|
|||||||
action: Action,
|
action: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clap)]
|
#[derive(Parser)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
/// Perform a backup
|
/// Perform a backup
|
||||||
Backup {
|
Backup {
|
||||||
|
/// Backup all files, not just the most recent ones
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
all: bool,
|
all: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Generate and show a backup plan
|
/// Generate and show a backup plan
|
||||||
ShowPlan,
|
ShowPlan {
|
||||||
|
/// Backup all files, not just the most recent ones
|
||||||
|
#[clap(long)]
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// List all backups, and where they reside
|
/// List all backups, and where they reside
|
||||||
List,
|
List,
|
||||||
@ -59,11 +73,20 @@ pub enum Action {
|
|||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let opt = Opt::parse();
|
let opt = Opt::parse();
|
||||||
|
|
||||||
pretty_env_logger::init();
|
let log_level = match opt.verbose {
|
||||||
|
0 if opt.quiet => LevelFilter::Error,
|
||||||
|
0 => LevelFilter::Info,
|
||||||
|
1 => LevelFilter::Debug,
|
||||||
|
2.. => LevelFilter::Trace,
|
||||||
|
};
|
||||||
|
|
||||||
|
pretty_env_logger::formatted_builder()
|
||||||
|
.filter(None, log_level)
|
||||||
|
.init();
|
||||||
|
|
||||||
match opt.action {
|
match opt.action {
|
||||||
Action::Backup { all } => sync::run(&opt, all)?,
|
Action::Backup { all } => sync::run(&opt, all)?,
|
||||||
Action::ShowPlan => show_plan::run(&opt)?,
|
Action::ShowPlan { all } => show_plan::run(&opt, all)?,
|
||||||
Action::List => list::run(&opt)?,
|
Action::List => list::run(&opt)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ pub enum Presence {
|
|||||||
LocalAndRemote,
|
LocalAndRemote,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For every local file that is not in the remote, return a backup plan.
|
/// Check which files exist on remote, local, or both
|
||||||
pub fn presence(local: &FileList, remote: &FileList) -> BTreeMap<TimeStamp, Presence> {
|
pub fn presence(local: &FileList, remote: &FileList) -> BTreeMap<TimeStamp, Presence> {
|
||||||
let mut presence = BTreeMap::new();
|
let mut presence = BTreeMap::new();
|
||||||
|
|
||||||
@ -41,17 +41,24 @@ pub struct Plan {
|
|||||||
pub transfers: BTreeMap<TimeStamp, TransferKind>,
|
pub transfers: BTreeMap<TimeStamp, TransferKind>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For every local file that is not in the remote, return a backup plan.
|
/// For every trailing local file that is not in the remote, return a backup plan.
|
||||||
///
|
///
|
||||||
/// The backup plans may depend on each other, so they must be executed in order.
|
/// The backup plans may depend on each other, so they must be executed in order.
|
||||||
pub fn plan(local: &FileList, remote: &FileList) -> Plan {
|
///
|
||||||
// go through the local files in order, starting with the latest
|
/// Set `include_all` to include all local files, not just the most recent.
|
||||||
let upload_list: BTreeSet<_> = local
|
pub fn plan(local: &FileList, remote: &FileList, include_all: bool) -> Plan {
|
||||||
.keys()
|
let upload_list: BTreeSet<_> = {
|
||||||
.rev()
|
// go through the local files in order, starting with the most recent
|
||||||
// keep going while the file doesn't exist in the remote
|
let local = local.keys().rev();
|
||||||
.take_while(|ts| !remote.contains_key(ts))
|
|
||||||
.collect();
|
if include_all {
|
||||||
|
// take all files that doesn't exist in the remote
|
||||||
|
local.filter(|ts| !remote.contains_key(ts)).collect()
|
||||||
|
} else {
|
||||||
|
// take only the most recent files that doesn't exist in the remote
|
||||||
|
local.take_while(|ts| !remote.contains_key(ts)).collect()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// find the closest parent file of the first planned upload
|
// find the closest parent file of the first planned upload
|
||||||
let head_item = upload_list.iter().next().copied();
|
let head_item = upload_list.iter().next().copied();
|
||||||
|
|||||||
@ -15,14 +15,15 @@ pub struct Remote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect(opt: &Opt) -> anyhow::Result<Session> {
|
pub fn connect(opt: &Opt) -> anyhow::Result<Session> {
|
||||||
let stream = TcpStream::connect(&opt.remote.remote)?;
|
|
||||||
let mut session = Session::new()?;
|
|
||||||
session.set_tcp_stream(stream);
|
|
||||||
session.handshake()?;
|
|
||||||
info!(
|
info!(
|
||||||
r#"connecting to {}@{}"#,
|
r#"connecting to {}@{}"#,
|
||||||
opt.remote.username, opt.remote.remote,
|
opt.remote.username, opt.remote.remote,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let stream = TcpStream::connect(&opt.remote.remote)?;
|
||||||
|
let mut session = Session::new()?;
|
||||||
|
session.set_tcp_stream(stream);
|
||||||
|
session.handshake()?;
|
||||||
session.userauth_pubkey_file(
|
session.userauth_pubkey_file(
|
||||||
&opt.remote.username,
|
&opt.remote.username,
|
||||||
None,
|
None,
|
||||||
@ -32,6 +33,7 @@ pub fn connect(opt: &Opt) -> anyhow::Result<Session> {
|
|||||||
if !session.authenticated() {
|
if !session.authenticated() {
|
||||||
anyhow::bail!("ssh not authenticated");
|
anyhow::bail!("ssh not authenticated");
|
||||||
}
|
}
|
||||||
|
session.set_allow_sigpipe(true);
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
@ -39,7 +41,7 @@ pub fn connect(opt: &Opt) -> anyhow::Result<Session> {
|
|||||||
pub fn file_list(opt: &Opt, session: &Session) -> anyhow::Result<FileList> {
|
pub fn file_list(opt: &Opt, session: &Session) -> anyhow::Result<FileList> {
|
||||||
let mut channel = session.channel_session()?;
|
let mut channel = session.channel_session()?;
|
||||||
channel.exec(&format!(
|
channel.exec(&format!(
|
||||||
r#"ls -1N "{}""#,
|
r#"ls -1NU "{}""#,
|
||||||
opt.remote
|
opt.remote
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -51,7 +53,6 @@ pub fn file_list(opt: &Opt, session: &Session) -> anyhow::Result<FileList> {
|
|||||||
let mut list = BTreeMap::new();
|
let mut list = BTreeMap::new();
|
||||||
for file in output.lines() {
|
for file in output.lines() {
|
||||||
let date = DateTime::parse_from_rfc3339(file)?;
|
let date = DateTime::parse_from_rfc3339(file)?;
|
||||||
//let path = PathBuf::from(file);
|
|
||||||
list.insert(date, file.to_string());
|
list.insert(date, file.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/util.rs
Normal file
20
src/util.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub fn format_duration(d: Duration) -> String {
|
||||||
|
let seconds = d.as_secs_f32() % 60.0;
|
||||||
|
let minutes = d.as_secs() / 60 % 60;
|
||||||
|
let hours = d.as_secs() / 60 / 60;
|
||||||
|
|
||||||
|
match (hours, minutes) {
|
||||||
|
(0, 0) => format!("{:.2}s", seconds),
|
||||||
|
(0, _) => format!("{}m {:.2}s", minutes, seconds),
|
||||||
|
(_, 0) => format!("{}h {:.2}s", hours, seconds),
|
||||||
|
(_, _) => format!("{}h {}m {:.2}s", hours, minutes, seconds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_as_utf8(path: &Path) -> anyhow::Result<&str> {
|
||||||
|
path.to_str()
|
||||||
|
.ok_or_else(|| anyhow::format_err!("path not utf-8"))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user