Compare commits
5 Commits
b1ceaaf636
...
deprecated
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a94fdd4a9 | ||
|
|
7230127f26 | ||
|
|
956d2d337c | ||
| 5cca1829ad | |||
| 5d08b1ea57 |
320
Cargo.lock
generated
320
Cargo.lock
generated
@@ -3,10 +3,19 @@
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -17,10 +26,26 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bsvfs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f3132262930b0522068049f5870a856ab8affc80c70d08b6ecb785771a6fc23"
|
||||
|
||||
[[package]]
|
||||
name = "cas-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"digest",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
@@ -30,6 +55,7 @@ dependencies = [
|
||||
name = "cas-simple"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cas-core",
|
||||
"digest",
|
||||
"sha2",
|
||||
@@ -37,6 +63,12 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -45,9 +77,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.1.5"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
|
||||
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -62,31 +94,93 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libbsv"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bsvfs",
|
||||
"camino",
|
||||
"cas-core",
|
||||
"cas-simple",
|
||||
"regex",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.98"
|
||||
version = "0.2.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
|
||||
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -94,12 +188,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.27"
|
||||
@@ -118,75 +206,57 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.9"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"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 = "rustix"
|
||||
version = "0.36.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.127"
|
||||
version = "1.0.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
|
||||
checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.5"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
|
||||
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"cfg-if",
|
||||
@@ -208,16 +278,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.2.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
|
||||
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rand",
|
||||
"fastrand",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
"rustix",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -242,18 +311,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.8"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.13.0"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -263,15 +332,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
@@ -294,3 +357,84 @@ 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-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
|
||||
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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"bsvfs",
|
||||
"cas-core",
|
||||
"cas-simple",
|
||||
# "libbsv",
|
||||
"libbsv",
|
||||
# "bsv",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "bsv"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Boyé <sim.boye@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::collections::{hash_map, HashMap};
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
pub mod command;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#[macro_use]
|
||||
|
||||
13
bsvfs/Cargo.toml
Normal file
13
bsvfs/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bsvfs"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Boyé <sim.boye@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
camino = { version = "1.0.7" }
|
||||
thiserror = "1.0.25"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.2.0"
|
||||
491
bsvfs/src/fs.rs
Normal file
491
bsvfs/src/fs.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
io::{Read, Result, Seek, Write},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
|
||||
// FileType
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
Dir,
|
||||
File,
|
||||
Symlink,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn is_dir(&self) -> bool {
|
||||
*self == FileType::Dir
|
||||
}
|
||||
pub fn is_file(&self) -> bool {
|
||||
*self == FileType::File
|
||||
}
|
||||
pub fn is_symlink(&self) -> bool {
|
||||
*self == FileType::Symlink
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Permissions
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Permissions {
|
||||
read: bool,
|
||||
write: bool,
|
||||
execute: bool,
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
pub fn new(read: bool, write: bool, execute: bool) -> Self {
|
||||
Permissions { read, write, execute }
|
||||
}
|
||||
|
||||
pub fn is_read(&self) -> bool {
|
||||
self.read
|
||||
}
|
||||
pub fn is_write(&self) -> bool {
|
||||
self.write
|
||||
}
|
||||
pub fn is_execute(&self) -> bool {
|
||||
self.execute
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Permissions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
|
||||
use std::fmt::Write;
|
||||
|
||||
f.write_char(if self.read { 'r' } else { '-' })?;
|
||||
f.write_char(if self.write { 'w' } else { '-' })?;
|
||||
f.write_char(if self.execute { 'x' } else { '-' })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Permissions {
|
||||
type Err = ParsePermissionError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
if s.len() == 3 {
|
||||
let mut chars = s.chars();
|
||||
let read = parse_permission_char(&mut chars, 'r')?;
|
||||
let write = parse_permission_char(&mut chars, 'w')?;
|
||||
let execute = parse_permission_char(&mut chars, 'x')?;
|
||||
assert!(chars.next().is_none());
|
||||
|
||||
Ok(Self { read, write, execute })
|
||||
}
|
||||
else {
|
||||
Err(ParsePermissionError::InvalidLength(s.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_permission_char(chars: &mut std::str::Chars, bit_char: char)
|
||||
-> std::result::Result<bool, ParsePermissionError>
|
||||
{
|
||||
let c = chars.next().unwrap();
|
||||
if c == bit_char || c == '-' {
|
||||
Ok(c == bit_char)
|
||||
}
|
||||
else {
|
||||
Err(ParsePermissionError::InvalidChar(bit_char, c))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ParsePermissionError {
|
||||
InvalidLength(usize),
|
||||
InvalidChar(char, char),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParsePermissionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidLength(length) => write!(f, "expected 3 characters, got {}", length),
|
||||
Self::InvalidChar(expected, actual) => write!(f, "exected character {}, got {}", expected, actual),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParsePermissionError {
|
||||
}
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
pub trait Metadata {
|
||||
fn file_type(&self) -> FileType;
|
||||
fn is_dir(&self) -> bool {
|
||||
self.file_type().is_dir()
|
||||
}
|
||||
fn is_file(&self) -> bool {
|
||||
self.file_type().is_file()
|
||||
}
|
||||
fn is_symlink(&self) -> bool {
|
||||
self.file_type().is_symlink()
|
||||
}
|
||||
fn len(&self) -> u64;
|
||||
fn permissions(&self) -> Permissions;
|
||||
fn modified(&self) -> Result<SystemTime>;
|
||||
fn created(&self) -> Result<SystemTime>;
|
||||
}
|
||||
|
||||
|
||||
// File
|
||||
|
||||
const OPEN_READ: u8 = 0x01;
|
||||
const OPEN_WRITE: u8 = 0x02;
|
||||
const OPEN_APPEND: u8 = 0x04;
|
||||
const OPEN_TRUNCATE: u8 = 0x08;
|
||||
const OPEN_CREATE: u8 = 0x10;
|
||||
const OPEN_CREATE_NEW: u8 = 0x20;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct OpenOptions {
|
||||
options: u8,
|
||||
}
|
||||
|
||||
impl OpenOptions {
|
||||
pub fn new() -> Self {
|
||||
OpenOptions {
|
||||
options: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_read(&self) -> bool {
|
||||
(self.options & OPEN_READ) != 0
|
||||
}
|
||||
pub fn is_write(&self) -> bool {
|
||||
(self.options & OPEN_WRITE) != 0
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
(self.options & OPEN_APPEND) != 0
|
||||
}
|
||||
pub fn is_truncate(&self) -> bool {
|
||||
(self.options & OPEN_TRUNCATE) != 0
|
||||
}
|
||||
pub fn is_create(&self) -> bool {
|
||||
(self.options & OPEN_CREATE) != 0
|
||||
}
|
||||
pub fn is_create_new(&self) -> bool {
|
||||
(self.options & OPEN_CREATE_NEW) != 0
|
||||
}
|
||||
|
||||
pub fn read(&mut self, read: bool) -> &mut Self {
|
||||
if read {
|
||||
self.options |= OPEN_READ;
|
||||
} else {
|
||||
self.options &= !OPEN_READ;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn write(&mut self, write: bool) -> &mut Self {
|
||||
if write {
|
||||
self.options |= OPEN_WRITE;
|
||||
} else {
|
||||
self.options &= !OPEN_WRITE;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn append(&mut self, append: bool) -> &mut Self {
|
||||
if append {
|
||||
self.options |= OPEN_APPEND;
|
||||
} else {
|
||||
self.options &= !OPEN_APPEND;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn truncate(&mut self, truncate: bool) -> &mut Self {
|
||||
if truncate {
|
||||
self.options |= OPEN_TRUNCATE;
|
||||
} else {
|
||||
self.options &= !OPEN_TRUNCATE;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create(&mut self, create: bool) -> &mut Self {
|
||||
if create {
|
||||
self.options |= OPEN_CREATE;
|
||||
} else {
|
||||
self.options &= !OPEN_CREATE;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create_new(&mut self, create_new: bool) -> &mut Self {
|
||||
if create_new {
|
||||
self.options |= OPEN_CREATE_NEW;
|
||||
} else {
|
||||
self.options &= !OPEN_CREATE_NEW;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait File: Sized + Read + Write + Seek {
|
||||
type Metadata: Metadata;
|
||||
|
||||
fn metadata(&self) -> Result<Self::Metadata>;
|
||||
|
||||
fn set_len(&self, size: u64) -> Result<()>;
|
||||
|
||||
fn sync_data(&self) -> Result<()>;
|
||||
fn sync_all(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
|
||||
// DirEntry
|
||||
|
||||
pub trait DirEntry {
|
||||
type Metadata: Metadata;
|
||||
|
||||
fn path(&self) -> Result<Utf8PathBuf>;
|
||||
fn metadata(&self) -> Result<Self::Metadata>;
|
||||
fn file_type(&self) -> Result<FileType> {
|
||||
Ok(self.metadata()?.file_type())
|
||||
}
|
||||
fn file_name(&self) -> Result<String> {
|
||||
Ok(
|
||||
self.path()?
|
||||
.file_name()
|
||||
.expect("DirEntry::path() returned an invalid path with no file name")
|
||||
.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FileSystem
|
||||
|
||||
pub trait FileSystem {
|
||||
type File: File;
|
||||
type DirEntry: DirEntry;
|
||||
type ReadDir: Iterator<Item=Result<Self::DirEntry>>;
|
||||
|
||||
fn open_with_options<P: AsRef<Utf8Path>>(&self, path: P, options: &OpenOptions) -> Result<Self::File>;
|
||||
|
||||
fn open<P: AsRef<Utf8Path>>(&self, path: P) -> Result<Self::File> {
|
||||
self.open_with_options(
|
||||
path,
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
)
|
||||
}
|
||||
fn create_or_truncate<P: AsRef<Utf8Path>>(&self, path: P) -> Result<Self::File> {
|
||||
self.open_with_options(
|
||||
path,
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
)
|
||||
}
|
||||
fn create_new<P: AsRef<Utf8Path>>(&self, path: P) -> Result<Self::File> {
|
||||
self.open_with_options(
|
||||
path,
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
)
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Utf8Path>>(&self, path: P) -> Result<Self::ReadDir>;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_file_type() {
|
||||
let dir = FileType::Dir;
|
||||
assert!(dir.is_dir());
|
||||
assert!(!dir.is_file());
|
||||
assert!(!dir.is_symlink());
|
||||
|
||||
let file = FileType::File;
|
||||
assert!(!file.is_dir());
|
||||
assert!(file.is_file());
|
||||
assert!(!file.is_symlink());
|
||||
|
||||
let symlink = FileType::Symlink;
|
||||
assert!(!symlink.is_dir());
|
||||
assert!(!symlink.is_file());
|
||||
assert!(symlink.is_symlink());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_options() {
|
||||
let mut options = OpenOptions::new();
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.read(true);
|
||||
assert!(options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.read(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.write(true);
|
||||
assert!(!options.is_read());
|
||||
assert!(options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.write(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.append(true);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.append(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.truncate(true);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.truncate(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.create(true);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.create(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
|
||||
options.create_new(true);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(options.is_create_new());
|
||||
|
||||
options.create_new(false);
|
||||
assert!(!options.is_read());
|
||||
assert!(!options.is_write());
|
||||
assert!(!options.is_append());
|
||||
assert!(!options.is_truncate());
|
||||
assert!(!options.is_create());
|
||||
assert!(!options.is_create_new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_display() {
|
||||
let expected = [
|
||||
"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx",
|
||||
];
|
||||
|
||||
for i in 0..8 {
|
||||
let read = (i & 0x04) != 0;
|
||||
let write = (i & 0x02) != 0;
|
||||
let execute = (i & 0x01) != 0;
|
||||
|
||||
let perm = Permissions { read, write, execute };
|
||||
let perm_str = perm.to_string();
|
||||
|
||||
assert_eq!(perm_str, expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_from_str() {
|
||||
let perms = [
|
||||
"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx",
|
||||
];
|
||||
|
||||
for i in 0..8 {
|
||||
let read = (i & 0x04) != 0;
|
||||
let write = (i & 0x02) != 0;
|
||||
let execute = (i & 0x01) != 0;
|
||||
|
||||
let perm = Permissions::from_str(perms[i]).unwrap();
|
||||
let expected = Permissions { read, write, execute };
|
||||
|
||||
assert_eq!(perm, expected);
|
||||
}
|
||||
|
||||
assert_eq!(Permissions::from_str("rw"), Err(ParsePermissionError::InvalidLength(2)));
|
||||
assert_eq!(Permissions::from_str("rw--"), Err(ParsePermissionError::InvalidLength(4)));
|
||||
assert_eq!(Permissions::from_str("-x-"), Err(ParsePermissionError::InvalidChar('w', 'x')));
|
||||
assert_eq!(Permissions::from_str("123"), Err(ParsePermissionError::InvalidChar('r', '1')));
|
||||
}
|
||||
}
|
||||
386
bsvfs/src/fs_impl.rs
Normal file
386
bsvfs/src/fs_impl.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
ffi::OsString,
|
||||
fmt::{Arguments, Display},
|
||||
fs::{
|
||||
self,
|
||||
DirEntry as StdDirEntry,
|
||||
File as StdFile,
|
||||
Metadata as StdMetadata,
|
||||
ReadDir as StdReadDir,
|
||||
},
|
||||
io::{self, IoSlice, IoSliceMut, Read, Result, Seek, SeekFrom, Write},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use crate::{
|
||||
fs::*,
|
||||
};
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct NonUtf8Filename {
|
||||
filename: OsString,
|
||||
}
|
||||
|
||||
impl NonUtf8Filename {
|
||||
fn new(filename: OsString) -> Self {
|
||||
Self {
|
||||
filename: filename,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NonUtf8Filename {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Non UTF-8 filename: '{:?}'", self.filename)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NonUtf8Filename {
|
||||
}
|
||||
|
||||
|
||||
fn as_io_error<E>(error: E) -> io::Error
|
||||
where E: Into<Box<dyn Error + Send + Sync>>,
|
||||
{
|
||||
io::Error::new(io::ErrorKind::Other, error)
|
||||
}
|
||||
|
||||
fn as_string(os_string: OsString) -> Result<String> {
|
||||
os_string.into_string()
|
||||
.map_err(|filename| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
NonUtf8Filename::new(filename),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn as_filetype(std_filetype: fs::FileType) -> FileType {
|
||||
if std_filetype.is_dir() {
|
||||
FileType::Dir
|
||||
} else if std_filetype.is_file() {
|
||||
FileType::File
|
||||
} else if std_filetype.is_symlink() {
|
||||
FileType::Symlink
|
||||
} else {
|
||||
FileType::Other
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SysMetadata {
|
||||
metadata: StdMetadata,
|
||||
}
|
||||
|
||||
impl Metadata for SysMetadata {
|
||||
fn file_type(&self) -> FileType {
|
||||
as_filetype(self.metadata.file_type())
|
||||
}
|
||||
|
||||
fn len(&self) -> u64 {
|
||||
self.metadata.len()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn permissions(&self) -> Permissions {
|
||||
Permissions(true, self.metadata.permissions().readonly(), false)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
fn permissions(&self) -> Permissions {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let mode = self.metadata.mode();
|
||||
Permissions::new(
|
||||
mode & 0o100 != 0,
|
||||
mode & 0o200 != 0,
|
||||
mode & 0o400 != 0,
|
||||
)
|
||||
}
|
||||
|
||||
fn modified(&self) -> Result<SystemTime> {
|
||||
Ok(self.metadata.modified()?)
|
||||
}
|
||||
|
||||
fn created(&self) -> Result<SystemTime> {
|
||||
Ok(self.metadata.created()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdMetadata> for SysMetadata {
|
||||
fn from(value: StdMetadata) -> Self {
|
||||
SysMetadata {
|
||||
metadata: value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct SysFile {
|
||||
file: StdFile,
|
||||
}
|
||||
|
||||
impl File for SysFile {
|
||||
type Metadata = SysMetadata;
|
||||
|
||||
fn metadata(&self) -> Result<Self::Metadata> {
|
||||
Ok(self.file.metadata()?.into())
|
||||
}
|
||||
|
||||
fn set_len(&self, size: u64) -> Result<()> {
|
||||
Ok(self.file.set_len(size)?)
|
||||
}
|
||||
|
||||
fn sync_data(&self) -> Result<()> {
|
||||
Ok(self.file.sync_data()?)
|
||||
}
|
||||
|
||||
fn sync_all(&self) -> Result<()> {
|
||||
Ok(self.file.sync_all()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for SysFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
self.file.read(buf)
|
||||
}
|
||||
|
||||
fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize> {
|
||||
self.file.read_vectored(bufs)
|
||||
}
|
||||
// fn is_read_vectored(&self) -> bool {
|
||||
// self.file.is_read_vectored()
|
||||
// }
|
||||
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> {
|
||||
self.file.read_to_end(buf)
|
||||
}
|
||||
fn read_to_string(&mut self, buf: &mut String) -> Result<usize> {
|
||||
self.file.read_to_string(buf)
|
||||
}
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
|
||||
self.file.read_exact(buf)
|
||||
}
|
||||
// fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()> {
|
||||
// self.file.read_buf(buf)
|
||||
// }
|
||||
// fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> Result<()> {
|
||||
// self.file.read_buf_exact(cursor)
|
||||
// }
|
||||
}
|
||||
|
||||
impl Seek for SysFile {
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
self.file.seek(pos)
|
||||
}
|
||||
|
||||
fn rewind(&mut self) -> Result<()> {
|
||||
self.file.rewind()
|
||||
}
|
||||
// fn stream_len(&mut self) -> Result<u64> {
|
||||
// self.file.stream_len()
|
||||
// }
|
||||
fn stream_position(&mut self) -> Result<u64> {
|
||||
self.file.stream_position()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SysFile {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
self.file.write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.file.flush()
|
||||
}
|
||||
|
||||
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize> {
|
||||
self.file.write_vectored(bufs)
|
||||
}
|
||||
// fn is_write_vectored(&self) -> bool {
|
||||
// self.file.is_write_vectored()
|
||||
// }
|
||||
fn write_all(&mut self, buf: &[u8]) -> Result<()> {
|
||||
self.file.write_all(buf)
|
||||
}
|
||||
// fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> {
|
||||
// self.file.write_all_vectored(bufs)
|
||||
// }
|
||||
fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> {
|
||||
self.file.write_fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct SysDirEntry {
|
||||
dir_entry: StdDirEntry,
|
||||
}
|
||||
|
||||
impl DirEntry for SysDirEntry {
|
||||
type Metadata = SysMetadata;
|
||||
|
||||
fn path(&self) -> Result<Utf8PathBuf> {
|
||||
Utf8PathBuf::try_from(
|
||||
self.dir_entry.path()
|
||||
).map_err(as_io_error)
|
||||
}
|
||||
fn metadata(&self) -> Result<Self::Metadata> {
|
||||
Ok(self.dir_entry.metadata()?.into())
|
||||
}
|
||||
fn file_type(&self) -> Result<FileType> {
|
||||
Ok(as_filetype(self.dir_entry.file_type()?))
|
||||
}
|
||||
fn file_name(&self) -> Result<String> {
|
||||
as_string(self.dir_entry.file_name())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdDirEntry> for SysDirEntry {
|
||||
fn from(dir_entry: StdDirEntry) -> Self {
|
||||
SysDirEntry {
|
||||
dir_entry: dir_entry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SysReadDir {
|
||||
read_dir: StdReadDir,
|
||||
}
|
||||
|
||||
impl Iterator for SysReadDir {
|
||||
type Item = Result<SysDirEntry>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.read_dir.next() {
|
||||
Some(Ok(dir_entry)) => Some(Ok(dir_entry.into())),
|
||||
Some(Err(error)) => Some(Err(error)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct SysFileSystem;
|
||||
|
||||
impl SysFileSystem {
|
||||
pub fn new() -> Self {
|
||||
Self{}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for SysFileSystem {
|
||||
type File = SysFile;
|
||||
type DirEntry = SysDirEntry;
|
||||
type ReadDir = SysReadDir;
|
||||
|
||||
fn open_with_options<P: AsRef<Utf8Path>>(&self, path: P, options: &OpenOptions) -> Result<Self::File> {
|
||||
Ok(SysFile{
|
||||
file: StdFile::options()
|
||||
.read(options.is_read())
|
||||
.write(options.is_write())
|
||||
.append(options.is_append())
|
||||
.truncate(options.is_truncate())
|
||||
.create(options.is_create())
|
||||
.create_new(options.is_create_new())
|
||||
.open(path.as_ref().as_std_path())?,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Utf8Path>>(&self, path: P) -> Result<SysReadDir> {
|
||||
Ok(SysReadDir {
|
||||
read_dir: fs::read_dir(path.as_ref().as_std_path())?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::FileSystem;
|
||||
|
||||
#[test]
|
||||
fn test_file_read_write() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let file_path = Utf8PathBuf::from_path_buf(dir.path().join("test.txt")).unwrap();
|
||||
|
||||
let message = b"Hello world!\nThis is a test\n";
|
||||
|
||||
let fs = SysFileSystem::new();
|
||||
let mut file = fs.create_new(&file_path).unwrap();
|
||||
file.write(message).unwrap();
|
||||
drop(file);
|
||||
|
||||
let mut file = fs.open(&file_path).unwrap();
|
||||
let mut buf = [0u8; 256];
|
||||
let read_size = file.read(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(read_size, message.len());
|
||||
assert_eq!(&buf[..message.len()], message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_dir() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let dir_path = Utf8Path::from_path(dir.path()).unwrap();
|
||||
let file0_path = dir_path.join("file.txt");
|
||||
let file1_path = dir_path.join("zzz.txt");
|
||||
let dir0_path = dir_path.join("some_dir");
|
||||
|
||||
let fs = SysFileSystem::new();
|
||||
|
||||
{
|
||||
let mut file = fs.create_new(file0_path).unwrap();
|
||||
file.write(b"file.txt").unwrap();
|
||||
}
|
||||
{
|
||||
let mut file = fs.create_new(file1_path).unwrap();
|
||||
file.write(b"zzz.txt").unwrap();
|
||||
}
|
||||
fs::create_dir(dir0_path).unwrap();
|
||||
|
||||
let mut entries = fs.read_dir(dir_path).unwrap()
|
||||
.collect::<Result<Vec<_>>>().unwrap();
|
||||
entries.sort_by_key(|entry| entry.file_name().unwrap());
|
||||
|
||||
let mut it = entries.iter();
|
||||
|
||||
let file0_entry = it.next().unwrap();
|
||||
assert_eq!(file0_entry.file_name().unwrap(), "file.txt");
|
||||
assert_eq!(file0_entry.file_type().unwrap(), FileType::File);
|
||||
let file0_metadata = file0_entry.metadata().unwrap();
|
||||
assert!(file0_metadata.is_file());
|
||||
assert_eq!(file0_metadata.len(), 8);
|
||||
|
||||
let dir0_entry = it.next().unwrap();
|
||||
assert_eq!(dir0_entry.file_name().unwrap(), "some_dir");
|
||||
assert_eq!(dir0_entry.file_type().unwrap(), FileType::Dir);
|
||||
let dir0_metadata = dir0_entry.metadata().unwrap();
|
||||
assert!(dir0_metadata.is_dir());
|
||||
|
||||
let file1_entry = it.next().unwrap();
|
||||
assert_eq!(file1_entry.file_name().unwrap(), "zzz.txt");
|
||||
assert_eq!(file1_entry.file_type().unwrap(), FileType::File);
|
||||
let file1_metadata = file1_entry.metadata().unwrap();
|
||||
assert!(file1_metadata.is_file());
|
||||
assert_eq!(file1_metadata.len(), 7);
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,28 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate camino;
|
||||
extern crate thiserror;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
|
||||
pub mod db;
|
||||
pub mod object;
|
||||
// mod error;
|
||||
mod fs;
|
||||
mod fs_impl;
|
||||
mod mem_fs;
|
||||
|
||||
|
||||
pub use db::SimpleDb;
|
||||
pub use object::{
|
||||
ObjectReader,
|
||||
pub use crate::{
|
||||
fs::*,
|
||||
fs_impl::*,
|
||||
};
|
||||
468
bsvfs/src/mem_fs.rs
Normal file
468
bsvfs/src/mem_fs.rs
Normal file
@@ -0,0 +1,468 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
fmt::{Arguments},
|
||||
io::{Cursor, Error, IoSlice, IoSliceMut, Read, Result, Seek, SeekFrom, Write, ErrorKind},
|
||||
time::SystemTime, rc::Rc, cell::RefCell,
|
||||
};
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use crate::{
|
||||
fs::*,
|
||||
};
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MemMetadata {
|
||||
file_type: FileType,
|
||||
len: u64,
|
||||
permissions: Permissions,
|
||||
modified: SystemTime,
|
||||
created: SystemTime,
|
||||
}
|
||||
|
||||
impl Metadata for MemMetadata {
|
||||
fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Permissions {
|
||||
self.permissions.clone()
|
||||
}
|
||||
|
||||
fn modified(&self) -> Result<SystemTime> {
|
||||
Ok(self.modified)
|
||||
}
|
||||
|
||||
fn created(&self) -> Result<SystemTime> {
|
||||
Ok(self.created)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MemFile {
|
||||
metadata: MemMetadata,
|
||||
data: RefCell<Cursor<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl File for MemFile {
|
||||
type Metadata = MemMetadata;
|
||||
|
||||
// fn open_with_options<P: AsRef<Utf8Path>>(path: P, options: &OpenOptions) -> Result<Self> {
|
||||
// Ok(SysFile{
|
||||
// file: StdFile::options()
|
||||
// .read(options.is_read())
|
||||
// .write(options.is_write())
|
||||
// .append(options.is_append())
|
||||
// .truncate(options.is_truncate())
|
||||
// .create(options.is_create())
|
||||
// .create_new(options.is_create_new())
|
||||
// .open(path.as_ref().as_std_path())?,
|
||||
// })
|
||||
// }
|
||||
|
||||
fn metadata(&self) -> Result<Self::Metadata> {
|
||||
Ok(self.metadata.clone())
|
||||
}
|
||||
|
||||
fn set_len(&self, size: u64) -> Result<()> {
|
||||
let mut data = self.data.borrow_mut();
|
||||
let pos = data.position();
|
||||
data.get_mut().resize(size as usize, 0);
|
||||
if size < pos {
|
||||
data.seek(SeekFrom::Start(pos));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_data(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_all(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MemFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.read(buf)
|
||||
}
|
||||
|
||||
fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.read_vectored(bufs)
|
||||
}
|
||||
// fn is_read_vectored(&self) -> bool {
|
||||
// self.data.is_read_vectored()
|
||||
// }
|
||||
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.read_to_end(buf)
|
||||
}
|
||||
fn read_to_string(&mut self, buf: &mut String) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.read_to_string(buf)
|
||||
}
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
|
||||
let data = self.data.get_mut();
|
||||
data.read_exact(buf)
|
||||
}
|
||||
// fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()> {
|
||||
// self.data.read_buf(buf)
|
||||
// }
|
||||
// fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> Result<()> {
|
||||
// self.data.read_buf_exact(cursor)
|
||||
// }
|
||||
}
|
||||
|
||||
impl Seek for MemFile {
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
let data = self.data.get_mut();
|
||||
data.seek(pos)
|
||||
}
|
||||
|
||||
fn rewind(&mut self) -> Result<()> {
|
||||
let data = self.data.get_mut();
|
||||
data.rewind()
|
||||
}
|
||||
// fn stream_len(&mut self) -> Result<u64> {
|
||||
// self.data.stream_len()
|
||||
// }
|
||||
fn stream_position(&mut self) -> Result<u64> {
|
||||
let data = self.data.get_mut();
|
||||
data.stream_position()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for MemFile {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
let data = self.data.get_mut();
|
||||
data.flush()
|
||||
}
|
||||
|
||||
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize> {
|
||||
let data = self.data.get_mut();
|
||||
data.write_vectored(bufs)
|
||||
}
|
||||
// fn is_write_vectored(&self) -> bool {
|
||||
// self.data.is_write_vectored()
|
||||
// }
|
||||
fn write_all(&mut self, buf: &[u8]) -> Result<()> {
|
||||
let data = self.data.get_mut();
|
||||
data.write_all(buf)
|
||||
}
|
||||
// fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> {
|
||||
// self.data.write_all_vectored(bufs)
|
||||
// }
|
||||
fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> {
|
||||
let data = self.data.get_mut();
|
||||
data.write_fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MemDir {
|
||||
metadata: MemMetadata,
|
||||
items: Vec<Rc<RefCell<DirItem>>>,
|
||||
}
|
||||
|
||||
impl MemDir {
|
||||
fn new() -> Self {
|
||||
let time = SystemTime::now();
|
||||
Self::new_with_time(&time)
|
||||
}
|
||||
|
||||
fn new_with_time(time: &SystemTime) -> Self {
|
||||
MemDir {
|
||||
metadata: MemMetadata {
|
||||
file_type: FileType::Dir,
|
||||
len: 0,
|
||||
permissions: Permissions::new(true, true, false),
|
||||
modified: time.clone(),
|
||||
created: time.clone(),
|
||||
},
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, name: &str) -> Option<&DirItem> {
|
||||
None
|
||||
}
|
||||
fn get_mut(&self, name: &str) -> Option<&mut DirItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn search(&self, name: &str) -> std::result::Result<usize, usize> {
|
||||
self.items.binary_search_by(
|
||||
|item| item.borrow().name().cmp(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum DirItem {
|
||||
Dir(String, MemDir),
|
||||
File(String, MemFile),
|
||||
Err(String, ErrorKind, String),
|
||||
}
|
||||
|
||||
impl DirItem {
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
DirItem::Dir(ref name, _) => name,
|
||||
DirItem::File(ref name, _) => name,
|
||||
DirItem::Err(ref name, _, _) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MemDirEntry {
|
||||
path: Utf8PathBuf,
|
||||
metadata: MemMetadata,
|
||||
}
|
||||
|
||||
impl DirEntry for MemDirEntry {
|
||||
type Metadata = MemMetadata;
|
||||
|
||||
fn path(&self) -> Result<Utf8PathBuf> {
|
||||
Ok(self.path.clone())
|
||||
}
|
||||
fn metadata(&self) -> Result<Self::Metadata> {
|
||||
Ok(self.metadata.clone())
|
||||
}
|
||||
fn file_type(&self) -> Result<FileType> {
|
||||
Ok(self.metadata.file_type())
|
||||
}
|
||||
fn file_name(&self) -> Result<String> {
|
||||
Ok(self.path.file_name().unwrap().into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemReadDir {
|
||||
base_path: Utf8PathBuf,
|
||||
items: Vec<DirItem>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Iterator for MemReadDir {
|
||||
type Item = Result<MemDirEntry>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.items.get(self.index) {
|
||||
Some(DirItem::Dir(ref name, ref dir)) => {
|
||||
let mut path = self.base_path.clone();
|
||||
path.push(name);
|
||||
|
||||
self.index += 1;
|
||||
Some(Ok(MemDirEntry{path, metadata: dir.metadata.clone()}))
|
||||
},
|
||||
Some(DirItem::File(ref name, ref file)) => {
|
||||
let mut path = self.base_path.clone();
|
||||
path.push(name);
|
||||
|
||||
self.index += 1;
|
||||
Some(Ok(MemDirEntry{path, metadata: file.metadata.clone()}))
|
||||
},
|
||||
Some(DirItem::Err(_, kind, ref msg)) => {
|
||||
self.index += 1;
|
||||
Some(Err(Error::new(*kind, msg.clone())))
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct MemFileSystem {
|
||||
root: MemDir,
|
||||
}
|
||||
|
||||
impl MemFileSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
root: MemDir::new_with_time(&SystemTime::UNIX_EPOCH),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mkdir<P: AsRef<Utf8Path>>(&mut self, path: P, parents: bool, exist_ok: bool) -> Result<()> {
|
||||
use camino::Utf8Component::*;
|
||||
let mut parents = Vec::new();
|
||||
let mut iter = path.as_ref().components();
|
||||
|
||||
match iter.next() {
|
||||
Some(Prefix(_)) => {
|
||||
return make_err("Windows-like path are not supported by MemFileSystem");
|
||||
},
|
||||
Some(RootDir) => {
|
||||
parents.push(&mut self.root);
|
||||
},
|
||||
Some(_) => {
|
||||
return make_err("path must be absolute");
|
||||
},
|
||||
None => {
|
||||
return make_err("path is empty")
|
||||
},
|
||||
}
|
||||
|
||||
for comp in iter {
|
||||
match comp {
|
||||
Prefix(_) |
|
||||
RootDir => {
|
||||
panic!();
|
||||
},
|
||||
CurDir => {},
|
||||
ParentDir => {
|
||||
if parents.len() <= 1 {
|
||||
return make_err("invalid path")
|
||||
}
|
||||
parents.pop();
|
||||
},
|
||||
Normal(name) => {
|
||||
let maybe_item = parents.last().unwrap().get_mut(name);
|
||||
match maybe_item {
|
||||
Some(DirItem::Dir(_, ref mut dir)) => {
|
||||
parents.push(dir);
|
||||
},
|
||||
Some(DirItem::File(_, _)) => {
|
||||
return make_err("item is a file")
|
||||
}
|
||||
Some(DirItem::Err(_, kind, msg)) => {
|
||||
return Err(std::io::Error::new(*kind, &**msg));
|
||||
}
|
||||
None => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
make_err("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for MemFileSystem {
|
||||
type File = MemFile;
|
||||
type DirEntry = MemDirEntry;
|
||||
type ReadDir = MemReadDir;
|
||||
|
||||
fn open_with_options<P: AsRef<Utf8Path>>(&self, path: P, options: &OpenOptions) -> Result<Self::File> {
|
||||
make_err("TODO")
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Utf8Path>>(&self, path: P) -> Result<MemReadDir> {
|
||||
Ok(MemReadDir {
|
||||
base_path: path.as_ref().into(),
|
||||
items: vec![], // TODO
|
||||
index: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn make_error(message: &str) -> std::io::Error {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
message,
|
||||
)
|
||||
}
|
||||
fn make_err<T>(message: &str) -> Result<T> {
|
||||
Err(make_error(message))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// use camino::Utf8PathBuf;
|
||||
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn test_file_read_write() {
|
||||
// let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
// let file_path = Utf8PathBuf::from_path_buf(dir.path().join("test.txt")).unwrap();
|
||||
|
||||
// let message = b"Hello world!\nThis is a test\n";
|
||||
|
||||
// let mut file = SysFile::create_new(&file_path).unwrap();
|
||||
// file.write(message).unwrap();
|
||||
// drop(file);
|
||||
|
||||
// let mut file = SysFile::open(&file_path).unwrap();
|
||||
// let mut buf = [0u8; 256];
|
||||
// let read_size = file.read(&mut buf).unwrap();
|
||||
|
||||
// assert_eq!(read_size, message.len());
|
||||
// assert_eq!(&buf[..message.len()], message);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_read_dir() {
|
||||
// let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
// let dir_path = Utf8Path::from_path(dir.path()).unwrap();
|
||||
// let file0_path = dir_path.join("file.txt");
|
||||
// let file1_path = dir_path.join("zzz.txt");
|
||||
// let dir0_path = dir_path.join("some_dir");
|
||||
|
||||
// {
|
||||
// let mut file = SysFile::create_new(file0_path).unwrap();
|
||||
// file.write(b"file.txt").unwrap();
|
||||
// }
|
||||
// {
|
||||
// let mut file = SysFile::create_new(file1_path).unwrap();
|
||||
// file.write(b"zzz.txt").unwrap();
|
||||
// }
|
||||
// fs::create_dir(dir0_path).unwrap();
|
||||
|
||||
// let mut entries = SysFileSystem::read_dir(dir_path).unwrap()
|
||||
// .collect::<Result<Vec<_>>>().unwrap();
|
||||
// entries.sort_by_key(|entry| entry.file_name().unwrap());
|
||||
|
||||
// let mut it = entries.iter();
|
||||
|
||||
// let file0_entry = it.next().unwrap();
|
||||
// assert_eq!(file0_entry.file_name().unwrap(), "file.txt");
|
||||
// assert_eq!(file0_entry.file_type().unwrap(), FileType::File);
|
||||
// let file0_metadata = file0_entry.metadata().unwrap();
|
||||
// assert!(file0_metadata.is_file());
|
||||
// assert_eq!(file0_metadata.len(), 8);
|
||||
|
||||
// let dir0_entry = it.next().unwrap();
|
||||
// assert_eq!(dir0_entry.file_name().unwrap(), "some_dir");
|
||||
// assert_eq!(dir0_entry.file_type().unwrap(), FileType::Dir);
|
||||
// let dir0_metadata = dir0_entry.metadata().unwrap();
|
||||
// assert!(dir0_metadata.is_dir());
|
||||
|
||||
// let file1_entry = it.next().unwrap();
|
||||
// assert_eq!(file1_entry.file_name().unwrap(), "zzz.txt");
|
||||
// assert_eq!(file1_entry.file_type().unwrap(), FileType::File);
|
||||
// let file1_metadata = file1_entry.metadata().unwrap();
|
||||
// assert!(file1_metadata.is_file());
|
||||
// assert_eq!(file1_metadata.len(), 7);
|
||||
// }
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
name = "cas-core"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Boyé <sim.boye@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0.25"
|
||||
digest = { version = "0.9.0", features = ["alloc"] }
|
||||
camino = { version = "1.0.7" }
|
||||
|
||||
[dev-dependencies]
|
||||
sha2 = "0.9.5"
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::Path;
|
||||
use camino::Utf8Path;
|
||||
|
||||
use super::err;
|
||||
use super::error::{Error, Result};
|
||||
use super::object_id::ObjectId;
|
||||
use super::object_type::ObjectType;
|
||||
@@ -40,21 +41,21 @@ pub trait Cas {
|
||||
let mut data = Vec::new();
|
||||
|
||||
reader.read_to_end(&mut data).or_else(|err|
|
||||
Err(Error::error(format!("failed to read object {}: {}", oid, err))))?;
|
||||
err!("failed to read object {}: {}", oid, err))?;
|
||||
|
||||
let result_oid = reader.finalize()?;
|
||||
if &result_oid == oid {
|
||||
Ok((metadata, data))
|
||||
}
|
||||
else {
|
||||
Err(Error::error(format!("object id mismatch: requested {}, read {}", oid, result_oid)))
|
||||
err!("object id mismatch: requested {}, read {}", oid, result_oid)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_object(&mut self, otype: &ObjectType, data: &[u8]) -> Result<ObjectId> {
|
||||
let mut writer = self.new_writer(otype, data.len() as u64)?;
|
||||
writer.write_all(data).or_else(|err|
|
||||
Err(Error::error(format!("failed to write object: {}", err))))?;
|
||||
err!("failed to write object: {}", err))?;
|
||||
writer.finalize()
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ pub trait Cas {
|
||||
}
|
||||
|
||||
pub trait RefStore {
|
||||
fn get_ref<P: AsRef<Path>>(&self, key: P) -> Result<ObjectId>;
|
||||
fn set_ref<P: AsRef<Path>>(&mut self, key: P, value: &ObjectId) -> Result<()>;
|
||||
fn remove_ref<P: AsRef<Path>>(&mut self, key: P) -> Result<()>;
|
||||
fn get_ref<P: AsRef<Utf8Path>>(&self, key: P) -> Result<ObjectId>;
|
||||
fn set_ref<P: AsRef<Utf8Path>>(&mut self, key: P, value: &ObjectId) -> Result<()>;
|
||||
fn remove_ref<P: AsRef<Utf8Path>>(&mut self, key: P) -> Result<()>;
|
||||
}
|
||||
@@ -5,24 +5,24 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
// use std::path::PathBuf;
|
||||
// use std::path::Utf8PathBuf;
|
||||
|
||||
|
||||
/// Result type used through cas-core.
|
||||
pub type Result<T> = std::result::Result<T, Box<Error>>;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error type used through cas-core.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
// #[error("failed to create repository: {message}")]
|
||||
// RepositoryCreationFailed {
|
||||
@@ -44,7 +44,7 @@ pub enum Error {
|
||||
|
||||
// #[error("non-empty directory ({dir})")]
|
||||
// NonEmptyDirectory {
|
||||
// dir: PathBuf
|
||||
// dir: Utf8PathBuf
|
||||
// },
|
||||
|
||||
// #[error("invalid character(s) ({characters})")]
|
||||
@@ -67,16 +67,28 @@ pub enum Error {
|
||||
// UnsupportedFileType,
|
||||
|
||||
// #[error("invalid path ({path})")]
|
||||
// InvalidPath { path: PathBuf },
|
||||
// InvalidPath { path: Utf8PathBuf },
|
||||
|
||||
// #[error("io error{}", format_optional_path(path))]
|
||||
// IoError {
|
||||
// source: std::io::Error,
|
||||
// path: Option<PathBuf>,
|
||||
// path: Option<Utf8PathBuf>,
|
||||
// },
|
||||
|
||||
#[error("item skipped")]
|
||||
Skipped,
|
||||
|
||||
#[error("logic error: {0}")]
|
||||
LogicError(String),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("non-unicode file name: '{0}'")]
|
||||
NonUnicodeFileName(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Error(String),
|
||||
UnknownError(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@@ -108,17 +120,17 @@ impl Error {
|
||||
// })
|
||||
// }
|
||||
|
||||
pub fn error<M: Into<String>>(message: M) -> Box<Error> {
|
||||
Box::new(Error::Error(message.into()))
|
||||
pub fn unknown<M: Into<String>>(message: M) -> Error {
|
||||
Error::UnknownError(message.into())
|
||||
}
|
||||
|
||||
pub fn err<M: Into<String>, T>(message: M) -> Result<T> {
|
||||
Err(Self::error(message))
|
||||
Err(Self::unknown(message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// fn format_optional_path(maybe_path: &Option<PathBuf>) -> String {
|
||||
// fn format_optional_path(maybe_path: &Option<Utf8PathBuf>) -> String {
|
||||
// match maybe_path {
|
||||
// Some(path) => { format!(" ({:?})", path) },
|
||||
// None => { String::new() }
|
||||
|
||||
@@ -5,24 +5,23 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! # cas-core
|
||||
//!
|
||||
//! `cas-core` provides traits and types to interface with content-addressable
|
||||
//! storage.
|
||||
|
||||
#![feature(inherent_ascii_escape)]
|
||||
|
||||
|
||||
extern crate thiserror;
|
||||
extern crate digest;
|
||||
extern crate camino;
|
||||
|
||||
|
||||
mod error;
|
||||
@@ -42,3 +41,11 @@ pub use crate::{
|
||||
cas::{Cas, RefStore},
|
||||
};
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
($($arg:tt)*) => {{
|
||||
let res = std::fmt::format(format_args!($($arg)*));
|
||||
Error::err(res)
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::fmt;
|
||||
use std::str::{FromStr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::err;
|
||||
use super::error::*;
|
||||
|
||||
|
||||
@@ -43,7 +44,7 @@ impl ObjectId {
|
||||
}
|
||||
|
||||
impl FromStr for ObjectId {
|
||||
type Err = Box<Error>;
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(id_str: &str) -> Result<ObjectId> {
|
||||
Ok(ObjectId {
|
||||
@@ -56,6 +57,14 @@ impl FromStr for ObjectId {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ObjectId {
|
||||
fn default() -> Self {
|
||||
return Self {
|
||||
id: Arc::new(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjectId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
|
||||
write_hex(f, self.id.as_slice())
|
||||
@@ -138,7 +147,7 @@ impl<'a> Iterator for CharPairs<'a> {
|
||||
Some(Ok(&self.string[start_index..self.index]))
|
||||
}
|
||||
else {
|
||||
Some(Err(Error::error(&format!("invalid string: got {} characters, expected even number", self.char_count))))
|
||||
Some(err!("invalid string: got {} characters, expected even number", self.char_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +157,7 @@ fn parse_byte(maybe_str_byte: Result<&str>) -> Result<u8> {
|
||||
match maybe_str_byte {
|
||||
Ok(str_byte) =>
|
||||
u8::from_str_radix(str_byte, 16)
|
||||
.map_err(|_| Error::error("invalid character")),
|
||||
.map_err(|_| Error::unknown("invalid character")),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -194,19 +203,23 @@ mod tests {
|
||||
fn str_to_object_id_invalid_size() {
|
||||
let result = ObjectId::from_str("000102030405060");
|
||||
|
||||
assert_eq!(
|
||||
result.expect_err("object id parsing should have failed"),
|
||||
Error::error("invalid string: got 15 characters, expected even number"),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.expect_err("result is not an error") {
|
||||
Error::UnknownError(ref msg) if msg == "invalid string: got 15 characters, expected even number"
|
||||
=> {},
|
||||
_ => panic!("result is not the expected error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_to_object_id_invalid_character() {
|
||||
let result = ObjectId::from_str("00010203 4050607");
|
||||
|
||||
assert_eq!(
|
||||
result.expect_err("object id parsing should have failed"),
|
||||
Error::error("invalid character"),
|
||||
);
|
||||
match result.expect_err("result is not an error") {
|
||||
Error::UnknownError(ref msg) if msg == "invalid character"
|
||||
=> {},
|
||||
_ => panic!("result is not the expected error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use super::object_type::{ObjectType};
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::err;
|
||||
use super::error::*;
|
||||
|
||||
|
||||
@@ -24,7 +25,10 @@ pub struct ObjectType {
|
||||
impl ObjectType {
|
||||
pub fn new(id: &[u8]) -> Result<Self> {
|
||||
if id.len() != 4 {
|
||||
Err(Error::error("Invalid object type size."))
|
||||
err!("invalid object type size")
|
||||
}
|
||||
else if let Err(err) = std::str::from_utf8(id) {
|
||||
err!("invalid object type, contain non-utf8 sequence: {}", err)
|
||||
}
|
||||
else {
|
||||
let mut buf = [0; 4];
|
||||
@@ -48,6 +52,16 @@ impl std::fmt::Debug for ObjectType {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ObjectType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
|
||||
// Safe, because we check at ObjectType creation that it is valid utf8
|
||||
let id = unsafe {
|
||||
std::str::from_utf8_unchecked(&self.id)
|
||||
};
|
||||
write!(f, "{}", id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use digest::DynDigest;
|
||||
|
||||
use super::err;
|
||||
use super::error::*;
|
||||
use super::object_id::ObjectId;
|
||||
|
||||
@@ -53,7 +54,7 @@ impl<R: std::io::Read> std::io::Read for ReadWrapper<R> {
|
||||
|
||||
impl<R: std::io::Read> Reader for ReadWrapper<R> {
|
||||
fn finalize(self: Box<Self>) -> Result<ObjectId> {
|
||||
Err(Error::error("reader pipline has no digest step"))
|
||||
err!("reader pipline has no digest step")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ impl<W: std::io::Write> std::io::Write for WriteWrapper<W> {
|
||||
|
||||
impl<W: std::io::Write> Writer for WriteWrapper<W> {
|
||||
fn finalize(self: Box<Self>) -> Result<ObjectId> {
|
||||
Err(Error::error("reader pipline has no digest step"))
|
||||
err!("reader pipline has no digest step")
|
||||
}
|
||||
|
||||
fn _finalize(self: Box<Self>, oid: ObjectId) -> Result<ObjectId> {
|
||||
@@ -97,7 +98,7 @@ pub struct DigestReader<'a> {
|
||||
|
||||
impl<'a> DigestReader<'a> {
|
||||
pub fn new(reader: Box<dyn Reader + 'a>, digest: Box<dyn DynDigest + 'a>) -> Self {
|
||||
return DigestReader {
|
||||
DigestReader {
|
||||
reader,
|
||||
digest: Some(digest),
|
||||
oid: None,
|
||||
@@ -128,7 +129,7 @@ impl<'a> std::io::Read for DigestReader<'a> {
|
||||
|
||||
impl<'a> Reader for DigestReader<'a> {
|
||||
fn finalize(self: Box<Self>) -> Result<ObjectId> {
|
||||
self.oid.ok_or_else(|| Error::error("Reader not finalized"))
|
||||
self.oid.ok_or_else(|| Error::unknown("Reader not finalized"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ pub struct DigestWriter<'a> {
|
||||
|
||||
impl<'a> DigestWriter<'a> {
|
||||
pub fn new(writer: Box<dyn Writer + 'a>, digest: Box<dyn DynDigest + 'a>) -> Self {
|
||||
return DigestWriter {
|
||||
DigestWriter {
|
||||
writer,
|
||||
digest,
|
||||
}
|
||||
@@ -166,7 +167,7 @@ impl<'a> Writer for DigestWriter<'a> {
|
||||
}
|
||||
|
||||
fn _finalize(self: Box<Self>, _oid: ObjectId) -> Result<ObjectId> {
|
||||
Err(Error::error("writer pipeline has several digest steps"))
|
||||
err!("writer pipeline has several digest steps")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
[package]
|
||||
name = "cas-simple"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["Simon Boyé <sim.boye@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
digest = { version = "0.9.0", features = ["alloc"] }
|
||||
sha2 = "0.9.5"
|
||||
camino = { version = "1.0.7" }
|
||||
toml = "0.5.8"
|
||||
cas-core = { path = "../cas-core" }
|
||||
tempfile = "3.2.0"
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use digest::DynDigest;
|
||||
use toml::Value;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use cas_core::{
|
||||
Cas, DefaultPipeline, Error, ObjectId, ObjectMetadata, ObjectType,
|
||||
Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType,
|
||||
Pipeline, Reader, ReadWrapper, RefStore, Result, Writer,
|
||||
};
|
||||
|
||||
@@ -34,27 +34,48 @@ use crate::wfile::WFile;
|
||||
|
||||
|
||||
pub struct SimpleCas {
|
||||
db_path: PathBuf,
|
||||
db_path: Utf8PathBuf,
|
||||
digest: Box<dyn DynDigest>,
|
||||
pipeline: DefaultPipeline,
|
||||
config: Value,
|
||||
// config: Value,
|
||||
}
|
||||
|
||||
|
||||
impl SimpleCas {
|
||||
pub fn create(db_path: PathBuf, config: Value) -> Result<Self> {
|
||||
pub fn create(db_path: Utf8PathBuf, mut config: Value) -> Result<Self> {
|
||||
if !config.is_table() {
|
||||
return Error::err("invalid config object: must be table");
|
||||
}
|
||||
|
||||
let maybe_engine = config.as_table_mut().unwrap()
|
||||
.entry("cas")
|
||||
.or_insert_with(|| toml::value::Table::new().into())
|
||||
.as_table_mut().unwrap()
|
||||
.entry("engine")
|
||||
.or_insert("simple".into())
|
||||
.as_str();
|
||||
match maybe_engine {
|
||||
Some(engine) if engine != "simple" => {
|
||||
return err!("invalid cas.engine in config: got {}, expected simple", engine);
|
||||
},
|
||||
None => {
|
||||
return Error::err("invalid casengine in config: expected String");
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let digest_id = config["cas"]["digest"].as_str()
|
||||
.ok_or_else(|| Error::error(
|
||||
.ok_or_else(|| Error::unknown(
|
||||
"mandatory cas.digest value is invalid or missing from config"
|
||||
))?;
|
||||
let digest = new_digest(digest_id)?;
|
||||
let pipeline = DefaultPipeline::new(digest.box_clone());
|
||||
|
||||
if db_path.exists() {
|
||||
return Err(Error::error(format!(
|
||||
return err!(
|
||||
"failed to create SimpleCas: target directory already exists ({})",
|
||||
db_path.to_string_lossy(),
|
||||
)));
|
||||
db_path,
|
||||
);
|
||||
}
|
||||
|
||||
for path in [
|
||||
@@ -64,10 +85,10 @@ impl SimpleCas {
|
||||
&tmp_dir(&db_path),
|
||||
] {
|
||||
std::fs::create_dir(path).or_else(|e|
|
||||
Err(Error::error(format!(
|
||||
err!(
|
||||
"failed to create directory ({}): {}",
|
||||
path.to_string_lossy(), e,
|
||||
)))
|
||||
path, e,
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -77,15 +98,15 @@ impl SimpleCas {
|
||||
db_path,
|
||||
digest,
|
||||
pipeline,
|
||||
config,
|
||||
// config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(db_path: PathBuf) -> Result<Self> {
|
||||
pub fn open(db_path: Utf8PathBuf) -> Result<Self> {
|
||||
let config = read_config(&db_path)?;
|
||||
|
||||
let digest_id = config["cas"]["digest"].as_str()
|
||||
.ok_or_else(|| Error::error(
|
||||
.ok_or_else(|| Error::unknown(
|
||||
"mandatory cas.digest value is invalid or missing from config"
|
||||
))?;
|
||||
let digest = new_digest(digest_id)?;
|
||||
@@ -95,13 +116,13 @@ impl SimpleCas {
|
||||
db_path,
|
||||
digest,
|
||||
pipeline,
|
||||
config,
|
||||
// config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
write_config(&self.config, &self.db_path)
|
||||
}
|
||||
// pub fn save_config(&self) -> Result<()> {
|
||||
// write_config(&self.config, &self.db_path)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -111,10 +132,10 @@ impl Cas for SimpleCas {
|
||||
ObjectId::from_str(hex)
|
||||
}
|
||||
else {
|
||||
Err(Error::error(format!(
|
||||
err!(
|
||||
"invalid object id size: got {}, expected {}",
|
||||
hex.len(), self.digest.output_size() * 2,
|
||||
)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +152,11 @@ impl Cas for SimpleCas {
|
||||
fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box<dyn Reader>)> {
|
||||
let opath = obj_path(&self.db_path, oid);
|
||||
if !opath.is_file() {
|
||||
return Err(Error::error(format!("object not found: {}", oid)));
|
||||
return err!("object not found: {}", oid);
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(opath).or_else(|err|
|
||||
Err(Error::error(format!("failed to open object {}: {}", oid, err))))?;
|
||||
err!("failed to open object {}: {}", oid, err))?;
|
||||
let wrapper = Box::new(ReadWrapper::new(file));
|
||||
let mut reader = self.pipeline.new_reader(wrapper);
|
||||
let metadata = read_metadata(&mut reader)?;
|
||||
@@ -154,60 +175,60 @@ impl Cas for SimpleCas {
|
||||
fn remove_object(&mut self, oid: &ObjectId) -> Result<()> {
|
||||
let opath = obj_path(&self.db_path, oid);
|
||||
if !opath.is_file() {
|
||||
return Err(Error::error(format!("object not found: {}", oid)));
|
||||
return err!("object not found: {}", oid);
|
||||
}
|
||||
std::fs::remove_file(opath).or_else(|err|
|
||||
Err(Error::error(format!("failed to remove object {}: {}", oid, err))))?;
|
||||
err!("failed to remove object {}: {}", oid, err))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RefStore for SimpleCas {
|
||||
fn get_ref<P: AsRef<Path>>(&self, key: P) -> Result<ObjectId> {
|
||||
fn get_ref<P: AsRef<Utf8Path>>(&self, key: P) -> Result<ObjectId> {
|
||||
let path = ref_dir(&self.db_path).join(key.as_ref());
|
||||
if !path.exists() {
|
||||
Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy()))
|
||||
err!("reference {} does not exists", key.as_ref())
|
||||
}
|
||||
else if !path.is_file() {
|
||||
Error::err(format!("reference {} is not a file", key.as_ref().to_string_lossy()))
|
||||
err!("reference {} is not a file", key.as_ref())
|
||||
}
|
||||
else {
|
||||
let file = std::fs::read(path).or_else(|err|
|
||||
Error::err(format!("failed to read reference file for {}: {}", key.as_ref().to_string_lossy(), err))
|
||||
err!("failed to read reference file for {}: {}", key.as_ref(), err)
|
||||
)?;
|
||||
Ok(
|
||||
ObjectId::from_str(
|
||||
std::str::from_utf8(&file).or_else(|err|
|
||||
Error::err(format!("invalid reference file at {}: {}", key.as_ref().to_string_lossy(), err))
|
||||
err!("invalid reference file at {}: {}", key.as_ref(), err)
|
||||
)?
|
||||
)?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ref<P: AsRef<Path>>(&mut self, key: P, value: &ObjectId) -> Result<()> {
|
||||
fn set_ref<P: AsRef<Utf8Path>>(&mut self, key: P, value: &ObjectId) -> Result<()> {
|
||||
let path = ref_dir(&self.db_path).join(key.as_ref());
|
||||
std::fs::create_dir_all(path.parent().ok_or_else(||
|
||||
Error::error(format!("reference file {} has no parent dir?", key.as_ref().to_string_lossy()))
|
||||
Error::unknown(format!("reference file {} has no parent dir?", key.as_ref()))
|
||||
)?).or_else(|err|
|
||||
Error::err(format!("failed to create reference dir for {}: {}", key.as_ref().to_string_lossy(), err))
|
||||
err!("failed to create reference dir for {}: {}", key.as_ref(), err)
|
||||
)?;
|
||||
std::fs::write(path, value.to_string()).or_else(|err|
|
||||
Error::err(format!("failed to write reference {}: {}", key.as_ref().to_string_lossy(), err))
|
||||
err!("failed to write reference {}: {}", key.as_ref(), err)
|
||||
)
|
||||
}
|
||||
|
||||
fn remove_ref<P: AsRef<Path>>(&mut self, key: P) -> Result<()> {
|
||||
fn remove_ref<P: AsRef<Utf8Path>>(&mut self, key: P) -> Result<()> {
|
||||
let path = ref_dir(&self.db_path).join(key.as_ref());
|
||||
if !path.exists() {
|
||||
Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy()))
|
||||
err!("reference {} does not exists", key.as_ref())
|
||||
}
|
||||
else if !path.is_file() {
|
||||
Error::err(format!("reference {} is not a file", key.as_ref().to_string_lossy()))
|
||||
err!("reference {} is not a file", key.as_ref())
|
||||
}
|
||||
else {
|
||||
std::fs::remove_file(path).or_else(|err|
|
||||
Error::err(format!("failed to remove reference file {}: {}", key.as_ref().to_string_lossy(), err))
|
||||
err!("failed to remove reference file {}: {}", key.as_ref(), err)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -215,9 +236,9 @@ impl RefStore for SimpleCas {
|
||||
|
||||
|
||||
pub struct ObjectIdIterator {
|
||||
root_dirs: Vec<PathBuf>,
|
||||
inner_dirs: Vec<PathBuf>,
|
||||
objects: Vec<PathBuf>,
|
||||
root_dirs: Vec<Utf8PathBuf>,
|
||||
inner_dirs: Vec<Utf8PathBuf>,
|
||||
objects: Vec<Utf8PathBuf>,
|
||||
root_index: usize,
|
||||
inner_index: usize,
|
||||
object_index: usize,
|
||||
@@ -247,7 +268,7 @@ impl ObjectIdIterator {
|
||||
let path = &self.objects[self.object_index];
|
||||
|
||||
if !path.is_file() {
|
||||
return Error::err(format!("item in object database is not a file: {:?}", path));
|
||||
return err!("item in object database is not a file: {:?}", path);
|
||||
}
|
||||
|
||||
let id = path.ancestors()
|
||||
@@ -255,9 +276,9 @@ impl ObjectIdIterator {
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|p| p.file_name()?.to_str())
|
||||
.map(|p| p.file_name())
|
||||
.collect::<Option<String>>()
|
||||
.ok_or_else(|| Error::error(format!("invalid object in object database: {:?}", path)))?;
|
||||
.ok_or_else(|| Error::unknown(format!("invalid object in object database: {:?}", path)))?;
|
||||
ObjectId::from_str(&id)
|
||||
}
|
||||
|
||||
@@ -342,14 +363,19 @@ impl Iterator for ObjectIdIterator {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir(path: &Path) -> Result<Vec<PathBuf>> {
|
||||
fn read_dir(path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
|
||||
let mut paths = std::fs::read_dir(path)
|
||||
.or_else(|err| Err(Error::error(format!(
|
||||
"failed to read directory {:?}: {}", path, err))))?
|
||||
.map(|dir| dir.map(|dir| dir.path()))
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.or_else(|err| Err(Error::error(format!(
|
||||
"error while reading directory {:?}: {}", path, err))))?;
|
||||
.or_else(|err| err!("failed to read directory {:?}: {}", path, err))?
|
||||
.map(|res| match res {
|
||||
Ok(dir) => Ok(
|
||||
Utf8PathBuf::from_path_buf(dir.path())
|
||||
.or_else(|e| err!("non-unicode character in path: {}: {:?}", path, e))?
|
||||
),
|
||||
Err(err) => err!("error while reading directory {:?}: {}", path, err),
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.or_else(|err| err!(
|
||||
"error while reading directory {:?}: {}", path, err))?;
|
||||
paths.sort();
|
||||
Ok(paths)
|
||||
}
|
||||
@@ -357,7 +383,7 @@ fn read_dir(path: &Path) -> Result<Vec<PathBuf>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
use camino::Utf8Path;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -368,7 +394,7 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn get_cas_path(dir: &Path) -> PathBuf {
|
||||
fn get_cas_path(dir: &Utf8Path) -> Utf8PathBuf {
|
||||
let mut cas_path = dir.to_path_buf();
|
||||
cas_path.push(".bsv");
|
||||
cas_path
|
||||
@@ -377,7 +403,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_create_simple_cas() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(Utf8Path::from_path(dir.path()).unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let cas = SimpleCas::create(cas_path, config)
|
||||
@@ -391,7 +417,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_write_object() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let otype = ObjectType::new(b"blob").expect("failed to create object type");
|
||||
@@ -421,7 +447,7 @@ mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let otype = ObjectType::new(b"blob").expect("failed to create object type");
|
||||
@@ -460,7 +486,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_read_write_object() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let otype = ObjectType::new(b"blob").expect("failed to create object type");
|
||||
@@ -480,7 +506,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_remove_object() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let otype = ObjectType::new(b"blob").expect("failed to create object type");
|
||||
@@ -501,7 +527,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_object_id_iterator() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let mut cas = SimpleCas::create(cas_path.clone(), config)
|
||||
@@ -615,7 +641,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_reference() {
|
||||
let dir = tempfile::tempdir().expect("failed to create temp test dir");
|
||||
let cas_path = get_cas_path(dir.path());
|
||||
let cas_path = get_cas_path(dir.path().try_into().unwrap());
|
||||
let config = get_config();
|
||||
|
||||
let mut cas = SimpleCas::create(cas_path.clone(), config)
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! # cas-simple
|
||||
//!
|
||||
@@ -21,12 +21,13 @@
|
||||
|
||||
extern crate digest;
|
||||
extern crate sha2;
|
||||
extern crate camino;
|
||||
extern crate toml;
|
||||
extern crate cas_core;
|
||||
extern crate tempfile;
|
||||
|
||||
|
||||
mod utils;
|
||||
pub mod utils;
|
||||
mod wfile;
|
||||
mod cas;
|
||||
|
||||
|
||||
@@ -6,30 +6,29 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use digest::DynDigest;
|
||||
use toml::Value;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use cas_core::{
|
||||
Error, hex, ObjectId, ObjectMetadata, ObjectType, Result,
|
||||
err, Error, hex, ObjectId, ObjectMetadata, ObjectType, Result,
|
||||
};
|
||||
|
||||
|
||||
pub fn obj_dir(cas_path: &Path) -> PathBuf {
|
||||
pub fn obj_dir(cas_path: &Utf8Path) -> Utf8PathBuf {
|
||||
cas_path.join("obj")
|
||||
}
|
||||
|
||||
pub fn obj_path(cas_path: &Path, oid: &ObjectId) -> PathBuf {
|
||||
pub fn obj_path(cas_path: &Utf8Path, oid: &ObjectId) -> Utf8PathBuf {
|
||||
let mut path = obj_dir(cas_path);
|
||||
path.push(hex(&oid.id()[0..1]));
|
||||
path.push(hex(&oid.id()[1..2]));
|
||||
@@ -37,49 +36,49 @@ pub fn obj_path(cas_path: &Path, oid: &ObjectId) -> PathBuf {
|
||||
path
|
||||
}
|
||||
|
||||
pub fn ref_dir(cas_path: &Path) -> PathBuf {
|
||||
pub fn ref_dir(cas_path: &Utf8Path) -> Utf8PathBuf {
|
||||
cas_path.join("ref")
|
||||
}
|
||||
|
||||
pub fn tmp_dir(cas_path: &Path) -> PathBuf {
|
||||
pub fn tmp_dir(cas_path: &Utf8Path) -> Utf8PathBuf {
|
||||
cas_path.join("tmp")
|
||||
}
|
||||
|
||||
pub fn config_path(cas_path: &Path) -> PathBuf {
|
||||
pub fn config_path(cas_path: &Utf8Path) -> Utf8PathBuf {
|
||||
cas_path.join("config")
|
||||
}
|
||||
|
||||
|
||||
pub fn read_config(db_path: &Path) -> Result<Value> {
|
||||
pub fn read_config(db_path: &Utf8Path) -> Result<Value> {
|
||||
use std::io::Read;
|
||||
|
||||
let mut file = std::fs::File::open(config_path(db_path)).or_else(|err|
|
||||
Err(Error::error(format!("invalid repository: no config file: {}", err)))
|
||||
err!("invalid repository: no config file: {}", err)
|
||||
)?;
|
||||
let mut config_str = String::new();
|
||||
file.read_to_string(&mut config_str).or_else(|err|
|
||||
Err(Error::error(format!("cannot read config file: {}", err)))
|
||||
err!("cannot read config file: {}", err)
|
||||
)?;
|
||||
let config = toml::from_str(&config_str).or_else(|err|
|
||||
Err(Error::error(format!("error while reading config file: {}", err)))
|
||||
err!("error while reading config file: {}", err)
|
||||
)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn write_config(config: &Value, db_path: &Path) -> Result<()> {
|
||||
pub fn write_config(config: &Value, db_path: &Utf8Path) -> Result<()> {
|
||||
use std::io::Write;
|
||||
|
||||
let config_str = toml::to_string_pretty(config).or_else(|err|
|
||||
Err(Error::error(format!("failed to serialize config: {}", err)))
|
||||
err!("failed to serialize config: {}", err)
|
||||
)?;
|
||||
let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err|
|
||||
Err(Error::error(format!("cannot create temp config file: {}", err)))
|
||||
err!("cannot create temp config file: {}", err)
|
||||
)?;
|
||||
file.write_all(config_str.as_bytes()).or_else(|err|
|
||||
Err(Error::error(format!("failed to write to temp config: {}", err)))
|
||||
err!("failed to write to temp config: {}", err)
|
||||
)?;
|
||||
file.persist(config_path(db_path)).or_else(|err|
|
||||
Err(Error::error(format!("failed to (over)write config: {}", err)))
|
||||
err!("failed to (over)write config: {}", err)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,13 +88,13 @@ pub fn read_metadata(read: &mut dyn std::io::Read) -> Result<ObjectMetadata> {
|
||||
let otype = ObjectType::new(&{
|
||||
let mut buf = [0; 4];
|
||||
read.read_exact(&mut buf).or_else(|err|
|
||||
Err(Error::error(format!("failed to read object type: {}", err))))?;
|
||||
err!("failed to read object type: {}", err))?;
|
||||
buf
|
||||
})?;
|
||||
let size = u64::from_be_bytes({
|
||||
let mut buf = [0; 8];
|
||||
read.read_exact(&mut buf).or_else(|err|
|
||||
Err(Error::error(format!("failed to read object size: {}", err))))?;
|
||||
err!("failed to read object size: {}", err))?;
|
||||
buf
|
||||
});
|
||||
Ok(ObjectMetadata::new(otype, size))
|
||||
@@ -103,9 +102,9 @@ pub fn read_metadata(read: &mut dyn std::io::Read) -> Result<ObjectMetadata> {
|
||||
|
||||
pub fn write_metadata(write: &mut dyn std::io::Write, otype: &ObjectType, size: u64) -> Result<()> {
|
||||
write.write_all(otype.id()).or_else(|err|
|
||||
Err(Error::error(format!("failed to write object type: {}", err))))?;
|
||||
err!("failed to write object type: {}", err))?;
|
||||
write.write_all(&size.to_be_bytes()).or_else(|err|
|
||||
Err(Error::error(format!("failed to write object size: {}", err))))?;
|
||||
err!("failed to write object size: {}", err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -113,7 +112,7 @@ pub fn write_metadata(write: &mut dyn std::io::Write, otype: &ObjectType, size:
|
||||
pub fn new_digest(id: &str) -> Result<Box<dyn DynDigest>> {
|
||||
match id {
|
||||
"sha256" => { Ok(Box::new(sha2::Sha256::default())) },
|
||||
_ => { Err(Error::error(format!("unknown digest '{}'", id))) }
|
||||
_ => { err!("unknown digest '{}'", id) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,29 +125,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dirs() {
|
||||
let base_path = PathBuf::from("/foo/bar");
|
||||
let base_path = Utf8PathBuf::from("/foo/bar");
|
||||
let oid = ObjectId::from_str("0123456789abcdef")
|
||||
.expect("failed to create object id");
|
||||
|
||||
assert_eq!(
|
||||
config_path(&base_path),
|
||||
PathBuf::from("/foo/bar/config")
|
||||
Utf8PathBuf::from("/foo/bar/config")
|
||||
);
|
||||
assert_eq!(
|
||||
obj_dir(&base_path),
|
||||
PathBuf::from("/foo/bar/obj")
|
||||
Utf8PathBuf::from("/foo/bar/obj")
|
||||
);
|
||||
assert_eq!(
|
||||
obj_path(&base_path, &oid),
|
||||
PathBuf::from("/foo/bar/obj/01/23/456789abcdef")
|
||||
Utf8PathBuf::from("/foo/bar/obj/01/23/456789abcdef")
|
||||
);
|
||||
assert_eq!(
|
||||
ref_dir(&base_path),
|
||||
PathBuf::from("/foo/bar/ref")
|
||||
Utf8PathBuf::from("/foo/bar/ref")
|
||||
);
|
||||
assert_eq!(
|
||||
tmp_dir(&base_path),
|
||||
PathBuf::from("/foo/bar/tmp")
|
||||
Utf8PathBuf::from("/foo/bar/tmp")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,10 +164,10 @@ mod tests {
|
||||
|
||||
let dir = tempfile::TempDir::new()
|
||||
.expect("failed to create tmp dir");
|
||||
std::fs::create_dir(tmp_dir(dir.path())).expect("failed to create db/tmp dir");
|
||||
std::fs::create_dir(tmp_dir(Utf8Path::from_path(dir.path()).unwrap())).expect("failed to create db/tmp dir");
|
||||
|
||||
write_config(&config, dir.path()).expect("failed to write config");
|
||||
let config2 = read_config(dir.path()).expect("failed to read config");
|
||||
write_config(&config, Utf8Path::from_path(dir.path()).unwrap()).expect("failed to write config");
|
||||
let config2 = read_config(Utf8Path::from_path(dir.path()).unwrap()).expect("failed to read config");
|
||||
|
||||
assert_eq!(config2, config);
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::{PathBuf};
|
||||
use camino::{Utf8PathBuf};
|
||||
|
||||
use cas_core::{
|
||||
Error, ObjectId, Result, Writer,
|
||||
err, Error, ObjectId, Result, Writer,
|
||||
};
|
||||
|
||||
use crate::utils::{obj_path, tmp_dir};
|
||||
@@ -25,13 +25,13 @@ use crate::utils::{obj_path, tmp_dir};
|
||||
|
||||
pub struct WFile {
|
||||
file: tempfile::NamedTempFile,
|
||||
db_path: PathBuf,
|
||||
db_path: Utf8PathBuf,
|
||||
}
|
||||
|
||||
impl WFile {
|
||||
pub fn new(db_path: PathBuf) -> Result<Self> {
|
||||
pub fn new(db_path: Utf8PathBuf) -> Result<Self> {
|
||||
let file = tempfile::NamedTempFile::new_in(tmp_dir(&db_path)).or_else(|err|
|
||||
Err(Error::error(format!("failed to create_file: {}", err))))?;
|
||||
err!("failed to create_file: {}", err))?;
|
||||
Ok(WFile {
|
||||
file,
|
||||
db_path,
|
||||
@@ -51,7 +51,7 @@ impl std::io::Write for WFile {
|
||||
|
||||
impl Writer for WFile {
|
||||
fn finalize(self: Box<Self>) -> Result<ObjectId> {
|
||||
Err(Error::error("reader pipline has no digest step"))
|
||||
err!("reader pipline has no digest step")
|
||||
}
|
||||
|
||||
fn _finalize(self: Box<Self>, oid: ObjectId) -> Result<ObjectId> {
|
||||
@@ -59,9 +59,9 @@ impl Writer for WFile {
|
||||
std::fs::create_dir_all(
|
||||
path.parent().expect("cannot access to object parent directory")
|
||||
).or_else(|err|
|
||||
Err(Error::error(format!("failed to create object dir: {}", err))))?;
|
||||
err!("failed to create object dir: {}", err))?;
|
||||
self.file.persist(path).or_else(|err|
|
||||
Err(Error::error(format!("failed to persist file: {}", err))))?;
|
||||
err!("failed to persist file: {}", err))?;
|
||||
Ok(oid)
|
||||
}
|
||||
}
|
||||
@@ -86,18 +86,18 @@ mod tests {
|
||||
let dir = tempfile::TempDir::new()
|
||||
.expect("failed to create tmp dir");
|
||||
|
||||
std::fs::create_dir(tmp_dir(dir.path())).expect("failed to create db/obj dir");
|
||||
std::fs::create_dir(obj_dir(dir.path())).expect("failed to create db/tmp dir");
|
||||
std::fs::create_dir(tmp_dir(dir.path().try_into().unwrap())).expect("failed to create db/obj dir");
|
||||
std::fs::create_dir(obj_dir(dir.path().try_into().unwrap())).expect("failed to create db/tmp dir");
|
||||
|
||||
{
|
||||
let mut writer = Box::new(WFile::new(dir.path().to_path_buf()).expect("failed to create WFile"));
|
||||
let mut writer = Box::new(WFile::new(dir.path().to_path_buf().try_into().unwrap()).expect("failed to create WFile"));
|
||||
writer.write(data).expect("failed to write to WFile");
|
||||
let oid2 = writer._finalize(oid.clone()).expect("failed to finalize WFile");
|
||||
|
||||
assert_eq!(oid2, oid);
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::open(obj_path(dir.path(), &oid)).expect("failed to open object file");
|
||||
let mut file = std::fs::File::open(obj_path(dir.path().try_into().unwrap(), &oid)).expect("failed to open object file");
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).expect("failed to read object file");
|
||||
|
||||
|
||||
2
libbsv/.gitignore
vendored
2
libbsv/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
@@ -2,15 +2,13 @@
|
||||
name = "libbsv"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Boyé <sim.boye@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0.25"
|
||||
serde = { version = "1.0.106", features = ["derive"] }
|
||||
toml = "0.5.6"
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
||||
tempfile = "3.1.0"
|
||||
digest = "0.9.0"
|
||||
sha2 = "0.9.1"
|
||||
flate2 = "1.0.17"
|
||||
toml = "0.5.8"
|
||||
camino = "1.0.7"
|
||||
regex = "1.6.0"
|
||||
bsvfs = { path = "../bsvfs" }
|
||||
cas-core = { path = "../cas-core" }
|
||||
cas-simple = { path = "../cas-simple" }
|
||||
@@ -5,30 +5,27 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::collections::{HashMap};
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use cas_core::{Cas, Result};
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryConfig {
|
||||
pub path: PathBuf,
|
||||
pub device_name: String,
|
||||
pub uuid: Uuid,
|
||||
pub trait CasConfig {
|
||||
fn build_cas(&self) -> Result<Box<dyn Cas>>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub repository: RepositoryConfig,
|
||||
pub struct BsvConfig {
|
||||
cas: Box<dyn CasConfig>,
|
||||
dir_map: HashMap<Utf8PathBuf, Utf8PathBuf>,
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Box<Error>>;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("failed to create repository: {message}")]
|
||||
RepositoryCreationFailed {
|
||||
message: String,
|
||||
source: Option<Box<dyn std::error::Error>>,
|
||||
},
|
||||
|
||||
#[error("invalid object id: {message}")]
|
||||
InvalidObjectId {
|
||||
message: String,
|
||||
source: Option<Box<dyn std::error::Error>>,
|
||||
},
|
||||
|
||||
#[error("invalid size (got: {size}, expected: {expected})")]
|
||||
InvalidSize {
|
||||
size: usize,
|
||||
expected: usize,
|
||||
},
|
||||
|
||||
#[error("non-empty directory ({dir})")]
|
||||
NonEmptyDirectory {
|
||||
dir: PathBuf
|
||||
},
|
||||
|
||||
#[error("invalid character(s) ({characters})")]
|
||||
InvalidObjectIdCharacter {
|
||||
characters: String,
|
||||
},
|
||||
|
||||
#[error("invalid object type ({otype:?})")]
|
||||
InvalidObjectType {
|
||||
otype: [u8; 4],
|
||||
},
|
||||
|
||||
#[error("invalid object size (expected {expected}, got {size})")]
|
||||
InvalidObjectSize {
|
||||
size: u64,
|
||||
expected: u64,
|
||||
},
|
||||
|
||||
#[error("unsupported file type")]
|
||||
UnsupportedFileType,
|
||||
|
||||
#[error("invalid path ({path})")]
|
||||
InvalidPath { path: PathBuf },
|
||||
|
||||
#[error("io error{}", format_optional_path(path))]
|
||||
IoError {
|
||||
source: std::io::Error,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
|
||||
pub fn repository_creation_failed<M: Into<String>>(message: M) -> Box<Error> {
|
||||
Box::new(Error::RepositoryCreationFailed {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn repository_creation_failed_from<M: Into<String>>(source: Box<dyn std::error::Error>, message: M) -> Box<Error> {
|
||||
Box::new(Error::RepositoryCreationFailed {
|
||||
message: message.into(),
|
||||
source: Some(source),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invalid_object_id<M: Into<String>>(message: M) -> Box<Error> {
|
||||
Box::new(Error::InvalidObjectId {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invalid_object_id_from<M: Into<String>>(source: Box<dyn std::error::Error>, message: M) -> Box<Error> {
|
||||
Box::new(Error::InvalidObjectId {
|
||||
message: message.into(),
|
||||
source: Some(source),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn format_optional_path(maybe_path: &Option<PathBuf>) -> String {
|
||||
match maybe_path {
|
||||
Some(path) => { format!(" ({:?})", path) },
|
||||
None => { String::new() }
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
pub mod error;
|
||||
// pub mod config;
|
||||
// pub mod object_id;
|
||||
// pub mod object;
|
||||
// pub mod repository;
|
||||
|
||||
|
||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
||||
pub use error::{Error, Result};
|
||||
// pub use config::{Config, RepositoryConfig};
|
||||
// pub use object_id::ObjectId;
|
||||
// pub use object::{ObjectType, OTYPE_BLOB, OTYPE_TREE};
|
||||
// pub use repository::Repository;
|
||||
@@ -1,180 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::{Path};
|
||||
use std::fs::Metadata;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use crate::core::error::*;
|
||||
use crate::{ObjectId};
|
||||
|
||||
|
||||
pub type ObjectType = [u8; 4];
|
||||
|
||||
pub const OTYPE_BLOB: &ObjectType = b"blob";
|
||||
pub const OTYPE_TREE: &ObjectType = b"tree";
|
||||
|
||||
pub fn object_type_from_metadata(md: &Metadata) -> Result<ObjectType> {
|
||||
if md.is_file() {
|
||||
Ok(*OTYPE_BLOB)
|
||||
}
|
||||
else if md.is_dir() {
|
||||
Ok(*OTYPE_TREE)
|
||||
}
|
||||
else {
|
||||
Err(Error::UnsupportedFileType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum PermissionsFlag {
|
||||
Read = 0x04,
|
||||
Write = 0x02,
|
||||
Execute = 0x01,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Permissions {
|
||||
flags: u8,
|
||||
}
|
||||
|
||||
impl Default for Permissions {
|
||||
fn default() -> Permissions {
|
||||
Permissions { flags: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
pub fn read_only() -> Permissions {
|
||||
*Self::default().set_read(true)
|
||||
}
|
||||
|
||||
pub fn read_write() -> Permissions {
|
||||
*Self::read_only().set_write(true)
|
||||
}
|
||||
|
||||
pub fn read_execute() -> Permissions {
|
||||
*Self::read_only().set_execute(true)
|
||||
}
|
||||
|
||||
pub fn read_write_execute() -> Permissions {
|
||||
*Self::read_write().set_execute(true)
|
||||
}
|
||||
|
||||
pub fn from_unix_mode(mode: u32) -> Permissions {
|
||||
*Self::default()
|
||||
.set_read(mode & 0o400 != 0)
|
||||
.set_write(mode & 0o200 != 0)
|
||||
.set_execute(mode & 0o100 != 0)
|
||||
}
|
||||
|
||||
pub fn is_read(&self) -> bool {
|
||||
self.test_flag(PermissionsFlag::Read)
|
||||
}
|
||||
|
||||
pub fn is_write(&self) -> bool {
|
||||
self.test_flag(PermissionsFlag::Write)
|
||||
}
|
||||
|
||||
pub fn is_execute(&self) -> bool {
|
||||
self.test_flag(PermissionsFlag::Execute)
|
||||
}
|
||||
|
||||
pub fn set_read(&mut self, read: bool) -> &mut Self {
|
||||
self.set_flag(PermissionsFlag::Read, read)
|
||||
}
|
||||
|
||||
pub fn set_write(&mut self, write: bool) -> &mut Self {
|
||||
self.set_flag(PermissionsFlag::Write, write)
|
||||
}
|
||||
|
||||
pub fn set_execute(&mut self, execute: bool) -> &mut Self {
|
||||
self.set_flag(PermissionsFlag::Execute, execute)
|
||||
}
|
||||
|
||||
fn test_flag(&self, flag: PermissionsFlag) -> bool {
|
||||
(self.flags & flag as u8) != 0
|
||||
}
|
||||
|
||||
fn set_flag(&mut self, flag: PermissionsFlag, value: bool) -> &mut Self {
|
||||
if value { self.flags |= flag as u8; }
|
||||
else { self.flags &= !(flag as u8); }
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct TreeItem {
|
||||
otype: ObjectType,
|
||||
oid: ObjectId,
|
||||
modification_time: i64,
|
||||
permissions: Permissions,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl TreeItem {
|
||||
pub fn new(otype: &ObjectType, oid: &ObjectId, name: &str) -> TreeItem {
|
||||
TreeItem {
|
||||
otype: *otype,
|
||||
oid: oid.clone(),
|
||||
modification_time: 0,
|
||||
permissions: Permissions::read_write_execute(),
|
||||
name: name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file_path_and_id(file_path: &Path, oid: &ObjectId) -> Result<TreeItem> {
|
||||
let md = file_path.symlink_metadata()
|
||||
.map_err(|err| Error::IoError {
|
||||
source: err,
|
||||
path: Some(file_path.to_path_buf())
|
||||
})?;
|
||||
|
||||
Ok(TreeItem {
|
||||
otype: object_type_from_metadata(&md)?,
|
||||
oid: oid.clone(),
|
||||
modification_time: md.mtime(),
|
||||
permissions: Permissions::from_unix_mode(md.mode()),
|
||||
name: file_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or_else(|| Error::InvalidPath { path: file_path.into() })?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn object_type(&self) -> ObjectType {
|
||||
self.otype
|
||||
}
|
||||
|
||||
pub fn object_id(&self) -> &ObjectId {
|
||||
&self.oid
|
||||
}
|
||||
|
||||
pub fn modification_time(&self) -> i64 {
|
||||
self.modification_time
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> &Permissions {
|
||||
&self.permissions
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type TreeObject = Vec<TreeItem>;
|
||||
@@ -1,118 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::fmt;
|
||||
use std::str::{FromStr};
|
||||
|
||||
use super::error::*;
|
||||
|
||||
|
||||
/// A unique identifier for an object.
|
||||
///
|
||||
/// This is the handle used to reference an Object. This is an opaque type that uniquely identify an
|
||||
/// object. It can be compared to another ObjectId, be hashed and that's about it.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ObjectId {
|
||||
id: Vec<u8>,
|
||||
}
|
||||
|
||||
|
||||
impl ObjectId {
|
||||
pub fn new(id: &[u8]) -> ObjectId {
|
||||
ObjectId {
|
||||
id: id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl FromStr for ObjectId {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(id_str: &str) -> Result<ObjectId> {
|
||||
if id_str.len() % 2 != 0 {
|
||||
return Err(Error::InvalidObjectIdSize);
|
||||
}
|
||||
|
||||
let byte_count = id_str.len() / 2;
|
||||
let mut id = Vec::with_capacity(byte_count);
|
||||
|
||||
for byte_index in 0..byte_count {
|
||||
let str_index = byte_index * 2;
|
||||
let byte_str = id_str.get(str_index..(str_index + 2))
|
||||
.ok_or(Error::InvalidObjectIdCharacter)?;
|
||||
id.push(u8::from_str_radix(byte_str, 16)
|
||||
.or(Err(Error::InvalidObjectIdCharacter))?);
|
||||
}
|
||||
|
||||
Ok(ObjectId {
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl fmt::Display for ObjectId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
|
||||
for byte in self.id.iter() {
|
||||
write!(f, "{:02x}", byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl fmt::Debug for ObjectId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
|
||||
write!(f, "ObjectId::new(\"{}\")", self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::{FromStr};
|
||||
|
||||
use super::ObjectId;
|
||||
|
||||
#[test]
|
||||
fn object_id_display() {
|
||||
let obj = ObjectId::new(&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
]);
|
||||
|
||||
assert_eq!(format!("{}", obj), "0001020304050607");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_id_debug() {
|
||||
let obj = ObjectId::new(&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
]);
|
||||
|
||||
assert_eq!(format!("{:?}", obj), "ObjectId::new(\"0001020304050607\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_to_object_id() {
|
||||
let obj = ObjectId::from_str("0001020304050607").unwrap();
|
||||
let obj_ref = ObjectId::new(&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
]);
|
||||
|
||||
assert_eq!(obj, obj_ref);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, create_dir};
|
||||
use std::rc::Rc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::error::*;
|
||||
use super::config::{Config, RepositoryConfig};
|
||||
|
||||
use crate::simple_db::SimpleDb;
|
||||
|
||||
|
||||
const CONFIG_FILENAME: &str = ".bsv";
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Repository {
|
||||
config: Rc<Config>,
|
||||
db: SimpleDb,
|
||||
}
|
||||
|
||||
|
||||
impl Repository {
|
||||
pub fn create(path: &Path, device_name: &str) -> Result<Repository> {
|
||||
if path.exists() {
|
||||
return Err(Error::NonEmptyDirectory { dir: path.to_path_buf() });
|
||||
}
|
||||
|
||||
if device_name.is_empty() {
|
||||
return Err(Error::RepositoryCreationFailed("Device name must not be empty.".to_string()));
|
||||
}
|
||||
|
||||
create_dir(&path).map_err(|err| Error::IoError("Failed to create repository.")?;
|
||||
|
||||
let config = Rc::new(
|
||||
Config {
|
||||
repository: RepositoryConfig {
|
||||
path: path.into(),
|
||||
device_name: device_name.into(),
|
||||
uuid: Uuid::new_v4(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let config_path = {
|
||||
let mut config_path = PathBuf::from(path);
|
||||
config_path.push(CONFIG_FILENAME);
|
||||
config_path
|
||||
};
|
||||
|
||||
fs::write(
|
||||
config_path,
|
||||
toml::to_string(&*config)
|
||||
.chain_err(|| format!("Failed to serialize {}.", CONFIG_FILENAME))?,
|
||||
).chain_err(||
|
||||
format!("Failed to create repository: failed to write {}.", CONFIG_FILENAME)
|
||||
)?;
|
||||
|
||||
Ok(Repository {
|
||||
config: Rc::clone(&config),
|
||||
db: SimpleDb::new(path)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
163
libbsv/src/ignore.rs
Normal file
163
libbsv/src/ignore.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use regex::RegexSet;
|
||||
|
||||
use cas_core::{err, Error, Result};
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
Ignore,
|
||||
Accept,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IgnoreRules {
|
||||
patterns: RegexSet,
|
||||
actions: Vec<Action>,
|
||||
}
|
||||
|
||||
|
||||
impl IgnoreRules {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
patterns: RegexSet::new(&[] as &[&str]).unwrap(),
|
||||
actions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_source<P: AsRef<Utf8Path>>(source: &str, root: P) -> Result<Self> {
|
||||
assert!(root.as_ref().is_absolute());
|
||||
|
||||
let separator = if std::path::MAIN_SEPARATOR == '/' {
|
||||
"/"
|
||||
}
|
||||
else {
|
||||
assert_eq!(std::path::MAIN_SEPARATOR, '\\');
|
||||
"\\\\"
|
||||
};
|
||||
|
||||
let mut patterns = Vec::<String>::default();
|
||||
let mut actions = Vec::<Action>::default();
|
||||
|
||||
for line in source.lines() {
|
||||
let rule = line.trim();
|
||||
let mut rule_it = rule.chars().peekable();
|
||||
if rule_it.peek().is_none() || rule_it.peek() == Some(&'#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule_it.peek() == Some(&'!') {
|
||||
rule_it.next();
|
||||
actions.push(Action::Accept);
|
||||
}
|
||||
else {
|
||||
actions.push(Action::Ignore);
|
||||
}
|
||||
|
||||
let mut pat = String::new();
|
||||
let mut last_is_sep = false;
|
||||
|
||||
if rule_it.peek() == Some(&'/') {
|
||||
pat.push_str("^");
|
||||
pat.push_str(®ex::escape(root.as_ref().as_str()));
|
||||
}
|
||||
|
||||
while let Some(c) = rule_it.next() {
|
||||
if c == '/' {
|
||||
pat.push_str(separator);
|
||||
last_is_sep = true;
|
||||
continue;
|
||||
}
|
||||
else if c == '*' {
|
||||
if rule_it.peek() == Some(&'*') {
|
||||
rule_it.next();
|
||||
if !last_is_sep || (!rule_it.peek().is_none() && rule_it.peek() != Some(&'/')) {
|
||||
return err!("** pattern can only be used as a whole path segment");
|
||||
}
|
||||
pat.push_str(".*");
|
||||
}
|
||||
else {
|
||||
pat.push_str("[^/]*");
|
||||
}
|
||||
}
|
||||
else if c == '\\' {
|
||||
let c2 = rule_it.next().ok_or(Error::unknown("invalid \\ at end of rule"))?;
|
||||
let mut buf = [0u8; 4];
|
||||
pat.push_str(®ex::escape(c2.encode_utf8(&mut buf)));
|
||||
}
|
||||
else {
|
||||
let mut buf = [0u8; 4];
|
||||
pat.push_str(®ex::escape(c.encode_utf8(&mut buf)));
|
||||
}
|
||||
last_is_sep = false;
|
||||
}
|
||||
|
||||
if last_is_sep {
|
||||
pat.pop();
|
||||
}
|
||||
pat.push_str("(/.*)?$");
|
||||
|
||||
dbg!(&pat);
|
||||
patterns.push(pat);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
patterns: RegexSet::new(patterns)
|
||||
.or_else(|err| err!("failed to compile ignore rules: {err}"))?,
|
||||
actions: actions,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn action_for<P: AsRef<Utf8Path>>(&self, path: P) -> Action {
|
||||
assert!(path.as_ref().is_absolute());
|
||||
let index = self.patterns.matches(path.as_ref().as_str())
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(self.actions.len());
|
||||
*self.actions.get(index).unwrap_or(&Action::Accept)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_map() {
|
||||
let root = Utf8PathBuf::from("/foo.dir/bar");
|
||||
let patterns = "!hello/world\nhello\n/world/\n\\!\\*\n*.bak";
|
||||
let ignore = IgnoreRules::from_source(patterns, root).unwrap();
|
||||
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello/aoeu")), Action::Ignore);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello_world/aoeu")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/world/aoeu")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello/world/aoeu")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/world/aoeu")), Action::Ignore);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/world")), Action::Ignore);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/fooXdir/bar/world")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/!*/aoeu")), Action::Ignore);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/file.bak")), Action::Ignore);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/fileXbak")), Action::Accept);
|
||||
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test.bak/file")), Action::Ignore);
|
||||
}
|
||||
}
|
||||
@@ -5,29 +5,33 @@
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
// #[macro_use]
|
||||
extern crate thiserror;
|
||||
extern crate serde;
|
||||
extern crate toml;
|
||||
extern crate camino;
|
||||
extern crate regex;
|
||||
|
||||
extern crate bsvfs;
|
||||
extern crate cas_core;
|
||||
|
||||
|
||||
pub mod core;
|
||||
// pub mod simple_db;
|
||||
// mod permissions;
|
||||
mod tree_item;
|
||||
mod tree_walker;
|
||||
// mod config;
|
||||
mod path_map;
|
||||
mod ignore;
|
||||
mod repository;
|
||||
|
||||
|
||||
// pub use crate::core::{
|
||||
// Error, Result,
|
||||
// Config, RepositoryConfig,
|
||||
// ObjectId, ObjectType,
|
||||
// Repository,
|
||||
|
||||
// OTYPE_BLOB, OTYPE_TREE,
|
||||
// };
|
||||
// pub use crate::permissions::Permissions;
|
||||
pub use crate::tree_item::{Serialize, TreeItem};
|
||||
pub use crate::path_map::{PathMap, PathPair};
|
||||
pub use crate::repository::{Repository};
|
||||
|
||||
255
libbsv/src/path_map.rs
Normal file
255
libbsv/src/path_map.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use toml::Value;
|
||||
|
||||
use cas_core::{err, Error, Result};
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PathPair {
|
||||
pub logic: Utf8PathBuf,
|
||||
pub physic: Utf8PathBuf,
|
||||
}
|
||||
|
||||
impl PathPair {
|
||||
pub fn new(logic: Utf8PathBuf, physic: Utf8PathBuf) -> Self {
|
||||
Self { logic, physic }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PathMap {
|
||||
pairs: Vec<PathPair>,
|
||||
logic_order: Vec<usize>,
|
||||
physic_order: Vec<usize>,
|
||||
}
|
||||
|
||||
|
||||
impl PathMap {
|
||||
pub fn new() -> PathMap {
|
||||
PathMap {
|
||||
pairs: vec![],
|
||||
logic_order: vec![],
|
||||
physic_order: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_vec(vec: Vec<PathPair>) -> Result<Self> {
|
||||
if let Some(pair) =
|
||||
vec.iter()
|
||||
.filter(|p| !p.logic.is_absolute())
|
||||
.next() {
|
||||
return err!("relative logic path: {}", pair.logic);
|
||||
}
|
||||
|
||||
let mut logic_order: Vec<_> = (0..vec.len()).collect();
|
||||
logic_order.sort_by(|&i0, &i1| vec[i0].logic.cmp(&vec[i1].logic).reverse());
|
||||
|
||||
if let Some((&index, _)) =
|
||||
logic_order[..logic_order.len()-1].iter()
|
||||
.zip(logic_order[1..].iter())
|
||||
.filter(|(&i0, &i1)| vec[i0].logic == vec[i1].logic)
|
||||
.next() {
|
||||
return err!("duplicate logic path: {:?}", vec[index]);
|
||||
}
|
||||
|
||||
let mut physic_order: Vec<_> = (0..vec.len()).collect();
|
||||
physic_order.sort_by(|&i0, &i1| vec[i0].physic.cmp(&vec[i1].physic).reverse());
|
||||
|
||||
if let Some((&index, _)) =
|
||||
physic_order[..physic_order.len()-1].iter()
|
||||
.zip(physic_order[1..].iter())
|
||||
.filter(|(&i0, &i1)| vec[i0].physic == vec[i1].physic)
|
||||
.next() {
|
||||
return err!("duplicate physic path: {:?}", vec[index]);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
pairs: vec,
|
||||
logic_order,
|
||||
physic_order,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_toml_value(config: &Value) -> Result<Self> {
|
||||
let map = if let Some(mapping) = config.get("mapping") {
|
||||
if let Some(ref table) = mapping.as_table() {
|
||||
table.iter()
|
||||
.map(|(k, v)| Ok(PathPair{
|
||||
logic: k.into(),
|
||||
physic: Utf8PathBuf::from_path_buf(
|
||||
Utf8Path::new(
|
||||
v.as_str()
|
||||
.ok_or_else(|| Error::unknown("mapping values must be strings"))?
|
||||
).canonicalize()?
|
||||
).or_else(|e| err!("non-unicode character in path: {:?}", e))?
|
||||
}))
|
||||
.collect::<Result<_>>()?
|
||||
}
|
||||
else {
|
||||
err!("mapping must be a table, got {}", mapping)?
|
||||
}
|
||||
}
|
||||
else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Self::from_vec(map)
|
||||
}
|
||||
|
||||
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
|
||||
let path = logic_path.as_ref();
|
||||
|
||||
let pair = self.logic_order.iter()
|
||||
.map(|&i| &self.pairs[i])
|
||||
.filter(|p| path.starts_with(&p.logic))
|
||||
.next()?;
|
||||
|
||||
let mut physic = pair.physic.clone();
|
||||
physic.push(
|
||||
path.strip_prefix(&pair.logic)
|
||||
.ok()?
|
||||
);
|
||||
|
||||
return Some(physic)
|
||||
}
|
||||
|
||||
pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
|
||||
let path = physic_path.as_ref();
|
||||
|
||||
let pair = self.physic_order.iter()
|
||||
.map(|&i| &self.pairs[i])
|
||||
.filter(|p| path.starts_with(&p.physic))
|
||||
.next()?;
|
||||
|
||||
let mut logic = pair.logic.clone();
|
||||
logic.push(
|
||||
path.strip_prefix(&pair.physic)
|
||||
.ok()?
|
||||
);
|
||||
|
||||
return Some(logic)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_map() {
|
||||
let path_map = PathMap::from_vec(vec![
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar".into()),
|
||||
]).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/bar"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar"),
|
||||
Some("/home/user/bar".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar/file.txt"),
|
||||
Some("/home/user/bar/file.txt".into()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar"),
|
||||
Some("/foo/bar".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar/file.txt"),
|
||||
Some("/foo/bar/file.txt".into()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_map_subdirs() {
|
||||
let path_map = PathMap::from_vec(vec![
|
||||
PathPair::new("/foo/bar/baz".into(), "/home/user/bar/test".into()),
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar".into()),
|
||||
]).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar/file.txt"),
|
||||
Some("/home/user/bar/file.txt".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar/baz/file.txt"),
|
||||
Some("/home/user/bar/test/file.txt".into()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar/file.txt"),
|
||||
Some("/foo/bar/file.txt".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar/test/file.txt"),
|
||||
Some("/foo/bar/baz/file.txt".into()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_map_crossed_subdirs() {
|
||||
let path_map = PathMap::from_vec(vec![
|
||||
PathPair::new("/foo/bar/baz".into(), "/home/user/bar".into()),
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar/test".into()),
|
||||
]).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar/baz/file.txt"),
|
||||
Some("/home/user/bar/file.txt".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.physic_from_logic("/foo/bar/file.txt"),
|
||||
Some("/home/user/bar/test/file.txt".into()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar/test/file.txt"),
|
||||
Some("/foo/bar/file.txt".into()),
|
||||
);
|
||||
assert_eq!(
|
||||
path_map.logic_from_physic("/home/user/bar/file.txt"),
|
||||
Some("/foo/bar/baz/file.txt".into()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_path_fail_on_duplicates() {
|
||||
PathMap::from_vec(vec![
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar".into()),
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar/test".into()),
|
||||
]).expect_err("should fail on duplicate logic path");
|
||||
|
||||
PathMap::from_vec(vec![
|
||||
PathPair::new("/foo/bar/baz".into(), "/home/user/bar".into()),
|
||||
PathPair::new("/foo/bar".into(), "/home/user/bar".into()),
|
||||
]).expect_err("should fail on duplicate physic path");
|
||||
}
|
||||
}
|
||||
129
libbsv/src/permissions.rs
Normal file
129
libbsv/src/permissions.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use bsvfs::{Metadata};
|
||||
use cas_core::{err, Error, Result};
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Permissions {
|
||||
pub read: bool,
|
||||
pub write: bool,
|
||||
pub execute: bool,
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
pub fn from_metadata<M: Metadata>(metadata: &M) -> Result<Self> {
|
||||
let permissions = metadata.permissions();
|
||||
Ok(Self {
|
||||
read: permissions.is_read(),
|
||||
write: permissions.is_write(),
|
||||
execute: permissions.is_execute(),
|
||||
})
|
||||
}
|
||||
|
||||
pub const READ_ONLY: Self = Self { read: true, write: false, execute: false };
|
||||
pub const READ_WRITE: Self = Self { read: true, write: true, execute: false };
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Permissions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
|
||||
use std::fmt::Write;
|
||||
|
||||
f.write_char(if self.read { 'r' } else { '-' })?;
|
||||
f.write_char(if self.write { 'w' } else { '-' })?;
|
||||
f.write_char(if self.execute { 'x' } else { '-' })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Permissions {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut chars = s.chars();
|
||||
let read = parse_permission_char(&mut chars, 'r', 0)?;
|
||||
let write = parse_permission_char(&mut chars, 'w', 0)?;
|
||||
let execute = parse_permission_char(&mut chars, 'x', 0)?;
|
||||
if chars.next().is_some() {
|
||||
return err!("expected 3 characters, got {}", s.len());
|
||||
}
|
||||
|
||||
Ok(Self { read, write, execute })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_permission_char(chars: &mut std::str::Chars, bit_char: char, index: u8) -> Result<bool> {
|
||||
let c = chars.next().ok_or_else(|| Error::unknown(format!("expected 3 characters, got {}", index)))?;
|
||||
if c != bit_char && c != '-' {
|
||||
err!("expected character {} or -, got {}", bit_char, c)
|
||||
}
|
||||
else {
|
||||
Ok(c == bit_char)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_permission_display() {
|
||||
let expected = [
|
||||
"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx",
|
||||
];
|
||||
|
||||
for i in 0..8 {
|
||||
let read = (i & 0x04) != 0;
|
||||
let write = (i & 0x02) != 0;
|
||||
let execute = (i & 0x01) != 0;
|
||||
|
||||
let perm = Permissions { read, write, execute };
|
||||
let perm_str = perm.to_string();
|
||||
|
||||
assert_eq!(perm_str, expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_from_str() {
|
||||
let perms = [
|
||||
"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx",
|
||||
];
|
||||
|
||||
for i in 0..8 {
|
||||
let read = (i & 0x04) != 0;
|
||||
let write = (i & 0x02) != 0;
|
||||
let execute = (i & 0x01) != 0;
|
||||
|
||||
let perm = Permissions::from_str(perms[i]).unwrap();
|
||||
let expected = Permissions { read, write, execute };
|
||||
|
||||
assert_eq!(perm, expected);
|
||||
}
|
||||
|
||||
assert!(Permissions::from_str("rw").is_err());
|
||||
assert!(Permissions::from_str("rw--").is_err());
|
||||
assert!(Permissions::from_str("-x-").is_err());
|
||||
assert!(Permissions::from_str("123").is_err());
|
||||
}
|
||||
}
|
||||
298
libbsv/src/repository.rs
Normal file
298
libbsv/src/repository.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use toml::Value;
|
||||
|
||||
use cas_core::{Cas, err, Error, ObjectId, Result};
|
||||
use cas_simple::{SimpleCas};
|
||||
|
||||
pub use crate::tree_item::{Serialize, TreeItem};
|
||||
|
||||
|
||||
pub fn create_cas(path: Utf8PathBuf, config: Value) -> Result<Box<dyn Cas>> {
|
||||
let engine = config
|
||||
.get("cas")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas item"))?
|
||||
.get("engine")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?;
|
||||
|
||||
match engine {
|
||||
"simple" => { SimpleCas::create(path, config).map(|cas| Box::new(cas) as Box<dyn Cas>) }
|
||||
_ => { err!("unknown cas engine {}", engine) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn open_cas(path: Utf8PathBuf, config: &Value) -> Result<Box<dyn Cas>> {
|
||||
let engine = config
|
||||
.get("cas")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas item"))?
|
||||
.get("engine")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?;
|
||||
|
||||
match engine {
|
||||
"simple" => { SimpleCas::open(path).map(|cas| Box::new(cas) as Box<dyn Cas>) }
|
||||
_ => { err!("unknown cas engine {}", engine) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// pub trait FsVisitor {
|
||||
// fn accept(&self, path: &Utf8Path, metadata: &Metadata) -> bool;
|
||||
// fn handle_error(&mut self, error: Error) -> Option<Error>;
|
||||
|
||||
// fn handle_result<T>(&mut self, result: Result<T>) -> Result<T>
|
||||
// {
|
||||
// result.map_err(|error|
|
||||
// self.handle_error(error)
|
||||
// .unwrap_or(Error::Skipped)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// fn read_path_map(config: &Value) -> Result<Vec<PathPair>> {
|
||||
// if let Some(mapping) = config.get("mapping") {
|
||||
// if let Some(ref table) = mapping.as_table() {
|
||||
// table.iter()
|
||||
// .map(|(k, v)| Ok(PathPair{
|
||||
// logic: k.into(),
|
||||
// physic: v.as_str()
|
||||
// .ok_or_else(|| Error::unknown("mapping values must be strings"))?
|
||||
// .into()
|
||||
// }))
|
||||
// .collect()
|
||||
// }
|
||||
// else {
|
||||
// err!("mapping must be a table, got {}", mapping)
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// Ok(vec![])
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
pub struct Repository {
|
||||
cas: Box<dyn Cas>,
|
||||
// path_map: Vec<PathPair>,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn create(path: Utf8PathBuf, config: Value) -> Result<Self> {
|
||||
if path.exists() {
|
||||
return err!("cannot create bsv repository, path {} already exists", path);
|
||||
}
|
||||
|
||||
let cas = create_cas(path, config)?;
|
||||
|
||||
Ok(Self {
|
||||
cas,
|
||||
// path_map: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(path: Utf8PathBuf) -> Result<Self> {
|
||||
if !path.is_dir() {
|
||||
return err!("failed to open repository, path {} is not a directory", path);
|
||||
}
|
||||
|
||||
let config_file = cas_simple::utils::config_path(&path);
|
||||
let config = cas_simple::utils::read_config(&config_file)?;
|
||||
|
||||
let cas = open_cas(path, &config)?;
|
||||
// let path_map = read_path_map(&config)?;
|
||||
|
||||
Ok(Self {
|
||||
cas,
|
||||
// path_map,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cas(&self) -> &dyn Cas {
|
||||
self.cas.as_ref()
|
||||
}
|
||||
|
||||
// pub fn path_pair_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<PathPair> {
|
||||
// None
|
||||
// }
|
||||
|
||||
// pub fn path_pair_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<PathPair> {
|
||||
// None
|
||||
// }
|
||||
|
||||
pub fn oid_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Result<ObjectId> {
|
||||
err!("not implemented")
|
||||
}
|
||||
|
||||
pub fn oid_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<ObjectId> {
|
||||
err!("not implemented")
|
||||
}
|
||||
|
||||
pub fn read_tree(&self, oid: &ObjectId) -> Result<Option<Vec<TreeItem>>> {
|
||||
err!("not implemented")
|
||||
}
|
||||
|
||||
// pub fn add<P, V>(&mut self, path: P, visitor: &mut V) -> Result<ObjectId>
|
||||
// where
|
||||
// P: AsRef<Utf8Path>,
|
||||
// V: FsVisitor,
|
||||
// {
|
||||
// let path_ref = path.as_ref();
|
||||
// let metadata = std::fs::symlink_metadata(path_ref)
|
||||
// .or_else(|err| err!("failed to read file {}: {}", path_ref, err))?;
|
||||
|
||||
// if visitor.accept(path_ref, &metadata) {
|
||||
// self.add_impl(path_ref, &metadata, visitor)
|
||||
// }
|
||||
// else {
|
||||
// err!("cannot add {}: path is excluded", path_ref)
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn add_impl<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<ObjectId>
|
||||
// where
|
||||
// V: FsVisitor,
|
||||
// {
|
||||
// let file_type = metadata.file_type();
|
||||
|
||||
// if file_type.is_file() {
|
||||
// self.add_file(path, metadata)
|
||||
// }
|
||||
// else if file_type.is_dir() {
|
||||
// self.add_tree(path, metadata, visitor)
|
||||
// }
|
||||
// else if file_type.is_symlink() {
|
||||
// err!("symlink are not supported yet")
|
||||
// }
|
||||
// else {
|
||||
// err!("not implemented")
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn add_tree_item<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<TreeItem>
|
||||
// where
|
||||
// V: FsVisitor,
|
||||
// {
|
||||
// let item_name = path
|
||||
// .file_name()
|
||||
// .ok_or_else(|| Error::unknown("dir entry with no name?"))?
|
||||
// .to_str()
|
||||
// .ok_or_else(|| Error::unknown(format!("file name contains non-unicode characters: {}", path)))?;
|
||||
|
||||
// let oid = self.add(&path, visitor)?;
|
||||
|
||||
// TreeItem::from_metadata(
|
||||
// item_name,
|
||||
// &metadata,
|
||||
// oid,
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn add_file(&mut self, path: &Utf8Path, metadata: &Metadata) -> Result<ObjectId> {
|
||||
// if !metadata.is_file() {
|
||||
// return err!("expected file");
|
||||
// }
|
||||
|
||||
// let file_size = metadata.len();
|
||||
|
||||
// let mut reader = std::fs::File::open(path)
|
||||
// .or_else(|err| err!("failed to open file {}: {}", path, err))?;
|
||||
|
||||
// let mut writer = self.cas.new_writer(
|
||||
// &ObjectType::new(b"blob")?,
|
||||
// file_size
|
||||
// )?;
|
||||
|
||||
// let mut buffer = [0; BUFFER_SIZE];
|
||||
// let mut byte_count = 0u64;
|
||||
// loop {
|
||||
// use std::io::{Read, Write};
|
||||
// let count = reader.read(&mut buffer)
|
||||
// .or_else(|err| err!("error while reading {}: {}", path, err))?;
|
||||
// if count == 0 {
|
||||
// break;
|
||||
// }
|
||||
// byte_count += count as u64;
|
||||
// writer.write_all(&buffer[..count])
|
||||
// .or_else(|err| err!("error while writing blob for {}: {}", path, err))?;
|
||||
// }
|
||||
|
||||
// if byte_count == file_size {
|
||||
// writer.finalize()
|
||||
// }
|
||||
// else {
|
||||
// err!("file size changed during processing for {}", path)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn add_tree<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<ObjectId>
|
||||
// where
|
||||
// V: FsVisitor,
|
||||
// {
|
||||
// if !metadata.is_dir() {
|
||||
// return err!("expected directory");
|
||||
// }
|
||||
|
||||
// let mut tree = std::fs::read_dir(path)
|
||||
// .or_else(|err| err!("error while reading dir {}: {}", path, err))?
|
||||
// .filter_map(|entry| self.add_dir_entry(path, entry, visitor))
|
||||
// .collect::<Result<Vec<_>>>()?;
|
||||
|
||||
// tree.sort_unstable_by(|item0, item1| item0.name.cmp(&item1.name));
|
||||
|
||||
// let mut out = Vec::new();
|
||||
// tree.serialize(&mut out)?;
|
||||
// self.cas.write_object(
|
||||
// &ObjectType::new(b"tree")?,
|
||||
// &out,
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn add_dir_entry<V>(&mut self, dir_path: &Utf8Path, entry: std::io::Result<std::fs::DirEntry>, visitor: &mut V) -> Option<Result<TreeItem>>
|
||||
// where
|
||||
// V: FsVisitor
|
||||
// {
|
||||
// let (entry_path, metadata) = match visitor.handle_result(Self::dir_entry_path_metadata(dir_path, entry)) {
|
||||
// Ok(path_metadata) => { path_metadata },
|
||||
// Err(error) => { return Some(Err(error)) }
|
||||
// };
|
||||
|
||||
// if visitor.accept(&entry_path, &metadata) {
|
||||
// let result = self.add_tree_item(&entry_path, &metadata, visitor);
|
||||
// Some(visitor.handle_result(result))
|
||||
// }
|
||||
// else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn dir_entry_path_metadata(dir_path: &Utf8Path, entry: std::io::Result<std::fs::DirEntry>) -> Result<(Utf8PathBuf, Metadata)> {
|
||||
// let dir_entry = entry
|
||||
// .or_else(|err| err!("error while reading dir {}: {}", dir_path, err))?;
|
||||
|
||||
// let entry_path = dir_entry.path();
|
||||
// let metadata = dir_entry.metadata()
|
||||
// .or_else(|err| err!("failed to read metadata for dir entry {}: {}", entry_path, err))?;
|
||||
|
||||
// Ok((entry_path, metadata))
|
||||
// }
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
extern crate tempfile;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::{Read, Seek};
|
||||
use std::fs::{File, OpenOptions, create_dir, create_dir_all};
|
||||
|
||||
use tempfile::{NamedTempFile};
|
||||
|
||||
use crate::core::error::*;
|
||||
use crate::core::ObjectId;
|
||||
use crate::core::{ObjectType, OTYPE_BLOB};
|
||||
|
||||
use super::object::{
|
||||
ObjectReader, WriteAsObject,
|
||||
};
|
||||
|
||||
|
||||
const OBJECTS_DIR: &str = "objects";
|
||||
const TMP_DIR: &str = "tmp";
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SimpleDb {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
impl SimpleDb {
|
||||
pub fn new(path: &Path) -> Result<SimpleDb> {
|
||||
Ok(SimpleDb {
|
||||
path: path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
create_dir(self.objects_dir())?;
|
||||
create_dir(self.tmp_dir())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn has_object(&self, oid: &ObjectId) -> bool {
|
||||
let obj_path = self.path_from_id(oid);
|
||||
if let Ok(metadata) = std::fs::metadata(obj_path) {
|
||||
metadata.is_file()
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_object<R: Read + Seek>(&self, otype: &ObjectType, mut reader: R)
|
||||
-> Result<ObjectId> {
|
||||
let mut tmp_file = self.create_tmp_file()?;
|
||||
let oid = reader.write_as_object(&mut tmp_file, otype)?;
|
||||
let dst_file_path = self.path_from_id(&oid);
|
||||
|
||||
// TODO: Check if dst_file_path exists
|
||||
|
||||
create_dir_all(dst_file_path.parent().unwrap())?;
|
||||
match tmp_file.persist(dst_file_path) {
|
||||
Ok(file) => {
|
||||
file.sync_data()?;
|
||||
Ok(oid)
|
||||
},
|
||||
Err(err) => Err(err.error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_file_as_blob(&self, file_path: &Path) -> Result<ObjectId> {
|
||||
let file = OpenOptions::new().read(true).open(file_path)?;
|
||||
self.store_object(OTYPE_BLOB, file)
|
||||
}
|
||||
|
||||
pub fn read_object(&self, oid: &ObjectId) -> Result<ObjectReader<File>> {
|
||||
let path = self.path_from_id(oid);
|
||||
let file = OpenOptions::new().read(true).open(path)?;
|
||||
ObjectReader::new(file)
|
||||
}
|
||||
|
||||
fn objects_dir(&self) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
path.push(OBJECTS_DIR);
|
||||
path
|
||||
}
|
||||
|
||||
fn tmp_dir(&self) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
path.push(TMP_DIR);
|
||||
path
|
||||
}
|
||||
|
||||
fn path_from_id(&self, oid: &ObjectId) -> PathBuf {
|
||||
let oid_str = oid.to_string();
|
||||
let mut path = self.path.clone();
|
||||
|
||||
path.push(OBJECTS_DIR);
|
||||
path.push(oid_str.get(..4).unwrap()); // unwrap is ok here because we know oid_str is a
|
||||
path.push(oid_str.get(4..).unwrap()); // hexadecimal number (i.e. ascii only).
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
fn create_tmp_file(&self) -> Result<NamedTempFile> {
|
||||
let tmp_dir = self.tmp_dir();
|
||||
|
||||
Ok(tempfile::Builder::new().tempfile_in(tmp_dir)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::{FromStr};
|
||||
|
||||
use super::ObjectId;
|
||||
use super::SimpleDb;
|
||||
|
||||
#[test]
|
||||
fn simple_db_path_from_id() {
|
||||
let db = SimpleDb::new(std::path::Path::new(".bsv")).unwrap();
|
||||
let oid = ObjectId::from_str("0001020304050607").unwrap();
|
||||
|
||||
let path = db.path_from_id(&oid);
|
||||
assert_eq!(path, std::path::PathBuf::from(".bsv/objects/0001/020304050607"));
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
extern crate digest;
|
||||
extern crate sha2;
|
||||
extern crate flate2;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom, Write, copy, sink};
|
||||
|
||||
use digest::Digest;
|
||||
|
||||
use flate2::{
|
||||
Compression,
|
||||
write::GzEncoder,
|
||||
read::GzDecoder,
|
||||
};
|
||||
|
||||
use crate::core::error::*;
|
||||
use crate::core::ObjectId;
|
||||
use crate::core::ObjectType;
|
||||
|
||||
|
||||
pub struct ObjectWriter<W: Write, D: Digest> {
|
||||
writer: GzEncoder<W>,
|
||||
digest: D,
|
||||
size: u64,
|
||||
written_size: u64,
|
||||
}
|
||||
|
||||
|
||||
impl<W: Write, D: Digest> ObjectWriter<W, D> {
|
||||
pub fn new(writer: W, otype: &ObjectType, size: u64)
|
||||
-> Result<ObjectWriter<W, D>> {
|
||||
|
||||
let mut digest = D::new();
|
||||
|
||||
digest.update(otype);
|
||||
digest.update(&size.to_be_bytes());
|
||||
|
||||
let mut zwriter = GzEncoder::new(writer, Compression::default());
|
||||
|
||||
zwriter.write_all(otype)?;
|
||||
zwriter.write_all(&size.to_be_bytes())?;
|
||||
|
||||
Ok(ObjectWriter {
|
||||
writer: zwriter,
|
||||
digest,
|
||||
size,
|
||||
written_size: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<(ObjectId, W)> {
|
||||
self.writer.try_finish()?;
|
||||
|
||||
if self.written_size == self.size {
|
||||
Ok((
|
||||
ObjectId::new(&self.digest.finalize()),
|
||||
self.writer.finish()?,
|
||||
))
|
||||
}
|
||||
else {
|
||||
Err(ErrorKind::MismatchingObjectSize(self.written_size, self.size).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<W: Write, D: Digest> Write for ObjectWriter<W, D> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let size = self.writer.write(buf)?;
|
||||
self.digest.update(&buf[..size]);
|
||||
self.written_size += size as u64;
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct ObjectReader<R: Read> {
|
||||
otype: ObjectType,
|
||||
size: u64,
|
||||
reader: GzDecoder<R>,
|
||||
}
|
||||
|
||||
|
||||
impl<R: Read> ObjectReader<R> {
|
||||
pub fn new(reader: R)
|
||||
-> Result<ObjectReader<R>> {
|
||||
|
||||
let mut zreader = GzDecoder::new(reader);
|
||||
|
||||
let mut buffer = [0u8; 12];
|
||||
zreader.read_exact(&mut buffer)?;
|
||||
|
||||
let otype = {
|
||||
let mut otype = [0; 4];
|
||||
otype.copy_from_slice(&buffer[0..4]);
|
||||
otype
|
||||
};
|
||||
let size = {
|
||||
let mut size_bytes = [0; 8];
|
||||
size_bytes.copy_from_slice(&buffer[4..12]);
|
||||
u64::from_be_bytes(size_bytes)
|
||||
};
|
||||
|
||||
Ok(ObjectReader {
|
||||
otype,
|
||||
size,
|
||||
reader: zreader,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn object_type(&self) -> ObjectType {
|
||||
self.otype
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn close(self) -> Result<R> {
|
||||
Ok(self.reader.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<R: Read> Read for ObjectReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.reader.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn write_object<R, W>(reader: &mut R, writer: W,
|
||||
otype: &ObjectType, size: u64)
|
||||
-> Result<(ObjectId, W)> where
|
||||
R: Read,
|
||||
W: Write {
|
||||
let mut owriter: ObjectWriter<W, sha2::Sha224>
|
||||
= ObjectWriter::new(writer, otype, size)?;
|
||||
|
||||
copy(reader, &mut owriter)?;
|
||||
|
||||
owriter.finish()
|
||||
}
|
||||
|
||||
|
||||
pub trait WriteAsObject {
|
||||
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId>;
|
||||
|
||||
fn object_id(&mut self, otype: &ObjectType) -> Result<ObjectId> {
|
||||
self.write_as_object(sink(), otype)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<T: Read + Seek> WriteAsObject for T {
|
||||
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId> {
|
||||
let start = self.seek(SeekFrom::Current(0))?;
|
||||
let end = self.seek(SeekFrom::End(0))?;
|
||||
self.seek(SeekFrom::Start(start))?;
|
||||
|
||||
let (oid, _) = write_object(self, writer, otype, end - start)?;
|
||||
Ok(oid)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::{FromStr};
|
||||
|
||||
use crate::core::OTYPE_BLOB;
|
||||
|
||||
use super::*;
|
||||
|
||||
const PAYLOAD: &[u8; 12] = b"Hello World!";
|
||||
const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083";
|
||||
|
||||
#[test]
|
||||
fn object_read_write() -> Result<()> {
|
||||
use std::io::{Cursor, Seek, SeekFrom};
|
||||
|
||||
let mut source = Cursor::new(PAYLOAD);
|
||||
let mut fake_file = Cursor::new(vec![]);
|
||||
let mut output = vec![];
|
||||
|
||||
let (oid, _) = write_object(&mut source, &mut fake_file,
|
||||
OTYPE_BLOB, PAYLOAD.len() as u64)?;
|
||||
|
||||
assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?);
|
||||
|
||||
fake_file.seek(SeekFrom::Start(0))?;
|
||||
let mut reader = ObjectReader::new(fake_file)?;
|
||||
let read_size = reader.read_to_end(&mut output)?;
|
||||
|
||||
assert_eq!(reader.object_type(), *OTYPE_BLOB);
|
||||
assert_eq!(reader.size(), PAYLOAD.len() as u64);
|
||||
assert_eq!(read_size, PAYLOAD.len());
|
||||
assert_eq!(output, PAYLOAD);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_write_invalid_size() {
|
||||
let mut source = PAYLOAD.as_ref();
|
||||
let fake_file = vec![];
|
||||
|
||||
let result = write_object(&mut source, fake_file, OTYPE_BLOB, 13);
|
||||
|
||||
match result {
|
||||
Err(Error(ErrorKind::MismatchingObjectSize(actual, expected), _))
|
||||
if actual == 12 && expected == 13 => (),
|
||||
Err(error) => panic!("Unexpected error: {:?}", error),
|
||||
Ok(_) => panic!("Unexpected success"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_as_object() -> Result<()> {
|
||||
use std::io::{Cursor, sink};
|
||||
|
||||
let mut source = Cursor::new(PAYLOAD);
|
||||
let oid = source.write_as_object(sink(), OTYPE_BLOB)?;
|
||||
|
||||
assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
243
libbsv/src/tree_item.rs
Normal file
243
libbsv/src/tree_item.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
use bsvfs::{Metadata, Permissions};
|
||||
use cas_core::{err, Error, ObjectId, ObjectType, Result};
|
||||
|
||||
|
||||
pub trait Serialize {
|
||||
fn serialize<W: Write>(&self, out: &mut W) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait Deserialize {
|
||||
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self>
|
||||
where Self: Sized;
|
||||
|
||||
fn deserialize<R: BufRead>(stream: &mut R) -> Result<Self>
|
||||
where Self: Sized
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
Self::deserialize_with_buf(stream, &mut buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TreeItem {
|
||||
pub name: String,
|
||||
pub otype: ObjectType,
|
||||
pub size: u64,
|
||||
pub created: SystemTime,
|
||||
pub modified: SystemTime,
|
||||
pub permissions: Permissions,
|
||||
pub oid: ObjectId,
|
||||
}
|
||||
|
||||
impl TreeItem {
|
||||
// pub fn from_metadata(name: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result<Self> {
|
||||
pub fn from_metadata<M: Metadata>(name: String, metadata: &M, oid: ObjectId) -> Result<Self> {
|
||||
let otype = otype_from_metadata(metadata)?;
|
||||
let permissions = metadata.permissions();
|
||||
|
||||
Ok(Self {
|
||||
name: name,
|
||||
otype,
|
||||
size: metadata.len(),
|
||||
created: metadata.created().unwrap_or(UNIX_EPOCH),
|
||||
modified: metadata.modified().unwrap_or(UNIX_EPOCH),
|
||||
permissions,
|
||||
oid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for TreeItem {
|
||||
fn serialize<W: Write>(&self, out: &mut W) -> Result<()> {
|
||||
// TODO: Check that name do not contain invalid characters
|
||||
writeln!(out, "{}\t{}\t{}\t{}\t{}\t{}\t{}/",
|
||||
self.oid,
|
||||
self.otype,
|
||||
self.size,
|
||||
self.permissions,
|
||||
self.created.duration_since(UNIX_EPOCH)
|
||||
.or_else(|err| err!("failed to serialize creation time while serializing tree item: {}", err))?
|
||||
.as_millis(),
|
||||
self.modified.duration_since(UNIX_EPOCH)
|
||||
.or_else(|err| err!("failed to serialize creation time while serializing tree item: {}", err))?
|
||||
.as_millis(),
|
||||
self.name,
|
||||
).or_else(|err| err!("error while serializing item: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for TreeItem {
|
||||
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self> {
|
||||
let oid: ObjectId = read_field_parse(stream, buf, "object ID", b'\t')?;
|
||||
read_field(stream, buf, "object type", b'\t')?;
|
||||
let otype = ObjectType::new(buf)?;
|
||||
let size: u64 = read_field_parse(stream, buf, "object type", b'\t')?;
|
||||
let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?;
|
||||
let created = UNIX_EPOCH + Duration::from_millis(
|
||||
read_field_parse(stream, buf, "creation date", b'\t')?
|
||||
);
|
||||
let modified = UNIX_EPOCH + Duration::from_millis(
|
||||
read_field_parse(stream, buf, "modification date", b'\t')?
|
||||
);
|
||||
let name = read_field_str(stream, buf, "name", b'/')?
|
||||
.to_string();
|
||||
|
||||
stream.read_exact(&mut buf[..1])
|
||||
.or_else(|err| err!("failed to read new line character: {}", err))?;
|
||||
if buf[0] != b'\n' {
|
||||
err!("expected new line character, got {:x}", buf[0])
|
||||
}
|
||||
else {
|
||||
Ok(TreeItem {
|
||||
name,
|
||||
otype,
|
||||
size,
|
||||
created,
|
||||
modified,
|
||||
permissions,
|
||||
oid,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for [TreeItem] {
|
||||
fn serialize<W: Write>(&self, out: &mut W) -> Result<()> {
|
||||
// assert!(self.is_sorted_by_key(|item| &item.name));
|
||||
|
||||
for item in self.iter() {
|
||||
item.serialize(out)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn otype_from_metadata<M: Metadata>(metadata: &M) -> Result<ObjectType> {
|
||||
let file_type = metadata.file_type();
|
||||
|
||||
if file_type.is_file() {
|
||||
Ok(ObjectType::new(b"blob")?)
|
||||
}
|
||||
else if file_type.is_dir() {
|
||||
Ok(ObjectType::new(b"tree")?)
|
||||
}
|
||||
else if file_type.is_symlink() {
|
||||
Ok(ObjectType::new(b"link")?)
|
||||
}
|
||||
else {
|
||||
err!("unsupported file type, must be file, dir or symlink")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn read_field<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<()> {
|
||||
buf.clear();
|
||||
stream.read_until(byte, buf)
|
||||
.or_else(|err| err!("failed to read TreeItem {}: {}", field_name, err))?;
|
||||
buf.pop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec<u8>, field_name: &str, byte: u8) -> Result<&'a str> {
|
||||
read_field(stream, buf, field_name, byte)?;
|
||||
std::str::from_utf8(buf)
|
||||
.or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err))
|
||||
}
|
||||
|
||||
fn read_field_parse<R, I>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<I>
|
||||
where
|
||||
R: BufRead,
|
||||
I: std::str::FromStr,
|
||||
<I as std::str::FromStr>::Err: std::fmt::Display
|
||||
{
|
||||
let int_str = read_field_str(stream, buf, field_name, byte)?;
|
||||
I::from_str(int_str)
|
||||
.or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_tree_item() {
|
||||
let item = TreeItem {
|
||||
name: "Test $¢ह€한".to_string(),
|
||||
otype: ObjectType::new(b"test").unwrap(),
|
||||
size: 42,
|
||||
created: UNIX_EPOCH + Duration::from_secs(1234),
|
||||
modified: UNIX_EPOCH + Duration::from_secs(5678),
|
||||
permissions: Permissions::new(true, false, true),
|
||||
oid: ObjectId::from_str("0123456789abcdef").unwrap(),
|
||||
};
|
||||
let expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes();
|
||||
|
||||
let mut result = Vec::new();
|
||||
item.serialize(&mut result).unwrap();
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_tree_item() {
|
||||
use std::io::Cursor;
|
||||
|
||||
let item_bytes = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes();
|
||||
let mut item_cursor = Cursor::new(item_bytes);
|
||||
let expected = TreeItem {
|
||||
name: "Test $¢ह€한".to_string(),
|
||||
otype: ObjectType::new(b"test").unwrap(),
|
||||
size: 42,
|
||||
created: UNIX_EPOCH + Duration::from_secs(1234),
|
||||
modified: UNIX_EPOCH + Duration::from_secs(5678),
|
||||
permissions: Permissions::new(true, false, true),
|
||||
oid: ObjectId::from_str("0123456789abcdef").unwrap(),
|
||||
};
|
||||
|
||||
let item = TreeItem::deserialize(&mut item_cursor).unwrap();
|
||||
|
||||
assert_eq!(item, expected);
|
||||
|
||||
assert!(TreeItem::deserialize(&mut Cursor::new(
|
||||
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/".as_bytes()
|
||||
)).is_err());
|
||||
assert!(TreeItem::deserialize(&mut Cursor::new(
|
||||
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/bar/\n".as_bytes()
|
||||
)).is_err());
|
||||
assert!(TreeItem::deserialize(&mut Cursor::new(
|
||||
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes()
|
||||
)).is_err());
|
||||
assert!(TreeItem::deserialize(&mut Cursor::new(
|
||||
"0123456789abcdef\ttest\t42\tr-x\t5678000\tTest $¢ह€한\t\n".as_bytes()
|
||||
)).is_err());
|
||||
assert!(TreeItem::deserialize(&mut Cursor::new(
|
||||
"0123456789abcdef\ttest\tab\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes()
|
||||
)).is_err());
|
||||
}
|
||||
}
|
||||
130
libbsv/src/tree_walker.rs
Normal file
130
libbsv/src/tree_walker.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::iter::Peekable;
|
||||
// use std::fs::{DirEntry, Metadata, ReadDir, read_dir};
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use camino::{Utf8Path};
|
||||
|
||||
use bsvfs::{DirEntry, FileSystem};
|
||||
use cas_core::{ObjectId, Result};
|
||||
|
||||
use crate::{TreeItem};
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
Add,
|
||||
Update,
|
||||
Remove,
|
||||
Skip,
|
||||
// Ignore,
|
||||
}
|
||||
|
||||
pub struct TreeWalker {
|
||||
dir_it: Peekable<IntoIter<Result<TreeItem>>>,
|
||||
prev_tree_it: Peekable<IntoIter<TreeItem>>,
|
||||
}
|
||||
|
||||
|
||||
impl TreeWalker {
|
||||
pub fn new<Fs: FileSystem, P: AsRef<Utf8Path>>(fs: Fs, path: P, prev_tree: Vec<TreeItem>) -> Result<Self> {
|
||||
let dir_entries = fs.read_dir(path.as_ref().to_path_buf())?
|
||||
.map(|res| res.map_err(|err| err.into()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mut dir_items: Vec<_> = dir_entries.into_iter()
|
||||
.map(|dir_entry| {
|
||||
let file_name = dir_entry.file_name()?;
|
||||
let metadata = dir_entry.metadata()?;
|
||||
Ok(TreeItem::from_metadata(
|
||||
file_name,
|
||||
&metadata,
|
||||
ObjectId::default()
|
||||
)?
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
dir_items.sort_unstable_by_key(|result| {
|
||||
match result {
|
||||
Ok(entry) => entry.name.clone(),
|
||||
Err(_) => String::default(),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
dir_it: dir_items.into_iter().peekable(),
|
||||
prev_tree_it: prev_tree.into_iter().peekable(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TreeWalker {
|
||||
type Item = Result<(Action, TreeItem)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.dir_it.peek(), self.prev_tree_it.peek()) {
|
||||
(Some(Err(_)), _) => {
|
||||
Some(Err(self.dir_it.next().unwrap().unwrap_err()))
|
||||
}
|
||||
(Some(Ok(curr_item)), Some(prev_item)) => {
|
||||
if curr_item.name == prev_item.name {
|
||||
let action =
|
||||
if curr_item.modified != prev_item.modified {
|
||||
Action::Update
|
||||
}
|
||||
else {
|
||||
Action::Skip
|
||||
};
|
||||
self.prev_tree_it.next();
|
||||
Some(Ok((action, self.dir_it.next().unwrap().unwrap())))
|
||||
}
|
||||
else if curr_item.name < prev_item.name {
|
||||
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap())))
|
||||
}
|
||||
else {
|
||||
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap())))
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap())))
|
||||
},
|
||||
(None, Some(_)) => {
|
||||
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap())))
|
||||
},
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bsvfs::SysFileSystem;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tree_walker() {
|
||||
for item in TreeWalker::new(SysFileSystem::new(), "/home/draklaw/tmp", vec![]).unwrap() {
|
||||
match item {
|
||||
Ok((action, tree_item)) => println!("{:?} {:?}", action, tree_item.name),
|
||||
Err(err) => println!("error while iterating directory: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// This file is part of bsv.
|
||||
//
|
||||
// bsv is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the Affero GNU General Public License
|
||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
extern crate tempfile;
|
||||
extern crate libbsv;
|
||||
|
||||
|
||||
use std::str::{FromStr};
|
||||
use std::path::{PathBuf};
|
||||
use std::io::{Cursor, Read};
|
||||
use std::fs::{create_dir_all, write};
|
||||
|
||||
use tempfile::{TempDir};
|
||||
|
||||
use libbsv::{
|
||||
Result,
|
||||
ObjectId,
|
||||
OTYPE_BLOB,
|
||||
simple_db::{
|
||||
SimpleDb,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const PAYLOAD: &[u8; 12] = b"Hello World!";
|
||||
const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083";
|
||||
|
||||
|
||||
#[test]
|
||||
fn simple_db_has_object() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let db = SimpleDb::new(temp_dir.path()).unwrap();
|
||||
db.setup().unwrap();
|
||||
|
||||
let oid = ObjectId::from_str("0001020304050607").unwrap();
|
||||
|
||||
assert!(!db.has_object(&oid));
|
||||
|
||||
let mut object_path: PathBuf = temp_dir.path().into();
|
||||
object_path.push("objects");
|
||||
object_path.push("0001");
|
||||
|
||||
create_dir_all(&object_path).unwrap();
|
||||
|
||||
object_path.push("020304050607");
|
||||
|
||||
write(object_path, "test").unwrap();
|
||||
|
||||
assert!(db.has_object(&oid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_db_store_read() -> Result<()> {
|
||||
let expected_oid = ObjectId::from_str(PAYLOAD_OID)?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let db = SimpleDb::new(temp_dir.path())?;
|
||||
db.setup()?;
|
||||
|
||||
assert!(!db.has_object(&expected_oid));
|
||||
|
||||
let oid = db.store_object(OTYPE_BLOB, Cursor::new(PAYLOAD))?;
|
||||
|
||||
assert!(db.has_object(&expected_oid));
|
||||
assert_eq!(oid, expected_oid);
|
||||
|
||||
let mut reader = db.read_object(&oid)?;
|
||||
|
||||
assert_eq!(reader.object_type(), *OTYPE_BLOB);
|
||||
assert_eq!(reader.size(), PAYLOAD.len() as u64);
|
||||
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
|
||||
assert_eq!(data, *PAYLOAD);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user