├── .dockerignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bench ├── Dockerfile ├── bench.cat ├── bench.catfs ├── bench.catfs_over_sshfs ├── bench.catfs_vs_sshfs.data ├── bench.catfs_vs_sshfs.png ├── bench.catsshfs ├── bench.data ├── bench.local ├── bench.png ├── bench.sh ├── bench.sshfs ├── bench_format.py ├── bench_graph.gnuplot └── run_bench.sh ├── src ├── catfs │ ├── dir.rs │ ├── error.rs │ ├── file.rs │ ├── flags.rs │ ├── inode.rs │ ├── mod.rs │ ├── rlibc.rs │ ├── substr.rs │ └── tests.rs ├── evicter │ ├── dir_walker.rs │ └── mod.rs ├── flags.rs ├── lib.rs ├── main.rs └── pcatfs │ └── mod.rs ├── tests ├── integration_tests.rs ├── resources │ ├── dir1 │ │ ├── file1 │ │ └── file2 │ ├── file1 │ ├── file2 │ └── file3 └── test_suite │ └── mod.rs └── validate_cache.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build_and_test: 13 | name: Rust project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: apt-get 17 | run: sudo apt-get install -y fuse libfuse-dev 18 | - uses: actions/checkout@v2 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | target/ 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "0.6.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "ansi_term" 31 | version = "0.12.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 34 | dependencies = [ 35 | "winapi", 36 | ] 37 | 38 | [[package]] 39 | name = "atty" 40 | version = "0.2.14" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 43 | dependencies = [ 44 | "hermit-abi 0.1.19", 45 | "libc", 46 | "winapi", 47 | ] 48 | 49 | [[package]] 50 | name = "backtrace" 51 | version = "0.3.69" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 54 | dependencies = [ 55 | "addr2line", 56 | "cc", 57 | "cfg-if 1.0.0", 58 | "libc", 59 | "miniz_oxide", 60 | "object", 61 | "rustc-demangle", 62 | ] 63 | 64 | [[package]] 65 | name = "bit-set" 66 | version = "0.4.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" 69 | dependencies = [ 70 | "bit-vec", 71 | ] 72 | 73 | [[package]] 74 | name = "bit-vec" 75 | version = "0.4.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "02b4ff8b16e6076c3e14220b39fbc1fabb6737522281a388998046859400895f" 78 | 79 | [[package]] 80 | name = "bitflags" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 84 | 85 | [[package]] 86 | name = "block-buffer" 87 | version = "0.10.4" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 90 | dependencies = [ 91 | "generic-array", 92 | ] 93 | 94 | [[package]] 95 | name = "catfs" 96 | version = "0.9.0" 97 | dependencies = [ 98 | "backtrace", 99 | "chan-signal", 100 | "clap", 101 | "daemonize", 102 | "env_logger", 103 | "fd", 104 | "fuse", 105 | "generic-array", 106 | "itertools", 107 | "libc", 108 | "log 0.3.9", 109 | "rand 0.3.23", 110 | "sha2", 111 | "syslog", 112 | "threadpool", 113 | "time", 114 | "twox-hash", 115 | "xattr", 116 | ] 117 | 118 | [[package]] 119 | name = "cc" 120 | version = "1.0.83" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 123 | dependencies = [ 124 | "libc", 125 | ] 126 | 127 | [[package]] 128 | name = "cfg-if" 129 | version = "0.1.10" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "chan" 141 | version = "0.1.23" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "d14956a3dae065ffaa0d92ece848ab4ced88d32361e7fdfbfd653a5c454a1ed8" 144 | dependencies = [ 145 | "rand 0.3.23", 146 | ] 147 | 148 | [[package]] 149 | name = "chan-signal" 150 | version = "0.2.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "0f3bb6c3bc387004ad914f0c5b7f33ace8bf7604bbec35f228b1a017f52cd3a0" 153 | dependencies = [ 154 | "bit-set", 155 | "chan", 156 | "lazy_static 0.2.11", 157 | "libc", 158 | ] 159 | 160 | [[package]] 161 | name = "clap" 162 | version = "2.34.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 165 | dependencies = [ 166 | "ansi_term", 167 | "atty", 168 | "bitflags", 169 | "strsim", 170 | "textwrap", 171 | "unicode-width", 172 | "vec_map", 173 | ] 174 | 175 | [[package]] 176 | name = "cpufeatures" 177 | version = "0.2.9" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 180 | dependencies = [ 181 | "libc", 182 | ] 183 | 184 | [[package]] 185 | name = "crypto-common" 186 | version = "0.1.6" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 189 | dependencies = [ 190 | "generic-array", 191 | "typenum", 192 | ] 193 | 194 | [[package]] 195 | name = "daemonize" 196 | version = "0.2.3" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "0239832c1b4ca406d5ec73728cf4c7336d25cf85dd32db9e047e9e706ee0e935" 199 | dependencies = [ 200 | "libc", 201 | ] 202 | 203 | [[package]] 204 | name = "digest" 205 | version = "0.10.7" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 208 | dependencies = [ 209 | "block-buffer", 210 | "crypto-common", 211 | ] 212 | 213 | [[package]] 214 | name = "either" 215 | version = "1.9.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 218 | 219 | [[package]] 220 | name = "env_logger" 221 | version = "0.4.3" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3ddf21e73e016298f5cb37d6ef8e8da8e39f91f9ec8b0df44b7deb16a9f8cd5b" 224 | dependencies = [ 225 | "log 0.3.9", 226 | "regex", 227 | ] 228 | 229 | [[package]] 230 | name = "fd" 231 | version = "0.2.3" 232 | source = "git+https://github.com/stemjail/fd-rs.git?rev=3bc3e3587f8904cce8bf29163a2021c2f5906557#3bc3e3587f8904cce8bf29163a2021c2f5906557" 233 | dependencies = [ 234 | "libc", 235 | ] 236 | 237 | [[package]] 238 | name = "fuchsia-cprng" 239 | version = "0.1.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 242 | 243 | [[package]] 244 | name = "fuse" 245 | version = "0.3.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "80e57070510966bfef93662a81cb8aa2b1c7db0964354fa9921434f04b9e8660" 248 | dependencies = [ 249 | "libc", 250 | "log 0.3.9", 251 | "pkg-config", 252 | "thread-scoped", 253 | "time", 254 | ] 255 | 256 | [[package]] 257 | name = "generic-array" 258 | version = "0.14.7" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 261 | dependencies = [ 262 | "typenum", 263 | "version_check", 264 | ] 265 | 266 | [[package]] 267 | name = "getrandom" 268 | version = "0.2.10" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 271 | dependencies = [ 272 | "cfg-if 1.0.0", 273 | "libc", 274 | "wasi 0.11.0+wasi-snapshot-preview1", 275 | ] 276 | 277 | [[package]] 278 | name = "gimli" 279 | version = "0.28.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 282 | 283 | [[package]] 284 | name = "hermit-abi" 285 | version = "0.1.19" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 288 | dependencies = [ 289 | "libc", 290 | ] 291 | 292 | [[package]] 293 | name = "hermit-abi" 294 | version = "0.3.3" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 297 | 298 | [[package]] 299 | name = "itertools" 300 | version = "0.6.5" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "d3f2be4da1690a039e9ae5fd575f706a63ad5a2120f161b1d653c9da3930dd21" 303 | dependencies = [ 304 | "either", 305 | ] 306 | 307 | [[package]] 308 | name = "lazy_static" 309 | version = "0.2.11" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 312 | 313 | [[package]] 314 | name = "lazy_static" 315 | version = "1.4.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 318 | 319 | [[package]] 320 | name = "libc" 321 | version = "0.2.149" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" 324 | 325 | [[package]] 326 | name = "log" 327 | version = "0.3.9" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 330 | dependencies = [ 331 | "log 0.4.20", 332 | ] 333 | 334 | [[package]] 335 | name = "log" 336 | version = "0.4.20" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 339 | 340 | [[package]] 341 | name = "memchr" 342 | version = "2.6.4" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 345 | 346 | [[package]] 347 | name = "miniz_oxide" 348 | version = "0.7.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 351 | dependencies = [ 352 | "adler", 353 | ] 354 | 355 | [[package]] 356 | name = "num_cpus" 357 | version = "1.16.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 360 | dependencies = [ 361 | "hermit-abi 0.3.3", 362 | "libc", 363 | ] 364 | 365 | [[package]] 366 | name = "object" 367 | version = "0.32.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 370 | dependencies = [ 371 | "memchr", 372 | ] 373 | 374 | [[package]] 375 | name = "pkg-config" 376 | version = "0.3.27" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 379 | 380 | [[package]] 381 | name = "ppv-lite86" 382 | version = "0.2.17" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 385 | 386 | [[package]] 387 | name = "rand" 388 | version = "0.3.23" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 391 | dependencies = [ 392 | "libc", 393 | "rand 0.4.6", 394 | ] 395 | 396 | [[package]] 397 | name = "rand" 398 | version = "0.4.6" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 401 | dependencies = [ 402 | "fuchsia-cprng", 403 | "libc", 404 | "rand_core 0.3.1", 405 | "rdrand", 406 | "winapi", 407 | ] 408 | 409 | [[package]] 410 | name = "rand" 411 | version = "0.8.5" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 414 | dependencies = [ 415 | "libc", 416 | "rand_chacha", 417 | "rand_core 0.6.4", 418 | ] 419 | 420 | [[package]] 421 | name = "rand_chacha" 422 | version = "0.3.1" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 425 | dependencies = [ 426 | "ppv-lite86", 427 | "rand_core 0.6.4", 428 | ] 429 | 430 | [[package]] 431 | name = "rand_core" 432 | version = "0.3.1" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 435 | dependencies = [ 436 | "rand_core 0.4.2", 437 | ] 438 | 439 | [[package]] 440 | name = "rand_core" 441 | version = "0.4.2" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 444 | 445 | [[package]] 446 | name = "rand_core" 447 | version = "0.6.4" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 450 | dependencies = [ 451 | "getrandom", 452 | ] 453 | 454 | [[package]] 455 | name = "rdrand" 456 | version = "0.4.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 459 | dependencies = [ 460 | "rand_core 0.3.1", 461 | ] 462 | 463 | [[package]] 464 | name = "regex" 465 | version = "0.2.11" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" 468 | dependencies = [ 469 | "aho-corasick", 470 | "memchr", 471 | "regex-syntax", 472 | "thread_local", 473 | "utf8-ranges", 474 | ] 475 | 476 | [[package]] 477 | name = "regex-syntax" 478 | version = "0.5.6" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" 481 | dependencies = [ 482 | "ucd-util", 483 | ] 484 | 485 | [[package]] 486 | name = "rustc-demangle" 487 | version = "0.1.23" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 490 | 491 | [[package]] 492 | name = "sha2" 493 | version = "0.10.8" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 496 | dependencies = [ 497 | "cfg-if 1.0.0", 498 | "cpufeatures", 499 | "digest", 500 | ] 501 | 502 | [[package]] 503 | name = "static_assertions" 504 | version = "1.1.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 507 | 508 | [[package]] 509 | name = "strsim" 510 | version = "0.8.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 513 | 514 | [[package]] 515 | name = "syslog" 516 | version = "3.3.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "bbc9b0acde4f7c05fdc1cfb05239b8a53a66815dd86c67fee5aa9bfac5b4ed42" 519 | dependencies = [ 520 | "libc", 521 | "log 0.3.9", 522 | "time", 523 | "unix_socket", 524 | ] 525 | 526 | [[package]] 527 | name = "textwrap" 528 | version = "0.11.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 531 | dependencies = [ 532 | "unicode-width", 533 | ] 534 | 535 | [[package]] 536 | name = "thread-scoped" 537 | version = "1.0.2" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "bcbb6aa301e5d3b0b5ef639c9a9c7e2f1c944f177b460c04dc24c69b1fa2bd99" 540 | 541 | [[package]] 542 | name = "thread_local" 543 | version = "0.3.6" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" 546 | dependencies = [ 547 | "lazy_static 1.4.0", 548 | ] 549 | 550 | [[package]] 551 | name = "threadpool" 552 | version = "1.8.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 555 | dependencies = [ 556 | "num_cpus", 557 | ] 558 | 559 | [[package]] 560 | name = "time" 561 | version = "0.1.45" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 564 | dependencies = [ 565 | "libc", 566 | "wasi 0.10.0+wasi-snapshot-preview1", 567 | "winapi", 568 | ] 569 | 570 | [[package]] 571 | name = "twox-hash" 572 | version = "1.6.3" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" 575 | dependencies = [ 576 | "cfg-if 1.0.0", 577 | "rand 0.8.5", 578 | "static_assertions", 579 | ] 580 | 581 | [[package]] 582 | name = "typenum" 583 | version = "1.17.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 586 | 587 | [[package]] 588 | name = "ucd-util" 589 | version = "0.1.10" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" 592 | 593 | [[package]] 594 | name = "unicode-width" 595 | version = "0.1.11" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 598 | 599 | [[package]] 600 | name = "unix_socket" 601 | version = "0.5.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" 604 | dependencies = [ 605 | "cfg-if 0.1.10", 606 | "libc", 607 | ] 608 | 609 | [[package]] 610 | name = "utf8-ranges" 611 | version = "1.0.5" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" 614 | 615 | [[package]] 616 | name = "vec_map" 617 | version = "0.8.2" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 620 | 621 | [[package]] 622 | name = "version_check" 623 | version = "0.9.4" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 626 | 627 | [[package]] 628 | name = "wasi" 629 | version = "0.10.0+wasi-snapshot-preview1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 632 | 633 | [[package]] 634 | name = "wasi" 635 | version = "0.11.0+wasi-snapshot-preview1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 638 | 639 | [[package]] 640 | name = "winapi" 641 | version = "0.3.9" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 644 | dependencies = [ 645 | "winapi-i686-pc-windows-gnu", 646 | "winapi-x86_64-pc-windows-gnu", 647 | ] 648 | 649 | [[package]] 650 | name = "winapi-i686-pc-windows-gnu" 651 | version = "0.4.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 654 | 655 | [[package]] 656 | name = "winapi-x86_64-pc-windows-gnu" 657 | version = "0.4.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 660 | 661 | [[package]] 662 | name = "xattr" 663 | version = "0.2.3" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" 666 | dependencies = [ 667 | "libc", 668 | ] 669 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "catfs" 3 | version = "0.9.0" 4 | authors = ["Ka-Hing Cheung "] 5 | description = "Cache AnyThing filesystem" 6 | keywords = ["filesystem", "fuse", "cache"] 7 | categories = ["caching", "filesystem"] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/kahing/catfs" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | backtrace = "0.3" 14 | chan-signal = "0.2" 15 | clap = "2.29.0" 16 | daemonize = "0.2" 17 | env_logger = "0.4" 18 | fd = { git = "https://github.com/stemjail/fd-rs.git", rev = "3bc3e3587f8904cce8bf29163a2021c2f5906557" } 19 | fuse = "0.3.0" 20 | generic-array = "0.14.7" 21 | itertools = "0.6" 22 | log = "0.3" 23 | libc = "0.2.66" 24 | rand = "0.3" 25 | sha2 = "0.10.8" 26 | syslog = "3.3" 27 | threadpool = "1.4" 28 | time = "0.1" 29 | twox-hash = "1.5.0" 30 | xattr = "0.2" 31 | 32 | [lib] 33 | test = false 34 | harness = false 35 | 36 | [badges] 37 | travis-ci = { repository = "kahing/catfs", branch = "master" } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Catfs is a caching filesystem written in Rust. 2 | 3 | [![Build Status](https://travis-ci.org/kahing/catfs.svg?branch=master)](https://travis-ci.org/kahing/catfs) 4 | [![Crates.io](https://img.shields.io/crates/v/catfs.svg)](https://crates.io/crates/catfs) 5 | [![Crates.io Downloads](https://img.shields.io/crates/d/catfs.svg)](https://crates.io/crates/catfs) 6 | [![GitHub All Releases](https://img.shields.io/github/downloads/kahing/catfs/total.svg)](https://github.com/kahing/catfs/releases/) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/CatfsFuse.svg?style=social&label=Follow)](https://twitter.com/CatfsFuse) 8 | 9 | 10 | # Overview 11 | 12 | Catfs allows you to have cached access to another (possibly remote) 13 | filesystem. Caching semantic is read-ahead and write-through (see 14 | [Current Status](#current-status)). Currently it only provides a data 15 | cache and all metadata operations hit the source filesystem. 16 | 17 | Catfs is ALPHA software. Don't use this if you value your data. 18 | 19 | # Installation 20 | 21 | * On Linux, install via 22 | [pre-built binaries](https://github.com/kahing/catfs/releases/). You 23 | may also need to install fuse-utils first. 24 | 25 | * Or build from source which requires [Cargo](http://doc.crates.io/). 26 | 27 | ```ShellSession 28 | :~/catfs$ cargo install catfs 29 | $ # optimized binary now in $HOME/.cargo/bin/catfs 30 | ``` 31 | 32 | # Usage 33 | 34 | Catfs requires extended attributes (xattr) to be enabled on the 35 | filesystem where files are cached to. Typically this means you need to 36 | have `user_xattr` mount option turned on. 37 | 38 | ```ShellSession 39 | $ catfs 40 | ``` 41 | 42 | Catfs will expose files in `` under ``, and cache 43 | them to `` as they are accessed. You can use `--free` to control 44 | how much free space ``'s filesystem has. 45 | 46 | To mount catfs on startup, add this to `/etc/fstab`: 47 | 48 | ``` 49 | catfs#/src/dir#/cache/dir /mnt/point fuse allow_other,--uid=1001,--gid=1001,--free=1% 0 0 50 | ``` 51 | 52 | # Benchmark 53 | 54 | Compare using catfs to cache sshfs vs sshfs only. Topology is 55 | laptop - 802.11n - router - 1Gbps wired - desktop. Laptop has SSD 56 | whereas desktop has spinning rust. 57 | 58 | ![Benchmark result](/bench/bench.catfs_vs_sshfs.png?raw=true "Benchmark") 59 | 60 | Compare running catfs with two local directories on the same 61 | filesystem with direct access. This is not a realistic use case but 62 | should give you an idea of the worst case slowdown. 63 | 64 | ![Benchmark result](/bench/bench.png?raw=true "Benchmark") 65 | 66 | Write is twice as slow as expected since we are writing twice the 67 | amount. 68 | 69 | 70 | To run the benchmark, do: 71 | 72 | ```ShellSession 73 | $ sudo docker run -e SSHFS_SERVER=user@host --rm --privileged --net=host -v $PWD/target:/root/catfs/target kahing/catfs-bench 74 | # result is written to $PWD/target 75 | ``` 76 | 77 | The docker container will need to be able to ssh to `user@host`. Typically I arrange that by mounting the ssh socket from the host 78 | 79 | ```ShellSession 80 | $ sudo docker run -e SSHFS_OPTS="-o ControlPath=/root/.ssh/sockets/%r@%h_%p -o ControlMaster=auto -o StrictHostKeyChecking=no -o Cipher=arcfour user@host:/tmp" -e SSHFS_SERVER=user@host --rm --privileged --net=host -v $HOME/.ssh/sockets:/root/.ssh/sockets -v $PWD/target:/root/catfs/target kahing/catfs-bench 81 | ``` 82 | 83 | # License 84 | 85 | Copyright (C) 2017 Ka-Hing Cheung 86 | 87 | Licensed under the Apache License, Version 2.0 88 | 89 | # Current Status 90 | 91 | Catfs is ALPHA software. Don't use this if you value your data. 92 | 93 | Entire file is cached if it's open for read, even if nothing is 94 | actually read. 95 | 96 | Data is written-through to the source and also cached for each 97 | write. In case of non-sequential writes, `catfs` detects `ENOTSUP` 98 | emitted by filesystems like `goofys` and falls back to flush the 99 | entire file on `close()`. Note that in the latter case even changing 100 | one byte will cause the entire file to be re-written. 101 | 102 | # References 103 | 104 | * Catfs is designed to work with [goofys](https://github.com/kahing/goofys/) 105 | * [FS-Cache](https://www.kernel.org/doc/Documentation/filesystems/caching/fscache.txt) 106 | provides caching for some in kernel filesystems but doesn't support 107 | other FUSE filesystems. 108 | * Other similar fuse caching filesystems, no idea about their completeness: 109 | * [CacheFS](https://github.com/cconstantine/CacheFS) - written in 110 | Python, not to be confused with FS-Cache above which is in kernel 111 | * [fuse-cache](https://sourceforge.net/projects/fuse-cache/) 112 | * [gocachefs](https://bitbucket.org/photocrati/gocachefs) 113 | * [mcachefs](https://github.com/Doloops/mcachefs) 114 | * [pcachefs](https://github.com/ibizaman/pcachefs) 115 | -------------------------------------------------------------------------------- /bench/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kahing/goofys-bench 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends sshfs && apt-get clean 4 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 5 | 6 | ENV PATH=$PATH:/root/.cargo/bin 7 | ADD Cargo.lock Cargo.toml /root/catfs/ 8 | WORKDIR /root/catfs 9 | RUN mkdir /root/catfs/src && touch /root/catfs/src/lib.rs 10 | # there's no source yet, just build the dependencies 11 | RUN cargo fetch 12 | RUN (cargo build --release || true) && rm /root/catfs/src/lib.rs 13 | 14 | ADD . /root/catfs 15 | RUN cargo install && rm -Rf target/ 16 | 17 | ENTRYPOINT ["/root/catfs/bench/run_bench.sh"] 18 | -------------------------------------------------------------------------------- /bench/bench.cat: -------------------------------------------------------------------------------- 1 | create_files 0.012 2 | rm_files 0.079 3 | create_files 0.012 4 | rm_files 0.093 5 | create_files 0.017 6 | rm_files 0.092 7 | create_files 0.018 8 | rm_files 0.081 9 | create_files 0.021 10 | rm_files 0.084 11 | create_files 0.020 12 | rm_files 0.117 13 | create_files 0.024 14 | rm_files 0.097 15 | create_files 0.018 16 | rm_files 0.077 17 | create_files 0.022 18 | rm_files 0.071 19 | create_files 0.022 20 | rm_files 0.106 21 | create_files_parallel 0.028 22 | rm_files_parallel 0.062 23 | create_files_parallel 0.043 24 | rm_files_parallel 0.064 25 | create_files_parallel 0.038 26 | rm_files_parallel 0.060 27 | create_files_parallel 0.040 28 | rm_files_parallel 0.059 29 | create_files_parallel 0.038 30 | rm_files_parallel 0.064 31 | create_files_parallel 0.035 32 | rm_files_parallel 0.060 33 | create_files_parallel 0.044 34 | rm_files_parallel 0.058 35 | create_files_parallel 0.041 36 | rm_files_parallel 0.042 37 | create_files_parallel 0.043 38 | rm_files_parallel 0.077 39 | create_files_parallel 0.034 40 | rm_files_parallel 0.073 41 | write_md5 5.258 42 | read_md5 2.282 43 | read_first_byte 0.010 44 | write_md5 5.116 45 | read_md5 2.180 46 | read_first_byte 0.004 47 | write_md5 5.048 48 | read_md5 2.309 49 | read_first_byte 0.002 50 | write_md5 6.290 51 | read_md5 2.305 52 | read_first_byte 0.002 53 | write_md5 6.065 54 | read_md5 2.265 55 | read_first_byte 0.003 56 | write_md5 6.242 57 | read_md5 2.709 58 | read_first_byte 0.002 59 | write_md5 5.215 60 | read_md5 2.308 61 | read_first_byte 0.003 62 | write_md5 5.364 63 | read_md5 2.184 64 | read_first_byte 0.003 65 | write_md5 5.230 66 | read_md5 2.286 67 | read_first_byte 0.003 68 | write_md5 5.199 69 | read_md5 2.288 70 | read_first_byte 0.003 71 | ls_files 0.013 72 | ls_files 0.022 73 | ls_files 0.022 74 | ls_files 0.021 75 | ls_files 0.021 76 | ls_files 0.020 77 | ls_files 0.023 78 | ls_files 0.021 79 | ls_files 0.019 80 | ls_files 0.022 81 | -------------------------------------------------------------------------------- /bench/bench.catfs: -------------------------------------------------------------------------------- 1 | create_files 0.054 2 | rm_files 0.093 3 | create_files 0.059 4 | rm_files 0.135 5 | create_files 0.044 6 | rm_files 0.086 7 | create_files 0.048 8 | rm_files 0.111 9 | create_files 0.058 10 | rm_files 0.086 11 | create_files 0.048 12 | rm_files 0.088 13 | create_files 0.046 14 | rm_files 0.095 15 | create_files 0.052 16 | rm_files 0.100 17 | create_files 0.042 18 | rm_files 0.087 19 | create_files 0.041 20 | rm_files 0.074 21 | create_files_parallel 0.049 22 | rm_files_parallel 0.061 23 | create_files_parallel 0.051 24 | rm_files_parallel 0.092 25 | create_files_parallel 0.059 26 | rm_files_parallel 0.064 27 | create_files_parallel 0.061 28 | rm_files_parallel 0.048 29 | create_files_parallel 0.033 30 | rm_files_parallel 0.063 31 | create_files_parallel 0.052 32 | rm_files_parallel 0.064 33 | create_files_parallel 0.051 34 | rm_files_parallel 0.058 35 | create_files_parallel 0.043 36 | rm_files_parallel 0.056 37 | create_files_parallel 0.041 38 | rm_files_parallel 0.066 39 | create_files_parallel 0.044 40 | rm_files_parallel 0.061 41 | write_md5 8.535 42 | read_md5 2.181 43 | read_first_byte 0.013 44 | write_md5 9.704 45 | read_md5 2.203 46 | read_first_byte 0.006 47 | write_md5 9.458 48 | read_md5 2.215 49 | read_first_byte 0.004 50 | write_md5 11.117 51 | read_md5 2.171 52 | read_first_byte 0.005 53 | write_md5 9.727 54 | read_md5 2.174 55 | read_first_byte 0.005 56 | write_md5 8.740 57 | read_md5 2.159 58 | read_first_byte 0.005 59 | write_md5 9.595 60 | read_md5 2.186 61 | read_first_byte 0.005 62 | write_md5 9.252 63 | read_md5 2.144 64 | read_first_byte 0.005 65 | write_md5 8.057 66 | read_md5 2.161 67 | read_first_byte 0.006 68 | write_md5 8.447 69 | read_md5 2.184 70 | read_first_byte 0.005 71 | ls_files 0.025 72 | ls_files 0.023 73 | ls_files 0.019 74 | ls_files 0.023 75 | ls_files 0.021 76 | ls_files 0.021 77 | ls_files 0.022 78 | ls_files 0.022 79 | ls_files 0.023 80 | ls_files 0.024 81 | -------------------------------------------------------------------------------- /bench/bench.catfs_over_sshfs: -------------------------------------------------------------------------------- 1 | create_files 1.266 2 | rm_files 0.221 3 | create_files 0.908 4 | rm_files 0.272 5 | create_files 1.285 6 | rm_files 0.211 7 | create_files 0.969 8 | rm_files 0.210 9 | create_files 0.952 10 | rm_files 0.548 11 | create_files 1.344 12 | rm_files 0.363 13 | create_files 0.927 14 | rm_files 0.306 15 | create_files 1.437 16 | rm_files 0.214 17 | create_files 1.037 18 | rm_files 0.227 19 | create_files 0.900 20 | rm_files 0.228 21 | create_files_parallel 1.124 22 | rm_files_parallel 0.324 23 | create_files_parallel 0.870 24 | rm_files_parallel 0.184 25 | create_files_parallel 0.738 26 | rm_files_parallel 0.189 27 | create_files_parallel 0.731 28 | rm_files_parallel 0.687 29 | create_files_parallel 1.610 30 | rm_files_parallel 0.160 31 | create_files_parallel 0.700 32 | rm_files_parallel 0.168 33 | create_files_parallel 0.791 34 | rm_files_parallel 0.145 35 | create_files_parallel 0.693 36 | rm_files_parallel 0.164 37 | create_files_parallel 1.634 38 | rm_files_parallel 0.165 39 | create_files_parallel 1.521 40 | rm_files_parallel 0.347 41 | write_md5 53.219 42 | read_md5 0.245 43 | read_first_byte 0.012 44 | write_md5 45.938 45 | read_md5 0.255 46 | read_first_byte 0.007 47 | write_md5 46.551 48 | read_md5 0.238 49 | read_first_byte 0.006 50 | write_md5 44.929 51 | read_md5 0.296 52 | read_first_byte 0.006 53 | write_md5 45.228 54 | read_md5 0.284 55 | read_first_byte 0.006 56 | write_md5 42.821 57 | read_md5 0.233 58 | read_first_byte 0.009 59 | write_md5 41.232 60 | read_md5 0.329 61 | read_first_byte 0.008 62 | write_md5 45.534 63 | read_md5 0.277 64 | read_first_byte 0.004 65 | write_md5 43.075 66 | read_md5 0.289 67 | read_first_byte 0.007 68 | write_md5 44.911 69 | read_md5 0.243 70 | read_first_byte 0.008 71 | ls_files 0.067 72 | ls_files 0.083 73 | ls_files 0.066 74 | ls_files 0.099 75 | ls_files 0.091 76 | ls_files 0.092 77 | ls_files 0.104 78 | ls_files 0.069 79 | ls_files 0.098 80 | ls_files 0.061 81 | -------------------------------------------------------------------------------- /bench/bench.catfs_vs_sshfs.data: -------------------------------------------------------------------------------- 1 | #operation,time 2 | Create 100 files 1.0917 0.889 1.328 0.797888888889 0.729 0.964 3 | Create 100 files (parallel) 0.799 0.74 0.913 0.7948 0.571 1.031 4 | Unlink 100 files 0.268 0.213 0.35 0.453333333333 0.363 0.583 5 | Unlink 100 files (parallel) 0.208888888889 0.181 0.32 0.362666666667 0.291 0.483 6 | ls with 1000 files 0.018 0.013 0.023 0.0162222222222 0.011 0.022 7 | Write 10MB 4.51344444444 4.194 4.949 4.803 4.311 5.218 8 | Read 10MB 0.0528888888889 0.045 0.061 3.98833333333 3.43 4.865 9 | Time to 1st byte 0.006 0.003 0.015 0.0182 0.012 0.028 10 | -------------------------------------------------------------------------------- /bench/bench.catfs_vs_sshfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahing/catfs/35430f800e68da18fb6bbd25a8f15bf32fa1f166/bench/bench.catfs_vs_sshfs.png -------------------------------------------------------------------------------- /bench/bench.catsshfs: -------------------------------------------------------------------------------- 1 | create_files 1.328 2 | rm_files 0.255 3 | create_files 0.970 4 | rm_files 0.228 5 | create_files 1.134 6 | rm_files 0.314 7 | create_files 1.256 8 | rm_files 0.268 9 | create_files 1.126 10 | rm_files 0.222 11 | create_files 1.068 12 | rm_files 0.316 13 | create_files 0.991 14 | rm_files 0.251 15 | create_files 1.134 16 | rm_files 0.263 17 | create_files 1.021 18 | rm_files 0.213 19 | create_files 0.889 20 | rm_files 0.350 21 | create_files_parallel 0.740 22 | rm_files_parallel 0.187 23 | create_files_parallel 0.785 24 | rm_files_parallel 0.186 25 | create_files_parallel 0.765 26 | rm_files_parallel 0.181 27 | create_files_parallel 0.998 28 | rm_files_parallel 0.320 29 | create_files_parallel 0.758 30 | rm_files_parallel 6.780 31 | create_files_parallel 0.897 32 | rm_files_parallel 0.210 33 | create_files_parallel 0.743 34 | rm_files_parallel 0.191 35 | create_files_parallel 0.843 36 | rm_files_parallel 0.190 37 | create_files_parallel 0.747 38 | rm_files_parallel 0.200 39 | create_files_parallel 0.913 40 | rm_files_parallel 0.215 41 | write_md5 4.949 42 | read_md5 0.045 43 | read_first_byte 0.015 44 | write_md5 4.391 45 | read_md5 0.049 46 | read_first_byte 0.005 47 | write_md5 6.853 48 | read_md5 0.060 49 | read_first_byte 0.003 50 | write_md5 4.194 51 | read_md5 0.059 52 | read_first_byte 0.007 53 | write_md5 4.350 54 | read_md5 0.061 55 | read_first_byte 0.003 56 | write_md5 4.474 57 | read_md5 0.055 58 | read_first_byte 0.006 59 | write_md5 4.210 60 | read_md5 0.045 61 | read_first_byte 0.003 62 | write_md5 4.906 63 | read_md5 0.050 64 | read_first_byte 0.006 65 | write_md5 4.871 66 | read_md5 0.035 67 | read_first_byte 0.067 68 | write_md5 4.276 69 | read_md5 0.052 70 | read_first_byte 0.006 71 | ls_files 0.043 72 | ls_files 0.023 73 | ls_files 0.020 74 | ls_files 0.020 75 | ls_files 0.013 76 | ls_files 0.014 77 | ls_files 0.014 78 | ls_files 0.014 79 | ls_files 0.022 80 | ls_files 0.022 81 | -------------------------------------------------------------------------------- /bench/bench.data: -------------------------------------------------------------------------------- 1 | #operation,time 2 | Create 100 files 0.0492 0.041 0.059 0.0186 0.012 0.024 3 | Create 100 files (parallel) 0.0484 0.033 0.061 0.0395555555556 0.034 0.044 4 | Unlink 100 files 0.0911111111111 0.074 0.111 0.0866666666667 0.071 0.106 5 | Unlink 100 files (parallel) 0.0601111111111 0.048 0.066 0.0641111111111 0.058 0.077 6 | ls with 1000 files 0.0226666666667 0.021 0.025 0.0212222222222 0.019 0.023 7 | Write 10MB 9.05722222222 8.057 9.727 5.5027 5.048 6.29 8 | Read 10MB 2.1778 2.144 2.215 2.26744444444 2.18 2.309 9 | Time to 1st byte 0.00511111111111 0.004 0.006 0.00277777777778 0.002 0.004 10 | -------------------------------------------------------------------------------- /bench/bench.local: -------------------------------------------------------------------------------- 1 | create_files 0.021 2 | rm_files 0.084 3 | create_files 0.020 4 | rm_files 0.080 5 | create_files 0.017 6 | rm_files 0.078 7 | create_files 0.019 8 | rm_files 0.127 9 | create_files 0.022 10 | rm_files 0.088 11 | create_files 0.017 12 | rm_files 0.088 13 | create_files 0.020 14 | rm_files 0.085 15 | create_files 0.021 16 | rm_files 0.093 17 | create_files 0.023 18 | rm_files 0.083 19 | create_files 0.019 20 | rm_files 0.093 21 | create_files_parallel 0.039 22 | rm_files_parallel 0.051 23 | create_files_parallel 0.040 24 | rm_files_parallel 0.041 25 | create_files_parallel 0.024 26 | rm_files_parallel 0.049 27 | create_files_parallel 0.040 28 | rm_files_parallel 0.050 29 | create_files_parallel 0.046 30 | rm_files_parallel 0.048 31 | create_files_parallel 0.040 32 | rm_files_parallel 0.049 33 | create_files_parallel 0.041 34 | rm_files_parallel 0.055 35 | create_files_parallel 0.040 36 | rm_files_parallel 0.049 37 | create_files_parallel 0.040 38 | rm_files_parallel 0.048 39 | create_files_parallel 0.044 40 | rm_files_parallel 0.059 41 | write_md5 4.950 42 | read_md5 2.256 43 | read_first_byte 0.010 44 | write_md5 4.566 45 | read_md5 2.264 46 | read_first_byte 0.004 47 | write_md5 5.370 48 | read_md5 2.360 49 | read_first_byte 0.004 50 | write_md5 5.813 51 | read_md5 2.189 52 | read_first_byte 0.003 53 | write_md5 4.557 54 | read_md5 2.230 55 | read_first_byte 0.003 56 | write_md5 4.603 57 | read_md5 2.206 58 | read_first_byte 0.005 59 | write_md5 4.911 60 | read_md5 2.193 61 | read_first_byte 0.004 62 | write_md5 5.745 63 | read_md5 2.168 64 | read_first_byte 0.003 65 | write_md5 4.395 66 | read_md5 2.149 67 | read_first_byte 0.005 68 | write_md5 4.789 69 | read_md5 2.292 70 | read_first_byte 0.005 71 | ls_files 0.028 72 | ls_files 0.022 73 | ls_files 0.023 74 | ls_files 0.023 75 | ls_files 0.024 76 | ls_files 0.025 77 | ls_files 0.023 78 | ls_files 0.023 79 | ls_files 0.023 80 | ls_files 0.023 81 | -------------------------------------------------------------------------------- /bench/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahing/catfs/35430f800e68da18fb6bbd25a8f15bf32fa1f166/bench/bench.png -------------------------------------------------------------------------------- /bench/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${TRAVIS:="false"} 4 | : ${FAST:="false"} 5 | : ${test:=""} 6 | 7 | iter=10 8 | 9 | if [ "$TRAVIS" != "false" ]; then 10 | set -o xtrace 11 | iter=1 12 | fi 13 | 14 | set -o errexit 15 | set -o nounset 16 | 17 | if [ $# -lt 2 ]; then 18 | echo "Usage: $0 " 19 | exit 1 20 | fi 21 | 22 | cmd=$1 23 | mnt=$2 24 | if [ $# -gt 2 ]; then 25 | t=$3 26 | else 27 | t= 28 | fi 29 | 30 | prefix=$mnt/test_dir 31 | 32 | MOUNTED=0 33 | 34 | $cmd >& mount.log & 35 | PID=$! 36 | 37 | function cleanup { 38 | if [ $MOUNTED == 1 ]; then 39 | popd >/dev/null 40 | if [ "$TRAVIS" != "false" ]; then 41 | rmdir $prefix 42 | else 43 | rmdir $prefix >& /dev/null || true # riofs doesn't support rmdir 44 | fi 45 | fi 46 | 47 | if [ "$PID" != "" ]; then 48 | #kill $PID >& /dev/null || true 49 | fusermount -u $mnt >& /dev/null || true 50 | fi 51 | } 52 | 53 | function cleanup_err { 54 | err=$? 55 | if [ $MOUNTED == 1 ]; then 56 | popd >&/dev/null || true 57 | rmdir $prefix >&/dev/null || true 58 | fi 59 | 60 | if [ "$PID" != "" ]; then 61 | kill $PID >& /dev/null || true 62 | fusermount -u $mnt >& /dev/null || true 63 | fi 64 | 65 | return $err 66 | } 67 | 68 | trap cleanup EXIT 69 | trap cleanup_err ERR 70 | 71 | if [ "$TRAVIS" == "false" -a "$cmd" != "cat" ]; then 72 | for i in $(seq 1 10); do 73 | (grep -q $mnt /proc/mounts && break) || true 74 | sleep 1 75 | done 76 | if ! grep -q $mnt /proc/mounts; then 77 | echo "$mnt not mounted by $cmd" 78 | cat mount.log 79 | exit 1 80 | fi 81 | MOUNTED=1 82 | else 83 | # in travis we mount things externally so we know we are mounted 84 | MOUNTED=1 85 | fi 86 | 87 | mkdir -p "$prefix" 88 | pushd "$prefix" >/dev/null 89 | 90 | SUDO= 91 | if [ $(id -u) != 0 ]; then 92 | SUDO=sudo 93 | fi 94 | 95 | function drop_cache { 96 | if [ "$TRAVIS" == "false" ]; then 97 | (echo 3 | $SUDO tee /proc/sys/vm/drop_caches) > /dev/null 98 | fi 99 | } 100 | 101 | export TIMEFORMAT=%R 102 | 103 | function run_test { 104 | test=$1 105 | drop_cache 106 | sleep 1 107 | # make sure riofs cache got cleared 108 | if [ -d /tmp/riofs-cache ]; then 109 | cache=$(ls -1 /tmp/riofs-cache) 110 | rm -Rf /tmp/riofs-cache 2>/dev/null || true 111 | mkdir -p /tmp/riofs-cache/$cache 112 | fi 113 | echo -n "$test " 114 | if [ $# -gt 1 ]; then 115 | time $test $2 116 | else 117 | time $test 118 | fi 119 | } 120 | 121 | function get_howmany { 122 | if [ "$TRAVIS" != "false" ]; then 123 | howmany=10 124 | else 125 | if [ $# == 0 ]; then 126 | howmany=100 127 | else 128 | howmany=$1 129 | fi 130 | fi 131 | } 132 | 133 | function create_files { 134 | get_howmany $@ 135 | 136 | for i in $(seq 1 $howmany); do 137 | echo $i > file$i 138 | done 139 | } 140 | 141 | function ls_files { 142 | get_howmany $@ 143 | # people usually use ls in the terminal when color is on 144 | numfiles=$(ls -1 --color=always | wc -l) 145 | if [ "$numfiles" != "$howmany" ]; then 146 | echo "$numfiles != $howmany" 147 | false 148 | fi 149 | } 150 | 151 | function rm_files { 152 | get_howmany $@ 153 | 154 | for i in $(seq 1 $howmany); do 155 | rm file$i >&/dev/null || true 156 | done 157 | } 158 | 159 | function find_files { 160 | numfiles=$(find | wc -l) 161 | 162 | if [ "$numfiles" != 820 ]; then 163 | echo "$numfiles != 820" 164 | rm_tree 165 | exit 1 166 | fi 167 | } 168 | 169 | function create_tree_parallel { 170 | (for i in $(seq 1 9); do 171 | mkdir $i 172 | for j in $(seq 1 9); do 173 | mkdir $i/$j 174 | 175 | for k in $(seq 1 9); do 176 | touch $i/$j/$k & true 177 | done 178 | done 179 | done 180 | wait) 181 | } 182 | 183 | function rm_tree { 184 | for i in $(seq 1 9); do 185 | if [ "$TRAVIS" != "false" ]; then 186 | rm -Rf $i 187 | else 188 | rm -Rf $i >& /dev/null || true # riofs doesn't support rmdir 189 | fi 190 | done 191 | } 192 | 193 | function create_files_parallel { 194 | get_howmany $@ 195 | 196 | (for i in $(seq 1 $howmany); do 197 | echo $i > file$i & true 198 | done 199 | wait) 200 | } 201 | 202 | function rm_files_parallel { 203 | get_howmany $@ 204 | 205 | (for i in $(seq 1 $howmany); do 206 | rm file$i & true 207 | done 208 | wait) 209 | } 210 | 211 | function read_first_byte { 212 | dd if=largefile of=/dev/null bs=1 count=1 iflag=nocache status=none 213 | } 214 | 215 | if [ "$t" = "" -o "$t" = "create" ]; then 216 | for i in $(seq 1 $iter); do 217 | run_test create_files 218 | run_test rm_files 219 | done 220 | fi 221 | 222 | if [ "$t" = "" -o "$t" = "create_parallel" ]; then 223 | for i in $(seq 1 $iter); do 224 | run_test create_files_parallel 225 | run_test rm_files_parallel 226 | done 227 | fi 228 | 229 | function write_md5 { 230 | seed=$(dd if=/dev/urandom bs=128 count=1 status=none | base64 -w 0) 231 | random_cmd="openssl enc -aes-256-ctr -pass pass:$seed -nosalt" 232 | count=1000 233 | if [ "$FAST" == "true" ]; then 234 | count=10 235 | fi 236 | MD5=$(dd if=/dev/zero bs=1MB count=$count status=none | $random_cmd | \ 237 | tee >(md5sum) >(dd of=largefile bs=1MB oflag=nocache status=none) >/dev/null | cut -f 1 '-d ') 238 | } 239 | 240 | function read_md5 { 241 | READ_MD5=$(md5sum largefile | cut -f 1 '-d ') 242 | if [ "$READ_MD5" != "$MD5" ]; then 243 | echo "$READ_MD5 != $MD5" >&2 244 | rm largefile 245 | exit 1 246 | fi 247 | } 248 | 249 | if [ "$t" = "" -o "$t" = "io" ]; then 250 | for i in $(seq 1 $iter); do 251 | run_test write_md5 252 | run_test read_md5 253 | run_test read_first_byte 254 | rm largefile 255 | done 256 | fi 257 | 258 | if [ "$t" = "" -o "$t" = "ls" ]; then 259 | create_files_parallel 1000 260 | for i in $(seq 1 $iter); do 261 | run_test ls_files 1000 262 | done 263 | rm_files 1000 264 | fi 265 | 266 | if [ "$t" = "ls_create" ]; then 267 | create_files_parallel 1000 268 | test=dummy 269 | sleep 10 270 | fi 271 | 272 | if [ "$t" = "ls_ls" ]; then 273 | run_test ls_files 1000 274 | fi 275 | 276 | if [ "$t" = "ls_rm" ]; then 277 | rm_files 1000 278 | test=dummy 279 | fi 280 | 281 | if [ "$t" = "" -o "$t" = "find" ]; then 282 | create_tree_parallel 283 | for i in $(seq 1 $iter); do 284 | run_test find_files 285 | done 286 | rm_tree 287 | fi 288 | 289 | if [ "$t" = "find_create" ]; then 290 | create_tree_parallel 291 | test=dummy 292 | sleep 10 293 | fi 294 | 295 | if [ "$t" = "find_find" ]; then 296 | for i in $(seq 1 $iter); do 297 | run_test find_files 298 | done 299 | fi 300 | 301 | if [ "$t" = "cleanup" ]; then 302 | rm -Rf * 303 | test=dummy 304 | fi 305 | 306 | # for https://github.com/kahing/goofys/issues/64 307 | # quote: There are 5 concurrent transfers gong at a time. 308 | # Data file size is often 100-400MB. 309 | # Regarding the number of transfers, I think it's about 200 files. 310 | # We read from the goofys mounted s3 bucket and write to a local spring webapp using curl. 311 | if [ "$t" = "disable" -o "$t" = "issue64" ]; then 312 | # setup the files 313 | (for i in $(seq 0 9); do 314 | dd if=/dev/zero of=file$i bs=1MB count=300 oflag=nocache status=none & true 315 | done 316 | wait) 317 | if [ $? != 0 ]; then 318 | exit $? 319 | fi 320 | 321 | # 200 files and 5 concurrent transfer means 40 times, do 50 times for good measure 322 | (for i in $(seq 0 9); do 323 | dd if=file$i of=/dev/null bs=1MB iflag=nocache status=none & 324 | done 325 | 326 | for i in $(seq 10 300); do 327 | # wait for 1 to finish, then invoke more 328 | wait -n 329 | running=$(ps -ef | grep ' dd if=' | grep -v grep | sed 's/.*dd if=file\([0-9]\).*/\1/') 330 | for i in $(seq 0 9); do 331 | if echo $running | grep -v -q $i; then 332 | dd if=file$i of=/dev/null bs=1MB iflag=nocache status=none & 333 | break 334 | fi 335 | done 336 | done 337 | wait) 338 | if [ $? != 0 ]; then 339 | exit $? 340 | fi 341 | 342 | # cleanup 343 | (for i in $(seq 0 9); do 344 | rm -f file$i & true 345 | done 346 | wait) 347 | fi 348 | 349 | if [ "$test" = "" ]; then 350 | echo "No test was run: $t" 351 | exit 1 352 | fi 353 | -------------------------------------------------------------------------------- /bench/bench.sshfs: -------------------------------------------------------------------------------- 1 | create_files 0.829 2 | rm_files 0.363 3 | create_files 0.761 4 | rm_files 0.434 5 | create_files 0.880 6 | rm_files 0.452 7 | create_files 0.730 8 | rm_files 0.431 9 | create_files 2.102 10 | rm_files 1.064 11 | create_files 0.735 12 | rm_files 0.452 13 | create_files 0.729 14 | rm_files 0.583 15 | create_files 0.964 16 | rm_files 0.423 17 | create_files 0.780 18 | rm_files 0.424 19 | create_files 0.773 20 | rm_files 0.518 21 | create_files_parallel 0.832 22 | rm_files_parallel 0.327 23 | create_files_parallel 0.669 24 | rm_files_parallel 0.299 25 | create_files_parallel 0.571 26 | rm_files_parallel 0.291 27 | create_files_parallel 0.760 28 | rm_files_parallel 0.349 29 | create_files_parallel 0.849 30 | rm_files_parallel 0.435 31 | create_files_parallel 0.969 32 | rm_files_parallel 0.483 33 | create_files_parallel 0.911 34 | rm_files_parallel 0.554 35 | create_files_parallel 0.687 36 | rm_files_parallel 0.443 37 | create_files_parallel 1.031 38 | rm_files_parallel 0.333 39 | create_files_parallel 0.669 40 | rm_files_parallel 0.304 41 | write_md5 4.780 42 | read_md5 3.738 43 | read_first_byte 0.023 44 | write_md5 4.828 45 | read_md5 3.726 46 | read_first_byte 0.012 47 | write_md5 5.218 48 | read_md5 3.903 49 | read_first_byte 0.014 50 | write_md5 4.751 51 | read_md5 7.497 52 | read_first_byte 0.025 53 | write_md5 5.040 54 | read_md5 3.430 55 | read_first_byte 0.013 56 | write_md5 4.446 57 | read_md5 3.546 58 | read_first_byte 0.013 59 | write_md5 5.130 60 | read_md5 4.519 61 | read_first_byte 0.028 62 | write_md5 4.571 63 | read_md5 4.865 64 | read_first_byte 0.017 65 | write_md5 4.955 66 | read_md5 4.024 67 | read_first_byte 0.016 68 | write_md5 4.311 69 | read_md5 4.144 70 | read_first_byte 0.021 71 | ls_files 0.045 72 | ls_files 0.020 73 | ls_files 0.018 74 | ls_files 0.019 75 | ls_files 0.022 76 | ls_files 0.018 77 | ls_files 0.011 78 | ls_files 0.014 79 | ls_files 0.012 80 | ls_files 0.012 81 | -------------------------------------------------------------------------------- /bench/bench_format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import uncertainties 4 | import numpy 5 | import os 6 | import sys 7 | 8 | def filter_outliers(numbers, mean, std): 9 | return filter(lambda x: abs(x - mean) < 2 * std, numbers) 10 | 11 | op_str = { 12 | 'create_files' : 'Create 100 files', 13 | 'create_files_parallel' : 'Create 100 files (parallel)', 14 | 'rm_files' : 'Unlink 100 files', 15 | 'rm_files_parallel' : 'Unlink 100 files (parallel)', 16 | 'ls_files' : 'ls with 1000 files', 17 | 'write_md5' : 'Write 1GB', 18 | 'read_first_byte' : 'Time to 1st byte', 19 | 'read_md5' : 'Read 1GB', 20 | } 21 | 22 | if os.environ.get("FAST") == "true": 23 | op_str['write_md5'] = 'Write 10MB' 24 | op_str['read_md5'] = 'Read 10MB' 25 | 26 | outputOrder = [ 27 | 'create_files', 28 | 'create_files_parallel', 29 | 'rm_files', 30 | 'rm_files_parallel', 31 | 'ls_files', 32 | 'write_md5', 33 | 'read_md5', 34 | 'read_first_byte', 35 | ] 36 | 37 | f = sys.argv[1] 38 | data = open(f).readlines() 39 | #print 'operation | goofys | s3fs | speedup' 40 | #print '----------| ------ | ------ | -------' 41 | 42 | table = [{}, {}] 43 | 44 | print '#operation,time' 45 | for l in data: 46 | dataset = l.strip().split('\t') 47 | for d in range(0, len(dataset)): 48 | op, num = dataset[d].split(' ') 49 | if not op in table[d]: 50 | table[d][op] = [] 51 | table[d][op] += [float(num)] 52 | 53 | 54 | for c in outputOrder: 55 | print op_str[c], 56 | for d in table: 57 | mean = numpy.mean(d[c]) 58 | err = numpy.std(d[c]) 59 | x = filter_outliers(d[c], mean, err) 60 | print "\t%s\t%s\t%s" % (numpy.mean(x), numpy.min(x), numpy.max(x)), 61 | print 62 | 63 | # op = op_str[nums[0]] 64 | 65 | # for i in range(1, len(nums)): 66 | 67 | # x = map(lambda x: float(x), nums[1].strip().split(' ')) 68 | # y = map(lambda x: float(x), nums[2].strip().split(' ')) 69 | # mean_x = numpy.mean(x) 70 | # err_x = numpy.std(x) 71 | # mean_y = numpy.mean(y) 72 | # err_y = numpy.std(y) 73 | # fixed_x = fixed_y = "" 74 | 75 | # x2 = filter_outliers(x, mean_x, err_x) 76 | # y2 = filter_outliers(y, mean_y, err_y) 77 | # if x != x2: 78 | # fixed_x = "*" * abs(len(x) - len(x2)) 79 | # mean_x = numpy.mean(x2) 80 | # err_x = numpy.std(x2) 81 | # if y != y2: 82 | # fixed_y = "*" * abs(len(y) - len(y2)) 83 | # mean_y = numpy.mean(y2) 84 | # err_y = numpy.std(y2) 85 | 86 | # print "%s, %s, %s, %s", op, mean_x, mean_x - err_x, mean_x + err_x 87 | # # u_x = uncertainties.ufloat(mean_x, err_x) 88 | # # u_y = uncertainties.ufloat(mean_y, err_y) 89 | # # delta = u_y/u_x 90 | # # print "%s | %s%s | %s%s | %sx" % (op, u_x, fixed_x, u_y, fixed_y, delta) 91 | -------------------------------------------------------------------------------- /bench/bench_graph.gnuplot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gnuplot 2 | 3 | reset 4 | #fontsize = 12 5 | set terminal pngcairo crop size 1000,640 6 | set output ARG2 7 | #set key at graph 0.24, 0.8 horizontal samplen 0.1 8 | 9 | set key at graph 0.0, 0.8 horizontal samplen 0.1 10 | 11 | set style data histogram 12 | set style histogram errorbars gap 2 lw 1 13 | set style fill solid 1.00 border 0 14 | set boxwidth 0.8 15 | set xtic rotate 16 | unset ytics 17 | set y2tics rotate by 90 18 | 19 | #set yrange [0:100]; 20 | 21 | set y2label 'Time (seconds)' offset -2.5 22 | set xlabel ' ' 23 | set size 1, 1 24 | 25 | set label 1 ARG3 at graph -0.3, 0.8 left rotate by 90 26 | set label 2 ARG4 at graph -0.17, 0.8 left rotate by 90 27 | 28 | set lmargin at screen 0.1 29 | 30 | set datafile separator "\t" 31 | 32 | set multiplot #layout 1,3 33 | set bmargin at screen 0.4 34 | #set size 1, 1 35 | 36 | set origin 0.0,0.1 37 | set size 0.5,0.8 38 | set xrange [-1:4.8] 39 | 40 | plot ARG1 using 2:3:4 title " ", \ 41 | '' using 5:6:7 title " ", \ 42 | '' using 8:9:10 title " ", \ 43 | '' using 0:(0):xticlabel(1) w l title '' 44 | 45 | set key off 46 | unset label 1 47 | unset label 2 48 | unset label 3 49 | set lmargin 50 | 51 | set origin 0.45,0.1 52 | set size 0.3,0.8 53 | set xrange [4.5:6.8] 54 | 55 | plot ARG1 using 2:3:4 title " ", \ 56 | '' using 5:6:7 title " ", \ 57 | '' using 8:9:10 title " ", \ 58 | '' using 0:(0):xticlabel(1) w l title '' 59 | 60 | set origin 0.7,0.1 61 | set size 0.2,0.8 62 | set xrange [6.5:7.7] 63 | set yrange [0:0.05] 64 | 65 | plot ARG1 using 2:3:4 title " ", \ 66 | '' using 5:6:7 title " ", \ 67 | '' using 8:9:10 title " ", \ 68 | '' using 0:(0):xticlabel(1) w l title '' 69 | 70 | unset multiplot 71 | -------------------------------------------------------------------------------- /bench/run_bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | : ${BUCKET:="goofys-bench"} 8 | : ${FAST:="false"} 9 | : ${SSHFS_OPTS:="-o StrictHostKeyChecking=no -o Cipher=arcfour ${SSHFS_SERVER}:/tmp"} 10 | 11 | if [ $# = 1 ]; then 12 | t=$1 13 | else 14 | t= 15 | fi 16 | 17 | dir=$(dirname $0) 18 | 19 | mkdir -p target/src 20 | mkdir -p target/cache 21 | mkdir -p target/mnt 22 | 23 | CATFS="catfs target/src target/cache target/mnt" 24 | LOCAL="cat" 25 | SSHFS="sshfs -f ${SSHFS_OPTS} target/mnt" 26 | 27 | function catsshfs { 28 | # sometimes we wouldn't umount sshfs cleanly after the previous run 29 | fusermount -u target/src >& /dev/null || true 30 | sshfs ${SSHFS_OPTS} target/src 31 | sleep 1 32 | catfs target/src target/cache target/mnt 33 | fusermount -u target/src >& /dev/null || true 34 | } 35 | 36 | export -f catsshfs 37 | 38 | CATSSHFS="catsshfs" 39 | 40 | for fs in cat catfs sshfs catsshfs; do 41 | case $fs in 42 | cat) 43 | FS=$LOCAL 44 | export FAST=false 45 | ;; 46 | catfs) 47 | FS=$CATFS 48 | export FAST=false 49 | ;; 50 | sshfs) 51 | FS=$SSHFS 52 | export FAST=true 53 | ;; 54 | catsshfs) 55 | FS=$CATSSHFS 56 | export FAST=true 57 | ;; 58 | esac 59 | 60 | rm target/bench.$fs 2>/dev/null || true 61 | if [ "$t" = "" ]; then 62 | for tt in create create_parallel io cleanup ls; do 63 | $dir/bench.sh "$FS" target/mnt $tt |& tee -a target/bench.$fs 64 | done 65 | else 66 | $dir/bench.sh "$FS" target/mnt $t |& tee target/bench.$fs 67 | fi 68 | 69 | done 70 | 71 | fusermount -u target/src >& /dev/null || true 72 | 73 | rmdir target/src 74 | rmdir target/cache 75 | rmdir target/mnt 76 | 77 | $dir/bench_format.py <(paste target/bench.catfs target/bench.cat) > target/bench.data 78 | FAST=true $dir/bench_format.py <(paste target/bench.catsshfs target/bench.sshfs) > target/bench.catfs_vs_sshfs.data 79 | 80 | gnuplot -c $dir/bench_graph.gnuplot target/bench.data target/bench.png catfs local 81 | gnuplot -c $dir/bench_graph.gnuplot target/bench.catfs_vs_sshfs.data target/bench.catfs_vs_sshfs.png \ 82 | "catfs over sshfs" "sshfs" 83 | 84 | convert -rotate 90 target/bench.png target/bench.png 85 | convert -rotate 90 target/bench.catfs_vs_sshfs.png target/bench.catfs_vs_sshfs.png 86 | -------------------------------------------------------------------------------- /src/catfs/dir.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::fs; 4 | use std::io; 5 | use std::os::unix::io::RawFd; 6 | use std::path::Path; 7 | 8 | use catfs::error; 9 | use catfs::rlibc; 10 | 11 | pub struct Handle { 12 | dh: *mut libc::DIR, 13 | offset: i64, 14 | entry: rlibc::Dirent, 15 | entry_valid: bool, 16 | } 17 | 18 | // no-op to workaround the fact that we send the entire CatFS at start 19 | // time, but we never send anything. Could have used Unique but that 20 | // bounds us to rust nightly 21 | unsafe impl Send for Handle {} 22 | 23 | impl Drop for Handle { 24 | fn drop(&mut self) { 25 | if let Err(e) = rlibc::closedir(self.dh) { 26 | error!("!closedir {:?} = {}", self.dh, e); 27 | } 28 | } 29 | } 30 | 31 | #[allow(dead_code)] 32 | pub fn openpath(path: &dyn AsRef) -> io::Result { 33 | rlibc::open(&path, rlibc::O_PATH, 0) 34 | } 35 | 36 | impl Handle { 37 | pub fn openat(dir: RawFd, path: &dyn AsRef) -> error::Result { 38 | let fd = if path.as_ref() == Path::new("") { 39 | rlibc::openat(dir, &".", rlibc::O_RDONLY, 0)? 40 | } else { 41 | rlibc::openat(dir, &path, rlibc::O_RDONLY, 0)? 42 | }; 43 | return Ok(Handle { 44 | dh: rlibc::fdopendir(fd)?, 45 | offset: 0, 46 | entry: Default::default(), 47 | entry_valid: false, 48 | }); 49 | } 50 | 51 | #[allow(dead_code)] 52 | pub fn open(path: &dyn AsRef) -> error::Result { 53 | let dh = rlibc::opendir(&path)?; 54 | return Ok(Handle { 55 | dh: dh, 56 | offset: 0, 57 | entry: Default::default(), 58 | entry_valid: false, 59 | }); 60 | } 61 | 62 | pub fn seekdir(&mut self, offset: i64) { 63 | if offset != self.offset { 64 | debug!( 65 | "seeking {} to {}", 66 | unsafe { libc::telldir(self.dh) }, 67 | offset 68 | ); 69 | rlibc::seekdir(self.dh, offset); 70 | self.offset = offset; 71 | self.entry_valid = false; 72 | } 73 | } 74 | 75 | pub fn push(&mut self, en: rlibc::Dirent) { 76 | self.entry = en; 77 | self.entry_valid = true; 78 | } 79 | 80 | pub fn consumed(&mut self, en: &rlibc::Dirent) { 81 | self.offset = en.off(); 82 | self.entry_valid = false; 83 | } 84 | 85 | pub fn readdir(&mut self) -> error::Result> { 86 | if self.entry_valid { 87 | return Ok(Some(self.entry.clone())); 88 | } else { 89 | match rlibc::readdir(self.dh)? { 90 | Some(entry) => { 91 | return Ok(Some(entry)); 92 | } 93 | None => return Ok(None), 94 | } 95 | } 96 | } 97 | 98 | #[allow(dead_code)] 99 | pub fn mkdir(path: &dyn AsRef, mode: libc::mode_t) -> io::Result<()> { 100 | rlibc::mkdir(path, mode) 101 | } 102 | 103 | pub fn rmdirat(src_dir: RawFd, cache_dir: RawFd, path: &dyn AsRef) -> io::Result<()> { 104 | if let Err(e) = rlibc::unlinkat(cache_dir, path, libc::AT_REMOVEDIR as u32) { 105 | if !error::is_enoent(&e) { 106 | return Err(e); 107 | } 108 | } 109 | 110 | return rlibc::unlinkat(src_dir, path, libc::AT_REMOVEDIR as u32); 111 | } 112 | 113 | #[allow(dead_code)] 114 | pub fn rmdir(src_path: &dyn AsRef, cache_path: &dyn AsRef) -> io::Result<()> { 115 | if let Err(e) = fs::remove_dir(cache_path) { 116 | if !error::is_enoent(&e) { 117 | return Err(e); 118 | } 119 | } 120 | return fs::remove_dir(src_path); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/catfs/error.rs: -------------------------------------------------------------------------------- 1 | extern crate backtrace; 2 | extern crate libc; 3 | 4 | use std::fmt; 5 | use std::ops::Deref; 6 | use std::io; 7 | use std::string::FromUtf8Error; 8 | 9 | use self::backtrace::Backtrace; 10 | use self::backtrace::BacktraceFrame; 11 | 12 | #[derive(Debug)] 13 | pub struct RError { 14 | e: E, 15 | bt: Option, 16 | } 17 | 18 | pub fn is_enoent(e: &io::Error) -> bool { 19 | return e.kind() == io::ErrorKind::NotFound; 20 | } 21 | 22 | pub fn try_enoent(e: io::Error) -> Result { 23 | if is_enoent(&e) { 24 | return Ok(true); 25 | } else { 26 | return Err(RError::from(e)); 27 | } 28 | } 29 | 30 | pub fn propagate(e: io::Error) -> Result { 31 | return Err(RError::propagate(e)); 32 | } 33 | 34 | pub fn errno(e: &RError) -> libc::c_int { 35 | if RError::expected(e) { 36 | return e.e.raw_os_error().unwrap(); 37 | } else { 38 | return libc::EIO; 39 | } 40 | } 41 | 42 | 43 | impl RError { 44 | pub fn propagate(e: E) -> RError { 45 | RError { 46 | e: e, 47 | bt: Default::default(), 48 | } 49 | } 50 | 51 | pub fn from(e: E) -> RError { 52 | let mut bt = Backtrace::new(); 53 | let mut i: usize = 0; 54 | let mut chop: usize = 0; 55 | for f in bt.frames() { 56 | if let Some(sym) = f.symbols().first() { 57 | if let Some(p) = sym.filename() { 58 | if p.file_name().unwrap() == "error.rs" { 59 | chop = i; 60 | break; 61 | } 62 | } 63 | } 64 | i += 1; 65 | } 66 | 67 | if chop != 0 { 68 | let mut frames: Vec = bt.into(); 69 | let _: Vec<_> = frames.drain(0..i).collect(); 70 | bt = Backtrace::from(frames); 71 | } 72 | 73 | RError { e: e, bt: Some(bt) } 74 | } 75 | 76 | fn expected(&self) -> bool { 77 | return self.bt.is_none(); 78 | } 79 | } 80 | 81 | impl RError { 82 | pub fn errno(&self) -> i32 { 83 | return self.e.raw_os_error().unwrap(); 84 | } 85 | } 86 | 87 | impl fmt::Display for RError { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | match self.bt { 90 | Some(ref bt) => write!(f, "{} {:?}", self.e, bt), 91 | None => write!(f, "{}", self.e), 92 | } 93 | 94 | } 95 | } 96 | 97 | impl Deref for RError { 98 | type Target = E; 99 | 100 | fn deref(&self) -> &E { 101 | &self.e 102 | } 103 | } 104 | 105 | // XXX not really a clone 106 | impl Clone for RError { 107 | fn clone(&self) -> Self { 108 | RError { 109 | e: io::Error::from_raw_os_error(self.e.raw_os_error().unwrap()), 110 | bt: Default::default(), 111 | } 112 | } 113 | } 114 | 115 | impl From for RError { 116 | fn from(e: io::Error) -> RError { 117 | RError::from(e) 118 | } 119 | } 120 | 121 | impl From for RError { 122 | fn from(e: FromUtf8Error) -> RError { 123 | RError::from(e) 124 | } 125 | } 126 | 127 | pub type Result = ::std::result::Result>; 128 | -------------------------------------------------------------------------------- /src/catfs/file.rs: -------------------------------------------------------------------------------- 1 | extern crate fd; 2 | extern crate generic_array; 3 | extern crate libc; 4 | extern crate sha2; 5 | extern crate threadpool; 6 | extern crate xattr; 7 | 8 | use std::ffi::{OsStr, OsString}; 9 | use std::io; 10 | use std::os::unix::ffi::OsStrExt; 11 | use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; 12 | use std::path::{Component, Path, PathBuf}; 13 | use std::sync::{Arc, Condvar, Mutex}; 14 | 15 | use self::generic_array::GenericArray; 16 | use self::generic_array::typenum::U64; 17 | use self::sha2::{Sha512, Digest}; 18 | use self::threadpool::ThreadPool; 19 | use self::xattr::FileExt; 20 | 21 | use catfs::error; 22 | use catfs::error::RError; 23 | use catfs::rlibc; 24 | use catfs::rlibc::File; 25 | 26 | type CvData = Arc<(Mutex, Condvar)>; 27 | 28 | #[derive(Default)] 29 | struct PageInInfo { 30 | offset: i64, 31 | dirty: bool, 32 | eof: bool, 33 | err: Option>, 34 | } 35 | 36 | pub struct Handle { 37 | src_file: File, 38 | cache_file: File, 39 | dirty: bool, 40 | write_through_failed: bool, 41 | has_page_in_thread: bool, 42 | page_in_res: CvData, 43 | } 44 | 45 | // no-op to workaround the fact that we send the entire CatFS at start 46 | // time, but we never send anything. Could have used Unique but that 47 | // bounds us to rust nightly 48 | unsafe impl Send for Handle {} 49 | 50 | fn make_rdwr(f: &mut u32) { 51 | *f = (*f & !rlibc::O_ACCMODE) | rlibc::O_RDWR; 52 | } 53 | 54 | fn maybe_unlinkat(dir: RawFd, path: &dyn AsRef) -> io::Result<()> { 55 | if let Err(e) = rlibc::unlinkat(dir, path, 0) { 56 | if !error::is_enoent(&e) { 57 | return Err(e); 58 | } 59 | } 60 | return Ok(()); 61 | } 62 | 63 | pub fn mkdirat_all(dir: RawFd, path: &dyn AsRef, mode: libc::mode_t) -> io::Result<()> { 64 | let mut p = PathBuf::new(); 65 | 66 | for c in path.as_ref().components() { 67 | if let Component::Normal(n) = c { 68 | p.push(n); 69 | 70 | if let Err(e) = rlibc::mkdirat(dir, &p, mode) { 71 | if e.raw_os_error().unwrap() != libc::EEXIST { 72 | return Err(e); 73 | } 74 | } 75 | } 76 | } 77 | 78 | return Ok(()); 79 | } 80 | 81 | impl Handle { 82 | pub fn create( 83 | src_dir: RawFd, 84 | cache_dir: RawFd, 85 | path: &dyn AsRef, 86 | flags: u32, 87 | mode: libc::mode_t, 88 | ) -> error::Result { 89 | // need to read the cache file for writeback 90 | let mut cache_flags = flags; 91 | if (cache_flags & rlibc::O_ACCMODE) == rlibc::O_WRONLY { 92 | make_rdwr(&mut cache_flags); 93 | } 94 | //debug!("create {:b} {:b} {:#o}", flags, cache_flags, mode); 95 | 96 | if let Some(parent) = path.as_ref().parent() { 97 | mkdirat_all(cache_dir, &parent, 0o777)?; 98 | } 99 | 100 | let src_file = File::openat(src_dir, path, flags, mode)?; 101 | // we are able to create the src file, then the cache file 102 | // shouldn't be here, but it could be because of bug/crash, 103 | // so unlink it first 104 | maybe_unlinkat(cache_dir, path)?; 105 | 106 | return Ok(Handle { 107 | src_file: src_file, 108 | cache_file: File::openat(cache_dir, path, cache_flags, mode)?, 109 | dirty: true, 110 | write_through_failed: false, 111 | has_page_in_thread: false, 112 | page_in_res: Arc::new((Default::default(), Condvar::new())), 113 | }); 114 | } 115 | 116 | pub fn open( 117 | src_dir: RawFd, 118 | cache_dir: RawFd, 119 | path: &dyn AsRef, 120 | flags: u32, 121 | cache_valid_if_present: bool, 122 | disable_splice: bool, 123 | tp: &Mutex, 124 | ) -> error::Result { 125 | // even if file is open for write only, I still need to be 126 | // able to read the src for read-modify-write 127 | let mut flags = flags; 128 | if (flags & rlibc::O_ACCMODE) == rlibc::O_WRONLY { 129 | make_rdwr(&mut flags); 130 | } 131 | 132 | let valid = 133 | Handle::validate_cache(src_dir, cache_dir, &path, cache_valid_if_present, false)?; 134 | debug!( 135 | "{:?} {} a valid cache file", 136 | path.as_ref(), 137 | if valid { "is" } else { "is not" }, 138 | ); 139 | let mut cache_flags = flags; 140 | 141 | if !valid { 142 | // mkdir the parents 143 | if let Some(parent) = path.as_ref().parent() { 144 | mkdirat_all(cache_dir, &parent, 0o777)?; 145 | } 146 | // need to cache this file so need to open it for write 147 | cache_flags |= rlibc::O_CREAT; 148 | if (cache_flags & rlibc::O_ACCMODE) == rlibc::O_RDONLY { 149 | make_rdwr(&mut cache_flags); 150 | } 151 | } 152 | 153 | let src_file = if valid && (flags & rlibc::O_ACCMODE) == rlibc::O_RDONLY { 154 | Default::default() 155 | } else { 156 | File::openat(src_dir, path, flags, 0o666)? 157 | }; 158 | 159 | let mut handle = Handle { 160 | src_file: src_file, 161 | cache_file: File::openat(cache_dir, path, cache_flags, 0o666)?, 162 | dirty: false, 163 | write_through_failed: false, 164 | has_page_in_thread: false, 165 | page_in_res: Arc::new((Default::default(), Condvar::new())), 166 | }; 167 | 168 | if !valid && (flags & rlibc::O_TRUNC) == 0 { 169 | debug!("read ahead {:?}", path.as_ref()); 170 | handle.has_page_in_thread = true; 171 | let mut h = handle.clone(); 172 | let path = path.as_ref().to_path_buf(); 173 | tp.lock().unwrap().execute(move || { 174 | if let Err(e) = h.copy(true, disable_splice) { 175 | let mut is_cancel = false; 176 | 177 | { 178 | let page_in_res = h.page_in_res.0.lock().unwrap(); 179 | if let Some(ref e2) = page_in_res.err { 180 | if e2.raw_os_error().unwrap() == libc::ECANCELED { 181 | is_cancel = true; 182 | } 183 | } 184 | } 185 | 186 | if !is_cancel { 187 | error!("read ahead {:?} failed: {}", path, e); 188 | h.notify_offset(Err(e), false).unwrap(); 189 | } else { 190 | debug!("read ahead {:?} canceled", path); 191 | } 192 | } 193 | // the files are always closed in the main IO path, consume 194 | // the fds to prevent closing 195 | h.src_file.into_raw(); 196 | h.cache_file.into_raw(); 197 | }); 198 | } 199 | 200 | return Ok(handle); 201 | } 202 | 203 | // see validate_cache.sh on how to replicate this 204 | pub fn src_str_to_checksum(f: &File) -> error::Result { 205 | let mut s = OsString::new(); 206 | for x in ["s3.etag"].iter() { 207 | match f.get_xattr(&x) { 208 | Ok(v) => { 209 | if let Some(v) = v { 210 | s.push(x); 211 | s.push(OsStr::new("=")); 212 | s.push("0x"); 213 | for b in v { 214 | s.push(format!("{:x}", b)); 215 | } 216 | s.push("\n"); 217 | } 218 | } 219 | Err(e) => { 220 | let errno = e.raw_os_error().unwrap(); 221 | if errno != libc::ENOENT && errno != libc::ENOTSUP { 222 | return Err(RError::from(e)); 223 | } 224 | } 225 | } 226 | } 227 | 228 | let st = f.stat()?; 229 | s.push(format!("{}\n", st.st_mtime)); 230 | s.push(format!("{}\n", st.st_size)); 231 | return Ok(s); 232 | } 233 | 234 | fn src_chksum(f: &File) -> error::Result> { 235 | let s = Handle::src_str_to_checksum(f)?; 236 | //debug!("checksum is {:?}", s); 237 | let mut hasher = Sha512::default(); 238 | hasher.update(s.as_bytes()); 239 | return Ok(hasher.finalize()); 240 | } 241 | 242 | pub fn make_pristine( 243 | src_dir: RawFd, 244 | cache_dir: RawFd, 245 | path: &dyn AsRef, 246 | ) -> error::Result<()> { 247 | match File::openat(cache_dir, path, rlibc::O_WRONLY, 0) { 248 | Err(e) => { 249 | return Err(RError::from(e)); 250 | } 251 | Ok(mut cache) => { 252 | let mut src = File::openat(src_dir, path, rlibc::O_RDONLY, 0)?; 253 | cache.set_xattr( 254 | "user.catfs.src_chksum", 255 | Handle::src_chksum(&src)?.as_slice(), 256 | )?; 257 | src.close()?; 258 | cache.close()?; 259 | } 260 | } 261 | 262 | return Ok(()); 263 | } 264 | 265 | pub fn set_pristine(&self, pristine: bool) -> error::Result<()> { 266 | if pristine { 267 | self.cache_file.set_xattr( 268 | "user.catfs.src_chksum", 269 | Handle::src_chksum(&self.src_file)? 270 | .as_slice(), 271 | )?; 272 | } else { 273 | if let Err(e) = self.cache_file.remove_xattr("user.catfs.src_chksum") { 274 | let my_errno = e.raw_os_error().unwrap(); 275 | if my_errno != libc::ENODATA && my_errno != libc::ENOATTR { 276 | return Err(RError::from(e)); 277 | } 278 | } 279 | } 280 | return Ok(()); 281 | } 282 | 283 | fn is_pristine(src_file: &File, cache_file: &File) -> error::Result { 284 | if let Some(v) = cache_file.get_xattr("user.catfs.src_chksum")? { 285 | let expected = Handle::src_chksum(src_file)?; 286 | if v == expected.as_slice() { 287 | return Ok(true); 288 | } else { 289 | debug!("{:?} != {:?}, {} {}", v, expected, v.len(), expected.len()); 290 | return Ok(false); 291 | } 292 | } 293 | debug!("user.catfs.src_chksum missing for cache_file"); 294 | 295 | return Ok(false); 296 | } 297 | 298 | pub fn unlink(src_dir: RawFd, cache_dir: RawFd, path: &dyn AsRef) -> io::Result<()> { 299 | maybe_unlinkat(cache_dir, path)?; 300 | return rlibc::unlinkat(src_dir, path, 0); 301 | } 302 | 303 | pub fn validate_cache( 304 | src_dir: RawFd, 305 | cache_dir: RawFd, 306 | path: &dyn AsRef, 307 | cache_valid_if_present: bool, 308 | check_only: bool, 309 | ) -> error::Result { 310 | match File::openat(src_dir, path, rlibc::O_RDONLY, 0) { 311 | Ok(mut src_file) => { 312 | match File::openat(cache_dir, path, rlibc::O_RDONLY, 0) { 313 | Ok(mut cache_file) => { 314 | let valid: bool; 315 | if cache_valid_if_present || Handle::is_pristine(&src_file, &cache_file)? { 316 | valid = true; 317 | } else { 318 | valid = false; 319 | if !check_only { 320 | error!("{:?} is not a valid cache file, deleting", path.as_ref()); 321 | rlibc::unlinkat(cache_dir, path, 0)?; 322 | } 323 | } 324 | src_file.close()?; 325 | cache_file.close()?; 326 | return Ok(valid); 327 | } 328 | Err(e) => { 329 | src_file.close()?; 330 | if error::try_enoent(e)? { 331 | return Ok(false); 332 | } 333 | } 334 | } 335 | } 336 | Err(e) => { 337 | if error::try_enoent(e)? { 338 | // the source file doesn't exist, the cache file shouldn't either 339 | if !check_only { 340 | maybe_unlinkat(cache_dir, path)?; 341 | } 342 | } 343 | } 344 | } 345 | 346 | return Ok(false); 347 | } 348 | 349 | pub fn read(&mut self, offset: i64, buf: &mut [u8]) -> error::Result { 350 | let nwant = buf.len(); 351 | let mut bytes_read: usize = 0; 352 | 353 | if self.has_page_in_thread { 354 | self.wait_for_offset(offset + (buf.len() as i64), false)?; 355 | } 356 | 357 | while bytes_read < nwant { 358 | match self.cache_file.read_at( 359 | &mut buf[bytes_read..], 360 | offset + (bytes_read as i64), 361 | ) { 362 | Ok(nread) => { 363 | if nread == 0 { 364 | return Ok(bytes_read); 365 | } 366 | bytes_read += nread; 367 | } 368 | Err(e) => { 369 | if bytes_read > 0 { 370 | return Ok(bytes_read); 371 | } else { 372 | return Err(RError::from(e)); 373 | } 374 | } 375 | } 376 | } 377 | 378 | return Ok(bytes_read); 379 | } 380 | 381 | pub fn truncate(&mut self, size: u64) -> error::Result<()> { 382 | // pristiness comes from size as well so this automatically 383 | // invalidates the cache file if it's used again 384 | self.src_file.set_size(size)?; 385 | 386 | // wait for the background thread to finish so we won't have 387 | // more bytes being concurrently written to cache_file 388 | if self.has_page_in_thread { 389 | self.wait_for_eof()?; 390 | } 391 | 392 | self.cache_file.set_size(size)?; 393 | // caller is responsible for setting this to pristine if necessary 394 | return Ok(()); 395 | } 396 | 397 | pub fn chmod(&self, mode: libc::mode_t) -> io::Result<()> { 398 | self.src_file.chmod(mode)?; 399 | return Ok(()); 400 | } 401 | 402 | pub fn write(&mut self, offset: i64, buf: &[u8]) -> error::Result { 403 | let nwant = buf.len(); 404 | let mut bytes_written: usize = 0; 405 | 406 | if !self.dirty { 407 | // assumes that the metadata will hit the disk before the 408 | // incoming data will, and not flushing 409 | self.set_pristine(false)?; 410 | } 411 | 412 | if self.has_page_in_thread { 413 | self.wait_for_offset(offset + (buf.len() as i64), true)?; 414 | } 415 | 416 | while bytes_written < nwant { 417 | if !self.write_through_failed { 418 | if let Err(e) = self.src_file.write_at( 419 | &buf[bytes_written..], 420 | offset + (bytes_written as i64), 421 | ) 422 | { 423 | if e.raw_os_error().unwrap() == libc::ENOTSUP { 424 | self.write_through_failed = true; 425 | return Err(RError::propagate(e)); 426 | } else { 427 | if bytes_written != 0 { 428 | self.dirty = true; 429 | } 430 | return Err(RError::from(e)); 431 | } 432 | } 433 | 434 | } 435 | 436 | match self.cache_file.write_at( 437 | &buf[bytes_written..], 438 | offset + (bytes_written as i64), 439 | ) { 440 | Ok(nwritten) => { 441 | bytes_written += nwritten; 442 | } 443 | Err(e) => { 444 | if bytes_written > 0 { 445 | break; 446 | } else { 447 | if bytes_written != 0 { 448 | self.dirty = true; 449 | } 450 | return Err(RError::from(e)); 451 | } 452 | } 453 | } 454 | } 455 | 456 | if bytes_written != 0 { 457 | self.dirty = true; 458 | } 459 | 460 | return Ok(bytes_written); 461 | } 462 | 463 | pub fn flush(&mut self) -> error::Result { 464 | let mut flushed_to_src = false; 465 | if self.dirty { 466 | if self.write_through_failed { 467 | if self.has_page_in_thread { 468 | self.wait_for_eof()?; 469 | } 470 | 471 | self.copy(false, false)?; 472 | } else { 473 | self.set_pristine(true)?; 474 | } 475 | self.cache_file.flush()?; 476 | if let Err(e) = self.src_file.flush() { 477 | error!("!flush(src) = {}", e); 478 | // flush failed, now the fd is invalid, get rid of it 479 | self.src_file.into_raw(); 480 | 481 | // we couldn't flush the src_file, because of some 482 | // linux vfs oddity the file would appear to be 483 | // "normal" until we try to read it (the inode is 484 | // cached), so we need to invalidate our cache (which 485 | // would validate and thus we would never read from 486 | // src). 487 | 488 | // we only have the fd and there's no funlink, so we 489 | // will just unset the xattr 490 | self.set_pristine(false)?; 491 | 492 | return Err(RError::propagate(e)); 493 | } 494 | self.dirty = false; 495 | flushed_to_src = true; 496 | } else { 497 | if self.has_page_in_thread { 498 | // tell it to cancel 499 | let mut page_in_res = self.page_in_res.0.lock().unwrap(); 500 | page_in_res.err = Some(RError::propagate( 501 | io::Error::from_raw_os_error(libc::ECANCELED), 502 | )); 503 | } 504 | } 505 | return Ok(flushed_to_src); 506 | } 507 | 508 | fn wait_for_eof(&mut self) -> error::Result<()> { 509 | let mut page_in_res = self.page_in_res.0.lock().unwrap(); 510 | loop { 511 | if page_in_res.eof { 512 | self.has_page_in_thread = false; 513 | return Ok(()); 514 | } else { 515 | page_in_res = self.page_in_res.1.wait(page_in_res).unwrap(); 516 | } 517 | } 518 | } 519 | 520 | fn wait_for_offset(&mut self, offset: i64, set_dirty: bool) -> error::Result<()> { 521 | let &(ref lock, ref cvar) = &*self.page_in_res; 522 | 523 | let mut page_in_res = lock.lock().unwrap(); 524 | if set_dirty { 525 | // setting this to dirty prevents us from marking this as pristine 526 | page_in_res.dirty = true; 527 | } 528 | loop { 529 | if page_in_res.eof { 530 | self.has_page_in_thread = false; 531 | return Ok(()); 532 | } 533 | 534 | if page_in_res.offset >= offset { 535 | return Ok(()); 536 | } else if let Some(e) = page_in_res.err.clone() { 537 | return Err(e); 538 | } else { 539 | page_in_res = cvar.wait(page_in_res).unwrap(); 540 | } 541 | } 542 | } 543 | 544 | fn notify_offset(&self, res: error::Result, eof: bool) -> error::Result<()> { 545 | let &(ref lock, ref cvar) = &*self.page_in_res; 546 | 547 | let mut page_in_res = lock.lock().unwrap(); 548 | if !eof && page_in_res.err.is_some() { 549 | // main IO thread sets this to cancel paging, but if eof 550 | // is reached then we might as well finish it 551 | return Err(page_in_res.err.clone().unwrap()); 552 | } 553 | 554 | match res { 555 | Ok(offset) => page_in_res.offset = offset, 556 | Err(e) => page_in_res.err = Some(e), 557 | } 558 | page_in_res.eof = eof; 559 | if eof && !page_in_res.dirty { 560 | self.set_pristine(true)?; 561 | } 562 | cvar.notify_all(); 563 | return Ok(()); 564 | } 565 | 566 | pub fn reopen_src( 567 | &mut self, 568 | dir: RawFd, 569 | path: &dyn AsRef, 570 | create: bool, 571 | ) -> error::Result<()> { 572 | let _unused = self.page_in_res.0.lock().unwrap(); 573 | 574 | let mut buf = [0u8; 0]; 575 | let mut flags = rlibc::O_RDWR; 576 | if let Err(e) = self.src_file.read_at(&mut buf, 0) { 577 | if e.raw_os_error().unwrap() == libc::EBADF { 578 | // this was not open for read 579 | flags = rlibc::O_WRONLY; 580 | } else { 581 | return Err(RError::from(e)); 582 | } 583 | } 584 | 585 | let mut mode: libc::mode_t = 0; 586 | if create { 587 | let st = self.src_file.stat()?; 588 | mode = st.st_mode & !libc::S_IFMT; 589 | flags |= rlibc::O_CREAT; 590 | } 591 | 592 | if let Err(e) = self.src_file.close() { 593 | // normal for this close to fail 594 | if e.raw_os_error().unwrap() != libc::ENOTSUP { 595 | return Err(RError::from(e)); 596 | } 597 | } 598 | 599 | self.src_file = File::openat(dir, path, flags, mode)?; 600 | return Ok(()); 601 | } 602 | 603 | fn copy_user(&self, rh: &File, wh: &File) -> error::Result { 604 | let mut buf = [0u8; 32 * 1024]; 605 | let mut offset = 0; 606 | loop { 607 | let nread = rh.read_at(&mut buf, offset)?; 608 | if nread == 0 { 609 | break; 610 | } 611 | wh.write_at(&buf[..nread], offset)?; 612 | offset += nread as i64; 613 | 614 | self.notify_offset(Ok(offset), false)?; 615 | } 616 | 617 | return Ok(offset); 618 | } 619 | 620 | #[cfg(not(target_os = "macos"))] 621 | fn copy_splice(&self, rh: &File, wh: &File) -> error::Result { 622 | let (pin, pout) = rlibc::pipe()?; 623 | let pin = fd::FileDesc::new(pin, /*close_on_drop=*/ true); 624 | let pout = fd::FileDesc::new(pout, /*close_on_drop=*/ true); 625 | 626 | let mut offset = 0; 627 | loop { 628 | let nread = rlibc::splice(rh.as_raw_fd(), offset, pout.as_raw_fd(), -1, 128 * 1024)?; 629 | if nread == 0 { 630 | break; 631 | } 632 | 633 | let mut written = 0; 634 | while written < nread { 635 | let nxfer = rlibc::splice(pin.as_raw_fd(), -1, wh.as_raw_fd(), offset, 128 * 1024)?; 636 | 637 | written += nxfer; 638 | offset += nxfer as i64; 639 | 640 | self.notify_offset(Ok(offset), false)?; 641 | } 642 | } 643 | 644 | if let Err(e) = rlibc::close(pin.into_raw_fd()) { 645 | rlibc::close(pout.into_raw_fd())?; 646 | return Err(RError::from(e)); 647 | } else { 648 | rlibc::close(pout.into_raw_fd())?; 649 | } 650 | 651 | return Ok(offset); 652 | } 653 | 654 | #[cfg(target_os = "macos")] 655 | fn copy_splice(&self, rh: &File, wh: &File) -> error::Result { 656 | self.copy_user(rh, wh) 657 | } 658 | 659 | fn copy(&self, to_cache: bool, disable_splice: bool) -> error::Result<()> { 660 | let rh: &File; 661 | let wh: &File; 662 | if to_cache { 663 | rh = &self.src_file; 664 | wh = &self.cache_file; 665 | } else { 666 | rh = &self.cache_file; 667 | wh = &self.src_file; 668 | } 669 | 670 | let size = rh.filesize()?; 671 | if size < wh.filesize()? { 672 | wh.truncate(size)?; 673 | } 674 | 675 | let offset: i64; 676 | 677 | if disable_splice { 678 | offset = self.copy_user(rh, wh)?; 679 | } else { 680 | match self.copy_splice(rh, wh) { 681 | Err(e) => { 682 | if e.raw_os_error().unwrap() == libc::EINVAL { 683 | offset = self.copy_user(rh, wh)?; 684 | } else { 685 | return Err(e); 686 | } 687 | } 688 | Ok(off) => offset = off, 689 | } 690 | } 691 | 692 | self.notify_offset(Ok(offset), true)?; 693 | return Ok(()); 694 | } 695 | } 696 | 697 | impl Drop for Handle { 698 | fn drop(&mut self) { 699 | if self.cache_file.valid() { 700 | if let Err(e) = self.cache_file.close() { 701 | error!("!close(cache) = {}", RError::from(e)); 702 | } 703 | } 704 | 705 | if self.src_file.valid() { 706 | if let Err(e) = self.src_file.close() { 707 | error!("!close(src) = {}", RError::from(e)); 708 | } 709 | } 710 | } 711 | } 712 | 713 | impl Clone for Handle { 714 | fn clone(&self) -> Self { 715 | return Handle { 716 | src_file: File::with_fd(self.src_file.as_raw_fd()), 717 | cache_file: File::with_fd(self.cache_file.as_raw_fd()), 718 | dirty: self.dirty, 719 | write_through_failed: self.write_through_failed, 720 | has_page_in_thread: false, 721 | page_in_res: self.page_in_res.clone(), 722 | }; 723 | } 724 | } 725 | -------------------------------------------------------------------------------- /src/catfs/flags.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::ffi::OsString; 4 | use std::num::{ParseFloatError, ParseIntError}; 5 | use std::str::FromStr; 6 | 7 | #[derive(PartialEq)] 8 | #[derive(Clone)] 9 | #[derive(Debug)] 10 | pub enum DiskSpace { 11 | Percent(f64), 12 | Bytes(u64), 13 | } 14 | 15 | impl Default for DiskSpace { 16 | fn default() -> DiskSpace { 17 | DiskSpace::Bytes(0) 18 | } 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct DiskSpaceParseError(String); 23 | 24 | impl DiskSpaceParseError { 25 | pub fn to_str(&self) -> &str { 26 | &self.0 27 | } 28 | } 29 | 30 | impl From for DiskSpaceParseError { 31 | fn from(e: ParseIntError) -> DiskSpaceParseError { 32 | return DiskSpaceParseError(e.to_string()); 33 | } 34 | } 35 | 36 | impl From for DiskSpaceParseError { 37 | fn from(e: ParseFloatError) -> DiskSpaceParseError { 38 | return DiskSpaceParseError(e.to_string()); 39 | } 40 | } 41 | 42 | impl FromStr for DiskSpace { 43 | type Err = DiskSpaceParseError; 44 | 45 | fn from_str(s: &str) -> Result { 46 | if s.ends_with('%') { 47 | return Ok(DiskSpace::Percent(s[0..s.len() - 1].parse()?)); 48 | } else { 49 | // interpret it as a byte size 50 | let unit = match s.chars().last().unwrap() { 51 | 'T' => 1024 * 1024 * 1024 * 1024, 52 | 'G' => 1024 * 1024 * 1024, 53 | 'M' => 1024 * 1024, 54 | 'K' => 1024, 55 | '0'..='9' => 1, 56 | _ => return Err(DiskSpaceParseError("unrecognize unit in ".to_owned() + s)), 57 | }; 58 | if unit > 1 { 59 | return Ok(DiskSpace::Bytes(s[0..s.len() - 1].parse::()? * unit)); 60 | } else { 61 | return Ok(DiskSpace::Bytes(s.parse()?)); 62 | } 63 | } 64 | } 65 | } 66 | 67 | #[derive(Default)] 68 | pub struct FlagStorage { 69 | pub cat_from: OsString, 70 | pub cat_to: OsString, 71 | pub mount_point: OsString, 72 | pub mount_options: Vec, 73 | pub foreground: bool, 74 | pub free_space: DiskSpace, 75 | pub uid: libc::uid_t, 76 | pub gid: libc::gid_t, 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | 83 | #[test] 84 | fn parse() { 85 | assert_eq!( 86 | DiskSpace::from_str("25G").unwrap(), 87 | DiskSpace::Bytes(25 * 1024 * 1024 * 1024) 88 | ); 89 | assert_eq!(DiskSpace::from_str("25").unwrap(), DiskSpace::Bytes(25)); 90 | assert_eq!( 91 | DiskSpace::from_str("25%").unwrap(), 92 | DiskSpace::Percent(25.0) 93 | ); 94 | } 95 | 96 | #[test] 97 | #[should_panic] 98 | fn parse_negative() { 99 | DiskSpace::from_str("-25").unwrap(); 100 | } 101 | 102 | #[test] 103 | #[should_panic] 104 | fn parse_unknown_unit() { 105 | DiskSpace::from_str("25W").unwrap(); 106 | } 107 | 108 | #[test] 109 | #[should_panic] 110 | #[allow(non_snake_case)] 111 | fn parse_NaN() { 112 | DiskSpace::from_str("CAT").unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/catfs/inode.rs: -------------------------------------------------------------------------------- 1 | extern crate fuse; 2 | extern crate libc; 3 | extern crate threadpool; 4 | extern crate time; 5 | 6 | use self::threadpool::ThreadPool; 7 | use self::time::{Duration, Timespec}; 8 | 9 | use std::ffi::OsStr; 10 | use std::ffi::OsString; 11 | use std::io; 12 | use std::os::unix::io::RawFd; 13 | use std::path::{Path, PathBuf}; 14 | use std::sync::Mutex; 15 | 16 | use catfs::dir; 17 | use catfs::error; 18 | use catfs::file; 19 | use catfs::rlibc; 20 | use catfs::rlibc::File; 21 | 22 | #[derive(Clone)] 23 | pub struct Inode { 24 | src_dir: RawFd, 25 | cache_dir: RawFd, 26 | 27 | name: OsString, 28 | path: PathBuf, 29 | 30 | attr: fuse::FileAttr, 31 | time: Timespec, 32 | cache_valid_if_present: bool, 33 | flush_failed: bool, 34 | 35 | refcnt: u64, 36 | } 37 | 38 | fn to_filetype(t: libc::mode_t) -> fuse::FileType { 39 | match t & libc::S_IFMT { 40 | libc::S_IFLNK => fuse::FileType::Symlink, 41 | libc::S_IFREG => fuse::FileType::RegularFile, 42 | libc::S_IFBLK => fuse::FileType::BlockDevice, 43 | libc::S_IFDIR => fuse::FileType::Directory, 44 | libc::S_IFCHR => fuse::FileType::CharDevice, 45 | libc::S_IFIFO => fuse::FileType::NamedPipe, 46 | v => panic!("unknown type: {}", v), 47 | } 48 | } 49 | 50 | 51 | impl Inode { 52 | pub fn new( 53 | src_dir: RawFd, 54 | cache_dir: RawFd, 55 | name: OsString, 56 | path: PathBuf, 57 | attr: fuse::FileAttr, 58 | ) -> Inode { 59 | return Inode { 60 | src_dir: src_dir, 61 | cache_dir: cache_dir, 62 | name: name, 63 | path: path, 64 | attr: attr, 65 | time: time::get_time(), 66 | cache_valid_if_present: false, 67 | flush_failed: false, 68 | refcnt: 1, 69 | }; 70 | } 71 | 72 | pub fn take(&mut self, other: Inode) { 73 | self.attr = other.attr; 74 | self.time = other.time; 75 | } 76 | 77 | pub fn not_expired(&self, ttl: &Duration) -> bool { 78 | (time::get_time() - self.time) > *ttl 79 | } 80 | 81 | pub fn get_child_name(&self, name: &OsStr) -> PathBuf { 82 | let mut path = self.path.clone(); 83 | path.push(name); 84 | return path; 85 | } 86 | 87 | pub fn get_path(&self) -> &Path { 88 | return &self.path; 89 | } 90 | 91 | pub fn get_attr(&self) -> &fuse::FileAttr { 92 | return &self.attr; 93 | } 94 | 95 | pub fn get_kind(&self) -> fuse::FileType { 96 | return self.attr.kind; 97 | } 98 | 99 | pub fn get_ino(&self) -> u64 { 100 | return self.attr.ino; 101 | } 102 | 103 | pub fn extend(&mut self, offset: u64) { 104 | if self.attr.size < offset { 105 | self.attr.size = offset; 106 | } 107 | } 108 | 109 | pub fn lookup_path(dir: RawFd, path: &dyn AsRef) -> io::Result { 110 | let st = rlibc::fstatat(dir, path)?; 111 | let attr = fuse::FileAttr { 112 | ino: st.st_ino, 113 | size: st.st_size as u64, 114 | blocks: st.st_blocks as u64, 115 | atime: Timespec { 116 | sec: st.st_atime as i64, 117 | nsec: st.st_atime_nsec as i32, 118 | }, 119 | mtime: Timespec { 120 | sec: st.st_mtime as i64, 121 | nsec: st.st_mtime_nsec as i32, 122 | }, 123 | ctime: Timespec { 124 | sec: st.st_ctime as i64, 125 | nsec: st.st_ctime_nsec as i32, 126 | }, 127 | crtime: Timespec { 128 | sec: st.st_ctime as i64, 129 | nsec: st.st_ctime_nsec as i32, 130 | }, 131 | kind: to_filetype(st.st_mode), 132 | perm: (st.st_mode & !libc::S_IFMT) as u16, 133 | nlink: st.st_nlink as u32, 134 | uid: st.st_uid, 135 | gid: st.st_gid, 136 | rdev: st.st_rdev as u32, 137 | flags: 0, 138 | }; 139 | return Ok(attr); 140 | } 141 | 142 | pub fn flushed(&mut self) { 143 | // we know that this file really exist now, demand more from the pristineness 144 | self.cache_valid_if_present = false; 145 | self.flush_failed = false; 146 | } 147 | 148 | pub fn refresh(&mut self) -> error::Result<()> { 149 | match Inode::lookup_path(self.src_dir, &self.path) { 150 | Ok(attr) => self.attr = attr, 151 | Err(e) => { 152 | if error::is_enoent(&e) { 153 | return Err(error::RError::propagate(e)); 154 | } else { 155 | return Err(error::RError::from(e)); 156 | } 157 | } 158 | } 159 | 160 | return Ok(()); 161 | } 162 | 163 | pub fn flush_failed(&mut self) { 164 | // we know that flush failed, demand more from the pristineness 165 | self.cache_valid_if_present = false; 166 | self.flush_failed = true; 167 | } 168 | 169 | pub fn was_flush_failed(&self) -> bool { 170 | self.flush_failed 171 | } 172 | 173 | pub fn lookup(&self, name: &OsStr) -> error::Result { 174 | let path = self.get_child_name(name); 175 | match Inode::lookup_path(self.src_dir, &path) { 176 | Ok(attr) => { 177 | return Ok(Inode::new( 178 | self.src_dir, 179 | self.cache_dir, 180 | name.to_os_string(), 181 | path, 182 | attr, 183 | )) 184 | } 185 | Err(e) => return error::propagate(e), 186 | } 187 | } 188 | 189 | pub fn create(&self, name: &OsStr, mode: libc::mode_t) -> error::Result<(Inode, file::Handle)> { 190 | let path = self.get_child_name(name); 191 | 192 | let flags = rlibc::O_WRONLY | rlibc::O_CREAT | rlibc::O_EXCL; 193 | 194 | let wh = file::Handle::create(self.src_dir, self.cache_dir, &path, flags, mode)?; 195 | 196 | let attr = Inode::lookup_path(self.src_dir, &path)?; 197 | let mut inode = Inode::new( 198 | self.src_dir, 199 | self.cache_dir, 200 | name.to_os_string(), 201 | path, 202 | attr, 203 | ); 204 | // we just created this file, it's gotta be valid 205 | inode.cache_valid_if_present = true; 206 | 207 | return Ok((inode, wh)); 208 | } 209 | 210 | pub fn open(&mut self, flags: u32, tp: &Mutex) -> error::Result { 211 | let f = file::Handle::open( 212 | self.src_dir, 213 | self.cache_dir, 214 | &self.path, 215 | flags, 216 | self.cache_valid_if_present, 217 | self.flush_failed, 218 | tp, 219 | )?; 220 | // Handle::open deletes the cache file if it was invalid, so 221 | // at this point it must be valid, even after we start writing to it 222 | self.cache_valid_if_present = true; 223 | return Ok(f); 224 | } 225 | 226 | pub fn reopen_src(&self, file: &mut file::Handle) -> error::Result<()> { 227 | file.reopen_src(self.src_dir, &self.path, self.cache_valid_if_present) 228 | } 229 | 230 | pub fn unlink(&self, name: &OsStr) -> io::Result<()> { 231 | return file::Handle::unlink(self.src_dir, self.cache_dir, &self.get_child_name(name)); 232 | } 233 | 234 | pub fn rename(&mut self, new_name: &OsStr, new_path: &dyn AsRef) -> error::Result<()> { 235 | // XXX emulate some sort of atomicity 236 | 237 | // rename src first because if it's a directory, underlining 238 | // filesystem may reject if it's non-empty, where as if it's 239 | // the cache it may not contain anything or may even not exist 240 | rlibc::renameat(self.src_dir, &self.path, new_path)?; 241 | // source is renamed and now rename what's in the 242 | // cache. If things fail here we are inconsistent. XXX 243 | // delete cache path (could be a dir) if we failed to 244 | // rename it 245 | if rlibc::existat(self.cache_dir, &self.path)? { 246 | if let Some(parent) = new_path.as_ref().parent() { 247 | file::mkdirat_all(self.cache_dir, &parent, 0o777)?; 248 | } 249 | rlibc::renameat(self.cache_dir, &self.path, new_path)?; 250 | } 251 | 252 | self.name = new_name.to_os_string(); 253 | self.path = new_path.as_ref().to_path_buf(); 254 | return Ok(()); 255 | } 256 | 257 | pub fn truncate(&mut self, size: u64) -> error::Result<()> { 258 | let mut f = File::openat(self.src_dir, &self.path, rlibc::O_WRONLY, 0)?; 259 | f.set_size(size)?; 260 | f.close()?; 261 | 262 | match File::openat(self.cache_dir, &self.path, rlibc::O_WRONLY, 0) { 263 | Ok(mut f) => { 264 | f.set_size(size)?; 265 | f.close()?; 266 | } 267 | Err(e) => { 268 | error::try_enoent(e)?; 269 | } 270 | } 271 | 272 | return Ok(()); 273 | } 274 | 275 | pub fn utimes(&self, atime: &Timespec, mtime: &Timespec, flags: u32) -> io::Result<()> { 276 | rlibc::utimensat(self.src_dir, &self.path, atime, mtime, flags) 277 | } 278 | 279 | pub fn chmod(&self, mode: libc::mode_t, flags: u32) -> io::Result<()> { 280 | rlibc::fchmodat(self.src_dir, &self.path, mode, flags)?; 281 | return Ok(()); 282 | } 283 | 284 | pub fn mkdir(&self, name: &OsStr, mode: libc::mode_t) -> error::Result { 285 | let path = self.get_child_name(name); 286 | 287 | rlibc::mkdirat(self.src_dir, &path, mode)?; 288 | 289 | let attr = Inode::lookup_path(self.src_dir, &path)?; 290 | let inode = Inode::new( 291 | self.src_dir, 292 | self.cache_dir, 293 | name.to_os_string(), 294 | path, 295 | attr, 296 | ); 297 | 298 | return Ok(inode); 299 | } 300 | 301 | pub fn rmdir(&self, name: &OsStr) -> io::Result<()> { 302 | return dir::Handle::rmdirat(self.src_dir, self.cache_dir, &self.get_child_name(name)); 303 | } 304 | 305 | pub fn opendir(&self) -> error::Result { 306 | return dir::Handle::openat(self.src_dir, &self.path); 307 | } 308 | 309 | pub fn use_ino(&mut self, ino: u64) { 310 | self.attr.ino = ino; 311 | } 312 | 313 | pub fn inc_ref(&mut self) -> u64 { 314 | self.refcnt += 1; 315 | return self.refcnt; 316 | } 317 | 318 | pub fn get_refcnt(&self) -> u64 { 319 | return self.refcnt; 320 | } 321 | 322 | // return stale 323 | pub fn deref(&mut self, n: u64) -> bool { 324 | if self.refcnt < n { 325 | panic!( 326 | "ino 0x{:016x} refcnt {} deref {}", 327 | self.attr.ino, 328 | self.refcnt, 329 | n 330 | ); 331 | } 332 | self.refcnt -= n; 333 | return self.refcnt == 0; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/catfs/rlibc.rs: -------------------------------------------------------------------------------- 1 | extern crate fuse; 2 | extern crate libc; 3 | extern crate time; 4 | extern crate xattr; 5 | 6 | use std::ffi::{CStr, CString, OsStr, OsString}; 7 | use std::fmt; 8 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 9 | use std::io; 10 | use std::mem::MaybeUninit; 11 | use std::path::Path; 12 | #[cfg(not(target_os = "macos"))] 13 | use std::ptr; 14 | use std::os::unix::io::AsRawFd; 15 | use std::os::unix::io::RawFd; 16 | use std::os::unix::fs::FileExt; 17 | 18 | use self::fuse::FileType; 19 | use self::time::Timespec; 20 | use self::xattr::FileExt as XattrFileExt; 21 | 22 | #[cfg(not(target_os = "macos"))] 23 | use self::libc::{fstat64, fstatvfs64, ftruncate64, open64, openat64, pread64, pwrite64, stat64, statvfs64}; 24 | #[cfg(target_os = "macos")] 25 | use self::libc::{fstat as fstat64, fstatvfs as fstatvfs64, ftruncate as ftruncate64, open as open64, openat as openat64, pread as pread64, pwrite as pwrite64, stat as stat64, statvfs as statvfs64}; 26 | 27 | use catfs::error; 28 | use catfs::error::RError; 29 | 30 | // libc defines these as i32 which means they can't naturally be OR'ed 31 | // with u32 32 | pub static O_ACCMODE: u32 = libc::O_ACCMODE as u32; 33 | pub static O_RDONLY: u32 = libc::O_RDONLY as u32; 34 | pub static O_WRONLY: u32 = libc::O_WRONLY as u32; 35 | pub static O_RDWR: u32 = libc::O_RDWR as u32; 36 | 37 | pub static O_CLOEXEC: u32 = libc::O_CLOEXEC as u32; 38 | pub static O_CREAT: u32 = libc::O_CREAT as u32; 39 | pub static O_EXCL: u32 = libc::O_EXCL as u32; 40 | // XXX for some reason this is not found 41 | //pub static O_PATH: u32 = libc::O_PATH as u32; 42 | #[allow(dead_code)] 43 | pub static O_PATH: u32 = 2097152; 44 | pub static O_TRUNC: u32 = libc::O_TRUNC as u32; 45 | 46 | pub fn to_cstring(path: &dyn AsRef) -> CString { 47 | let bytes = path.as_ref().as_os_str().to_os_string().into_vec(); 48 | return CString::new(bytes).unwrap(); 49 | } 50 | 51 | macro_rules! libc_wrap { 52 | ($( pub fn $name:ident($($arg:ident : $argtype:ty),*) $body:block )*) => ( 53 | $( 54 | pub fn $name($($arg : $argtype),*) -> io::Result<()> { 55 | let err: libc::c_int; 56 | unsafe { err = libc::$name($($arg),*) } 57 | match err { 58 | 0 => return Ok(()), 59 | _ => return Err(io::Error::last_os_error()), 60 | } 61 | } 62 | )* 63 | ); 64 | } 65 | 66 | libc_wrap!{ 67 | pub fn setuid(uid: libc::uid_t) {} 68 | pub fn setgid(gid: libc::gid_t) {} 69 | } 70 | 71 | pub fn opendir(path: &dyn AsRef) -> io::Result<*mut libc::DIR> { 72 | let s = to_cstring(path); 73 | let dh = unsafe { libc::opendir(s.as_ptr()) }; 74 | if dh.is_null() { 75 | return Err(io::Error::last_os_error()); 76 | } else { 77 | return Ok(dh); 78 | } 79 | } 80 | 81 | pub fn fdopendir(fd: RawFd) -> io::Result<*mut libc::DIR> { 82 | let dh = unsafe { libc::fdopendir(fd) }; 83 | if dh.is_null() { 84 | return Err(io::Error::last_os_error()); 85 | } else { 86 | return Ok(dh); 87 | } 88 | } 89 | 90 | pub fn closedir(dir: *mut libc::DIR) -> io::Result<()> { 91 | let err: libc::c_int; 92 | unsafe { err = libc::closedir(dir) } 93 | match err { 94 | 0 => return Ok(()), 95 | _ => return Err(io::Error::last_os_error()), 96 | } 97 | } 98 | 99 | pub fn seekdir(dir: *mut libc::DIR, loc: i64) { 100 | unsafe { 101 | libc::seekdir(dir, loc as libc::c_long); 102 | } 103 | } 104 | 105 | #[derive(Clone)] 106 | pub struct Dirent { 107 | pub en: libc::dirent, 108 | } 109 | 110 | impl Default for Dirent { 111 | #[cfg(not(target_os = "macos"))] 112 | fn default() -> Dirent { 113 | return Dirent { 114 | en: libc::dirent { 115 | d_ino: 0, 116 | d_off: 0, 117 | d_reclen: 0, 118 | d_type: libc::DT_REG, 119 | d_name: [0i8 as libc::c_char; 256], // FIXME: don't hardcode 256 120 | }, 121 | }; 122 | } 123 | #[cfg(target_os = "macos")] 124 | fn default() -> Dirent { 125 | return Dirent { 126 | en: libc::dirent { 127 | d_ino: 0, 128 | d_seekoff: 0, 129 | d_reclen: 0, 130 | d_type: libc::DT_REG, 131 | d_name: [0i8; 1024], // FIXME: don't hardcode 1024 132 | d_namlen: 0, 133 | }, 134 | }; 135 | } 136 | } 137 | 138 | impl fmt::Debug for Dirent { 139 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 140 | write!( 141 | f, 142 | "ino: {} type: {:?} name: {:?}", 143 | self.ino(), 144 | self.kind(), 145 | self.name() 146 | ) 147 | } 148 | } 149 | 150 | fn array_to_osstring(cslice: &[libc::c_char]) -> OsString { 151 | let s = unsafe { CStr::from_ptr(cslice.as_ptr()) }; 152 | return OsStr::from_bytes(s.to_bytes()).to_os_string(); 153 | } 154 | 155 | impl Dirent { 156 | pub fn ino(&self) -> u64 { 157 | return self.en.d_ino as u64; 158 | } 159 | pub fn off(&self) -> i64 { 160 | #[cfg(not(target_os = "macos"))] 161 | return self.en.d_off as i64; 162 | #[cfg(target_os = "macos")] 163 | return self.en.d_seekoff as i64; 164 | } 165 | pub fn kind(&self) -> fuse::FileType { 166 | match self.en.d_type { 167 | libc::DT_BLK => return FileType::BlockDevice, 168 | libc::DT_CHR => return FileType::CharDevice, 169 | libc::DT_DIR => return FileType::Directory, 170 | libc::DT_FIFO => return FileType::NamedPipe, 171 | libc::DT_LNK => return FileType::Symlink, 172 | _ => return FileType::RegularFile, 173 | } 174 | } 175 | pub fn name(&self) -> OsString { 176 | return array_to_osstring(&self.en.d_name); 177 | } 178 | } 179 | 180 | pub fn readdir(dir: *mut libc::DIR) -> io::Result> { 181 | let mut entry_p = MaybeUninit::::uninit(); 182 | let mut entry_pp = ptr::null_mut(); 183 | 184 | let err = unsafe { libc::readdir_r(dir, entry_p.as_mut_ptr(), &mut entry_pp) }; 185 | if err == 0 { 186 | if entry_pp == ptr::null_mut() { 187 | return Ok(None); 188 | } else { 189 | return Ok(Some(Dirent { en: unsafe { entry_p.assume_init() } })); 190 | } 191 | } else { 192 | return Err(io::Error::last_os_error()); 193 | } 194 | } 195 | 196 | pub fn mkdir(path: &dyn AsRef, mode: libc::mode_t) -> io::Result<()> { 197 | let s = to_cstring(path); 198 | let res = unsafe { libc::mkdir(s.as_ptr(), mode) }; 199 | if res < 0 { 200 | return Err(io::Error::last_os_error()); 201 | } else { 202 | return Ok(()); 203 | } 204 | } 205 | 206 | pub fn mkdirat(dir: RawFd, path: &dyn AsRef, mode: libc::mode_t) -> io::Result<()> { 207 | let s = to_cstring(path); 208 | let res = unsafe { libc::mkdirat(dir, s.as_ptr(), mode) }; 209 | if res < 0 { 210 | return Err(io::Error::last_os_error()); 211 | } else { 212 | return Ok(()); 213 | } 214 | } 215 | 216 | #[cfg(not(target_os = "macos"))] 217 | pub fn pipe() -> io::Result<(libc::c_int, libc::c_int)> { 218 | let mut p = [0; 2]; 219 | let res = unsafe { libc::pipe2(p.as_mut_ptr(), libc::O_CLOEXEC) }; 220 | if res < 0 { 221 | return Err(io::Error::last_os_error()); 222 | } else { 223 | return Ok((p[0], p[1])); 224 | } 225 | } 226 | 227 | #[cfg(not(target_os = "macos"))] 228 | pub fn splice( 229 | fd: libc::c_int, 230 | off_self: i64, 231 | other: libc::c_int, 232 | off_other: i64, 233 | len: usize, 234 | ) -> io::Result { 235 | let mut off_from = off_self; 236 | let mut off_to = off_other; 237 | 238 | let off_from_ptr = if off_from == -1 { 239 | ptr::null() 240 | } else { 241 | &mut off_from 242 | } as *mut i64; 243 | let off_to_ptr = if off_to == -1 { 244 | ptr::null() 245 | } else { 246 | &mut off_to 247 | } as *mut i64; 248 | 249 | let res = unsafe { libc::splice(fd, off_from_ptr, other, off_to_ptr, len, 0) }; 250 | if res < 0 { 251 | return Err(io::Error::last_os_error()); 252 | } else { 253 | return Ok(res as usize); 254 | } 255 | } 256 | 257 | pub fn close(fd: libc::c_int) -> io::Result<()> { 258 | let res = unsafe { libc::close(fd) }; 259 | if res < 0 { 260 | return Err(io::Error::last_os_error()); 261 | } else { 262 | return Ok(()); 263 | } 264 | } 265 | 266 | pub fn unlinkat(dir: RawFd, path: &dyn AsRef, flags: u32) -> io::Result<()> { 267 | let s = to_cstring(path); 268 | let res = unsafe { libc::unlinkat(dir, s.as_ptr(), flags as i32) }; 269 | if res < 0 { 270 | return Err(io::Error::last_os_error()); 271 | } else { 272 | return Ok(()); 273 | } 274 | } 275 | 276 | pub fn existat(dir: RawFd, path: &dyn AsRef) -> error::Result { 277 | if let Err(e) = fstatat(dir, path) { 278 | if error::try_enoent(e)? { 279 | return Ok(false); 280 | } 281 | } 282 | 283 | return Ok(true); 284 | } 285 | 286 | pub fn renameat(dir: RawFd, path: &dyn AsRef, newpath: &dyn AsRef) -> error::Result<()> { 287 | let s = to_cstring(path); 288 | let new_s = to_cstring(newpath); 289 | 290 | let res = unsafe { libc::renameat(dir, s.as_ptr(), dir, new_s.as_ptr()) }; 291 | if res < 0 { 292 | // rename(2): "On NFS filesystems, you can not assume that 293 | // if the operation failed, the file was not renamed" 294 | if existat(dir, path)? { 295 | // rename actually worked 296 | return Ok(()); 297 | } else { 298 | return Err(RError::from(io::Error::last_os_error())); 299 | } 300 | } else { 301 | return Ok(()); 302 | } 303 | } 304 | 305 | pub fn fstat(fd: libc::c_int) -> io::Result { 306 | let mut st = MaybeUninit::::uninit(); 307 | 308 | let res = unsafe { fstat64(fd, st.as_mut_ptr()) }; 309 | if res < 0 { 310 | return Err(io::Error::last_os_error()); 311 | } else { 312 | return Ok(unsafe { st.assume_init() }); 313 | } 314 | } 315 | 316 | pub fn fstatat(dir: RawFd, path: &dyn AsRef) -> io::Result { 317 | let mut st = MaybeUninit::::uninit(); 318 | let s = to_cstring(path); 319 | 320 | #[cfg(not(target_os = "macos"))] 321 | let res = unsafe { libc::fstatat64(dir, s.as_ptr(), st.as_mut_ptr(), libc::AT_EMPTY_PATH) }; 322 | #[cfg(target_os = "macos")] 323 | let res = unsafe { libc::fstatat(dir, s.as_ptr(), st.as_mut_ptr(), 0) }; 324 | 325 | if res < 0 { 326 | return Err(io::Error::last_os_error()); 327 | } else { 328 | return Ok(unsafe { st.assume_init() }); 329 | } 330 | } 331 | 332 | pub fn fstatvfs(fd: RawFd) -> io::Result { 333 | let mut st = MaybeUninit::::uninit(); 334 | let res = unsafe { fstatvfs64(fd, st.as_mut_ptr()) }; 335 | if res < 0 { 336 | return Err(io::Error::last_os_error()); 337 | } else { 338 | return Ok(unsafe { st.assume_init() }); 339 | } 340 | } 341 | 342 | pub fn openat(dir: RawFd, path: &dyn AsRef, flags: u32, mode: libc::mode_t) -> io::Result { 343 | let s = to_cstring(path); 344 | let fd = unsafe { openat64(dir, s.as_ptr(), (flags | O_CLOEXEC) as i32, mode as libc::c_uint) }; 345 | if fd == -1 { 346 | return Err(io::Error::last_os_error()); 347 | } else { 348 | return Ok(fd); 349 | } 350 | } 351 | 352 | #[allow(dead_code)] 353 | pub fn utimes(path: &dyn AsRef, atime: libc::time_t, mtime: libc::time_t) -> io::Result<()> { 354 | let s = to_cstring(path); 355 | let atv = libc::timeval { tv_sec: atime, tv_usec: 0 }; 356 | let mtv = libc::timeval { tv_sec: mtime, tv_usec: 0 }; 357 | let res = unsafe { libc::utimes(s.as_ptr(), [atv, mtv].as_ptr()) }; 358 | if res == 0 { 359 | return Ok(()); 360 | } else { 361 | return Err(io::Error::last_os_error()); 362 | } 363 | } 364 | 365 | pub fn utimensat( 366 | dir: RawFd, 367 | path: &dyn AsRef, 368 | atime: &Timespec, 369 | mtime: &Timespec, 370 | flags: u32, 371 | ) -> io::Result<()> { 372 | let s = to_cstring(path); 373 | let mut times = [ 374 | libc::timespec { 375 | tv_sec: atime.sec as libc::time_t, 376 | tv_nsec: atime.nsec as libc::c_long, 377 | }, 378 | libc::timespec { 379 | tv_sec: mtime.sec as libc::time_t, 380 | tv_nsec: mtime.nsec as libc::c_long, 381 | }, 382 | ]; 383 | 384 | let res = unsafe { libc::utimensat(dir, s.as_ptr(), times.as_mut_ptr(), flags as i32) }; 385 | if res == 0 { 386 | return Ok(()); 387 | } else { 388 | return Err(io::Error::last_os_error()); 389 | } 390 | } 391 | 392 | pub fn fchmodat(dir: RawFd, path: &dyn AsRef, mode: libc::mode_t, flags: u32) -> io::Result<()> { 393 | let s = to_cstring(path); 394 | let res = unsafe { libc::fchmodat(dir, s.as_ptr(), mode, flags as i32) }; 395 | if res == 0 { 396 | return Ok(()); 397 | } else { 398 | return Err(io::Error::last_os_error()); 399 | } 400 | } 401 | 402 | pub struct File { 403 | fd: libc::c_int, 404 | } 405 | 406 | fn as_void_ptr(s: &[T]) -> *const libc::c_void { 407 | return s.as_ptr() as *const libc::c_void; 408 | } 409 | 410 | fn as_mut_void_ptr(s: &mut [T]) -> *mut libc::c_void { 411 | return s.as_mut_ptr() as *mut libc::c_void; 412 | } 413 | 414 | pub fn open(path: &dyn AsRef, flags: u32, mode: u32) -> io::Result { 415 | let s = to_cstring(path); 416 | let fd = unsafe { open64(s.as_ptr(), (flags | O_CLOEXEC) as i32, mode as libc::c_uint) }; 417 | if fd == -1 { 418 | return Err(io::Error::last_os_error()); 419 | } else { 420 | return Ok(fd); 421 | } 422 | } 423 | 424 | impl File { 425 | pub fn openat(dir: RawFd, path: &dyn AsRef, flags: u32, mode: libc::mode_t) -> io::Result { 426 | let fd = openat(dir, path, flags, mode)?; 427 | debug!( 428 | "<-- openat {:?} {:b} {:#o} = {}", 429 | path.as_ref(), 430 | flags, 431 | mode, 432 | fd 433 | ); 434 | return Ok(File { fd: fd }); 435 | } 436 | 437 | #[allow(dead_code)] 438 | pub fn open(path: &dyn AsRef, flags: u32, mode: u32) -> io::Result { 439 | let fd = open(path, flags, mode)?; 440 | debug!( 441 | "<-- open {:?} {:b} {:#o} = {}", 442 | path.as_ref(), 443 | flags, 444 | mode, 445 | fd 446 | ); 447 | return Ok(File { fd: fd }); 448 | } 449 | 450 | pub fn with_fd(fd: libc::c_int) -> File { 451 | return File { fd: fd }; 452 | } 453 | 454 | pub fn valid(&self) -> bool { 455 | return self.fd != -1; 456 | } 457 | 458 | pub fn filesize(&self) -> io::Result { 459 | let st = fstat(self.fd)?; 460 | return Ok(st.st_size as u64); 461 | } 462 | 463 | pub fn stat(&self) -> io::Result { 464 | fstat(self.fd) 465 | } 466 | 467 | pub fn truncate(&self, size: u64) -> io::Result<()> { 468 | let res = unsafe { ftruncate64(self.fd, size as i64) }; 469 | if res < 0 { 470 | return Err(io::Error::last_os_error()); 471 | } else { 472 | return Ok(()); 473 | } 474 | } 475 | 476 | #[cfg(not(target_os = "macos"))] 477 | pub fn allocate(&self, offset: u64, len: u64) -> io::Result<()> { 478 | let res = unsafe { libc::posix_fallocate64(self.fd, offset as i64, len as i64) }; 479 | if res == 0 { 480 | return Ok(()); 481 | } else { 482 | return Err(io::Error::from_raw_os_error(res)); 483 | } 484 | } 485 | 486 | #[cfg(target_os = "macos")] 487 | pub fn allocate(&self, offset: u64, len: u64) -> io::Result<()> { 488 | self.truncate(offset + len) 489 | } 490 | 491 | #[allow(dead_code)] 492 | pub fn set_size(&self, size: u64) -> error::Result<()> { 493 | let old_size = self.filesize()?; 494 | 495 | if let Err(e) = self.truncate(size) { 496 | if size > old_size && e.raw_os_error().unwrap() == libc::EPERM { 497 | self.allocate(old_size as u64, size - old_size)?; 498 | } else { 499 | return Err(RError::from(e)); 500 | } 501 | } 502 | 503 | return Ok(()); 504 | } 505 | 506 | pub fn chmod(&self, mode: libc::mode_t) -> io::Result<()> { 507 | let res = unsafe { libc::fchmod(self.fd, mode) }; 508 | if res == 0 { 509 | return Ok(()); 510 | } else { 511 | return Err(io::Error::from_raw_os_error(res)); 512 | } 513 | } 514 | 515 | pub fn read_at(&self, buf: &mut [u8], offset: i64) -> io::Result { 516 | let nbytes = 517 | unsafe { pread64(self.fd, as_mut_void_ptr(buf), buf.len(), offset) }; 518 | if nbytes < 0 { 519 | return Err(io::Error::last_os_error()); 520 | } else { 521 | return Ok(nbytes as usize); 522 | } 523 | } 524 | 525 | pub fn write_at(&self, buf: &[u8], offset: i64) -> io::Result { 526 | let nbytes = unsafe { pwrite64(self.fd, as_void_ptr(buf), buf.len(), offset) }; 527 | if nbytes < 0 { 528 | return Err(io::Error::last_os_error()); 529 | } else { 530 | return Ok(nbytes as usize); 531 | } 532 | } 533 | 534 | pub fn flush(&self) -> io::Result<()> { 535 | debug!("flush {}", self.fd); 536 | // trigger a flush for the underly fd, this could be called 537 | // multiple times, for ex: 538 | // 539 | // int fd2 = dup(fd); close(fd2); close(fd) 540 | // 541 | // so the fd needs to stay valid. Note that this means when an 542 | // application sends close(), kernel will send us 543 | // flush()/release(), and we will send close()/close(), which 544 | // will be translated to flush()/flush()/release() to the 545 | // underlining filesystem 546 | let fd = unsafe { libc::dup(self.fd) }; 547 | if fd < 0 { 548 | return Err(io::Error::last_os_error()); 549 | } else { 550 | let res = unsafe { libc::close(fd) }; 551 | if res < 0 { 552 | return Err(io::Error::last_os_error()); 553 | } else { 554 | return Ok(()); 555 | } 556 | } 557 | } 558 | 559 | pub fn close(&mut self) -> io::Result<()> { 560 | let res = unsafe { libc::close(self.fd) }; 561 | self.fd = -1; 562 | if res < 0 { 563 | return Err(io::Error::last_os_error()); 564 | } else { 565 | return Ok(()); 566 | } 567 | } 568 | 569 | pub fn as_raw_fd(&self) -> RawFd { 570 | if !self.valid() { 571 | error!("as_raw_fd called on invalid fd"); 572 | } 573 | 574 | return self.fd; 575 | } 576 | 577 | pub fn into_raw(&mut self) -> RawFd { 578 | let fd = self.fd; 579 | self.fd = -1; 580 | fd 581 | } 582 | } 583 | 584 | impl Default for File { 585 | fn default() -> File { 586 | File { fd: -1 } 587 | } 588 | } 589 | 590 | impl Drop for File { 591 | fn drop(&mut self) { 592 | if self.fd != -1 { 593 | error!( 594 | "{} dropped but not closed: {}", 595 | self.fd, 596 | RError::from(io::Error::from_raw_os_error(libc::EIO)) 597 | ); 598 | if let Err(e) = self.close() { 599 | error!("!close({}) = {}", self.fd, RError::from(e)); 600 | } 601 | } 602 | } 603 | } 604 | 605 | impl FileExt for File { 606 | fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { 607 | File::read_at(self, buf, offset as i64) 608 | } 609 | 610 | fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { 611 | File::write_at(self, buf, offset as i64) 612 | } 613 | } 614 | 615 | impl AsRawFd for File { 616 | fn as_raw_fd(&self) -> RawFd { 617 | File::as_raw_fd(self) 618 | } 619 | } 620 | 621 | impl XattrFileExt for File {} 622 | -------------------------------------------------------------------------------- /src/catfs/substr.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::os::unix::ffi::OsStrExt; 3 | use std::path::Path; 4 | 5 | pub trait Substr { 6 | fn substr(&self, begin_inclusive: usize, end_exclusive: usize) -> &T; 7 | } 8 | 9 | impl Substr for OsStr { 10 | fn substr(&self, begin_inclusive: usize, end_exclusive: usize) -> &OsStr { 11 | OsStr::from_bytes(&self.as_bytes()[begin_inclusive..end_exclusive]) 12 | } 13 | } 14 | 15 | impl Substr for Path { 16 | fn substr(&self, begin_inclusive: usize, end_exclusive: usize) -> &Path { 17 | Path::new(self.as_os_str().substr(begin_inclusive, end_exclusive)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/catfs/tests.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | 3 | use std::env; 4 | use std::fs; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use self::rand::{thread_rng, Rng}; 8 | use catfs::error; 9 | 10 | #[allow(dead_code)] 11 | fn copy_all(dir1: &dyn AsRef, dir2: &dyn AsRef) -> error::Result<()> { 12 | fs::create_dir(dir2)?; 13 | 14 | for entry in fs::read_dir(dir1)? { 15 | let entry = entry?; 16 | let to = dir2.as_ref().join(entry.file_name()); 17 | 18 | if entry.file_type()?.is_dir() { 19 | copy_all(&entry.path(), &to)?; 20 | } else { 21 | fs::copy(entry.path(), to)?; 22 | } 23 | } 24 | 25 | return Ok(()); 26 | } 27 | 28 | 29 | #[allow(dead_code)] 30 | pub fn copy_resources() -> PathBuf { 31 | let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); 32 | let resources = manifest.join("tests/resources"); 33 | 34 | let prefix = manifest.join("target/test").join( 35 | thread_rng() 36 | .gen_ascii_chars() 37 | .take(10) 38 | .collect::(), 39 | ); 40 | 41 | fs::create_dir_all(&prefix).unwrap(); 42 | 43 | copy_all(&resources, &prefix.join("resources")).unwrap(); 44 | return prefix; 45 | } 46 | -------------------------------------------------------------------------------- /src/evicter/dir_walker.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::path::{Path, PathBuf}; 4 | use std::ptr; 5 | use std::os::unix::io::RawFd; 6 | 7 | use catfs::error; 8 | use catfs::rlibc; 9 | 10 | pub struct DirWalker { 11 | dir: RawFd, 12 | cur: *mut libc::DIR, 13 | cur_path: PathBuf, 14 | stack: Vec, 15 | } 16 | 17 | impl DirWalker { 18 | pub fn new(dir: RawFd) -> error::Result { 19 | let fd = rlibc::openat(dir, &".", rlibc::O_RDONLY, 0)?; 20 | Ok(DirWalker { 21 | dir: dir, 22 | cur: rlibc::fdopendir(fd)?, 23 | cur_path: Default::default(), 24 | stack: Default::default(), 25 | }) 26 | } 27 | 28 | fn next_internal(&mut self) -> error::Result> { 29 | loop { 30 | match rlibc::readdir(self.cur)? { 31 | Some(entry) => { 32 | if entry.en.d_type == libc::DT_DIR { 33 | let name = entry.name(); 34 | if name != Path::new(".") && name != Path::new("..") { 35 | self.stack.push(self.cur_path.join(entry.name())); 36 | } 37 | } else { 38 | return Ok(Some(self.cur_path.join(entry.name()))); 39 | } 40 | } 41 | None => { 42 | rlibc::closedir(self.cur)?; 43 | self.cur = ptr::null_mut(); 44 | 45 | if let Some(next) = self.stack.pop() { 46 | let fd = rlibc::openat(self.dir, &next, rlibc::O_RDONLY, 0)?; 47 | self.cur = rlibc::fdopendir(fd)?; 48 | self.cur_path = next; 49 | } else { 50 | return Ok(None); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | impl Drop for DirWalker { 59 | fn drop(&mut self) { 60 | if !self.cur.is_null() { 61 | if let Err(e) = rlibc::closedir(self.cur) { 62 | error!("!closedir {:?} = {}", self.cur, e); 63 | } 64 | } 65 | } 66 | } 67 | 68 | impl Iterator for DirWalker { 69 | type Item = PathBuf; 70 | 71 | fn next(&mut self) -> Option { 72 | match self.next_internal() { 73 | Ok(item) => item, 74 | Err(e) => { 75 | error!("!DirWalker::next {:?} = {}", self.cur, e); 76 | None 77 | } 78 | } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | extern crate env_logger; 85 | use std::env; 86 | use std::path::{Path, PathBuf}; 87 | use catfs::rlibc; 88 | use super::*; 89 | 90 | #[test] 91 | fn iterator_test() { 92 | let _ = env_logger::init(); 93 | 94 | let manifest = env::var_os("CARGO_MANIFEST_DIR").unwrap(); 95 | let resources = PathBuf::from(manifest).join("tests/resources"); 96 | let fd = rlibc::open(&resources, rlibc::O_RDONLY, 0).unwrap(); 97 | let mut files: Vec = DirWalker::new(fd).unwrap().collect(); 98 | files.sort(); 99 | 100 | assert_eq!(files.len(), 5); 101 | 102 | let mut iter = files.into_iter(); 103 | assert_eq!(iter.next().unwrap(), Path::new("dir1/file1")); 104 | assert_eq!(iter.next().unwrap(), Path::new("dir1/file2")); 105 | assert_eq!(iter.next().unwrap(), Path::new("file1")); 106 | assert_eq!(iter.next().unwrap(), Path::new("file2")); 107 | assert_eq!(iter.next().unwrap(), Path::new("file3")); 108 | assert_eq!(iter.next(), None); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/evicter/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate itertools; 2 | extern crate libc; 3 | extern crate rand; 4 | extern crate twox_hash; 5 | 6 | use std::collections::HashSet; 7 | use std::hash::{BuildHasherDefault, Hash, Hasher}; 8 | use std::io; 9 | use std::mem; 10 | use std::os::unix::io::RawFd; 11 | use std::path::Path; 12 | use std::sync::{Arc, Condvar, Mutex}; 13 | use std::thread; 14 | use std::thread::JoinHandle; 15 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 16 | 17 | use catfs; 18 | use catfs::flags::DiskSpace; 19 | use catfs::error; 20 | use catfs::rlibc; 21 | 22 | pub mod dir_walker; 23 | use self::dir_walker::DirWalker; 24 | use self::itertools::Itertools; 25 | use self::twox_hash::XxHash; 26 | 27 | #[cfg(not(target_os = "macos"))] 28 | use self::libc::statvfs64; 29 | #[cfg(target_os = "macos")] 30 | use self::libc::{statvfs as statvfs64}; 31 | 32 | pub struct Evicter { 33 | dir: RawFd, 34 | high_watermark: DiskSpace, 35 | low_watermark: DiskSpace, 36 | scan_freq: Duration, 37 | hot_percent: usize, // 25 to keep most recently used 25% 38 | request_weight: u32, 39 | statvfs: fn(RawFd) -> io::Result, 40 | cv: Arc, 41 | shutting_down: Arc>, 42 | t: Option>, 43 | } 44 | 45 | struct EvictItem { 46 | hash: u64, 47 | atime: SystemTime, 48 | size: usize, 49 | } 50 | 51 | impl EvictItem { 52 | fn new(dir: RawFd, path: &dyn AsRef) -> error::Result { 53 | let st = rlibc::fstatat(dir, path)?; 54 | 55 | Ok(EvictItem { 56 | hash: EvictItem::hash_of(path), 57 | size: (st.st_blocks * 512) as usize, 58 | atime: UNIX_EPOCH + Duration::new(st.st_atime as u64, st.st_atime_nsec as u32), 59 | }) 60 | } 61 | 62 | fn new_for_lookup(path: &dyn AsRef) -> EvictItem { 63 | EvictItem { 64 | hash: EvictItem::hash_of(path), 65 | size: Default::default(), 66 | atime: UNIX_EPOCH, 67 | } 68 | } 69 | 70 | fn hash_of(path: &dyn AsRef) -> u64 { 71 | let mut h = XxHash::with_seed(0); 72 | path.as_ref().hash(&mut h); 73 | h.finish() 74 | } 75 | } 76 | 77 | impl Hash for EvictItem { 78 | fn hash(&self, h: &mut H) { 79 | h.write_u64(self.hash); 80 | } 81 | } 82 | 83 | impl PartialEq for EvictItem { 84 | fn eq(&self, other: &EvictItem) -> bool { 85 | return self.hash == other.hash; 86 | } 87 | } 88 | 89 | impl Eq for EvictItem {} 90 | 91 | #[derive(Default)] 92 | struct IdentU64Hasher(u64); 93 | 94 | impl Hasher for IdentU64Hasher { 95 | fn finish(&self) -> u64 { 96 | self.0 97 | } 98 | fn write(&mut self, _b: &[u8]) { 99 | panic!("use write_u64 instead"); 100 | } 101 | fn write_u64(&mut self, v: u64) { 102 | self.0 = v; 103 | } 104 | } 105 | 106 | // in blocks 107 | fn to_evict(spec: &DiskSpace, st: &statvfs64) -> u64 { 108 | let desired = match *spec { 109 | DiskSpace::Percent(p) => ((st.f_blocks as u64 * st.f_frsize as u64) as f64 * p / 100.0) as u64, 110 | DiskSpace::Bytes(b) => b, 111 | } as i64; 112 | 113 | let x = desired - (st.f_bfree as u64 * st.f_frsize as u64) as i64; 114 | return if x > 0 { x as u64 } else { 0 }; 115 | } 116 | 117 | impl Evicter { 118 | fn should_evict(&self, st: &statvfs64) -> u64 { 119 | return to_evict(&self.high_watermark, st); 120 | } 121 | 122 | fn to_evict(&self, st: &statvfs64) -> u64 { 123 | return to_evict(&self.low_watermark, st); 124 | } 125 | 126 | pub fn loop_once(&self) -> error::Result<()> { 127 | let st = (self.statvfs)(self.dir)?; 128 | 129 | let to_evict_bytes = self.should_evict(&st); 130 | debug!( 131 | "total: {} free: {} to_evict: {}", 132 | st.f_blocks, 133 | st.f_bfree, 134 | to_evict_bytes 135 | ); 136 | 137 | if to_evict_bytes > 0 { 138 | let to_evict_bytes = self.to_evict(&st); 139 | let mut evicted_bytes = 0; 140 | 141 | let mut items = DirWalker::new(self.dir)? 142 | .map(|x| EvictItem::new(self.dir, &x)) 143 | .map_results(Box::new) 144 | .fold_results(Box::new(Vec::new()), |mut v, x| { 145 | v.push(x); 146 | v 147 | })?; 148 | 149 | if items.is_empty() { 150 | return Ok(()); 151 | } 152 | 153 | items.sort_by_key(|x| x.atime); 154 | 155 | let mut total_size = 0u64; 156 | for i in 0..items.len() { 157 | total_size += items[i].size as u64; 158 | 159 | if total_size >= to_evict_bytes && 160 | i >= items.len() * (100 - self.hot_percent) / 100 161 | { 162 | items.truncate(i + 1); 163 | break; 164 | } 165 | } 166 | 167 | let now = SystemTime::now(); 168 | let oldest = now.duration_since(items[0].atime).unwrap().as_secs(); 169 | 170 | // now I have items that have not been accessed recently, 171 | // weight them according to size and age 172 | items.sort_by_key(|x| { 173 | let cost = x.size as u64 + self.request_weight as u64; 174 | let age = now.duration_since(x.atime).unwrap().as_secs(); 175 | if oldest == 0 { 176 | cost 177 | } else { 178 | cost * age / oldest 179 | } 180 | }); 181 | 182 | let mut candidates_to_evict = 0u64; 183 | 184 | type EvictItemSet = HashSet, BuildHasherDefault>; 185 | let mut item_set = EvictItemSet::default(); 186 | 187 | for i in items.into_iter().rev() { 188 | candidates_to_evict += i.size as u64; 189 | item_set.insert(i); 190 | 191 | if candidates_to_evict >= to_evict_bytes { 192 | break; 193 | } 194 | } 195 | 196 | DirWalker::new(self.dir)? 197 | .map(|p| (Box::new(EvictItem::new_for_lookup(&p)), p)) 198 | .foreach(|i| if let Some(item) = item_set.get(&i.0) { 199 | evicted_bytes += item.size; 200 | if let Err(e) = rlibc::unlinkat(self.dir, &i.1, 0) { 201 | debug!("wanted to evict {:?}={} but got {}", i.1, item.size, e); 202 | } else { 203 | debug!("evicting {:?}={}", i.1, item.size); 204 | } 205 | }); 206 | } 207 | 208 | return Ok(()); 209 | } 210 | 211 | pub fn new(dir: RawFd, free: &DiskSpace) -> Evicter { 212 | Evicter::new_internal(dir, free, Duration::from_secs(60), rlibc::fstatvfs) 213 | } 214 | 215 | pub fn run(&mut self) { 216 | if self.scan_freq != Default::default() && self.high_watermark != Default::default() { 217 | let evicter = catfs::make_self(self); 218 | let builder = thread::Builder::new().name(String::from("evicter")); 219 | 220 | self.t = Some( 221 | builder 222 | .spawn(move || loop { 223 | if let Err(e) = evicter.loop_once() { 224 | error!("evicter error: {}", e); 225 | } 226 | 227 | let guard = evicter.shutting_down.lock().unwrap(); 228 | let res = evicter.cv.wait_timeout(guard, evicter.scan_freq).unwrap(); 229 | if *res.0 { 230 | debug!("shutting down"); 231 | break; 232 | } 233 | }) 234 | .unwrap(), 235 | ); 236 | } 237 | } 238 | 239 | fn new_internal( 240 | dir: RawFd, 241 | free: &DiskSpace, 242 | scan_freq: Duration, 243 | statvfs: fn(RawFd) -> io::Result, 244 | ) -> Evicter { 245 | let mut ev = Evicter { 246 | dir: dir, 247 | high_watermark: free.clone(), 248 | low_watermark: Default::default(), 249 | scan_freq: scan_freq, 250 | hot_percent: 25, 251 | // modeling by the google nearline operation cost: 252 | // $0.01/10000 requests and $0.01/GB = 0.000001/r and 253 | // $.00000000000931322574/byte = 107374/r and 1/byte 254 | request_weight: 107374, 255 | statvfs: statvfs, 256 | cv: Arc::new(Condvar::new()), 257 | shutting_down: Arc::new(Mutex::new(false)), 258 | t: Default::default(), 259 | }; 260 | 261 | if ev.high_watermark != DiskSpace::Bytes(0) { 262 | if ev.low_watermark == DiskSpace::Bytes(0) { 263 | ev.low_watermark = match ev.high_watermark { 264 | DiskSpace::Percent(p) => DiskSpace::Percent((p * 1.1).min(100.0)), 265 | DiskSpace::Bytes(b) => DiskSpace::Bytes((b as f64 * 1.1) as u64), 266 | }; 267 | } 268 | 269 | } 270 | 271 | return ev; 272 | } 273 | } 274 | 275 | impl Drop for Evicter { 276 | fn drop(&mut self) { 277 | { 278 | let mut b = self.shutting_down.lock().unwrap(); 279 | *b = true; 280 | debug!("requesting to shutdown"); 281 | self.cv.notify_one(); 282 | } 283 | 284 | let mut t: Option> = None; 285 | 286 | mem::swap(&mut self.t, &mut t); 287 | 288 | if let Some(t) = t { 289 | t.join().expect("evictor panic"); 290 | } 291 | } 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | extern crate env_logger; 297 | use std::fs; 298 | use std::path::PathBuf; 299 | use catfs::rlibc; 300 | use super::*; 301 | use self::dir_walker::DirWalker; 302 | 303 | fn count_cache_size(dir: RawFd) -> error::Result { 304 | fn get_file_size(dir: RawFd, p: PathBuf) -> io::Result { 305 | Ok(512 * rlibc::fstatat(dir, &p)?.st_blocks as u64) 306 | } 307 | 308 | return Ok(DirWalker::new(dir)? 309 | .map(|p| get_file_size(dir, p)) 310 | .fold_results(0u64, |mut t, s| { 311 | t += s as u64; 312 | t 313 | })?); 314 | } 315 | 316 | #[test] 317 | fn count_cache() { 318 | let _ = env_logger::init(); 319 | let prefix = catfs::tests::copy_resources(); 320 | let fd = rlibc::open(&prefix, rlibc::O_RDONLY, 0).unwrap(); 321 | 322 | // each file takes 4K (8 blocks) minimum 323 | assert_eq!(count_cache_size(fd).unwrap(), 5 * 4096); 324 | fs::remove_dir_all(&prefix).unwrap(); 325 | } 326 | 327 | #[test] 328 | fn to_evict_bytes() { 329 | let mut st: statvfs64 = unsafe { mem::zeroed() }; 330 | st.f_bsize = 4096; 331 | st.f_frsize = 4096; 332 | st.f_blocks = 100; 333 | st.f_bfree = 16; 334 | 335 | assert_eq!(to_evict(&DiskSpace::Bytes(1), &st), 0); 336 | assert_eq!(to_evict(&DiskSpace::Bytes(512), &st), 0); 337 | assert_eq!(to_evict(&DiskSpace::Bytes(17 * 4096), &st), 4096); 338 | assert_eq!( 339 | to_evict(&DiskSpace::Bytes(50 * 4096), &st), 340 | (50 - 16) * 4096 341 | ); 342 | assert_eq!(to_evict(&DiskSpace::Percent(1.0), &st), 0); 343 | assert_eq!(to_evict(&DiskSpace::Percent(10.0), &st), 0); 344 | assert_eq!(to_evict(&DiskSpace::Percent(30.0), &st), (30 - 16) * 4096); 345 | } 346 | 347 | #[test] 348 | fn evict_none() { 349 | let _ = env_logger::init(); 350 | let prefix = catfs::tests::copy_resources(); 351 | let fd = rlibc::open(&prefix, rlibc::O_RDONLY, 0).unwrap(); 352 | 353 | fn fake_statvfs(_dir: RawFd) -> io::Result { 354 | let mut st: statvfs64 = unsafe { mem::zeroed() }; 355 | st.f_bsize = 4096; 356 | st.f_frsize = 4096; 357 | st.f_blocks = 10; 358 | st.f_bfree = 1; 359 | return Ok(st); 360 | } 361 | 362 | let ev = Evicter::new_internal(fd, &DiskSpace::Bytes(1), Default::default(), fake_statvfs); 363 | let used = count_cache_size(fd).unwrap(); 364 | ev.loop_once().unwrap(); 365 | assert_eq!(count_cache_size(fd).unwrap(), used); 366 | fs::remove_dir_all(&prefix).unwrap(); 367 | } 368 | 369 | #[test] 370 | fn evict_one() { 371 | let _ = env_logger::init(); 372 | let prefix = catfs::tests::copy_resources(); 373 | let fd = rlibc::open(&prefix, rlibc::O_RDONLY, 0).unwrap(); 374 | 375 | fn fake_statvfs(dir: RawFd) -> io::Result { 376 | let cache_size = count_cache_size(dir).unwrap(); 377 | 378 | let mut st: statvfs64 = unsafe { mem::zeroed() }; 379 | st.f_bsize = 4096; 380 | st.f_frsize = 4096; 381 | st.f_blocks = 100; 382 | // want 1 free block at beginning. cache_size is 5 * 4K blocks so pretend 383 | // 94 blocks are used by other things 384 | st.f_bfree = st.f_blocks as u64 - cache_size / (st.f_frsize as u64) - 94; 385 | return Ok(st); 386 | } 387 | 388 | let ev = Evicter::new_internal( 389 | fd, 390 | &DiskSpace::Bytes(4096 + 2048), 391 | Default::default(), 392 | fake_statvfs, 393 | ); 394 | 395 | let st = fake_statvfs(fd).unwrap(); 396 | assert_eq!(st.f_bfree, 1); 397 | assert_eq!(ev.should_evict(&st), 2048); 398 | let used = count_cache_size(fd).unwrap(); 399 | ev.loop_once().unwrap(); 400 | // evicted one file 401 | assert_eq!(used - count_cache_size(fd).unwrap(), 4096); 402 | fs::remove_dir_all(&prefix).unwrap(); 403 | } 404 | 405 | #[test] 406 | fn evict_all() { 407 | let _ = env_logger::init(); 408 | let prefix = catfs::tests::copy_resources(); 409 | let fd = rlibc::open(&prefix, rlibc::O_RDONLY, 0).unwrap(); 410 | 411 | fn fake_statvfs(dir: RawFd) -> io::Result { 412 | let cache_size = count_cache_size(dir).unwrap(); 413 | 414 | let mut st: statvfs64 = unsafe { mem::zeroed() }; 415 | st.f_bsize = 4096; 416 | st.f_frsize = 4096; 417 | st.f_blocks = 100; 418 | // want 1 free block at beginning. cache_size is 5 * 4K blocks so pretend 419 | // 94 blocks are used by other things 420 | st.f_bfree = st.f_blocks as u64 - cache_size / (st.f_frsize as u64) - 94; 421 | return Ok(st); 422 | } 423 | 424 | 425 | let ev = Evicter::new_internal( 426 | fd, 427 | &DiskSpace::Percent(100.0), 428 | Default::default(), 429 | fake_statvfs, 430 | ); 431 | 432 | let st = fake_statvfs(fd).unwrap(); 433 | assert_eq!(st.f_bfree, 1); 434 | assert_eq!(ev.low_watermark, DiskSpace::Percent(100.0)); 435 | assert_eq!(ev.should_evict(&st), 99 * 4096); 436 | ev.loop_once().unwrap(); 437 | // evicted one file 438 | assert_eq!(count_cache_size(fd).unwrap(), 0); 439 | fs::remove_dir_all(&prefix).unwrap(); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/flags.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate libc; 3 | 4 | use std::any::Any; 5 | use std::env; 6 | use std::ffi::OsString; 7 | 8 | use catfs::flags::DiskSpace; 9 | 10 | pub struct Flag<'a, 'b> { 11 | pub arg: clap::Arg<'a, 'a>, 12 | pub value: &'b mut dyn Any, 13 | } 14 | 15 | pub fn parse_options<'a, 'b>(mut app: clap::App<'a, 'a>, flags: &'b mut [Flag<'a, 'b>]) { 16 | for f in flags.iter() { 17 | app = app.arg(f.arg.clone()); 18 | } 19 | let mut argv = env::args_os().collect::>(); 20 | if argv.len() == 5 && argv[3] == OsString::from("-o") { 21 | // looks like it's coming from fstab! 22 | // [0]: catfs, [1] = src_dir#cache_dir, [2] = mnt 23 | // [3]: -o, [4] = opt1,opt2 24 | // XXX str has split but OsString doesn't 25 | 26 | // XXX2 this is more convoluted than necessary because paths 27 | // immutably borrows from argv and we can't modify elements of 28 | // argv again because that requires another mutable borrow 29 | 30 | // XXX3 need to initialize src/cache because the compiler is 31 | // not smart enough 32 | let mut src: OsString = Default::default(); 33 | let mut cache: OsString = Default::default(); 34 | let mut is_fstab = false; 35 | 36 | { 37 | let paths = argv[1].to_str().unwrap().splitn(2, '#').collect::>(); 38 | if paths.len() == 2 { 39 | src = OsString::from(paths[0]); 40 | cache = OsString::from(paths[1]); 41 | is_fstab = true; 42 | } 43 | } 44 | 45 | if is_fstab { 46 | argv[1] = src; 47 | argv.insert(2, cache); 48 | let mut options = String::new(); 49 | let mut arguments: Vec = Default::default(); 50 | 51 | // options are now pushed down 52 | for opt in argv[5].to_str().unwrap().split(',') { 53 | if opt.starts_with("-") { 54 | arguments.push(OsString::from(opt)); 55 | } else { 56 | options += opt; 57 | options += ","; 58 | } 59 | } 60 | 61 | if options.len() != 0 { 62 | options.pop(); 63 | argv[5] = OsString::from(options); 64 | } else { 65 | // no options, pop -o and empty string 66 | argv.pop(); 67 | argv.pop(); 68 | } 69 | for arg in arguments { 70 | argv.insert(3, arg); 71 | } 72 | } 73 | } 74 | 75 | let matches = app.get_matches_from(argv); 76 | 77 | for f in flags.iter_mut() { 78 | let name = f.arg.b.name; 79 | 80 | if matches.is_present(name) { 81 | // cannot use else if here or rust would claim double 82 | // mutable borrow because apparently a borrow sends with 83 | // the last else if 84 | if let Some(v) = f.value.downcast_mut::() { 85 | let s = matches.value_of(name).unwrap(); 86 | *v = String::from(s); 87 | continue; 88 | } 89 | if let Some(v) = f.value.downcast_mut::() { 90 | let s = matches.value_of_os(name).unwrap(); 91 | *v = s.to_os_string(); 92 | continue; 93 | } 94 | if let Some(v) = f.value.downcast_mut::() { 95 | *v = true; 96 | continue; 97 | } 98 | if let Some(v) = f.value.downcast_mut::>() { 99 | let options = matches.values_of(name).unwrap(); 100 | for s in options { 101 | for s in s.split(',') { 102 | v.push(OsString::from("-o")); 103 | v.push(OsString::from(s)); 104 | } 105 | } 106 | continue; 107 | } 108 | if let Some(v) = f.value.downcast_mut::() { 109 | let s = matches.value_of(name).unwrap(); 110 | *v = s.parse().unwrap(); 111 | continue; 112 | } 113 | if let Some(v) = f.value.downcast_mut::() { 114 | let s = matches.value_of(name).unwrap(); 115 | *v = s.parse().unwrap(); 116 | continue; 117 | } 118 | if let Some(v) = f.value.downcast_mut::() { 119 | let s = matches.value_of(name).unwrap(); 120 | *v = s.parse().unwrap(); 121 | continue; 122 | } 123 | 124 | panic!("unknown type for {}", name); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod catfs; 2 | pub mod evicter; 3 | pub mod pcatfs; 4 | #[macro_use] 5 | extern crate log; 6 | 7 | pub use catfs::CatFS; 8 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate chan_signal; 2 | #[macro_use] 3 | extern crate clap; 4 | extern crate daemonize; 5 | extern crate env_logger; 6 | extern crate fuse; 7 | extern crate libc; 8 | #[macro_use] 9 | extern crate log; 10 | extern crate syslog; 11 | extern crate time; 12 | 13 | use std::env; 14 | use std::ffi::{OsStr, OsString}; 15 | use std::os::unix::ffi::OsStrExt; 16 | use std::io; 17 | use std::path::Path; 18 | use std::str::FromStr; 19 | use std::sync::{Arc, Mutex}; 20 | use std::thread; 21 | 22 | use chan_signal::Signal; 23 | use clap::{App, Arg}; 24 | use daemonize::{Daemonize}; 25 | use env_logger::LogBuilder; 26 | use log::LogRecord; 27 | use syslog::{Facility,Severity}; 28 | 29 | mod pcatfs; 30 | mod catfs; 31 | mod flags; 32 | mod evicter; 33 | 34 | use catfs::error; 35 | use catfs::flags::{DiskSpace, FlagStorage}; 36 | use catfs::rlibc; 37 | 38 | fn main() { 39 | if let Err(e) = main_internal() { 40 | error!("Cannot mount: {}", e); 41 | std::process::exit(1); 42 | } 43 | } 44 | 45 | static mut SYSLOG: bool = false; 46 | static mut SYSLOGGER: Option> = None; 47 | 48 | fn main_internal() -> error::Result<()> { 49 | let format = |record: &LogRecord| { 50 | let t = time::now(); 51 | let syslog: bool; 52 | unsafe { 53 | syslog = SYSLOG; 54 | } 55 | if !syslog { 56 | format!( 57 | "{} {:5} - {}", 58 | time::strftime("%Y-%m-%d %H:%M:%S", &t).unwrap(), 59 | record.level(), 60 | record.args() 61 | ) 62 | } else { 63 | unsafe { 64 | if let Some(ref logger) = SYSLOGGER { 65 | let level = match record.level() { 66 | log::LogLevel::Trace => Severity::LOG_DEBUG, 67 | log::LogLevel::Debug => Severity::LOG_DEBUG, 68 | log::LogLevel::Info => Severity::LOG_INFO, 69 | log::LogLevel::Warn => Severity::LOG_WARNING, 70 | log::LogLevel::Error => Severity::LOG_ERR, 71 | }; 72 | let msg = format!("{}", record.args()); 73 | for line in msg.split('\n') { 74 | // ignore error if we can't log, not much we can do anyway 75 | let _ = logger.send_3164(level, line); 76 | } 77 | } 78 | } 79 | format!("\u{08}") 80 | } 81 | }; 82 | 83 | let mut builder = LogBuilder::new(); 84 | builder.format(format); 85 | 86 | if env::var("RUST_LOG").is_ok() { 87 | builder.parse(&env::var("RUST_LOG").unwrap()); 88 | } else { 89 | // default to info 90 | builder.parse("info"); 91 | } 92 | 93 | builder.init().unwrap(); 94 | 95 | let mut flags: FlagStorage = Default::default(); 96 | let mut test = false; 97 | 98 | flags.mount_options.push(OsString::from("-o")); 99 | flags.mount_options.push(OsString::from("atomic_o_trunc")); 100 | flags.mount_options.push(OsString::from("-o")); 101 | flags.mount_options.push( 102 | OsString::from("default_permissions"), 103 | ); 104 | 105 | let app = App::new("catfs") 106 | .about("Cache Anything FileSystem") 107 | .version(crate_version!()); 108 | 109 | { 110 | fn diskspace_validator(s: String) -> Result<(), String> { 111 | DiskSpace::from_str(&s).map(|_| ()).map_err( 112 | |e| e.to_str().to_owned(), 113 | ) 114 | } 115 | 116 | fn path_validator(s: String) -> Result<(), String> { 117 | Path::new(&s) 118 | .canonicalize() 119 | .map_err(|e| e.to_string().to_owned()) 120 | .and_then(|p| if p.is_dir() { 121 | Ok(()) 122 | } else { 123 | Err("is not a directory".to_owned()) 124 | }) 125 | } 126 | 127 | let mut args = [ 128 | flags::Flag { 129 | arg: Arg::with_name("space") 130 | .long("free") 131 | .takes_value(true) 132 | .help( 133 | "Ensure filesystem has at least this much free space. (ex: 9.5%, 10G)", 134 | ) 135 | .validator(diskspace_validator), 136 | value: &mut flags.free_space, 137 | }, 138 | flags::Flag { 139 | arg: Arg::with_name("foreground").short("f").help( 140 | "Run catfs in foreground.", 141 | ), 142 | value: &mut flags.foreground, 143 | }, 144 | flags::Flag{ 145 | arg: Arg::with_name("uid") 146 | .long("uid") 147 | .takes_value(true) 148 | .help("Run as this uid"), 149 | value: &mut flags.uid, 150 | }, 151 | flags::Flag{ 152 | arg: Arg::with_name("gid") 153 | .long("gid") 154 | .takes_value(true) 155 | .help("Run as this gid"), 156 | value: &mut flags.gid, 157 | }, 158 | flags::Flag { 159 | arg: Arg::with_name("option") 160 | .short("o") 161 | .takes_value(true) 162 | .multiple(true) 163 | .help("Additional system-specific mount options. Be careful!"), 164 | value: &mut flags.mount_options, 165 | }, 166 | flags::Flag { 167 | arg: Arg::with_name("test").long("test").help( 168 | "Exit after parsing arguments", 169 | ), 170 | value: &mut test, 171 | }, 172 | flags::Flag { 173 | arg: Arg::with_name("from") 174 | .index(1) 175 | .required(true) 176 | .help("Cache files from this directory.") 177 | .validator(path_validator), 178 | value: &mut flags.cat_from, 179 | }, 180 | flags::Flag { 181 | arg: Arg::with_name("to") 182 | .index(2) 183 | .required(true) 184 | .help("Cache files to this directory.") 185 | .validator(path_validator), 186 | value: &mut flags.cat_to, 187 | }, 188 | flags::Flag { 189 | arg: Arg::with_name("mountpoint") 190 | .index(3) 191 | .required(true) 192 | .help("Expose the mount point at this directory.") 193 | .validator(path_validator), 194 | value: &mut flags.mount_point, 195 | }, 196 | ]; 197 | 198 | 199 | flags::parse_options(app, &mut args); 200 | } 201 | 202 | if test { 203 | return Ok(()); 204 | } 205 | 206 | if flags.gid != 0 { 207 | rlibc::setgid(flags.gid)?; 208 | } 209 | if flags.uid != 0 { 210 | rlibc::setuid(flags.uid)?; 211 | } 212 | 213 | if !flags.foreground { 214 | let daemonize = Daemonize::new() 215 | .working_directory(env::current_dir()?.as_path()) 216 | ; 217 | 218 | match daemonize.start() { 219 | Ok(_) => { 220 | if let Ok(mut logger) = syslog::unix(Facility::LOG_USER) { 221 | unsafe { 222 | logger.set_process_name("catfs".to_string()); 223 | logger.set_process_id(libc::getpid()); 224 | SYSLOGGER = Some(logger); 225 | SYSLOG = true; 226 | } 227 | } 228 | }, 229 | Err(e) => error!("unable to daemonize: {}", e), 230 | } 231 | } 232 | 233 | let signal = chan_signal::notify(&[Signal::INT, Signal::TERM]); 234 | let path_from = Path::new(&flags.cat_from).canonicalize()?; 235 | let path_to = Path::new(&flags.cat_to).canonicalize()?; 236 | let fs = catfs::CatFS::new(&path_from, &path_to)?; 237 | let fs = pcatfs::PCatFS::new(fs); 238 | let cache_dir = fs.get_cache_dir()?; 239 | let mut options: Vec<&OsStr> = Vec::new(); 240 | for i in 0..flags.mount_options.len() { 241 | options.push(&flags.mount_options[i]); 242 | } 243 | 244 | debug!("options are {:?}", flags.mount_options); 245 | 246 | { 247 | let mut session = fuse::Session::new(fs, Path::new(&flags.mount_point), &options)?; 248 | let need_unmount = Arc::new(Mutex::new(true)); 249 | let need_unmount2 = need_unmount.clone(); 250 | thread::spawn(move || { 251 | if let Err(e) = session.run() { 252 | error!("session.run() = {}", e); 253 | } 254 | info!("{:?} unmounted", session.mountpoint()); 255 | let mut need_unmount = need_unmount2.lock().unwrap(); 256 | *need_unmount = false; 257 | unsafe { libc::kill(libc::getpid(), libc::SIGTERM) }; 258 | }); 259 | 260 | let mut ev = evicter::Evicter::new(cache_dir, &flags.free_space); 261 | ev.run(); 262 | // unmount after we get signaled becausep session will go out of scope 263 | let s = signal.recv().unwrap(); 264 | info!( 265 | "Received {:?}, attempting to unmount {:?}", 266 | s, 267 | flags.mount_point 268 | ); 269 | let need_unmount = need_unmount.lock().unwrap(); 270 | if *need_unmount { 271 | unmount(Path::new(&flags.mount_point))?; 272 | } 273 | } 274 | rlibc::close(cache_dir)?; 275 | return Ok(()); 276 | } 277 | 278 | use libc::{c_char, c_int}; 279 | use std::ffi::{CString, CStr}; 280 | /// Unmount an arbitrary mount point 281 | pub fn unmount(mountpoint: &Path) -> io::Result<()> { 282 | // fuse_unmount_compat22 unfortunately doesn't return a status. Additionally, 283 | // it attempts to call realpath, which in turn calls into the filesystem. So 284 | // if the filesystem returns an error, the unmount does not take place, with 285 | // no indication of the error available to the caller. So we call unmount 286 | // directly, which is what osxfuse does anyway, since we already converted 287 | // to the real path when we first mounted. 288 | 289 | #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly", 290 | target_os = "openbsd", target_os = "bitrig", target_os = "netbsd"))] 291 | #[inline] 292 | fn libc_umount(mnt: &CStr) -> c_int { 293 | unsafe { libc::unmount(mnt.as_ptr(), 0) } 294 | } 295 | 296 | #[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly", 297 | target_os = "openbsd", target_os = "bitrig", target_os = "netbsd")))] 298 | #[inline] 299 | fn libc_umount(mnt: &CStr) -> c_int { 300 | use std::io::ErrorKind::PermissionDenied; 301 | 302 | let rc = unsafe { libc::umount(mnt.as_ptr()) }; 303 | if rc < 0 && io::Error::last_os_error().kind() == PermissionDenied { 304 | // Linux always returns EPERM for non-root users. We have to let the 305 | // library go through the setuid-root "fusermount -u" to unmount. 306 | unsafe { 307 | fuse_unmount_compat22(mnt.as_ptr()); 308 | } 309 | 0 310 | } else { 311 | rc 312 | } 313 | } 314 | 315 | let mnt = CString::new(mountpoint.as_os_str().as_bytes())?; 316 | let rc = libc_umount(&mnt); 317 | if rc < 0 { 318 | Err(io::Error::last_os_error()) 319 | } else { 320 | Ok(()) 321 | } 322 | } 323 | 324 | extern "system" { 325 | pub fn fuse_unmount_compat22(mountpoint: *const c_char); 326 | } 327 | -------------------------------------------------------------------------------- /src/pcatfs/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate fuse; 2 | extern crate threadpool; 3 | extern crate time; 4 | 5 | use self::fuse::{Filesystem, Request, ReplyEntry, ReplyAttr, ReplyOpen, ReplyEmpty, 6 | ReplyDirectory, ReplyData, ReplyWrite, ReplyCreate, ReplyStatfs}; 7 | use self::threadpool::ThreadPool; 8 | use self::time::Timespec; 9 | 10 | use std::ffi::OsStr; 11 | use std::ops::Deref; 12 | 13 | use catfs::CatFS; 14 | 15 | pub struct PCatFS { 16 | tp: ThreadPool, 17 | fs: CatFS, 18 | } 19 | 20 | impl Drop for PCatFS { 21 | fn drop(&mut self) { 22 | self.tp.join(); 23 | } 24 | } 25 | 26 | pub fn make_self(s: &mut T) -> &'static mut T { 27 | return unsafe { ::std::mem::transmute(s) }; 28 | } 29 | 30 | impl PCatFS { 31 | pub fn new(fs: CatFS) -> PCatFS { 32 | PCatFS { 33 | tp: ThreadPool::new(100), 34 | fs: fs, 35 | } 36 | } 37 | } 38 | 39 | impl Deref for PCatFS { 40 | type Target = CatFS; 41 | 42 | fn deref(&self) -> &CatFS { 43 | &self.fs 44 | } 45 | } 46 | 47 | macro_rules! run_in_threadpool { 48 | ($( fn $name:ident(&mut self, _req: &Request, parent: u64, name: &OsStr, $($arg:ident : $argtype:ty),* $(,)*) $body:block )*) => ( 49 | $( 50 | fn $name(&mut self, _req: &Request, parent: u64, name: &OsStr, $($arg : $argtype),*) { 51 | let s = make_self(self); 52 | let name = name.to_os_string(); 53 | self.tp.execute( 54 | move || { 55 | s.fs.$name(parent, name, $($arg),*); 56 | debug!("queue size is {}", s.tp.queued_count()); 57 | } 58 | ); 59 | } 60 | )* 61 | ); 62 | ($( fn $name:ident(&mut self, _req: &Request, $($arg:ident : $argtype:ty),* $(,)*) $body:block )*) => ( 63 | $( 64 | fn $name(&mut self, _req: &Request, $($arg : $argtype),*) { 65 | let s = make_self(self); 66 | self.tp.execute( 67 | move || { 68 | s.fs.$name($($arg),*); 69 | debug!("queue size is {}", s.tp.queued_count()); 70 | } 71 | ); 72 | } 73 | )* 74 | ); 75 | } 76 | 77 | impl Filesystem for PCatFS { 78 | fn write( 79 | &mut self, 80 | _req: &Request, 81 | ino: u64, 82 | fh: u64, 83 | offset: i64, 84 | data: &[u8], 85 | _flags: u32, 86 | reply: ReplyWrite, 87 | ) { 88 | let s = make_self(self); 89 | let data = data.to_vec(); 90 | self.tp.execute(move || { 91 | s.fs.write(ino, fh, offset, data, _flags, reply); 92 | }); 93 | } 94 | 95 | 96 | fn rename( 97 | &mut self, 98 | _req: &Request, 99 | parent: u64, 100 | name: &OsStr, 101 | newparent: u64, 102 | newname: &OsStr, 103 | reply: ReplyEmpty, 104 | ) { 105 | let s = make_self(self); 106 | let name = name.to_os_string(); 107 | let newname = newname.to_os_string(); 108 | self.tp.execute(move || { 109 | s.fs.rename(parent, name, newparent, newname, reply); 110 | }); 111 | } 112 | 113 | fn forget(&mut self, _req: &Request, ino: u64, nlookup: u64) { 114 | self.fs.forget(ino, nlookup); 115 | } 116 | 117 | run_in_threadpool!{ 118 | fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { 119 | } 120 | 121 | fn setattr( 122 | &mut self, 123 | _req: &Request, 124 | ino: u64, 125 | mode: Option, 126 | uid: Option, 127 | gid: Option, 128 | size: Option, 129 | atime: Option, 130 | mtime: Option, 131 | fh: Option, 132 | crtime: Option, 133 | chgtime: Option, 134 | bkuptime: Option, 135 | flags: Option, 136 | reply: ReplyAttr, 137 | ) { 138 | } 139 | 140 | fn opendir(&mut self, _req: &Request, ino: u64, flags: u32, reply: ReplyOpen) { 141 | } 142 | 143 | fn readdir( 144 | &mut self, 145 | _req: &Request, 146 | _ino: u64, 147 | dh: u64, 148 | offset: i64, 149 | reply: ReplyDirectory, 150 | ) { 151 | } 152 | 153 | fn releasedir(&mut self, _req: &Request, _ino: u64, dh: u64, _flags: u32, reply: ReplyEmpty) { 154 | } 155 | 156 | fn open(&mut self, _req: &Request, ino: u64, flags: u32, reply: ReplyOpen) { 157 | } 158 | 159 | fn read( 160 | &mut self, 161 | _req: &Request, 162 | _ino: u64, 163 | fh: u64, 164 | offset: i64, 165 | size: u32, 166 | reply: ReplyData, 167 | ) { 168 | } 169 | 170 | fn flush(&mut self, _req: &Request, ino: u64, fh: u64, _lock_owner: u64, reply: ReplyEmpty) { 171 | } 172 | 173 | fn release( 174 | &mut self, 175 | _req: &Request, 176 | _ino: u64, 177 | fh: u64, 178 | _flags: u32, 179 | _lock_owner: u64, 180 | _flush: bool, 181 | reply: ReplyEmpty, 182 | ) { 183 | } 184 | 185 | fn statfs(&mut self, _req: &Request, ino: u64, reply: ReplyStatfs) { 186 | } 187 | } 188 | 189 | run_in_threadpool!{ 190 | fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { 191 | } 192 | 193 | fn create( 194 | &mut self, 195 | _req: &Request, 196 | parent: u64, 197 | name: &OsStr, 198 | mode: u32, 199 | flags: u32, 200 | reply: ReplyCreate, 201 | ) { 202 | } 203 | 204 | fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { 205 | } 206 | 207 | 208 | fn rmdir(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { 209 | } 210 | 211 | fn mkdir(&mut self, _req: &Request, parent: u64, name: &OsStr, mode: u32, reply: ReplyEntry) { 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | #[macro_use] 3 | extern crate log; 4 | extern crate libc; 5 | extern crate env_logger; 6 | extern crate fuse; 7 | extern crate time; 8 | extern crate xattr; 9 | 10 | use std::env; 11 | use std::ffi::{OsStr, OsString}; 12 | use std::fs; 13 | use std::fs::{File, OpenOptions}; 14 | use std::io; 15 | use std::io::{Read, Seek, Write}; 16 | use std::process::{Command, Stdio}; 17 | use std::path::{Path, PathBuf}; 18 | use std::os::unix::fs::FileExt; 19 | 20 | use env_logger::LogBuilder; 21 | use log::LogRecord; 22 | 23 | extern crate catfs; 24 | 25 | use catfs::CatFS; 26 | use catfs::catfs::error; 27 | use catfs::catfs::flags::DiskSpace; 28 | use catfs::catfs::file; 29 | use catfs::catfs::rlibc; 30 | use catfs::evicter::Evicter; 31 | use catfs::pcatfs::PCatFS; 32 | 33 | #[macro_use] 34 | mod test_suite; 35 | 36 | 37 | trait Fixture { 38 | fn setup() -> error::Result 39 | where 40 | Self: std::marker::Sized; 41 | fn init(&mut self) -> error::Result<()>; 42 | fn teardown(self) -> error::Result<()>; 43 | } 44 | 45 | struct CatFSTests<'a> { 46 | prefix: PathBuf, 47 | mnt: PathBuf, 48 | src: PathBuf, 49 | cache: PathBuf, 50 | session: Option>, 51 | evicter: Option, 52 | nested: Option>>, 53 | } 54 | 55 | impl<'a> CatFSTests<'a> { 56 | fn get_orig_dir() -> PathBuf { 57 | let manifest = env::var_os("CARGO_MANIFEST_DIR").unwrap(); 58 | return PathBuf::from(manifest).join("tests/resources"); 59 | } 60 | 61 | fn get_from(&self) -> PathBuf { 62 | return self.src.clone(); 63 | } 64 | 65 | fn get_cache(&self) -> PathBuf { 66 | return self.cache.clone(); 67 | } 68 | 69 | fn mount(&self) -> error::Result<(fuse::BackgroundSession<'a>, Evicter)> { 70 | let fs = CatFS::new(&self.src, &self.cache)?; 71 | let fs = PCatFS::new(fs); 72 | 73 | let cache_dir = fs.get_cache_dir()?; 74 | // essentially no-op, but ensures that it starts and terminates 75 | let ev = Evicter::new(cache_dir, &DiskSpace::Bytes(1)); 76 | return Ok((unsafe { fuse::spawn_mount(fs, &self.mnt, &[])? }, ev)); 77 | } 78 | 79 | fn assert_cache_valid(&self, path: &dyn AsRef) { 80 | let src_dir = rlibc::open(&self.src, rlibc::O_RDONLY, 0).unwrap(); 81 | let cache_dir = rlibc::open(&self.cache, rlibc::O_RDONLY, 0).unwrap(); 82 | 83 | assert!(file::Handle::validate_cache(src_dir, cache_dir, path, false, true).unwrap()); 84 | rlibc::close(src_dir).unwrap(); 85 | rlibc::close(cache_dir).unwrap(); 86 | } 87 | } 88 | 89 | impl<'a> Fixture for CatFSTests<'a> { 90 | fn setup() -> error::Result> { 91 | let format = |record: &LogRecord| { 92 | let t = time::now(); 93 | format!( 94 | "{} {:5} - {}", 95 | time::strftime("%Y-%m-%d %H:%M:%S", &t).unwrap(), 96 | record.level(), 97 | record.args() 98 | ) 99 | }; 100 | 101 | let mut builder = LogBuilder::new(); 102 | builder.format(format); 103 | 104 | if env::var("RUST_LOG").is_ok() { 105 | builder.parse(&env::var("RUST_LOG").unwrap()); 106 | } 107 | 108 | let _ = builder.init(); 109 | 110 | let _ = fs::create_dir(CatFSTests::get_orig_dir().join("dir2")); 111 | 112 | let prefix = catfs::catfs::tests::copy_resources(); 113 | let mnt = prefix.join("mnt"); 114 | let resources = prefix.join("resources"); 115 | let cache = prefix.join("cache"); 116 | 117 | fs::create_dir_all(&mnt)?; 118 | fs::create_dir_all(&cache)?; 119 | 120 | let t = CatFSTests { 121 | prefix: prefix, 122 | mnt: mnt, 123 | src: resources, 124 | cache: cache, 125 | session: Default::default(), 126 | evicter: Default::default(), 127 | nested: Default::default(), 128 | }; 129 | 130 | if let Some(v) = env::var_os("CATFS_SELF_HOST") { 131 | if v == OsStr::new("1") || v == OsStr::new("true") { 132 | let mnt = t.mnt.clone(); 133 | let mut mnt2 = mnt.as_os_str().to_os_string(); 134 | mnt2.push("2"); 135 | let cache = t.cache.to_path_buf(); 136 | let mut cache2 = cache.as_os_str().to_os_string(); 137 | cache2.push("2"); 138 | 139 | let mnt2 = PathBuf::from(mnt2); 140 | let cache2 = PathBuf::from(cache2); 141 | 142 | fs::create_dir_all(&mnt2)?; 143 | fs::create_dir_all(&cache2)?; 144 | 145 | let t2 = CatFSTests { 146 | prefix: t.prefix.clone(), 147 | mnt: mnt2, 148 | src: mnt, 149 | cache: cache2, 150 | session: Default::default(), 151 | evicter: Default::default(), 152 | nested: Some(Box::new(t)), 153 | }; 154 | 155 | return Ok(t2); 156 | } 157 | } 158 | 159 | return Ok(t); 160 | } 161 | 162 | fn init(&mut self) -> error::Result<()> { 163 | if let Some(ref mut t) = self.nested { 164 | t.init()?; 165 | } 166 | 167 | let (session, ev) = self.mount()?; 168 | self.session = Some(session); 169 | self.evicter = Some(ev); 170 | if let Some(ref mut ev) = self.evicter { 171 | ev.run(); 172 | } 173 | return Ok(()); 174 | } 175 | 176 | fn teardown(self) -> error::Result<()> { 177 | { 178 | // move out the session to let us umount first 179 | std::mem::drop(self.session); 180 | } 181 | 182 | if let Some(t) = self.nested { 183 | // unmount the inner session 184 | std::mem::drop(t.session); 185 | } 186 | 187 | fs::remove_dir_all(&self.prefix)?; 188 | // TODO do I need to free self.src/cache? 189 | return Ok(()); 190 | } 191 | } 192 | 193 | fn diff(dir1: &dyn AsRef, dir2: &dyn AsRef) { 194 | debug!("diff {:?} {:?}", dir1.as_ref(), dir2.as_ref()); 195 | let status = Command::new("diff") 196 | .arg("-ru") 197 | .arg(dir1.as_ref().as_os_str()) 198 | .arg(dir2.as_ref().as_os_str()) 199 | .status() 200 | .expect("failed to execute `diff'"); 201 | assert!(status.success()); 202 | } 203 | 204 | unit_tests!{ 205 | fn read_one(f: &CatFSTests) { 206 | let mut s = String::new(); 207 | File::open(f.mnt.join("dir1/file1")).unwrap().read_to_string(&mut s).unwrap(); 208 | assert_eq!(s, "dir1/file1\n"); 209 | } 210 | 211 | fn read_all(f: &CatFSTests) { 212 | diff(&CatFSTests::get_orig_dir(), &f.mnt); 213 | } 214 | 215 | fn create(f: &CatFSTests) { 216 | { 217 | let mut fh = OpenOptions::new().write(true).create(true) 218 | .open(Path::new(&f.mnt).join("foo")).unwrap(); 219 | fh.write_all(b"hello world").unwrap(); 220 | } 221 | 222 | fs::symlink_metadata(&f.get_from().join("foo")).unwrap(); 223 | diff(&f.get_from(), &f.mnt); 224 | } 225 | 226 | fn large_write(f: &CatFSTests) { 227 | let mut of=OsString::from("of="); 228 | of.push(f.mnt.join("foo")); 229 | let status = Command::new("dd") 230 | .arg("if=/dev/zero").arg(of) 231 | .arg("bs=1048576").arg("count=10") 232 | .stderr(Stdio::null()) 233 | .status().expect("failed to execute `dd'"); 234 | assert!(status.success()); 235 | 236 | let foo = fs::symlink_metadata(&f.get_from().join("foo")).unwrap(); 237 | assert_eq!(foo.len(), 10 * 1024 * 1024); 238 | diff(&f.get_from(), &f.mnt); 239 | } 240 | 241 | fn large_seek(f: &CatFSTests) { 242 | let mut file = OpenOptions::new().write(true).create(true).open(f.mnt.join("foo")).unwrap(); 243 | let offset = 2 * 1024 * 1024 * 1024; 244 | file.seek(std::io::SeekFrom::Start(offset)).unwrap(); 245 | file.write_all(b"x").unwrap(); 246 | 247 | let foo = fs::symlink_metadata(&f.get_from().join("foo")).unwrap(); 248 | assert_eq!(foo.len(), offset + 1); 249 | diff(&f.get_from(), &f.mnt); 250 | } 251 | 252 | fn large_dir(f: &CatFSTests) { 253 | let dir2 = Path::new(&f.mnt).join("dir2"); 254 | for i in 1..1001 { 255 | let _fh = OpenOptions::new().write(true).create(true) 256 | .open(dir2.join(format!("{}", i))).unwrap(); 257 | } 258 | 259 | let mut i = 0; 260 | let mut total = 0; 261 | for entry in fs::read_dir(dir2).unwrap() { 262 | i += 1; 263 | let num: u32 = entry.unwrap().file_name().to_str().unwrap().parse().unwrap(); 264 | total += num; 265 | } 266 | assert_eq!(i, 1000); 267 | assert_eq!(total, (1000 * (1000 + 1)) / 2); 268 | diff(&f.get_from(), &f.mnt); 269 | } 270 | 271 | fn read_modify_write(f: &CatFSTests) { 272 | let file1 = f.mnt.join("dir1/file1"); 273 | { 274 | let fh = OpenOptions::new().write(true).open(&file1).unwrap(); 275 | let nbytes = fh.write_at("*".as_bytes(), 9).unwrap(); 276 | assert_eq!(nbytes, 1); 277 | } 278 | 279 | let mut s = String::new(); 280 | File::open(&file1).unwrap().read_to_string(&mut s).unwrap(); 281 | assert_eq!(s, "dir1/file*\n"); 282 | diff(&f.get_from(), &f.mnt); 283 | } 284 | 285 | fn write_twice(f: &CatFSTests) { 286 | for _ in 0..2 { 287 | let mut fh = OpenOptions::new().write(true).create(true) 288 | .open(Path::new(&f.mnt).join("foo")).unwrap(); 289 | fh.write_all(b"hello world").unwrap(); 290 | } 291 | 292 | fs::symlink_metadata(&f.get_from().join("foo")).unwrap(); 293 | diff(&f.get_from(), &f.mnt); 294 | } 295 | 296 | fn unlink_one(f: &CatFSTests) { 297 | let file1 = f.mnt.join("dir1/file1"); 298 | fs::remove_file(&file1).unwrap(); 299 | if let Err(e) = fs::symlink_metadata(&file1) { 300 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 301 | } else { 302 | panic!("{:?} still exists", file1); 303 | } 304 | diff(&f.get_from(), &f.mnt); 305 | } 306 | 307 | fn read_unlink(f: &CatFSTests) { 308 | let file1 = f.mnt.join("dir1/file1"); 309 | { 310 | let mut s = String::new(); 311 | File::open(&file1).unwrap().read_to_string(&mut s).unwrap(); 312 | assert_eq!(s, "dir1/file1\n"); 313 | } 314 | fs::remove_file(&file1).unwrap(); 315 | if let Err(e) = fs::symlink_metadata(&file1) { 316 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 317 | } else { 318 | panic!("{:?} still exists", file1); 319 | } 320 | diff(&f.get_from(), &f.mnt); 321 | } 322 | 323 | fn read_unlink_while_open(f: &CatFSTests) { 324 | let file1 = f.mnt.join("dir1/file1"); 325 | let mut s = String::new(); 326 | { 327 | let mut f = File::open(&file1).unwrap(); 328 | f.read_to_string(&mut s).unwrap(); 329 | assert_eq!(s, "dir1/file1\n"); 330 | 331 | fs::remove_file(&file1).unwrap(); 332 | } 333 | if let Err(e) = fs::symlink_metadata(&file1) { 334 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 335 | } else { 336 | panic!("{:?} still exists", file1); 337 | } 338 | diff(&f.get_from(), &f.mnt); 339 | } 340 | 341 | fn mkdir(f: &CatFSTests) { 342 | let foo = f.mnt.join("foo"); 343 | fs::create_dir(&foo).unwrap(); 344 | } 345 | 346 | fn rmdir(f: &CatFSTests) { 347 | let dir2 = f.mnt.join("dir2"); 348 | fs::remove_dir(&dir2).unwrap(); 349 | if let Err(e) = fs::symlink_metadata(&dir2) { 350 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 351 | } else { 352 | panic!("{:?} still exists", dir2); 353 | } 354 | diff(&f.get_from(), &f.mnt); 355 | } 356 | 357 | fn rmdir_not_empty(f: &CatFSTests) { 358 | let dir1 = f.mnt.join("dir1"); 359 | if let Err(e) = fs::remove_dir(&dir1) { 360 | assert_eq!(e.raw_os_error().unwrap(), libc::ENOTEMPTY); 361 | } else { 362 | panic!("{:?} deleted", dir1); 363 | } 364 | } 365 | 366 | fn checksum_str(f: &CatFSTests) { 367 | if let Some(v) = env::var_os("CATFS_SELF_HOST") { 368 | if v == OsStr::new("1") || v == OsStr::new("true") { 369 | // skip this test since we don't support xattr for now 370 | return; 371 | } 372 | } 373 | 374 | let foo = f.src.join("file1"); 375 | xattr::set(&foo, "user.catfs.random", b"hello").unwrap(); 376 | rlibc::utimes(&foo, 0, 100000000).unwrap(); 377 | let mut fh = rlibc::File::open(&foo, rlibc::O_RDONLY, 0).unwrap(); 378 | let s = file::Handle::src_str_to_checksum(&fh).unwrap(); 379 | assert_eq!(s, OsStr::new("100000000\n6\n")); 380 | fh.close().unwrap(); 381 | } 382 | 383 | fn check_dirty(f: &CatFSTests) { 384 | let foo = f.mnt.join("foo"); 385 | let foo_cache = f.get_cache().join("foo"); 386 | { 387 | let mut fh = OpenOptions::new().write(true).create(true) 388 | .open(&foo).unwrap(); 389 | fh.write_all(b"hello").unwrap(); 390 | 391 | // at this point the file is NOT flushed yet, so pristine 392 | // should not be set 393 | assert!(!xattr::get(&foo_cache, "user.catfs.src_chksum").unwrap().is_some()); 394 | } 395 | 396 | f.assert_cache_valid(&Path::new("foo")); 397 | let mut contents = String::new(); 398 | let mut rh = OpenOptions::new().read(true).open(&foo).unwrap(); 399 | rh.read_to_string(&mut contents).unwrap(); 400 | assert_eq!(contents, "hello"); 401 | 402 | { 403 | let mut fh = OpenOptions::new().write(true) 404 | .open(&foo).unwrap(); 405 | fh.write_all(b"world").unwrap(); 406 | 407 | // at this point the file is NOT flushed yet, so pristine 408 | // should be dirty 409 | assert!(!xattr::get(&foo_cache, "user.catfs.src_chksum").unwrap().is_some()); 410 | } 411 | 412 | f.assert_cache_valid(&Path::new("foo")); 413 | let mut contents = String::new(); 414 | let mut rh = OpenOptions::new().read(true).open(&foo).unwrap(); 415 | rh.read_to_string(&mut contents).unwrap(); 416 | assert_eq!(contents, "world"); 417 | } 418 | 419 | fn create_pristine(f: &CatFSTests) { 420 | let foo = Path::new(&f.mnt).join("foo"); 421 | { 422 | let mut wh = OpenOptions::new().write(true).create(true) 423 | .open(&foo).unwrap(); 424 | wh.write_all(b"hello world").unwrap(); 425 | 426 | // we haven't closed the file yet, but we should still be 427 | // reading from it 428 | let mut contents = String::new(); 429 | let mut rh = OpenOptions::new().read(true).open(&foo).unwrap(); 430 | rh.read_to_string(&mut contents).unwrap(); 431 | assert_eq!(contents, "hello world"); 432 | } 433 | } 434 | 435 | fn rename_one(f: &CatFSTests) { 436 | let file1 = f.mnt.join("dir1/file1"); 437 | let file_rename = f.mnt.join("dir1/file1_rename"); 438 | fs::rename(&file1, &file_rename).unwrap(); 439 | if let Err(e) = fs::symlink_metadata(&file1) { 440 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 441 | } else { 442 | panic!("{:?} still exists", file1); 443 | } 444 | fs::symlink_metadata(&file_rename).unwrap(); 445 | diff(&f.get_from(), &f.mnt); 446 | } 447 | 448 | fn read_rename(f: &CatFSTests) { 449 | let file1 = f.mnt.join("dir1/file1"); 450 | { 451 | let mut s = String::new(); 452 | File::open(&file1).unwrap().read_to_string(&mut s).unwrap(); 453 | assert_eq!(s, "dir1/file1\n"); 454 | } 455 | let file_rename = f.mnt.join("dir1/file1_rename"); 456 | fs::rename(&file1, &file_rename).unwrap(); 457 | if let Err(e) = fs::symlink_metadata(&file1) { 458 | assert_eq!(e.kind(), io::ErrorKind::NotFound); 459 | } else { 460 | panic!("{:?} still exists", file1); 461 | } 462 | fs::symlink_metadata(&file_rename).unwrap(); 463 | diff(&f.get_from(), &f.mnt); 464 | } 465 | 466 | fn chmod(f: &CatFSTests) { 467 | let file1 = f.mnt.join("file1"); 468 | let mut perm = fs::symlink_metadata(&file1).unwrap().permissions(); 469 | assert!(!perm.readonly()); 470 | perm.set_readonly(true); 471 | fs::set_permissions(&file1, perm).unwrap(); 472 | perm = fs::symlink_metadata(&file1).unwrap().permissions(); 473 | assert!(perm.readonly()); 474 | } 475 | 476 | fn read_chmod(f: &CatFSTests) { 477 | let file1 = f.mnt.join("file1"); 478 | { 479 | let mut s = String::new(); 480 | File::open(&file1).unwrap().read_to_string(&mut s).unwrap(); 481 | assert_eq!(s, "file1\n"); 482 | } 483 | 484 | let mut perm = fs::symlink_metadata(&file1).unwrap().permissions(); 485 | assert!(!perm.readonly()); 486 | perm.set_readonly(true); 487 | fs::set_permissions(&file1, perm).unwrap(); 488 | perm = fs::symlink_metadata(&file1).unwrap().permissions(); 489 | assert!(perm.readonly()); 490 | f.assert_cache_valid(&Path::new("file1")); 491 | } 492 | 493 | fn prefetch_canceled(f: &CatFSTests) { 494 | let file1 = f.mnt.join("file1"); 495 | { 496 | let _ = File::open(&file1).unwrap(); 497 | } 498 | 499 | { 500 | let file1 = Path::new(&f.cache).join("file1"); 501 | let mut fh = OpenOptions::new().write(true).truncate(true).create(true) 502 | .open(&file1).unwrap(); 503 | fh.write_all(b"f").unwrap(); 504 | let _ = xattr::remove(&file1, "user.catfs.src_chksum"); 505 | } 506 | 507 | { 508 | let mut s = String::new(); 509 | let mut f = File::open(&file1).unwrap(); 510 | f.read_to_string(&mut s).unwrap(); 511 | assert_eq!(s, "file1\n"); 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /tests/resources/dir1/file1: -------------------------------------------------------------------------------- 1 | dir1/file1 2 | -------------------------------------------------------------------------------- /tests/resources/dir1/file2: -------------------------------------------------------------------------------- 1 | dir1/file2 2 | -------------------------------------------------------------------------------- /tests/resources/file1: -------------------------------------------------------------------------------- 1 | file1 2 | -------------------------------------------------------------------------------- /tests/resources/file2: -------------------------------------------------------------------------------- 1 | file2 2 | -------------------------------------------------------------------------------- /tests/resources/file3: -------------------------------------------------------------------------------- 1 | file3 2 | -------------------------------------------------------------------------------- /tests/test_suite/mod.rs: -------------------------------------------------------------------------------- 1 | // copied from https://users.rust-lang.org/t/why-does-rust-test-framework-lack-fixtures-and-mocking/5622/21 2 | macro_rules! unit_tests { 3 | ($( fn $name:ident($fixt:ident : &$ftype:ty) $body:block )*) => ( 4 | $( 5 | #[test] 6 | fn $name() { 7 | match <$ftype as Fixture>::setup() { 8 | Ok(mut $fixt) => { 9 | $fixt.init().unwrap(); 10 | $body 11 | if let Err(e) = $fixt.teardown() { 12 | panic!("teardown failed: {}", e); 13 | } 14 | }, 15 | Err(e) => panic!("setup failed: {}", e), 16 | } 17 | } 18 | )* 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /validate_cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ $# -lt 2 ]; then 8 | echo "usage: $0 " 9 | exit 1 10 | fi 11 | 12 | SRC="$1" 13 | CACHE="$2" 14 | 15 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 16 | BIN="$DIR/$0" 17 | 18 | if [ $# == 2 ]; then 19 | pushd "$CACHE" > /dev/null 20 | find . -type f -print0 | xargs -0 -n 1 -P 100 "$BIN" "$SRC" "$CACHE" 21 | popd > /dev/null 22 | else 23 | FILE="$3" 24 | pushd "$SRC" > /dev/null 25 | strtosign=$(getfattr -e hex --match=s3.etag -d "$FILE" 2>/dev/null | grep =; \ 26 | /usr/bin/stat -t --printf "%Y\n%s\n" "$FILE") 27 | sum=$(echo "$strtosign" | sha512sum | cut -f1 '-d ') 28 | setfattr -n user.catfs.src_chksum -v 0x$sum "$CACHE/$FILE" 29 | echo "$FILE $strtosign $sum" 30 | popd > /dev/null 31 | fi 32 | --------------------------------------------------------------------------------