├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ost ├── rust-toolchain ├── rustfmt.toml └── src ├── clone.rs ├── cmds.rs ├── cmds ├── clone.rs ├── clone │ ├── create.rs │ ├── delete.rs │ └── list.rs ├── create.rs ├── inspect.rs └── mount.rs ├── collector.rs ├── filesystem.rs ├── filesystem ├── getattr.rs ├── lookup.rs ├── mk.rs ├── ops.rs ├── read.rs ├── readdir.rs ├── rename.rs ├── result.rs ├── rm.rs ├── setattr.rs └── write.rs ├── inode.rs ├── inode_id.rs ├── inodes.rs ├── main.rs ├── object.rs ├── object_id.rs ├── object_rw.rs ├── objects.rs ├── storage.rs └── transaction.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /mnt 3 | *.ofs 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 = "anyhow" 7 | version = "1.0.80" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "byteorder" 19 | version = "1.5.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "1.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 28 | 29 | [[package]] 30 | name = "clap" 31 | version = "2.34.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 34 | dependencies = [ 35 | "bitflags", 36 | "textwrap", 37 | "unicode-width", 38 | ] 39 | 40 | [[package]] 41 | name = "fuser" 42 | version = "0.14.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "2e697f6f62c20b6fad1ba0f84ae909f25971cf16e735273524e3977c94604cf8" 45 | dependencies = [ 46 | "libc", 47 | "log", 48 | "memchr", 49 | "page_size", 50 | "pkg-config", 51 | "smallvec", 52 | "zerocopy", 53 | ] 54 | 55 | [[package]] 56 | name = "heck" 57 | version = "0.3.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 60 | dependencies = [ 61 | "unicode-segmentation", 62 | ] 63 | 64 | [[package]] 65 | name = "lazy_static" 66 | version = "1.4.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 69 | 70 | [[package]] 71 | name = "libc" 72 | version = "0.2.153" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 75 | 76 | [[package]] 77 | name = "log" 78 | version = "0.4.21" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 81 | 82 | [[package]] 83 | name = "memchr" 84 | version = "2.7.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 87 | 88 | [[package]] 89 | name = "nu-ansi-term" 90 | version = "0.46.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 93 | dependencies = [ 94 | "overload", 95 | "winapi", 96 | ] 97 | 98 | [[package]] 99 | name = "once_cell" 100 | version = "1.19.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 103 | 104 | [[package]] 105 | name = "ostfs" 106 | version = "0.1.0" 107 | dependencies = [ 108 | "anyhow", 109 | "fuser", 110 | "libc", 111 | "structopt", 112 | "tracing", 113 | "tracing-subscriber", 114 | ] 115 | 116 | [[package]] 117 | name = "overload" 118 | version = "0.1.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 121 | 122 | [[package]] 123 | name = "page_size" 124 | version = "0.6.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" 127 | dependencies = [ 128 | "libc", 129 | "winapi", 130 | ] 131 | 132 | [[package]] 133 | name = "pin-project-lite" 134 | version = "0.2.13" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 137 | 138 | [[package]] 139 | name = "pkg-config" 140 | version = "0.3.30" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 143 | 144 | [[package]] 145 | name = "proc-macro-error" 146 | version = "1.0.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 149 | dependencies = [ 150 | "proc-macro-error-attr", 151 | "proc-macro2", 152 | "quote", 153 | "syn 1.0.109", 154 | "version_check", 155 | ] 156 | 157 | [[package]] 158 | name = "proc-macro-error-attr" 159 | version = "1.0.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 162 | dependencies = [ 163 | "proc-macro2", 164 | "quote", 165 | "version_check", 166 | ] 167 | 168 | [[package]] 169 | name = "proc-macro2" 170 | version = "1.0.78" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 173 | dependencies = [ 174 | "unicode-ident", 175 | ] 176 | 177 | [[package]] 178 | name = "quote" 179 | version = "1.0.35" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 182 | dependencies = [ 183 | "proc-macro2", 184 | ] 185 | 186 | [[package]] 187 | name = "sharded-slab" 188 | version = "0.1.7" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 191 | dependencies = [ 192 | "lazy_static", 193 | ] 194 | 195 | [[package]] 196 | name = "smallvec" 197 | version = "1.13.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 200 | 201 | [[package]] 202 | name = "structopt" 203 | version = "0.3.26" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 206 | dependencies = [ 207 | "clap", 208 | "lazy_static", 209 | "structopt-derive", 210 | ] 211 | 212 | [[package]] 213 | name = "structopt-derive" 214 | version = "0.4.18" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 217 | dependencies = [ 218 | "heck", 219 | "proc-macro-error", 220 | "proc-macro2", 221 | "quote", 222 | "syn 1.0.109", 223 | ] 224 | 225 | [[package]] 226 | name = "syn" 227 | version = "1.0.109" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 230 | dependencies = [ 231 | "proc-macro2", 232 | "quote", 233 | "unicode-ident", 234 | ] 235 | 236 | [[package]] 237 | name = "syn" 238 | version = "2.0.52" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 241 | dependencies = [ 242 | "proc-macro2", 243 | "quote", 244 | "unicode-ident", 245 | ] 246 | 247 | [[package]] 248 | name = "textwrap" 249 | version = "0.11.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 252 | dependencies = [ 253 | "unicode-width", 254 | ] 255 | 256 | [[package]] 257 | name = "thread_local" 258 | version = "1.1.8" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 261 | dependencies = [ 262 | "cfg-if", 263 | "once_cell", 264 | ] 265 | 266 | [[package]] 267 | name = "tracing" 268 | version = "0.1.40" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 271 | dependencies = [ 272 | "pin-project-lite", 273 | "tracing-attributes", 274 | "tracing-core", 275 | ] 276 | 277 | [[package]] 278 | name = "tracing-attributes" 279 | version = "0.1.27" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 282 | dependencies = [ 283 | "proc-macro2", 284 | "quote", 285 | "syn 2.0.52", 286 | ] 287 | 288 | [[package]] 289 | name = "tracing-core" 290 | version = "0.1.32" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 293 | dependencies = [ 294 | "once_cell", 295 | "valuable", 296 | ] 297 | 298 | [[package]] 299 | name = "tracing-log" 300 | version = "0.2.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 303 | dependencies = [ 304 | "log", 305 | "once_cell", 306 | "tracing-core", 307 | ] 308 | 309 | [[package]] 310 | name = "tracing-subscriber" 311 | version = "0.3.18" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 314 | dependencies = [ 315 | "nu-ansi-term", 316 | "sharded-slab", 317 | "smallvec", 318 | "thread_local", 319 | "tracing-core", 320 | "tracing-log", 321 | ] 322 | 323 | [[package]] 324 | name = "unicode-ident" 325 | version = "1.0.12" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 328 | 329 | [[package]] 330 | name = "unicode-segmentation" 331 | version = "1.11.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 334 | 335 | [[package]] 336 | name = "unicode-width" 337 | version = "0.1.11" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 340 | 341 | [[package]] 342 | name = "valuable" 343 | version = "0.1.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 346 | 347 | [[package]] 348 | name = "version_check" 349 | version = "0.9.4" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 352 | 353 | [[package]] 354 | name = "winapi" 355 | version = "0.3.9" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 358 | dependencies = [ 359 | "winapi-i686-pc-windows-gnu", 360 | "winapi-x86_64-pc-windows-gnu", 361 | ] 362 | 363 | [[package]] 364 | name = "winapi-i686-pc-windows-gnu" 365 | version = "0.4.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 368 | 369 | [[package]] 370 | name = "winapi-x86_64-pc-windows-gnu" 371 | version = "0.4.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 374 | 375 | [[package]] 376 | name = "zerocopy" 377 | version = "0.7.32" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 380 | dependencies = [ 381 | "byteorder", 382 | "zerocopy-derive", 383 | ] 384 | 385 | [[package]] 386 | name = "zerocopy-derive" 387 | version = "0.7.32" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 390 | dependencies = [ 391 | "proc-macro2", 392 | "quote", 393 | "syn 2.0.52", 394 | ] 395 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ostfs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | fuser = "0.14.0" 9 | libc = "0.2" 10 | structopt = { version = "0.3", default-features = false } 11 | tracing = "0.1" 12 | tracing-subscriber = "0.3" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Patryk Wychowaniec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OstFS 🧀 2 | 3 | [OstFS](https://sv.wiktionary.org/wiki/ost#Svenska) is a toy FUSE filesystem 4 | with support for zero-cost¹ snapshots and clones (think ZFS / Btrfs): 5 | 6 | ``` shell 7 | # (example requires `rustup`, since OstFS is a Rust application) 8 | 9 | # Create a file for storing the filesystem: 10 | $ ./ost create tank.ofs 11 | 12 | # Mount it: 13 | $ mkdir mnt 14 | $ ./ost mount tank.ofs mnt & 15 | 16 | # (`&` causes the process to run in background - press 17 | # the enter key to bring back your shell prompt) 18 | 19 | # Create a file *inside* the filesystem: 20 | $ echo 'Hello, World!' > mnt/hello.txt 21 | 22 | # Make a snapshot (aka a read-only clone) called `init`: 23 | $ ./ost clone create tank.ofs init --read-only 24 | 25 | # 26 | # What's cool is that creating a clone doesn't actually 27 | # copy the underlying data - rather, OstFS tracks which 28 | # files have changed *since* the clone and stores only 29 | # the diff (with a pinch of salt). 30 | # 31 | # That is to say, creating a clone is aaalmost¹ a no-op. 32 | # 33 | 34 | # Modify the file: 35 | $ echo 'nobody expects the spanish inquisition' > mnt/hello.txt 36 | 37 | # --- 38 | # Alright, now we can utilize the snapshot to time-travel! 39 | # --- 40 | 41 | # Umount filesystem: 42 | $ umount mnt 43 | 44 | # Invoke `./ofs mount` again, but instructing it to mount 45 | # the snapshot instead of the top filesystem: 46 | $ ./ost mount tank.ofs mnt --clone init & 47 | 48 | # Ha, ha! 49 | $ cat mnt/hello.txt 50 | Hello, World! 51 | 52 | # Note that mounting a clone doesn't rollback the changes - 53 | # if you remounted without `--clone init`, `cat mnt/hello.txt` 54 | # would say `nobody expects ...` again. 55 | ``` 56 | 57 | ¹ terms and conditions apply, see below 58 | 59 | ## Usage 60 | 61 | See the command line: 62 | 63 | ``` shell 64 | $ ./ost 65 | ``` 66 | 67 | ... but it mostly boils down to `./ost create` & `./ost mount`. 68 | 69 | ## Architecture 70 | 71 | OstFS stores everything in a tree structure which starts from the header and 72 | then descends into clones, the root directory and its children. 73 | 74 | Something that `ls` displays as: 75 | 76 | ``` 77 | drwxrwxrwx 1 PWY staff 0 Jan 1 1970 . 78 | drwxr-xr-x 15 PWY staff 480 Mar 8 20:49 .. 79 | -rw-r--r-- 1 PWY staff 12 Jan 1 1970 one.txt 80 | -rw-r--r-- 1 PWY staff 12 Jan 1 1970 two.txt 81 | ``` 82 | 83 | ... would internally be stored as a couple of objects forming a tree: 84 | 85 | ``` 86 | / ------ \ 87 | | header | 88 | \ ------ / 89 | | 90 | | 91 | has child 92 | | 93 | | 94 | v 95 | / ----- \ / ---------- \ 96 | | entry | -- has name -> | payload(/) | # root note starts at `/` 97 | \ ----- / \ ---------- / # (just a convention) 98 | | 99 | | 100 | has child 101 | | 102 | | 103 | v 104 | / ----- \ / ---------------- \ 105 | | | -- has name -> | payload(one.txt) | 106 | | | \ ---------------- / 107 | | entry | 108 | | | / -------------------- \ 109 | | | -- has body -> | payload(Hello, One!) | 110 | \ ----- / \ -------------------- / 111 | | 112 | | 113 | has sibling 114 | | 115 | | 116 | v 117 | { similar stuff for two.txt } 118 | ``` 119 | 120 | There are three basic types of objects: 121 | 122 | - header (represents the entrypoint - links to the root entry, aka root 123 | directory), 124 | - entry (represents either a file or a directory), 125 | - payload (represents a string (e.g. entry name) or binary data (e.g. file 126 | content)). 127 | 128 | Each object is assigned a unique identifier (starting from zero and going up) 129 | and each object _links_ to other objects using their identifiers as well - this 130 | can be observed through the `./ofs inspect` command: 131 | 132 | ``` 133 | [0] = Header(HeaderObj { root: ObjectId(29), clone: None, dead: None }) 134 | [1] = Entry(EntryObj { name: ObjectId(2), body: None, next: None, kind: Directory, size: 0, mode: 511, uid: 502, gid: 20 }) 135 | [2] = Payload(Payload { size: 1, next: None, data: "/" }) 136 | [3] = Payload(Payload { size: 7, next: None, data: "one.txt" }) 137 | /* ... */ 138 | ``` 139 | 140 | Now, all this keeping track of the objects, their edges and so on is a huge 141 | amount of work, so it's reasonable to ask: what do we gain? 142 | 143 | Well, we can become **copy on write**! 144 | 145 | That is, when you modify a file (change its attributes, content, rename it 146 | etc.), its original object remains intact - rather, OstFS duplicates that object 147 | (with the duplicate having a new identifier), then duplicates its parent, 148 | grandparent etc., up until and including the root. 149 | 150 | But we don't duplicate everything - if only the file name has changed, there's 151 | no point in duplicating payload containing the file's body, so if we did: 152 | 153 | ``` shell 154 | $ mv one.txt one.md 155 | ``` 156 | 157 | ... the graph would say: 158 | 159 | ``` 160 | /* ... */ 161 | 162 | / ----- \ / ---------------- \ 163 | | | -- has name -> | payload(one.txt) | 164 | | | \ ---------------- / 165 | | entry | 166 | | | / -------------------- \ 167 | | | -- has body -> | payload(Hello, One!) | 168 | \ ----- / \ -------------------- / 169 | | 170 | / ----- \ / --------------- \ | 171 | | | -- has name -> | payload(one.md) | | 172 | | | \ --------------- / | 173 | | entry | | 174 | | | | 175 | | | -- has body ----------------------- / 176 | \ ----- / 177 | 178 | /* ... */ 179 | 180 | - payload(one.txt) has one parent 181 | - payload(one.md) has one parent 182 | - payload(Hello, One!) has two parents 183 | ``` 184 | 185 | Similarly, given a structure like: 186 | 187 | ``` shell 188 | a 189 | / \ 190 | b c 191 | / \ / \ 192 | d e f g 193 | ``` 194 | 195 | ... if we modified `e`, we'd only need to duplicate `b` and `a` - the rest could 196 | be linked as-is. 197 | 198 | This is what makes (almost) zero-cost snapshots (almost) zero-cost - because we 199 | don't modify objects in-place, we can reuse this fact to time-travel back to the 200 | past, if only we can get our hands on the past object ids (and if we don't 201 | remove those past objects, of course). 202 | 203 | What's more, this also allows for perfectly safe **atomic updates**! -- remember 204 | the header object? 205 | 206 | Header is always located at the object slot 0 (i.e. the beginning of the file) 207 | and the most important information it contains is the reference (object id) of 208 | the root directory. Initially the root directory starts at slot 1 (right after 209 | the header), but as soon as the filesystem gets modified, we generate a _new_ 210 | root directory (with a brand new object id), which requires updating the header. 211 | 212 | Here's the second greatest part of using copy on writes: 213 | 214 | If the power goes down when we're building the new tree, nothing gets 215 | accidentally removed/updated! (that is, it's not possible to observe a partial 216 | update) 217 | 218 | See, since we update the header at the very end of the process (after we've 219 | built the entire tree), the only two possible options are: 220 | 221 | - power went down _before_ we've manged to write the header, in which case upon 222 | the next mount the filesystem will effectively rollback to its previous state 223 | (because we didn't overwrite the older objects), 224 | 225 | - power went down _after_ we've managed to write the header, in which case we're 226 | 100% sure the entire new tree is in place, because we update the header last. 227 | 228 | (well, it's technically also possible for the power to go down during the header 229 | update and that's why ZFS uses the concept of uberblock, but let's not go _that_ 230 | deep!) 231 | 232 | If you're into that, you might find this interesting: 233 | 234 | 235 | 236 | ## Limitations 237 | 238 | - OstFS uses 32-byte objects, which makes it easy to understand, but also 239 | impractical (doesn't match any typical sector size) 240 | - OstFS uses linked lists instead of b-treemaps & hashmaps, so a directory with 241 | 10k entries will open noticeably slower than a directory with 10 entries 242 | - OstFS keeps inodes entirely in memory 243 | - OstFS doesn't store checksums 244 | - OstFS doesn't store atime, ctime and a couple of other properties 245 | - OstFS has no caching layer 246 | - **OstFS is a toy** - I made it solely to learn a few cool concepts; if you're 247 | looking for actual filesystem, ZFS is your friend! 248 | 249 | (most - if not all - of this things could get improved -- it's just that I just 250 | wanted to hack something over three days, not thirty years) 251 | 252 | ## Hacking 253 | 254 | Code is a bit crude, but if you'd like to take a look, here's a couple of 255 | useful entrypoints: 256 | 257 | - `object.rs` 258 | - `objects.rs` 259 | - `ops.rs` 260 | - `filesystem.rs` (in particular the modules inside of it) 261 | 262 | ## License 263 | 264 | MIT License 265 | 266 | Copyright (c) 2024 Patryk Wychowaniec 267 | -------------------------------------------------------------------------------- /ost: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo run --release -- $@ 4 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2024-03-05 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "module" 2 | version = "Two" 3 | -------------------------------------------------------------------------------- /src/clone.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneObj, HeaderObj, Object, ObjectId, Objects}; 2 | use anyhow::{anyhow, Context, Result}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct Clone { 6 | pub oid: ObjectId, 7 | pub name: String, 8 | pub root: ObjectId, 9 | pub is_writable: bool, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct CloneController<'a> { 14 | objects: &'a mut Objects, 15 | } 16 | 17 | impl<'a> CloneController<'a> { 18 | pub fn new(objects: &'a mut Objects) -> Self { 19 | Self { objects } 20 | } 21 | 22 | pub fn create(&mut self, name: &str, is_writable: bool) -> Result<()> { 23 | let name = name.trim(); 24 | 25 | for clone in self.all()? { 26 | if clone.name == name { 27 | return Err(anyhow!("clone named `{}` already exists", name)); 28 | } 29 | } 30 | 31 | let header = self.objects.get_header()?; 32 | 33 | let name = self 34 | .objects 35 | .alloc_payload(None, name.as_bytes())? 36 | .context("name cannot be empty")?; 37 | 38 | let clone = self.objects.alloc( 39 | None, 40 | Object::Clone(CloneObj { 41 | name, 42 | root: header.root, 43 | is_writable, 44 | next: header.clone, 45 | }), 46 | )?; 47 | 48 | self.objects.set_header(HeaderObj { 49 | clone: Some(clone), 50 | ..header 51 | })?; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn delete(&mut self, name: &str) -> Result<()> { 57 | let header = self.objects.get_header()?; 58 | let oid_to_delete = self.find(name)?.oid; 59 | 60 | let mut clones = Vec::new(); 61 | let mut cursor = header.clone; 62 | 63 | while let Some(oid) = cursor { 64 | let obj = self.objects.get(oid)?.into_clone(oid)?; 65 | 66 | if oid != oid_to_delete { 67 | let new_oid = self.objects.alloc(None, Object::Clone(obj))?; 68 | 69 | clones.push((obj, new_oid)); 70 | } 71 | 72 | cursor = obj.next; 73 | } 74 | 75 | for i in 0..clones.len() { 76 | let next = clones.get(i + 1).map(|(_, new_oid)| *new_oid); 77 | let (curr, oid) = &mut clones[i]; 78 | 79 | curr.next = next; 80 | 81 | self.objects.set(*oid, Object::Clone(*curr))?; 82 | } 83 | 84 | self.objects.set_header(HeaderObj { 85 | clone: clones.first().map(|(_, oid)| *oid), 86 | ..header 87 | })?; 88 | 89 | Ok(()) 90 | } 91 | 92 | pub fn find(&mut self, name: &str) -> Result { 93 | let name = name.trim(); 94 | 95 | self.all()? 96 | .into_iter() 97 | .find(|clone| clone.name == name) 98 | .ok_or_else(|| anyhow!("no clone named `{}` found", name)) 99 | } 100 | 101 | pub fn all(&mut self) -> Result> { 102 | let mut clones = Vec::new(); 103 | let mut cursor = self.objects.get_header()?.clone; 104 | 105 | while let Some(oid) = cursor { 106 | let obj = self.objects.get(oid)?.into_clone(oid)?; 107 | 108 | clones.push(Clone { 109 | oid, 110 | name: self.objects.get_string(obj.name)?, 111 | root: obj.root, 112 | is_writable: obj.is_writable, 113 | }); 114 | 115 | cursor = obj.next; 116 | } 117 | 118 | clones.reverse(); 119 | 120 | Ok(clones) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/cmds.rs: -------------------------------------------------------------------------------- 1 | mod clone; 2 | mod create; 3 | mod inspect; 4 | mod mount; 5 | 6 | pub use self::clone::*; 7 | pub use self::create::*; 8 | pub use self::inspect::*; 9 | pub use self::mount::*; 10 | -------------------------------------------------------------------------------- /src/cmds/clone.rs: -------------------------------------------------------------------------------- 1 | mod create; 2 | mod delete; 3 | mod list; 4 | 5 | pub use self::create::*; 6 | pub use self::delete::*; 7 | pub use self::list::*; 8 | use anyhow::Result; 9 | use structopt::StructOpt; 10 | 11 | #[derive(Debug, StructOpt)] 12 | pub enum CloneCmd { 13 | Create(CreateCloneCmd), 14 | Delete(DeleteCloneCmd), 15 | List(ListCloneCmd), 16 | } 17 | 18 | impl CloneCmd { 19 | pub fn run(self) -> Result<()> { 20 | match self { 21 | CloneCmd::Create(cmd) => cmd.run(), 22 | CloneCmd::Delete(cmd) => cmd.run(), 23 | CloneCmd::List(cmd) => cmd.run(), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cmds/clone/create.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneController, Objects, Storage}; 2 | use anyhow::Result; 3 | use std::path::PathBuf; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt)] 7 | pub struct CreateCloneCmd { 8 | /// Path to the *.ofs file 9 | src: PathBuf, 10 | 11 | /// Name of the clone; must be unique across other clone names 12 | name: String, 13 | 14 | /// When specified, clone is read-only 15 | #[structopt(short, long)] 16 | read_only: bool, 17 | } 18 | 19 | impl CreateCloneCmd { 20 | pub fn run(self) -> Result<()> { 21 | let storage = Storage::open(&self.src, true)?; 22 | let mut objects = Objects::new(storage); 23 | 24 | CloneController::new(&mut objects).create(&self.name, !self.read_only)?; 25 | 26 | println!("ok"); 27 | 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cmds/clone/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneController, Objects, Storage}; 2 | use anyhow::Result; 3 | use std::path::PathBuf; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt)] 7 | pub struct DeleteCloneCmd { 8 | /// Path to the *.ofs file 9 | src: PathBuf, 10 | 11 | /// Name of the clone 12 | name: String, 13 | } 14 | 15 | impl DeleteCloneCmd { 16 | pub fn run(self) -> Result<()> { 17 | let storage = Storage::open(&self.src, true)?; 18 | let mut objects = Objects::new(storage); 19 | 20 | CloneController::new(&mut objects).delete(&self.name)?; 21 | 22 | println!("ok"); 23 | 24 | Ok(()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cmds/clone/list.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneController, Objects, Storage}; 2 | use anyhow::Result; 3 | use std::path::PathBuf; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt)] 7 | pub struct ListCloneCmd { 8 | /// Path to the *.ofs file 9 | src: PathBuf, 10 | } 11 | 12 | impl ListCloneCmd { 13 | pub fn run(self) -> Result<()> { 14 | let storage = Storage::open(&self.src, false)?; 15 | let mut objects = Objects::new(storage); 16 | let clones = CloneController::new(&mut objects).all()?; 17 | 18 | match clones.len() { 19 | 0 => println!("found no clones"), 20 | 1 => println!("found 1 clone:"), 21 | n => println!("found {} clones:", n), 22 | } 23 | 24 | for (clone_id, clone) in clones.into_iter().enumerate() { 25 | println!("- #{}: {}", clone_id, clone.name); 26 | } 27 | 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cmds/create.rs: -------------------------------------------------------------------------------- 1 | use crate::{EntryObj, HeaderObj, Object, ObjectId, Objects, Storage}; 2 | use anyhow::{Context, Result}; 3 | use fuser::FileType; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use structopt::StructOpt; 7 | 8 | #[derive(Debug, StructOpt)] 9 | pub struct CreateCmd { 10 | /// Path to the *.ofs file 11 | src: PathBuf, 12 | 13 | /// When set, tries to remove `src` first 14 | #[structopt(short, long)] 15 | recreate: bool, 16 | } 17 | 18 | impl CreateCmd { 19 | pub fn run(self) -> Result<()> { 20 | if self.recreate && self.src.exists() { 21 | fs::remove_file(&self.src) 22 | .with_context(|| format!("couldn't delete: {}", self.src.display()))?; 23 | } 24 | 25 | let preset = { 26 | let uid = unsafe { libc::getuid() }; 27 | let gid = unsafe { libc::getgid() }; 28 | 29 | [ 30 | Object::Header(HeaderObj { 31 | root: ObjectId::new(1), 32 | dead: None, 33 | clone: None, 34 | }), 35 | Object::Entry(EntryObj { 36 | name: ObjectId::new(2), 37 | body: None, 38 | next: None, 39 | kind: FileType::Directory, 40 | size: 0, 41 | mode: 0o777, 42 | uid, 43 | gid, 44 | }), 45 | Object::payload(b"/"), 46 | ] 47 | }; 48 | 49 | let storage = Storage::create(&self.src)?; 50 | let mut objects = Objects::new(storage); 51 | 52 | for object in preset { 53 | objects.alloc(None, object)?; 54 | } 55 | 56 | println!("ok"); 57 | 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cmds/inspect.rs: -------------------------------------------------------------------------------- 1 | use crate::{ObjectId, Objects, Storage}; 2 | use anyhow::Result; 3 | use std::path::PathBuf; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt)] 7 | pub struct InspectCmd { 8 | /// Path to the *.ofs file 9 | src: PathBuf, 10 | 11 | /// When set, shows just this particular object 12 | #[structopt(short, long)] 13 | oid: Option, 14 | } 15 | 16 | impl InspectCmd { 17 | pub fn run(self) -> Result<()> { 18 | let storage = Storage::open(&self.src, false)?; 19 | let mut objects = Objects::new(storage); 20 | 21 | if let Some(oid) = self.oid { 22 | println!("[{}] = {:?}", oid, objects.get(ObjectId::new(oid))); 23 | } else { 24 | for (oid, obj) in objects.all()? { 25 | println!("[{}] = {:?}", oid.get(), obj); 26 | } 27 | } 28 | 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/cmds/mount.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneController, Filesystem, FilesystemOrigin, InodeId, Objects, Storage}; 2 | use anyhow::{Context, Result}; 3 | use fuser::{MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request, TimeOrNow}; 4 | use std::ffi::OsStr; 5 | use std::path::PathBuf; 6 | use std::time::{Duration, SystemTime}; 7 | use structopt::StructOpt; 8 | 9 | #[derive(Debug, StructOpt)] 10 | pub struct MountCmd { 11 | /// Path to the *.ofs file 12 | src: PathBuf, 13 | 14 | /// Path to the mountpoint 15 | dst: PathBuf, 16 | 17 | /// When specified, mount given clone 18 | #[structopt(short, long)] 19 | clone: Option, 20 | 21 | /// Force a read-only mount 22 | #[structopt(short, long)] 23 | read_only: bool, 24 | 25 | /// When *.ofs runs out of space, throw an I/O error instead of growing the 26 | /// file 27 | #[structopt(short, long)] 28 | in_place: bool, 29 | } 30 | 31 | impl MountCmd { 32 | pub fn run(self) -> Result<()> { 33 | tracing_subscriber::fmt::init(); 34 | 35 | let storage = Storage::open(&self.src, !self.in_place)?; 36 | let mut objects = Objects::new(storage); 37 | 38 | let origin = if let Some(clone) = &self.clone { 39 | let clone = CloneController::new(&mut objects).find(clone)?; 40 | 41 | FilesystemOrigin::Clone { 42 | oid: clone.oid, 43 | is_writable: clone.is_writable && !self.read_only, 44 | } 45 | } else { 46 | FilesystemOrigin::Main { 47 | is_writable: !self.read_only, 48 | } 49 | }; 50 | 51 | let fs = Filesystem::new(objects, origin)?; 52 | 53 | let options = vec![ 54 | MountOption::FSName("ofs".into()), 55 | MountOption::AllowOther, 56 | MountOption::AutoUnmount, 57 | ]; 58 | 59 | fuser::mount2(FsController { fs }, self.dst, &options) 60 | .context("Couldn't mount filesystem")?; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | #[derive(Debug)] 67 | struct FsController { 68 | fs: Filesystem, 69 | } 70 | 71 | impl FsController { 72 | const TTL: Duration = Duration::from_secs(0); 73 | } 74 | 75 | impl fuser::Filesystem for FsController { 76 | fn lookup(&mut self, _: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { 77 | match self.fs.lookup(InodeId::new(parent), name) { 78 | Ok(val) => reply.entry(&Self::TTL, &val, 0), 79 | Err(err) => reply.error(err.log_and_convert()), 80 | } 81 | } 82 | 83 | fn getattr(&mut self, _: &Request, ino: u64, reply: ReplyAttr) { 84 | match self.fs.getattr(InodeId::new(ino)) { 85 | Ok(val) => reply.attr(&Self::TTL, &val), 86 | Err(err) => reply.error(err.log_and_convert()), 87 | } 88 | } 89 | 90 | fn setattr( 91 | &mut self, 92 | _: &Request<'_>, 93 | ino: u64, 94 | mode: Option, 95 | uid: Option, 96 | gid: Option, 97 | size: Option, 98 | _: Option, 99 | _: Option, 100 | _: Option, 101 | _: Option, 102 | _: Option, 103 | _: Option, 104 | _: Option, 105 | _: Option, 106 | reply: ReplyAttr, 107 | ) { 108 | match self.fs.setattr(InodeId::new(ino), mode, uid, gid, size) { 109 | Ok(val) => reply.attr(&Self::TTL, &val), 110 | Err(err) => reply.error(err.log_and_convert()), 111 | } 112 | } 113 | 114 | fn mknod( 115 | &mut self, 116 | req: &Request<'_>, 117 | parent: u64, 118 | name: &OsStr, 119 | mode: u32, 120 | _: u32, 121 | _: u32, 122 | reply: ReplyEntry, 123 | ) { 124 | match self 125 | .fs 126 | .mknod(InodeId::new(parent), name, mode, req.uid(), req.gid()) 127 | { 128 | Ok(val) => reply.entry(&Self::TTL, &val, 0), 129 | Err(err) => reply.error(err.log_and_convert()), 130 | } 131 | } 132 | 133 | fn mkdir( 134 | &mut self, 135 | req: &Request<'_>, 136 | parent: u64, 137 | name: &OsStr, 138 | mode: u32, 139 | _: u32, 140 | reply: ReplyEntry, 141 | ) { 142 | match self 143 | .fs 144 | .mkdir(InodeId::new(parent), name, mode, req.uid(), req.gid()) 145 | { 146 | Ok(val) => reply.entry(&Self::TTL, &val, 0), 147 | Err(err) => reply.error(err.log_and_convert()), 148 | } 149 | } 150 | 151 | fn unlink(&mut self, _: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) { 152 | match self.fs.unlink(InodeId::new(parent), name) { 153 | Ok(_) => reply.ok(), 154 | Err(err) => reply.error(err.log_and_convert()), 155 | } 156 | } 157 | 158 | fn rmdir(&mut self, _: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) { 159 | match self.fs.unlink(InodeId::new(parent), name) { 160 | Ok(_) => reply.ok(), 161 | Err(err) => reply.error(err.log_and_convert()), 162 | } 163 | } 164 | 165 | fn rename( 166 | &mut self, 167 | _req: &Request<'_>, 168 | parent: u64, 169 | name: &OsStr, 170 | newparent: u64, 171 | newname: &OsStr, 172 | _: u32, 173 | reply: fuser::ReplyEmpty, 174 | ) { 175 | match self 176 | .fs 177 | .rename(InodeId::new(parent), name, InodeId::new(newparent), newname) 178 | { 179 | Ok(_) => reply.ok(), 180 | Err(err) => reply.error(err.log_and_convert()), 181 | } 182 | } 183 | 184 | fn read( 185 | &mut self, 186 | _: &Request, 187 | ino: u64, 188 | _: u64, 189 | offset: i64, 190 | size: u32, 191 | _: i32, 192 | _: Option, 193 | reply: ReplyData, 194 | ) { 195 | match self.fs.read(InodeId::new(ino), offset, size) { 196 | Ok(val) => reply.data(&val), 197 | Err(err) => reply.error(err.log_and_convert()), 198 | } 199 | } 200 | 201 | fn write( 202 | &mut self, 203 | _: &Request<'_>, 204 | ino: u64, 205 | _: u64, 206 | offset: i64, 207 | data: &[u8], 208 | _: u32, 209 | _: i32, 210 | _: Option, 211 | reply: fuser::ReplyWrite, 212 | ) { 213 | match self.fs.write(InodeId::new(ino), offset, data) { 214 | Ok(_) => reply.written(data.len() as u32), 215 | Err(err) => reply.error(err.log_and_convert()), 216 | } 217 | } 218 | 219 | fn readdir(&mut self, _: &Request, ino: u64, _: u64, offset: i64, mut reply: ReplyDirectory) { 220 | match self.fs.readdir(InodeId::new(ino), offset) { 221 | Ok(attrs) => { 222 | for (attr_offset, attr_iid, attr_kind, attr_name) in attrs { 223 | if reply.add(attr_iid.get(), attr_offset, attr_kind, attr_name) { 224 | break; 225 | } 226 | } 227 | 228 | reply.ok(); 229 | } 230 | 231 | Err(err) => reply.error(err.log_and_convert()), 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/collector.rs: -------------------------------------------------------------------------------- 1 | use crate::{DeadObj, HeaderObj, Object, ObjectId, Objects}; 2 | use anyhow::{anyhow, Result}; 3 | use std::collections::{BTreeSet, VecDeque}; 4 | use std::iter; 5 | use tracing::{debug, instrument}; 6 | 7 | #[derive(Debug)] 8 | pub struct Collector<'a> { 9 | objects: &'a mut Objects, 10 | } 11 | 12 | impl<'a> Collector<'a> { 13 | pub fn new(objects: &'a mut Objects) -> Self { 14 | Self { objects } 15 | } 16 | 17 | #[instrument(skip(self))] 18 | pub fn run(mut self) -> Result<()> { 19 | debug!("starting garbage collector"); 20 | debug!("looking for all objects"); 21 | 22 | let all_objects = self.objects.len()?; 23 | 24 | debug!("... found {}", all_objects); 25 | 26 | let header = self.objects.get_header()?; 27 | let known_dead_objects = self.find_known_dead_objects(header)?; 28 | let alive_objects = self.find_alive_objects(header)?; 29 | 30 | self.collect_objects(header, all_objects, known_dead_objects, alive_objects)?; 31 | 32 | debug!("garbage collection completed"); 33 | 34 | Ok(()) 35 | } 36 | 37 | fn find_known_dead_objects(&mut self, header: HeaderObj) -> Result> { 38 | debug!("looking for known-dead objects"); 39 | 40 | let mut result = BTreeSet::new(); 41 | let mut cursor = header.dead; 42 | 43 | while let Some(oid) = cursor { 44 | result.insert(oid); 45 | 46 | cursor = self.objects.get(oid)?.into_dead(oid)?.next; 47 | } 48 | 49 | debug!("... found {}", result.len()); 50 | 51 | Ok(result) 52 | } 53 | 54 | fn find_alive_objects(&mut self, header: HeaderObj) -> Result> { 55 | debug!("looking for alive objects"); 56 | 57 | let mut result = BTreeSet::new(); 58 | let mut pending: VecDeque<_> = iter::once(header.root).chain(header.clone).collect(); 59 | 60 | while let Some(oid) = pending.pop_front() { 61 | result.insert(oid); 62 | 63 | match self.objects.get(oid)? { 64 | Object::Empty => { 65 | return Err(anyhow!( 66 | "filesystem seems damaged: {:?} is reachable, but it's empty", 67 | oid 68 | )); 69 | } 70 | 71 | Object::Dead(_) => { 72 | return Err(anyhow!( 73 | "filesystem seems damaged: {:?} is reachable, but it's dead", 74 | oid 75 | )); 76 | } 77 | 78 | Object::Header(_) => { 79 | return Err(anyhow!( 80 | "filesystem seems damaged: found second header object (at {:?})", 81 | oid 82 | )); 83 | } 84 | 85 | Object::Clone(obj) => { 86 | pending.push_back(obj.name); 87 | pending.push_back(obj.root); 88 | 89 | if let Some(next) = obj.next { 90 | pending.push_back(next); 91 | } 92 | } 93 | 94 | Object::Entry(obj) => { 95 | pending.push_back(obj.name); 96 | 97 | if let Some(body) = obj.body { 98 | pending.push_back(body); 99 | } 100 | 101 | if let Some(next) = obj.next { 102 | pending.push_back(next); 103 | } 104 | } 105 | 106 | Object::Payload(obj) => { 107 | if let Some(next) = obj.next { 108 | pending.push_back(next); 109 | } 110 | } 111 | } 112 | } 113 | 114 | debug!("... found {}", result.len()); 115 | 116 | Ok(result) 117 | } 118 | 119 | fn collect_objects( 120 | &mut self, 121 | header: HeaderObj, 122 | all_objects: u32, 123 | known_dead_objects: BTreeSet, 124 | alive_objects: BTreeSet, 125 | ) -> Result<()> { 126 | let collectible = (1..all_objects).map(ObjectId::new).collect::>(); 127 | 128 | let collectible = collectible 129 | .difference(&known_dead_objects) 130 | .copied() 131 | .collect::>(); 132 | 133 | let collectible = collectible 134 | .difference(&alive_objects) 135 | .copied() 136 | .collect::>(); 137 | 138 | match collectible.len() { 139 | 1 => debug!("got 1 object to collect"), 140 | n => debug!("got {} objects to collect", n), 141 | } 142 | 143 | if !collectible.is_empty() { 144 | let mut head = header.dead; 145 | 146 | for &oid in collectible.iter() { 147 | self.objects 148 | .set(oid, Object::Dead(DeadObj { next: head }))?; 149 | 150 | head = Some(oid); 151 | } 152 | 153 | self.objects.set_header(HeaderObj { 154 | dead: head, 155 | ..header 156 | })?; 157 | } 158 | 159 | Ok(()) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/filesystem.rs: -------------------------------------------------------------------------------- 1 | mod getattr; 2 | mod lookup; 3 | mod mk; 4 | mod ops; 5 | mod read; 6 | mod readdir; 7 | mod rename; 8 | mod result; 9 | mod rm; 10 | mod setattr; 11 | mod write; 12 | 13 | use self::result::*; 14 | use crate::{Collector, EntryObj, InodeId, Inodes, ObjectId, Objects, Transaction}; 15 | use anyhow::{Context, Result}; 16 | use fuser::{FileAttr, FileType}; 17 | use std::time::UNIX_EPOCH; 18 | 19 | #[derive(Debug)] 20 | pub struct Filesystem { 21 | objects: Objects, 22 | inodes: Inodes, 23 | origin: FilesystemOrigin, 24 | tx: Transaction, 25 | tx_since_last_gc: u32, 26 | } 27 | 28 | impl Filesystem { 29 | pub fn new(mut objects: Objects, origin: FilesystemOrigin) -> Result { 30 | Collector::new(&mut objects) 31 | .run() 32 | .context("garbage collection failed")?; 33 | 34 | let inodes = Inodes::new(origin.root_oid(&mut objects)?)?; 35 | 36 | Ok(Self { 37 | objects, 38 | inodes, 39 | origin, 40 | tx: Default::default(), 41 | tx_since_last_gc: 0, 42 | }) 43 | } 44 | 45 | fn attr(iid: InodeId, obj: EntryObj) -> FileAttr { 46 | const BASE_ATTR: FileAttr = FileAttr { 47 | ino: 0, 48 | size: 0, 49 | blocks: 0, 50 | atime: UNIX_EPOCH, 51 | mtime: UNIX_EPOCH, 52 | ctime: UNIX_EPOCH, 53 | crtime: UNIX_EPOCH, 54 | kind: FileType::RegularFile, 55 | perm: 0, 56 | nlink: 1, 57 | uid: 0, 58 | gid: 0, 59 | rdev: 0, 60 | flags: 0, 61 | blksize: 512, 62 | }; 63 | 64 | FileAttr { 65 | ino: iid.get(), 66 | kind: obj.kind, 67 | size: obj.size as u64, 68 | perm: obj.mode, 69 | uid: obj.uid, 70 | gid: obj.gid, 71 | ..BASE_ATTR 72 | } 73 | } 74 | 75 | fn begin_tx(&mut self) -> Result<()> { 76 | self.tx.begin(&mut self.objects)?; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn commit_tx(&mut self) -> Result<()> { 82 | let got_changes = self 83 | .tx 84 | .commit(&mut self.objects, Some(&mut self.inodes), self.origin)?; 85 | 86 | if got_changes { 87 | self.tx_since_last_gc += 1; 88 | } 89 | 90 | if self.tx_since_last_gc >= 250 { 91 | Collector::new(&mut self.objects) 92 | .run() 93 | .context("garbage collection failed")?; 94 | 95 | self.tx_since_last_gc = 0; 96 | } 97 | 98 | Ok(()) 99 | } 100 | } 101 | 102 | #[derive(Clone, Copy, Debug)] 103 | pub enum FilesystemOrigin { 104 | Main { is_writable: bool }, 105 | Clone { oid: ObjectId, is_writable: bool }, 106 | } 107 | 108 | impl FilesystemOrigin { 109 | fn root_oid(self, objects: &mut Objects) -> Result { 110 | match self { 111 | FilesystemOrigin::Main { .. } => Ok(objects.get_header()?.root), 112 | FilesystemOrigin::Clone { oid, .. } => Ok(objects.get(oid)?.into_clone(oid)?.root), 113 | } 114 | } 115 | 116 | fn is_writable(self) -> bool { 117 | match self { 118 | FilesystemOrigin::Main { is_writable } => is_writable, 119 | FilesystemOrigin::Clone { is_writable, .. } => is_writable, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/filesystem/getattr.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId}; 3 | use fuser::FileAttr; 4 | use tracing::{debug, instrument}; 5 | 6 | impl Filesystem { 7 | #[instrument(skip(self))] 8 | pub fn getattr(&mut self, iid: InodeId) -> FsResult { 9 | debug!("op: getattr()"); 10 | 11 | let oid = self 12 | .inodes 13 | .resolve_object(iid) 14 | .ok() 15 | .ok_or(FsError::NotFound)?; 16 | 17 | let obj = self.objects.get(oid)?.into_entry(oid)?; 18 | 19 | Ok(Self::attr(iid, obj)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/filesystem/lookup.rs: -------------------------------------------------------------------------------- 1 | use super::FsResult; 2 | use crate::{Filesystem, InodeId}; 3 | use fuser::FileAttr; 4 | use std::ffi::OsStr; 5 | use tracing::{debug, instrument}; 6 | 7 | impl Filesystem { 8 | #[instrument(skip(self))] 9 | pub fn lookup(&mut self, parent_iid: InodeId, name: &OsStr) -> FsResult { 10 | debug!("op: lookup()"); 11 | 12 | self.begin_tx()?; 13 | let (iid, obj) = self.find_node(parent_iid, name)?; 14 | self.commit_tx()?; 15 | 16 | Ok(Self::attr(iid, obj)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/filesystem/mk.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{EntryObj, Filesystem, InodeId, Object}; 3 | use anyhow::Context; 4 | use fuser::{FileAttr, FileType}; 5 | use std::ffi::OsStr; 6 | use std::os::unix::ffi::OsStrExt; 7 | use tracing::{debug, instrument}; 8 | 9 | impl Filesystem { 10 | #[instrument(skip(self))] 11 | pub fn mknod( 12 | &mut self, 13 | parent_iid: InodeId, 14 | name: &OsStr, 15 | mode: u32, 16 | uid: u32, 17 | gid: u32, 18 | ) -> FsResult { 19 | debug!("op: mknod()"); 20 | 21 | self.mk(parent_iid, name, mode, uid, gid, FileType::RegularFile) 22 | } 23 | 24 | #[instrument(skip(self))] 25 | pub fn mkdir( 26 | &mut self, 27 | parent_iid: InodeId, 28 | name: &OsStr, 29 | mode: u32, 30 | uid: u32, 31 | gid: u32, 32 | ) -> FsResult { 33 | debug!("op: mkdir()"); 34 | 35 | self.mk(parent_iid, name, mode, uid, gid, FileType::Directory) 36 | } 37 | 38 | fn mk( 39 | &mut self, 40 | parent_iid: InodeId, 41 | name: &OsStr, 42 | mode: u32, 43 | uid: u32, 44 | gid: u32, 45 | kind: FileType, 46 | ) -> FsResult { 47 | if !self.origin.is_writable() { 48 | return Err(FsError::ReadOnly); 49 | } 50 | 51 | self.begin_tx()?; 52 | 53 | let name_oid = self 54 | .objects 55 | .alloc_payload(Some(&mut self.tx), name.as_bytes())? 56 | .context("got an empty name")?; 57 | 58 | let new_parent_oid = self.clone_node(parent_iid)?; 59 | 60 | let obj = EntryObj { 61 | name: name_oid, 62 | body: None, 63 | next: None, 64 | kind, 65 | size: 0, 66 | mode: mode as u16, 67 | uid, 68 | gid, 69 | }; 70 | 71 | let new_oid = self.append_node(new_parent_oid, Object::Entry(obj))?; 72 | 73 | self.commit_tx()?; 74 | 75 | let new_iid = self.inodes.alloc(parent_iid, new_oid)?; 76 | 77 | Ok(Self::attr(new_iid, obj)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/filesystem/ops.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{EntryObj, Filesystem, InodeId, Object, ObjectId}; 3 | use anyhow::Result; 4 | use std::ffi::OsStr; 5 | use tracing::instrument; 6 | 7 | impl Filesystem { 8 | /// Appends object to parent (i.e. adds a file/directory into a directory). 9 | /// 10 | /// Note that this function works in-place, i.e. it assumes that the parent 11 | /// has been already cloned. 12 | #[instrument(skip(self))] 13 | pub fn append_node(&mut self, parent_oid: ObjectId, child: Object) -> Result { 14 | let parent = self.objects.get(parent_oid)?.into_entry(parent_oid)?; 15 | let child_oid = self.objects.alloc(Some(&mut self.tx), child)?; 16 | 17 | if let Some(mut oid) = parent.body { 18 | // If the parent already has a child, find the linked list's tail 19 | // and append our object there 20 | 21 | loop { 22 | let obj = self.objects.get(oid)?.into_entry(oid)?; 23 | 24 | if let Some(next) = obj.next { 25 | oid = next; 26 | } else { 27 | self.objects.set( 28 | oid, 29 | Object::Entry(EntryObj { 30 | next: Some(child_oid), 31 | ..obj 32 | }), 33 | )?; 34 | 35 | break; 36 | } 37 | } 38 | } else { 39 | // If our parent has no children (it's an empty directory), then 40 | // easy peasy - just modify the parent 41 | 42 | self.objects.set( 43 | parent_oid, 44 | Object::Entry(EntryObj { 45 | body: Some(child_oid), 46 | ..parent 47 | }), 48 | )?; 49 | } 50 | 51 | Ok(child_oid) 52 | } 53 | 54 | /// Goes through inode's children and looks for the one with given name. 55 | /// 56 | /// If no such child exists, bails out with [`FsError::NotFound`]. 57 | #[instrument(skip(self))] 58 | pub fn find_node( 59 | &mut self, 60 | parent_iid: InodeId, 61 | name: &OsStr, 62 | ) -> FsResult<(InodeId, EntryObj)> { 63 | let children = self 64 | .inodes 65 | .resolve_children(&mut self.objects, parent_iid) 66 | .ok() 67 | .ok_or(FsError::NotFound)?; 68 | 69 | for iid in children { 70 | let oid = self.inodes.resolve_object(iid)?; 71 | let obj = self.objects.get(oid)?.into_entry(oid)?; 72 | 73 | if self.objects.get_os_string(obj.name)? == name { 74 | return Ok((iid, obj)); 75 | } 76 | } 77 | 78 | Err(FsError::NotFound) 79 | } 80 | 81 | /// Resolves given inode to an object and then clones it recursively, 82 | /// yielding a new object id. 83 | /// 84 | /// This is called e.g. when an entry gets renamed - we clone the entry's 85 | /// object and then modify the object returned by this function. 86 | /// 87 | /// This function can be called at most once per transaction (since it 88 | /// affects inodes and modifies the root). 89 | pub fn clone_node(&mut self, iid: InodeId) -> FsResult { 90 | let new_oid = self.alter(Alter::clone(iid))?; 91 | 92 | // Unwrap-safety: Cloning doesn't remove any objects, so `new_oid` is 93 | // supposed to be `Some` here 94 | Ok(new_oid.unwrap()) 95 | } 96 | 97 | /// Resolves given inode to an object and then clones its siblings, parent 98 | /// etc., but without the object itself. 99 | /// 100 | /// This is called e.g. when an entry gets deleted - we clone entry's tree, 101 | /// but without given entry itself, effectively removing it. 102 | /// 103 | /// This function can be called at most once per transaction (since it 104 | /// affects inodes and modifies the root). 105 | pub fn delete_node(&mut self, iid: InodeId) -> FsResult<()> { 106 | self.alter(Alter::clone(iid).skipping(iid))?; 107 | 108 | Ok(()) 109 | } 110 | 111 | #[instrument(skip(self))] 112 | fn alter(&mut self, op: Alter) -> FsResult> { 113 | let parent_iid = self.inodes.resolve_parent(op.iid)?; 114 | let parent_oid = self.inodes.resolve_object(parent_iid)?; 115 | let parent = self.objects.get(parent_oid)?.into_entry(parent_oid)?; 116 | let head_oid = parent.body; 117 | 118 | let mut children: Vec<_> = self 119 | .inodes 120 | .resolve_children(&mut self.objects, parent_iid)? 121 | .into_iter() 122 | .filter(|iid| op.skipping.map_or(true, |skipping| skipping != *iid)) 123 | .map(|iid| { 124 | let old_oid = self.inodes.resolve_object(iid)?; 125 | let new_oid = self.objects.alloc(Some(&mut self.tx), Object::Empty)?; 126 | let obj = self.objects.get(old_oid)?.into_entry(old_oid)?; 127 | 128 | Ok(AlteredChild { 129 | iid, 130 | oid: new_oid, 131 | obj, 132 | }) 133 | }) 134 | .collect::>()?; 135 | 136 | if let Some((src, dst)) = op.replacing { 137 | for child in &mut children { 138 | if child.obj.body == Some(src) { 139 | child.obj.body = dst; 140 | break; 141 | } 142 | } 143 | } 144 | 145 | // Children form a linked list - since we've got brand new objects, we 146 | // must establish connections between them 147 | for i in 0..children.len() { 148 | let next = children.get(i + 1).map(|n| n.oid); 149 | let curr = &mut children[i]; 150 | 151 | curr.obj.next = next; 152 | } 153 | 154 | for child in &children { 155 | self.objects.set(child.oid, Object::Entry(child.obj))?; 156 | self.tx.remap_inode(child.iid, child.oid)?; 157 | } 158 | 159 | if let Some(iid) = op.skipping { 160 | self.tx.free_inode(iid)?; 161 | } 162 | 163 | let new_oid = children.iter().find_map(|child| { 164 | if child.iid == op.iid { 165 | Some(child.oid) 166 | } else { 167 | None 168 | } 169 | }); 170 | 171 | if parent_iid.is_root() { 172 | let new_root_oid = self.objects.alloc( 173 | Some(&mut self.tx), 174 | Object::Entry(EntryObj { 175 | body: children.first().map(|child| child.oid), 176 | ..parent 177 | }), 178 | )?; 179 | 180 | self.tx.remap_inode(parent_iid, new_root_oid)?; 181 | self.tx.set_root(new_root_oid)?; 182 | 183 | Ok(Some(new_oid.unwrap_or(new_root_oid))) 184 | } else { 185 | let old_head_oid = head_oid.unwrap(); 186 | let new_head_oid = children.first().map(|c| c.oid); 187 | 188 | self.alter(Alter::clone(parent_iid).replacing(old_head_oid, new_head_oid))?; 189 | 190 | Ok(new_oid) 191 | } 192 | } 193 | } 194 | 195 | #[derive(Clone, Copy, Debug)] 196 | struct Alter { 197 | iid: InodeId, 198 | skipping: Option, 199 | replacing: Option<(ObjectId, Option)>, 200 | } 201 | 202 | impl Alter { 203 | fn clone(iid: InodeId) -> Self { 204 | Self { 205 | iid, 206 | skipping: None, 207 | replacing: None, 208 | } 209 | } 210 | 211 | fn skipping(mut self, iid: InodeId) -> Self { 212 | self.skipping = Some(iid); 213 | self 214 | } 215 | 216 | fn replacing(mut self, src: ObjectId, dst: Option) -> Self { 217 | self.replacing = Some((src, dst)); 218 | self 219 | } 220 | } 221 | 222 | #[derive(Clone, Copy, Debug)] 223 | struct AlteredChild { 224 | iid: InodeId, 225 | oid: ObjectId, 226 | obj: EntryObj, 227 | } 228 | -------------------------------------------------------------------------------- /src/filesystem/read.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId}; 3 | use tracing::{debug, instrument}; 4 | 5 | impl Filesystem { 6 | #[instrument(skip(self))] 7 | pub fn read(&mut self, iid: InodeId, offset: i64, size: u32) -> FsResult> { 8 | debug!("op: read()"); 9 | 10 | let oid = self 11 | .inodes 12 | .resolve_object(iid) 13 | .ok() 14 | .ok_or(FsError::NotFound)?; 15 | 16 | let obj = self.objects.get(oid)?.into_entry(oid)?; 17 | 18 | if let Some(body) = obj.body { 19 | // TODO excruciatingly inefficient & wasteful 20 | let data = self.objects.get_payload(body)?; 21 | let data = data[offset as usize..][..size as usize].to_vec(); 22 | 23 | Ok(data) 24 | } else { 25 | Ok(Default::default()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/filesystem/readdir.rs: -------------------------------------------------------------------------------- 1 | use super::FsResult; 2 | use crate::{Filesystem, InodeId}; 3 | use fuser::FileType; 4 | use std::ffi::OsString; 5 | use tracing::{debug, instrument}; 6 | 7 | impl Filesystem { 8 | #[instrument(skip(self))] 9 | pub fn readdir( 10 | &mut self, 11 | iid: InodeId, 12 | mut offset: i64, 13 | ) -> FsResult> { 14 | debug!("op: readdir()"); 15 | 16 | let mut nth = 0; 17 | let mut entries = Vec::new(); 18 | 19 | // --- 20 | 21 | nth += 1; 22 | 23 | if offset == 0 { 24 | entries.push((nth, iid, FileType::Directory, ".".into())); 25 | } else { 26 | offset -= 1; 27 | } 28 | 29 | // --- 30 | 31 | if let Ok(parent_iid) = self.inodes.resolve_parent(iid) { 32 | nth += 1; 33 | 34 | if offset == 0 { 35 | entries.push((nth, parent_iid, FileType::Directory, "..".into())); 36 | } else { 37 | offset -= 1; 38 | } 39 | } 40 | 41 | // --- 42 | 43 | nth += offset; 44 | 45 | let children = self 46 | .inodes 47 | .resolve_children(&mut self.objects, iid)? 48 | .into_iter() 49 | .skip(offset as usize); 50 | 51 | for iid in children { 52 | let oid = self.inodes.resolve_object(iid)?; 53 | let obj = self.objects.get(oid)?.into_entry(oid)?; 54 | let kind = obj.kind; 55 | let name = self.objects.get_os_string(obj.name)?; 56 | 57 | nth += 1; 58 | 59 | entries.push((nth, iid, kind, name)); 60 | } 61 | 62 | Ok(entries) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/filesystem/rename.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId, Object}; 3 | use anyhow::Context; 4 | use std::ffi::OsStr; 5 | use std::os::unix::ffi::OsStrExt; 6 | use tracing::{debug, instrument}; 7 | 8 | impl Filesystem { 9 | #[instrument(skip(self))] 10 | pub fn rename( 11 | &mut self, 12 | old_parent_iid: InodeId, 13 | old_name: &OsStr, 14 | new_parent_iid: InodeId, 15 | new_name: &OsStr, 16 | ) -> FsResult<()> { 17 | debug!("op: rename()"); 18 | 19 | if !self.origin.is_writable() { 20 | return Err(FsError::ReadOnly); 21 | } 22 | 23 | self.begin_tx()?; 24 | let (iid, _) = self.find_node(old_parent_iid, old_name)?; 25 | 26 | if old_parent_iid == new_parent_iid { 27 | if new_name == old_name { 28 | return Ok(()); 29 | } 30 | 31 | let new_oid = self.clone_node(iid)?; 32 | let mut obj = self.objects.get(new_oid)?.into_entry(new_oid)?; 33 | 34 | obj.name = self 35 | .objects 36 | .alloc_payload(Some(&mut self.tx), new_name.as_bytes())? 37 | .context("got an empty name")?; 38 | 39 | self.objects.set(new_oid, Object::Entry(obj))?; 40 | self.commit_tx()?; 41 | 42 | Ok(()) 43 | } else { 44 | Err(FsError::NotImplemented) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/filesystem/result.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use libc::{EIO, ENOENT, ENOSYS, EROFS}; 3 | use std::ffi::c_int; 4 | use tracing::{debug, error}; 5 | 6 | pub type FsResult = Result; 7 | 8 | #[derive(Debug)] 9 | pub enum FsError { 10 | ReadOnly, 11 | NotFound, 12 | NotImplemented, 13 | Other(Error), 14 | } 15 | 16 | impl FsError { 17 | pub fn log_and_convert(self) -> c_int { 18 | match self { 19 | FsError::ReadOnly => { 20 | debug!("... read-only file system"); 21 | EROFS 22 | } 23 | 24 | FsError::NotFound => { 25 | debug!("... not found"); 26 | ENOENT 27 | } 28 | 29 | FsError::NotImplemented => { 30 | debug!("... not implemented"); 31 | ENOSYS 32 | } 33 | 34 | FsError::Other(err) => { 35 | error!("... {:?}", err); 36 | EIO 37 | } 38 | } 39 | } 40 | } 41 | 42 | impl From for FsError { 43 | fn from(err: Error) -> Self { 44 | FsError::Other(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/filesystem/rm.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId}; 3 | use std::ffi::OsStr; 4 | use tracing::{debug, instrument}; 5 | 6 | impl Filesystem { 7 | #[instrument(skip(self))] 8 | pub fn unlink(&mut self, parent_iid: InodeId, name: &OsStr) -> FsResult<()> { 9 | debug!("op: unlink()"); 10 | 11 | self.rm(parent_iid, name) 12 | } 13 | 14 | #[instrument(skip(self))] 15 | pub fn rmdir(&mut self, parent_iid: InodeId, name: &OsStr) -> FsResult<()> { 16 | debug!("op: rmdir()"); 17 | 18 | self.rm(parent_iid, name) 19 | } 20 | 21 | fn rm(&mut self, parent_iid: InodeId, name: &OsStr) -> FsResult<()> { 22 | if !self.origin.is_writable() { 23 | return Err(FsError::ReadOnly); 24 | } 25 | 26 | self.begin_tx()?; 27 | let iid = self.find_node(parent_iid, name)?.0; 28 | self.delete_node(iid)?; 29 | self.commit_tx()?; 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/filesystem/setattr.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId, Object}; 3 | use fuser::FileAttr; 4 | use tracing::{debug, instrument}; 5 | 6 | impl Filesystem { 7 | #[instrument(skip(self))] 8 | pub fn setattr( 9 | &mut self, 10 | iid: InodeId, 11 | mode: Option, 12 | uid: Option, 13 | gid: Option, 14 | size: Option, 15 | ) -> FsResult { 16 | debug!("op: setattr()"); 17 | 18 | if !self.origin.is_writable() { 19 | return Err(FsError::ReadOnly); 20 | } 21 | 22 | self.begin_tx()?; 23 | 24 | let new_oid = self.clone_node(iid)?; 25 | let mut obj = self.objects.get(new_oid)?.into_entry(new_oid)?; 26 | 27 | if let Some(mode) = mode { 28 | obj.mode = mode as u16; 29 | } 30 | 31 | if let Some(uid) = uid { 32 | obj.uid = uid; 33 | } 34 | 35 | if let Some(gid) = gid { 36 | obj.gid = gid; 37 | } 38 | 39 | let truncate = match size { 40 | Some(0) => true, 41 | Some(_) => return Err(FsError::NotImplemented), 42 | None => false, 43 | }; 44 | 45 | if truncate { 46 | obj.size = 0; 47 | obj.body = None; 48 | } 49 | 50 | self.objects.set(new_oid, Object::Entry(obj))?; 51 | self.commit_tx()?; 52 | 53 | Ok(Self::attr(iid, obj)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/filesystem/write.rs: -------------------------------------------------------------------------------- 1 | use super::{FsError, FsResult}; 2 | use crate::{Filesystem, InodeId, Object}; 3 | use tracing::{debug, instrument}; 4 | 5 | impl Filesystem { 6 | #[instrument(skip(self, incoming))] 7 | pub fn write(&mut self, iid: InodeId, offset: i64, incoming: &[u8]) -> FsResult<()> { 8 | debug!("op: write()"); 9 | 10 | if !self.origin.is_writable() { 11 | return Err(FsError::ReadOnly); 12 | } 13 | 14 | self.begin_tx()?; 15 | 16 | let new_oid = self.clone_node(iid)?; 17 | let mut obj = self.objects.get(new_oid)?.into_entry(new_oid)?; 18 | 19 | obj.body = { 20 | // TODO excruciatingly inefficient & wasteful 21 | let mut data = if let Some(body) = obj.body { 22 | self.objects.get_payload(body)? 23 | } else { 24 | Default::default() 25 | }; 26 | 27 | if (offset as usize) + incoming.len() > (obj.size as usize) { 28 | obj.size = (offset as u32) + (incoming.len() as u32); 29 | } 30 | 31 | data.resize(obj.size as usize, 0); 32 | data[offset as usize..][..incoming.len()].copy_from_slice(incoming); 33 | 34 | self.objects.alloc_payload(Some(&mut self.tx), &data)? 35 | }; 36 | 37 | self.objects.set(new_oid, Object::Entry(obj))?; 38 | self.commit_tx()?; 39 | 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/inode.rs: -------------------------------------------------------------------------------- 1 | use crate::{InodeId, ObjectId}; 2 | 3 | #[derive(Debug)] 4 | pub struct Inode { 5 | pub oid: ObjectId, 6 | pub parent: InodeId, 7 | pub children: Option>, 8 | } 9 | -------------------------------------------------------------------------------- /src/inode_id.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 4 | pub struct InodeId(u64); 5 | 6 | impl InodeId { 7 | pub const ROOT: Self = Self(1); 8 | 9 | pub fn new(iid: u64) -> Self { 10 | Self(iid) 11 | } 12 | 13 | pub fn fetch_add(&mut self) -> Result { 14 | let this = *self; 15 | 16 | self.0 = self 17 | .0 18 | .checked_add(1) 19 | .context("reached the maximum number of inodes")?; 20 | 21 | Ok(this) 22 | } 23 | 24 | pub fn is_root(self) -> bool { 25 | self == Self::ROOT 26 | } 27 | 28 | pub fn get(self) -> u64 { 29 | self.0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/inodes.rs: -------------------------------------------------------------------------------- 1 | use crate::{Inode, InodeId, ObjectId, Objects}; 2 | use anyhow::{Context, Result}; 3 | use std::collections::HashMap; 4 | use tracing::{instrument, trace}; 5 | 6 | #[derive(Debug)] 7 | pub struct Inodes { 8 | nodes: HashMap, 9 | next_iid: InodeId, 10 | } 11 | 12 | impl Inodes { 13 | pub fn new(root_oid: ObjectId) -> Result { 14 | let nodes = HashMap::from_iter([( 15 | InodeId::ROOT, 16 | Inode { 17 | oid: root_oid, 18 | parent: InodeId::ROOT, 19 | children: Default::default(), 20 | }, 21 | )]); 22 | 23 | Ok(Self { 24 | nodes, 25 | next_iid: InodeId::new(2), 26 | }) 27 | } 28 | 29 | #[instrument(skip(self))] 30 | pub fn alloc(&mut self, parent_iid: InodeId, oid: ObjectId) -> Result { 31 | let parent = self 32 | .nodes 33 | .get(&parent_iid) 34 | .with_context(|| format!("{:?} is dead", parent_iid))?; 35 | 36 | if let Some(children) = &parent.children { 37 | for iid in children { 38 | if self.nodes[iid].oid == oid { 39 | return Ok(*iid); 40 | } 41 | } 42 | } 43 | 44 | let iid = self.next_iid.fetch_add()?; 45 | 46 | self.nodes.insert( 47 | iid, 48 | Inode { 49 | oid, 50 | parent: parent_iid, 51 | children: Default::default(), 52 | }, 53 | ); 54 | 55 | self.nodes 56 | .get_mut(&parent_iid) 57 | .unwrap() 58 | .children 59 | .get_or_insert_with(Default::default) 60 | .push(iid); 61 | 62 | trace!("allocated inode {:?}", iid); 63 | 64 | Ok(iid) 65 | } 66 | 67 | #[instrument(skip(self))] 68 | pub fn remap(&mut self, iid: InodeId, oid: ObjectId) { 69 | trace!("remapping inode"); 70 | 71 | if let Some(inode) = self.nodes.get_mut(&iid) { 72 | inode.oid = oid; 73 | } 74 | } 75 | 76 | #[instrument(skip(self))] 77 | pub fn free(&mut self, iid: InodeId) { 78 | trace!("freeing inode"); 79 | 80 | let Some(inode) = self.nodes.remove(&iid) else { 81 | return; 82 | }; 83 | 84 | if let Some(parent) = self.nodes.get_mut(&inode.parent) { 85 | if let Some(children) = &mut parent.children { 86 | if let Some(chd_idx) = children.iter().position(|chd| *chd == iid) { 87 | children.swap_remove(chd_idx); 88 | } 89 | } 90 | } 91 | 92 | if let Some(children) = inode.children { 93 | for child_iid in children { 94 | self.free(child_iid); 95 | } 96 | } 97 | } 98 | 99 | pub fn mark_as_empty(&mut self, iid: InodeId) { 100 | if let Some(inode) = self.nodes.get_mut(&iid) { 101 | inode.children = Some(Default::default()); 102 | } 103 | } 104 | 105 | pub fn resolve_object(&self, iid: InodeId) -> Result { 106 | self.nodes 107 | .get(&iid) 108 | .map(|inode| inode.oid) 109 | .with_context(|| format!("{:?} is dead", iid)) 110 | } 111 | 112 | pub fn resolve_parent(&self, iid: InodeId) -> Result { 113 | self.nodes 114 | .get(&iid) 115 | .map(|inode| inode.parent) 116 | .with_context(|| format!("{:?} is dead", iid)) 117 | } 118 | 119 | pub fn resolve_children( 120 | &mut self, 121 | objects: &mut Objects, 122 | iid: InodeId, 123 | ) -> Result> { 124 | let inode = self 125 | .nodes 126 | .get(&iid) 127 | .with_context(|| format!("{:?} is dead", iid))?; 128 | 129 | if inode.children.is_none() { 130 | let obj = objects.get(inode.oid)?.into_entry(inode.oid)?; 131 | 132 | let children = if obj.body.is_none() { 133 | Some(Default::default()) 134 | } else { 135 | let mut children = Vec::new(); 136 | let mut cursor = obj.body; 137 | 138 | while let Some(oid) = cursor { 139 | children.push(self.alloc(iid, oid)?); 140 | 141 | cursor = objects.get(oid)?.into_entry(oid)?.next; 142 | } 143 | 144 | Some(children) 145 | }; 146 | 147 | self.nodes.get_mut(&iid).unwrap().children = children; 148 | } 149 | 150 | // TODO get rid of extra allocation 151 | Ok(self.nodes[&iid].children.clone().unwrap()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod clone; 2 | mod cmds; 3 | mod collector; 4 | mod filesystem; 5 | mod inode; 6 | mod inode_id; 7 | mod inodes; 8 | mod object; 9 | mod object_id; 10 | mod object_rw; 11 | mod objects; 12 | mod storage; 13 | mod transaction; 14 | 15 | pub use self::clone::*; 16 | pub use self::cmds::*; 17 | pub use self::collector::*; 18 | pub use self::filesystem::*; 19 | pub use self::inode::*; 20 | pub use self::inode_id::*; 21 | pub use self::inodes::*; 22 | pub use self::object::*; 23 | pub use self::object_id::*; 24 | pub use self::object_rw::*; 25 | pub use self::objects::*; 26 | pub use self::storage::*; 27 | pub use self::transaction::*; 28 | use anyhow::Result; 29 | use structopt::StructOpt; 30 | 31 | /// OstFS, a toy FUSE filesystem with support for zero-cost snapshots and clones 32 | #[derive(Debug, StructOpt)] 33 | enum Cmd { 34 | Clone(CloneCmd), 35 | Create(CreateCmd), 36 | Inspect(InspectCmd), 37 | Mount(MountCmd), 38 | } 39 | 40 | fn main() -> Result<()> { 41 | match Cmd::from_args() { 42 | Cmd::Clone(cmd) => cmd.run(), 43 | Cmd::Create(cmd) => cmd.run(), 44 | Cmd::Inspect(cmd) => cmd.run(), 45 | Cmd::Mount(cmd) => cmd.run(), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/object.rs: -------------------------------------------------------------------------------- 1 | use crate::{ObjectId, ObjectReader, ObjectWriter}; 2 | use anyhow::{anyhow, Result}; 3 | use fuser::FileType; 4 | use std::fmt::{self, Debug}; 5 | 6 | #[derive(Clone, Copy, Debug)] 7 | pub enum Object { 8 | Empty, 9 | Header(HeaderObj), 10 | Clone(CloneObj), 11 | Entry(EntryObj), 12 | Payload(PayloadObj), 13 | Dead(DeadObj), 14 | } 15 | 16 | impl Object { 17 | pub const SIZE: usize = 32; 18 | 19 | pub const TY_EMPTY: u8 = 0; 20 | pub const TY_HEADER: u8 = 1; 21 | pub const TY_CLONE: u8 = 2; 22 | pub const TY_ENTRY: u8 = 3; 23 | pub const TY_FILE: u8 = 4; 24 | pub const TY_PAYLOAD: u8 = 5; 25 | pub const TY_DEAD: u8 = 6; 26 | 27 | pub const PAYLOAD_LEN: usize = 26; 28 | 29 | pub fn decode(obj: [u8; Self::SIZE]) -> Result { 30 | let mut reader = ObjectReader::new(obj); 31 | 32 | match reader.u8() { 33 | Self::TY_EMPTY => Ok(Object::Empty), 34 | 35 | Self::TY_HEADER => Ok(Object::Header(HeaderObj { 36 | root: reader.oid(), 37 | dead: reader.oid_opt(), 38 | clone: reader.oid_opt(), 39 | })), 40 | 41 | Self::TY_CLONE => Ok(Object::Clone(CloneObj { 42 | name: reader.oid(), 43 | root: reader.oid(), 44 | is_writable: reader.bool(), 45 | next: reader.oid_opt(), 46 | })), 47 | 48 | Self::TY_ENTRY => Ok(Object::Entry(EntryObj { 49 | name: reader.oid(), 50 | body: reader.oid_opt(), 51 | next: reader.oid_opt(), 52 | kind: reader.kind(), 53 | size: reader.u32(), 54 | mode: reader.u16(), 55 | uid: reader.u32(), 56 | gid: reader.u32(), 57 | })), 58 | 59 | Self::TY_PAYLOAD => Ok(Object::Payload(PayloadObj { 60 | size: reader.u8(), 61 | next: reader.oid_opt(), 62 | data: reader.rest(), 63 | })), 64 | 65 | Self::TY_DEAD => Ok(Object::Dead(DeadObj { 66 | next: reader.oid_opt(), 67 | })), 68 | 69 | ty => Err(anyhow!("unknown object type: {}", ty)), 70 | } 71 | } 72 | 73 | pub fn encode(self) -> [u8; Self::SIZE] { 74 | let mut writer = ObjectWriter::default(); 75 | 76 | match self { 77 | Object::Empty => { 78 | // 79 | } 80 | 81 | Object::Header(HeaderObj { root, dead, clone }) => { 82 | writer.u8(Self::TY_HEADER); 83 | writer.oid(root); 84 | writer.oid_opt(dead); 85 | writer.oid_opt(clone); 86 | } 87 | 88 | Object::Clone(CloneObj { 89 | root, 90 | name, 91 | is_writable: is_snapshot, 92 | next, 93 | }) => { 94 | writer.u8(Self::TY_CLONE); 95 | writer.oid(name); 96 | writer.oid(root); 97 | writer.bool(is_snapshot); 98 | writer.oid_opt(next); 99 | } 100 | 101 | Object::Entry(EntryObj { 102 | name, 103 | body, 104 | next, 105 | kind, 106 | size, 107 | mode, 108 | uid, 109 | gid, 110 | }) => { 111 | writer.u8(Self::TY_ENTRY); 112 | writer.oid(name); 113 | writer.oid_opt(body); 114 | writer.oid_opt(next); 115 | writer.kind(kind); 116 | writer.u32(size); 117 | writer.u16(mode); 118 | writer.u32(uid); 119 | writer.u32(gid); 120 | } 121 | 122 | Object::Payload(PayloadObj { size, next, data }) => { 123 | let mut data = data.iter().copied(); 124 | 125 | writer.u8(Self::TY_PAYLOAD); 126 | writer.u8(size); 127 | writer.oid_opt(next); 128 | writer.rest(|| data.next()); 129 | } 130 | 131 | Object::Dead(DeadObj { next }) => { 132 | writer.u8(Self::TY_DEAD); 133 | writer.oid_opt(next); 134 | } 135 | } 136 | 137 | writer.finish() 138 | } 139 | 140 | pub fn into_header(self, oid: ObjectId) -> Result { 141 | if let Object::Header(obj) = self { 142 | Ok(obj) 143 | } else { 144 | Err(anyhow!("expected header object at {:?}", oid)) 145 | } 146 | } 147 | 148 | pub fn into_clone(self, oid: ObjectId) -> Result { 149 | if let Object::Clone(obj) = self { 150 | Ok(obj) 151 | } else { 152 | Err(anyhow!("expected clone object at {:?}", oid)) 153 | } 154 | } 155 | 156 | pub fn into_entry(self, oid: ObjectId) -> Result { 157 | if let Object::Entry(obj) = self { 158 | Ok(obj) 159 | } else { 160 | Err(anyhow!("expected entry object at {:?}", oid)) 161 | } 162 | } 163 | 164 | pub fn into_payload(self, oid: ObjectId) -> Result { 165 | if let Object::Payload(obj) = self { 166 | Ok(obj) 167 | } else { 168 | Err(anyhow!("expected payload object at {:?}", oid)) 169 | } 170 | } 171 | 172 | pub fn into_dead(self, oid: ObjectId) -> Result { 173 | if let Object::Dead(obj) = self { 174 | Ok(obj) 175 | } else { 176 | Err(anyhow!("expected dead object at {:?}", oid)) 177 | } 178 | } 179 | 180 | pub fn payload(data: &[u8]) -> Self { 181 | Object::Payload(PayloadObj::new(data)) 182 | } 183 | } 184 | 185 | #[derive(Clone, Copy, Debug)] 186 | pub struct HeaderObj { 187 | /// Points at [`EntryObj`] containing the root directory 188 | pub root: ObjectId, 189 | 190 | /// Points at first [`CloneObj`] 191 | pub clone: Option, 192 | 193 | /// Points at first [`DeadObj`] 194 | pub dead: Option, 195 | } 196 | 197 | #[derive(Clone, Copy, Debug)] 198 | pub struct CloneObj { 199 | /// Points at [`PayloadObj`] containing the clone's name 200 | pub name: ObjectId, 201 | 202 | /// Points at [`EntryObj`] containing the clone's root directory 203 | pub root: ObjectId, 204 | 205 | /// Whether it's a "clone-clone" or rather a snapshot 206 | pub is_writable: bool, 207 | 208 | /// Points at the sibling [`CloneObj`] (it's a linked list) 209 | pub next: Option, 210 | } 211 | 212 | /// File or directory 213 | #[derive(Clone, Copy, Debug)] 214 | pub struct EntryObj { 215 | /// Points at [`PayloadObj`] containing the entry's name 216 | pub name: ObjectId, 217 | 218 | /// Points either at enother [`EntryObj`] (if this is a directory) or at 219 | /// [`PayloadObj`] (if this is a file) 220 | pub body: Option, 221 | 222 | /// Points at the sibling [`EntryObj`] (intuitively, it points at the next 223 | /// item in the directory which this entry is contained within) 224 | pub next: Option, 225 | 226 | pub kind: FileType, 227 | pub size: u32, 228 | pub mode: u16, 229 | pub uid: u32, 230 | pub gid: u32, 231 | } 232 | 233 | /// String or binary data 234 | #[derive(Clone, Copy)] 235 | pub struct PayloadObj { 236 | pub size: u8, 237 | pub next: Option, 238 | pub data: [u8; Object::PAYLOAD_LEN], 239 | } 240 | 241 | impl PayloadObj { 242 | pub fn new(data: &[u8]) -> Self { 243 | assert!(data.len() <= Object::PAYLOAD_LEN); 244 | 245 | let mut buf = [0; Object::PAYLOAD_LEN]; 246 | 247 | buf[0..data.len()].copy_from_slice(data); 248 | 249 | Self { 250 | size: data.len() as u8, 251 | next: None, 252 | data: buf, 253 | } 254 | } 255 | } 256 | 257 | impl Debug for PayloadObj { 258 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 259 | let data = &self.data[0..(self.size as usize)]; 260 | 261 | let data = if let Ok(data) = String::from_utf8(data.to_vec()) { 262 | data 263 | } else { 264 | data.iter() 265 | .map(|b| format!("{:#02x}", b)) 266 | .collect::>() 267 | .join(" ") 268 | }; 269 | 270 | f.debug_struct("Payload") 271 | .field("size", &self.size) 272 | .field("next", &self.next) 273 | .field("data", &data) 274 | .finish() 275 | } 276 | } 277 | 278 | /// Object that's been garbage collected; used by the object allocator to reuse 279 | /// space instead of always allocating a new object 280 | #[derive(Clone, Copy, Debug)] 281 | pub struct DeadObj { 282 | pub next: Option, 283 | } 284 | -------------------------------------------------------------------------------- /src/object_id.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 2 | pub struct ObjectId(u32); 3 | 4 | impl ObjectId { 5 | pub const HEADER: Self = Self(0); 6 | 7 | pub fn new(oid: u32) -> Self { 8 | Self(oid) 9 | } 10 | 11 | pub fn get(self) -> u32 { 12 | self.0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/object_rw.rs: -------------------------------------------------------------------------------- 1 | use fuser::FileType; 2 | 3 | use crate::{Object, ObjectId}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct ObjectWriter { 7 | data: [u8; Object::SIZE], 8 | len: usize, 9 | } 10 | 11 | impl ObjectWriter { 12 | pub fn bool(&mut self, val: bool) { 13 | self.u8(val as u8); 14 | } 15 | 16 | pub fn u8(&mut self, val: u8) { 17 | self.data[self.len] = val; 18 | self.len += 1; 19 | } 20 | 21 | pub fn u16(&mut self, val: u16) { 22 | for b in val.to_be_bytes() { 23 | self.u8(b); 24 | } 25 | } 26 | 27 | pub fn u32(&mut self, val: u32) { 28 | for b in val.to_be_bytes() { 29 | self.u8(b); 30 | } 31 | } 32 | 33 | pub fn oid(&mut self, oid: ObjectId) { 34 | self.u32(oid.get()); 35 | } 36 | 37 | pub fn oid_opt(&mut self, oid: Option) { 38 | self.u32(oid.map(|oid| oid.get()).unwrap_or_default()); 39 | } 40 | 41 | pub fn kind(&mut self, kind: FileType) { 42 | self.u8(if kind == FileType::Directory { 0 } else { 1 }) 43 | } 44 | 45 | pub fn rest(&mut self, mut next_byte: impl FnMut() -> Option) { 46 | while let Some(byte) = next_byte() { 47 | self.u8(byte); 48 | } 49 | } 50 | 51 | pub fn finish(self) -> [u8; Object::SIZE] { 52 | self.data 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct ObjectReader { 58 | data: [u8; Object::SIZE], 59 | len: usize, 60 | } 61 | 62 | impl ObjectReader { 63 | pub fn new(data: [u8; Object::SIZE]) -> Self { 64 | Self { data, len: 0 } 65 | } 66 | 67 | pub fn bool(&mut self) -> bool { 68 | self.u8() == 1 69 | } 70 | 71 | pub fn u8(&mut self) -> u8 { 72 | let out = self.data[self.len]; 73 | 74 | self.len += 1; 75 | 76 | out 77 | } 78 | 79 | pub fn u16(&mut self) -> u16 { 80 | let d0 = self.u8(); 81 | let d1 = self.u8(); 82 | 83 | u16::from_be_bytes([d0, d1]) 84 | } 85 | 86 | pub fn u32(&mut self) -> u32 { 87 | let d0 = self.u8(); 88 | let d1 = self.u8(); 89 | let d2 = self.u8(); 90 | let d3 = self.u8(); 91 | 92 | u32::from_be_bytes([d0, d1, d2, d3]) 93 | } 94 | 95 | pub fn oid(&mut self) -> ObjectId { 96 | ObjectId::new(self.u32()) 97 | } 98 | 99 | pub fn oid_opt(&mut self) -> Option { 100 | let oid = self.u32(); 101 | 102 | if oid == 0 { 103 | None 104 | } else { 105 | Some(ObjectId::new(oid)) 106 | } 107 | } 108 | 109 | pub fn kind(&mut self) -> FileType { 110 | if self.u8() == 0 { 111 | FileType::Directory 112 | } else { 113 | FileType::RegularFile 114 | } 115 | } 116 | 117 | pub fn rest(&mut self) -> [u8; N] { 118 | self.data[self.len..] 119 | .try_into() 120 | .expect("rest of the block has unexpected size") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/objects.rs: -------------------------------------------------------------------------------- 1 | use crate::{HeaderObj, Object, ObjectId, PayloadObj, Storage, Transaction}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use std::ffi::OsString; 4 | use tracing::{instrument, trace, warn}; 5 | 6 | #[derive(Debug)] 7 | pub struct Objects { 8 | storage: Storage, 9 | } 10 | 11 | impl Objects { 12 | pub fn new(storage: Storage) -> Self { 13 | Self { storage } 14 | } 15 | 16 | #[allow(clippy::len_without_is_empty)] 17 | #[instrument(skip(self))] 18 | pub fn len(&mut self) -> Result { 19 | self.storage.len() 20 | } 21 | 22 | #[instrument(skip(self))] 23 | pub fn all(&mut self) -> Result> { 24 | let mut objs = Vec::new(); 25 | let mut oidx = 0; 26 | 27 | while let Ok(object) = self.storage.read(oidx) { 28 | let oid = ObjectId::new(oidx); 29 | 30 | let obj = 31 | Object::decode(object).with_context(|| format!("couldn't decode {:?}", oid))?; 32 | 33 | objs.push((oid, obj)); 34 | oidx += 1; 35 | } 36 | 37 | Ok(objs) 38 | } 39 | 40 | #[instrument(skip(self))] 41 | pub fn get(&mut self, oid: ObjectId) -> Result { 42 | trace!("reading object"); 43 | 44 | if oid == ObjectId::HEADER { 45 | return Err(anyhow!("tried to get() the header object")); 46 | } 47 | 48 | let obj = self 49 | .storage 50 | .read(oid.get()) 51 | .with_context(|| format!("couldn't read object {:?}", oid))?; 52 | 53 | Object::decode(obj).with_context(|| format!("couldn't decode {:?}", oid)) 54 | } 55 | 56 | #[instrument(skip(self))] 57 | pub fn get_header(&mut self) -> Result { 58 | let obj = self 59 | .storage 60 | .read(ObjectId::HEADER.get()) 61 | .context("couldn't read header")?; 62 | 63 | Object::decode(obj) 64 | .context("couldn't decode header")? 65 | .into_header(ObjectId::HEADER) 66 | } 67 | 68 | pub fn get_payload(&mut self, mut oid: ObjectId) -> Result> { 69 | let mut buf = Vec::new(); 70 | 71 | loop { 72 | let obj = self.get(oid)?.into_payload(oid)?; 73 | 74 | buf.extend_from_slice(&obj.data[0..(obj.size as usize)]); 75 | 76 | if let Some(next) = obj.next { 77 | oid = next; 78 | } else { 79 | break; 80 | } 81 | } 82 | 83 | Ok(buf) 84 | } 85 | 86 | pub fn get_string(&mut self, oid: ObjectId) -> Result { 87 | let buf = self.get_payload(oid)?; 88 | 89 | Ok(String::from_utf8_lossy(&buf).into()) 90 | } 91 | 92 | pub fn get_os_string(&mut self, oid: ObjectId) -> Result { 93 | Ok(self.get_string(oid)?.into()) 94 | } 95 | 96 | #[instrument(skip(self, oid, obj))] 97 | pub fn set(&mut self, oid: ObjectId, obj: Object) -> Result<()> { 98 | trace!("writing object [{:?}] = {:?}", oid, obj); 99 | 100 | if oid == ObjectId::HEADER { 101 | return Err(anyhow!("tried to set() the header object")); 102 | } 103 | 104 | self.storage 105 | .write(oid.get(), obj.encode()) 106 | .with_context(|| format!("couldn't write [{:?}] = {:?}", oid, obj))?; 107 | 108 | Ok(()) 109 | } 110 | 111 | #[instrument(skip(self, obj))] 112 | pub fn set_header(&mut self, obj: HeaderObj) -> Result<()> { 113 | trace!("writing header: {:?}", obj); 114 | 115 | self.storage 116 | .write(ObjectId::HEADER.get(), Object::Header(obj).encode()) 117 | .context("couldn't write header")?; 118 | 119 | Ok(()) 120 | } 121 | 122 | #[instrument(skip(self, tx, obj))] 123 | pub fn alloc(&mut self, tx: Option<&mut Transaction>, obj: Object) -> Result { 124 | if let Some(tx) = tx { 125 | let tx = tx.get_mut()?; 126 | 127 | if let Some(oid) = tx.new_header.dead { 128 | let dead_obj = self.get(oid)?; 129 | 130 | if let Ok(dead_obj) = dead_obj.into_dead(oid) { 131 | trace!("reusing {:?} = {:?}", oid, obj); 132 | 133 | self.storage 134 | .write(oid.get(), obj.encode()) 135 | .with_context(|| format!("couldn't write [{:?}] = {:?}", oid, obj))?; 136 | 137 | tx.dirty = true; 138 | tx.new_header.dead = dead_obj.next; 139 | 140 | return Ok(oid); 141 | } else { 142 | warn!("can't reuse {:?}, it's been already overwritten", oid); 143 | } 144 | } 145 | } 146 | 147 | let oid = self.storage.append(obj.encode())?; 148 | let oid = ObjectId::new(oid); 149 | 150 | trace!("creating {:?} = {:?}", oid, obj); 151 | 152 | Ok(oid) 153 | } 154 | 155 | pub fn alloc_payload( 156 | &mut self, 157 | mut tx: Option<&mut Transaction>, 158 | payload: &[u8], 159 | ) -> Result> { 160 | let mut next = None; 161 | 162 | for chunk in payload.chunks(Object::PAYLOAD_LEN).rev() { 163 | let curr = self.alloc( 164 | tx.as_deref_mut(), 165 | Object::Payload(PayloadObj { 166 | next, 167 | ..PayloadObj::new(chunk) 168 | }), 169 | )?; 170 | 171 | next = Some(curr); 172 | } 173 | 174 | Ok(next) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::Object; 2 | use anyhow::{anyhow, Context, Result}; 3 | use std::fs::File; 4 | use std::io::{Read, Seek, SeekFrom, Write}; 5 | use std::path::Path; 6 | use tracing::{info, instrument}; 7 | 8 | #[derive(Debug)] 9 | pub struct Storage { 10 | file: File, 11 | can_grow: bool, 12 | } 13 | 14 | impl Storage { 15 | #[instrument] 16 | pub fn create(path: &Path) -> Result { 17 | info!("creating store"); 18 | 19 | let file = File::create_new(path) 20 | .with_context(|| format!("couldn't create: {}", path.display()))?; 21 | 22 | Ok(Self { 23 | file, 24 | can_grow: true, 25 | }) 26 | } 27 | 28 | #[instrument] 29 | pub fn open(path: &Path, can_grow: bool) -> Result { 30 | info!("opening store"); 31 | 32 | let file = File::options() 33 | .read(true) 34 | .write(true) 35 | .open(path) 36 | .with_context(|| format!("couldn't open: {}", path.display()))?; 37 | 38 | Ok(Self { file, can_grow }) 39 | } 40 | 41 | #[allow(clippy::len_without_is_empty)] 42 | pub fn len(&mut self) -> Result { 43 | let bytes = self.file.seek(SeekFrom::End(0))?; 44 | 45 | Ok((bytes as u32) / (Object::SIZE as u32)) 46 | } 47 | 48 | pub fn seek(&mut self, object_id: u32) -> Result<()> { 49 | self.file 50 | .seek(SeekFrom::Start((object_id as u64) * (Object::SIZE as u64))) 51 | .with_context(|| format!("seek() failed: oid={}", object_id))?; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn read(&mut self, object_id: u32) -> Result<[u8; Object::SIZE]> { 57 | let mut object = [0; Object::SIZE]; 58 | 59 | self.seek(object_id)?; 60 | 61 | self.file 62 | .read_exact(&mut object) 63 | .with_context(|| format!("read() failed: oid={}", object_id))?; 64 | 65 | Ok(object) 66 | } 67 | 68 | pub fn write(&mut self, object_id: u32, object: [u8; Object::SIZE]) -> Result<()> { 69 | self.seek(object_id)?; 70 | 71 | self.file 72 | .write_all(&object) 73 | .with_context(|| format!("write() failed: ois={}", object_id))?; 74 | 75 | Ok(()) 76 | } 77 | 78 | pub fn append(&mut self, object: [u8; Object::SIZE]) -> Result { 79 | if !self.can_grow { 80 | return Err(anyhow!( 81 | "cannot create a new object, because storage was opened in non-growable mode" 82 | )); 83 | } 84 | 85 | let pos = self 86 | .file 87 | .seek(SeekFrom::End(0)) 88 | .context("seek() failed") 89 | .context("append() failed")?; 90 | 91 | self.file 92 | .write_all(&object) 93 | .context("write() failed") 94 | .context("append() failed")?; 95 | 96 | Ok((pos as u32) / (Object::SIZE as u32)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::{CloneObj, FilesystemOrigin, HeaderObj, InodeId, Inodes, Object, ObjectId, Objects}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use tracing::warn; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct Transaction { 7 | state: Option, 8 | } 9 | 10 | impl Transaction { 11 | pub fn begin(&mut self, objects: &mut Objects) -> Result<()> { 12 | if let Some(state) = self.state.take() { 13 | if state.dirty { 14 | warn!("previous transaction got aborted"); 15 | } 16 | } 17 | 18 | self.state = Some(TransactionState { 19 | dirty: false, 20 | new_root: None, 21 | new_header: objects.get_header()?, 22 | inodes_to_remap: Default::default(), 23 | inodes_to_free: Default::default(), 24 | }); 25 | 26 | Ok(()) 27 | } 28 | 29 | pub fn get_mut(&mut self) -> Result<&mut TransactionState> { 30 | self.state 31 | .as_mut() 32 | .context("tried to modify a closed transaction") 33 | } 34 | 35 | /// Schedules given node to replace the current root node. 36 | /// 37 | /// If we're working on a clone, this updates the clone's root - otherwise 38 | /// this updates the header's root. 39 | pub fn set_root(&mut self, new_root_oid: ObjectId) -> Result<()> { 40 | let tx = self.get_mut()?; 41 | 42 | if tx.new_root.is_some() { 43 | return Err(anyhow!("set_root() called twice in a single transaction")); 44 | } 45 | 46 | tx.dirty = true; 47 | tx.new_root = Some(new_root_oid); 48 | 49 | Ok(()) 50 | } 51 | 52 | /// Schedules given inode to point at new object. 53 | pub fn remap_inode(&mut self, src: InodeId, dst: ObjectId) -> Result<()> { 54 | let tx = self.get_mut()?; 55 | 56 | tx.dirty = true; 57 | tx.inodes_to_remap.push((src, dst)); 58 | 59 | Ok(()) 60 | } 61 | 62 | /// Schedules given inode to be freed. 63 | pub fn free_inode(&mut self, iid: InodeId) -> Result<()> { 64 | let tx = self.get_mut()?; 65 | 66 | tx.dirty = true; 67 | tx.inodes_to_free.push(iid); 68 | 69 | Ok(()) 70 | } 71 | 72 | /// Applies scheduled changes atomically and returns whether anything got 73 | /// changed. 74 | /// 75 | /// (i.e. false = transaction was a no-op) 76 | pub fn commit( 77 | &mut self, 78 | objects: &mut Objects, 79 | inodes: Option<&mut Inodes>, 80 | origin: FilesystemOrigin, 81 | ) -> Result { 82 | let mut tx = self 83 | .state 84 | .take() 85 | .context("tried to commit a closed transaction")?; 86 | 87 | if !tx.dirty { 88 | return Ok(false); 89 | } 90 | 91 | if let Some(new_root_oid) = tx.new_root { 92 | if let FilesystemOrigin::Main { .. } = origin { 93 | tx.new_header.root = new_root_oid; 94 | } 95 | } 96 | 97 | objects.set_header(tx.new_header)?; 98 | 99 | if let Some(new_root_oid) = tx.new_root { 100 | if let FilesystemOrigin::Clone { oid, .. } = origin { 101 | let obj = CloneObj { 102 | root: new_root_oid, 103 | ..objects.get(oid)?.into_clone(oid)? 104 | }; 105 | 106 | objects.set(oid, Object::Clone(obj))?; 107 | } 108 | } 109 | 110 | if !tx.inodes_to_remap.is_empty() || !tx.inodes_to_free.is_empty() { 111 | let inodes = inodes.context("tried to commit changeset without having any inodes")?; 112 | 113 | for (src, dst) in tx.inodes_to_remap { 114 | inodes.remap(src, dst); 115 | } 116 | 117 | for iid in tx.inodes_to_free { 118 | inodes.free(iid); 119 | } 120 | } 121 | 122 | Ok(true) 123 | } 124 | } 125 | 126 | #[derive(Debug)] 127 | pub struct TransactionState { 128 | pub dirty: bool, 129 | pub new_root: Option, 130 | pub new_header: HeaderObj, 131 | pub inodes_to_remap: Vec<(InodeId, ObjectId)>, 132 | pub inodes_to_free: Vec, 133 | } 134 | --------------------------------------------------------------------------------