diff options
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | NOTES.md | 3 | ||||
| -rwxr-xr-x | auth.sh | 8 | ||||
| -rw-r--r-- | deployment.yaml | 28 | ||||
| -rw-r--r-- | fctdrive/Cargo.lock | 351 | ||||
| -rw-r--r-- | fctdrive/Cargo.toml | 13 | ||||
| -rw-r--r-- | fctdrive/src/main.rs | 1025 | ||||
| -rw-r--r-- | filesystem.go | 244 | ||||
| -rw-r--r-- | go.mod | 8 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | index.html | 17 | ||||
| -rw-r--r-- | main.go | 75 | ||||
| -rw-r--r-- | path.go | 75 | ||||
| -rw-r--r-- | service.yaml | 0 |
14 files changed, 1398 insertions, 459 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99ffd03 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | /miei/ | ||
| 2 | /drive/ | ||
| 3 | /blobs/ | ||
| 4 | /fctdrive/blobs/ | ||
| 5 | write.lock | ||
| 6 | log.txt | ||
diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..8d55dcb --- /dev/null +++ b/NOTES.md | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | # fct drive | ||
| 2 | |||
| 3 | |||
diff --git a/auth.sh b/auth.sh deleted file mode 100755 index aa3dc1f..0000000 --- a/auth.sh +++ /dev/null | |||
| @@ -1,8 +0,0 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | |||
| 3 | podman run -it --rm --network host \ | ||
| 4 | -e SECRET="12345678912345678911111111111111" \ | ||
| 5 | -e APP_URL="http://10.0.0.92:3000" \ | ||
| 6 | -e USERS='diogo464:$2a$10$fE78J/Rq7kSik1cmXQ82Be6.zx3P4GEjhlifnI.ACHpHb5sDH/J1W' \ | ||
| 7 | ghcr.io/steveiliop56/tinyauth:v3 | ||
| 8 | |||
diff --git a/deployment.yaml b/deployment.yaml deleted file mode 100644 index 3bcd9cc..0000000 --- a/deployment.yaml +++ /dev/null | |||
| @@ -1,28 +0,0 @@ | |||
| 1 | --- | ||
| 2 | apiVersion: apps/v1 | ||
| 3 | kind: Deployment | ||
| 4 | metadata: | ||
| 5 | name: auth | ||
| 6 | spec: | ||
| 7 | selector: | ||
| 8 | matchLabels: | ||
| 9 | app: auth | ||
| 10 | template: | ||
| 11 | metadata: | ||
| 12 | labels: | ||
| 13 | app: auth | ||
| 14 | app.kubernetes.io/name: auth | ||
| 15 | spec: | ||
| 16 | containers: | ||
| 17 | - name: auth | ||
| 18 | image: ghcr.io/steveiliop56/tinyauth:v3 | ||
| 19 | resources: | ||
| 20 | requests: | ||
| 21 | memory: "64Mi" | ||
| 22 | cpu: "0" | ||
| 23 | limits: | ||
| 24 | memory: "256Mi" | ||
| 25 | cpu: "1" | ||
| 26 | ports: | ||
| 27 | - name: http | ||
| 28 | containerPort: 8000 | ||
diff --git a/fctdrive/Cargo.lock b/fctdrive/Cargo.lock new file mode 100644 index 0000000..9642104 --- /dev/null +++ b/fctdrive/Cargo.lock | |||
| @@ -0,0 +1,351 @@ | |||
| 1 | # This file is automatically @generated by Cargo. | ||
| 2 | # It is not intended for manual editing. | ||
| 3 | version = 4 | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "anstream" | ||
| 7 | version = "0.6.20" | ||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" | ||
| 10 | dependencies = [ | ||
| 11 | "anstyle", | ||
| 12 | "anstyle-parse", | ||
| 13 | "anstyle-query", | ||
| 14 | "anstyle-wincon", | ||
| 15 | "colorchoice", | ||
| 16 | "is_terminal_polyfill", | ||
| 17 | "utf8parse", | ||
| 18 | ] | ||
| 19 | |||
| 20 | [[package]] | ||
| 21 | name = "anstyle" | ||
| 22 | version = "1.0.11" | ||
| 23 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 24 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" | ||
| 25 | |||
| 26 | [[package]] | ||
| 27 | name = "anstyle-parse" | ||
| 28 | version = "0.2.7" | ||
| 29 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 30 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" | ||
| 31 | dependencies = [ | ||
| 32 | "utf8parse", | ||
| 33 | ] | ||
| 34 | |||
| 35 | [[package]] | ||
| 36 | name = "anstyle-query" | ||
| 37 | version = "1.1.4" | ||
| 38 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 39 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" | ||
| 40 | dependencies = [ | ||
| 41 | "windows-sys", | ||
| 42 | ] | ||
| 43 | |||
| 44 | [[package]] | ||
| 45 | name = "anstyle-wincon" | ||
| 46 | version = "3.0.10" | ||
| 47 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 48 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" | ||
| 49 | dependencies = [ | ||
| 50 | "anstyle", | ||
| 51 | "once_cell_polyfill", | ||
| 52 | "windows-sys", | ||
| 53 | ] | ||
| 54 | |||
| 55 | [[package]] | ||
| 56 | name = "block-buffer" | ||
| 57 | version = "0.10.4" | ||
| 58 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 59 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||
| 60 | dependencies = [ | ||
| 61 | "generic-array", | ||
| 62 | ] | ||
| 63 | |||
| 64 | [[package]] | ||
| 65 | name = "cfg-if" | ||
| 66 | version = "1.0.1" | ||
| 67 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 68 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" | ||
| 69 | |||
| 70 | [[package]] | ||
| 71 | name = "clap" | ||
| 72 | version = "4.5.43" | ||
| 73 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 74 | checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" | ||
| 75 | dependencies = [ | ||
| 76 | "clap_builder", | ||
| 77 | "clap_derive", | ||
| 78 | ] | ||
| 79 | |||
| 80 | [[package]] | ||
| 81 | name = "clap_builder" | ||
| 82 | version = "4.5.43" | ||
| 83 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 84 | checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" | ||
| 85 | dependencies = [ | ||
| 86 | "anstream", | ||
| 87 | "anstyle", | ||
| 88 | "clap_lex", | ||
| 89 | "strsim", | ||
| 90 | ] | ||
| 91 | |||
| 92 | [[package]] | ||
| 93 | name = "clap_derive" | ||
| 94 | version = "4.5.41" | ||
| 95 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 96 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" | ||
| 97 | dependencies = [ | ||
| 98 | "heck", | ||
| 99 | "proc-macro2", | ||
| 100 | "quote", | ||
| 101 | "syn", | ||
| 102 | ] | ||
| 103 | |||
| 104 | [[package]] | ||
| 105 | name = "clap_lex" | ||
| 106 | version = "0.7.5" | ||
| 107 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 108 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" | ||
| 109 | |||
| 110 | [[package]] | ||
| 111 | name = "colorchoice" | ||
| 112 | version = "1.0.4" | ||
| 113 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 114 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" | ||
| 115 | |||
| 116 | [[package]] | ||
| 117 | name = "cpufeatures" | ||
| 118 | version = "0.2.17" | ||
| 119 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 120 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | ||
| 121 | dependencies = [ | ||
| 122 | "libc", | ||
| 123 | ] | ||
| 124 | |||
| 125 | [[package]] | ||
| 126 | name = "crypto-common" | ||
| 127 | version = "0.1.6" | ||
| 128 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 129 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | ||
| 130 | dependencies = [ | ||
| 131 | "generic-array", | ||
| 132 | "typenum", | ||
| 133 | ] | ||
| 134 | |||
| 135 | [[package]] | ||
| 136 | name = "digest" | ||
| 137 | version = "0.10.7" | ||
| 138 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 139 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||
| 140 | dependencies = [ | ||
| 141 | "block-buffer", | ||
| 142 | "crypto-common", | ||
| 143 | ] | ||
| 144 | |||
| 145 | [[package]] | ||
| 146 | name = "fctdrive" | ||
| 147 | version = "0.1.0" | ||
| 148 | dependencies = [ | ||
| 149 | "clap", | ||
| 150 | "hex", | ||
| 151 | "sha2", | ||
| 152 | "slotmap", | ||
| 153 | ] | ||
| 154 | |||
| 155 | [[package]] | ||
| 156 | name = "generic-array" | ||
| 157 | version = "0.14.7" | ||
| 158 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 159 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | ||
| 160 | dependencies = [ | ||
| 161 | "typenum", | ||
| 162 | "version_check", | ||
| 163 | ] | ||
| 164 | |||
| 165 | [[package]] | ||
| 166 | name = "heck" | ||
| 167 | version = "0.5.0" | ||
| 168 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 169 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | ||
| 170 | |||
| 171 | [[package]] | ||
| 172 | name = "hex" | ||
| 173 | version = "1.1.0" | ||
| 174 | source = "git+https://git.d464.sh/hex-rs#ed091ffa8206658262a0e2409a35d8910e5d0682" | ||
| 175 | |||
| 176 | [[package]] | ||
| 177 | name = "is_terminal_polyfill" | ||
| 178 | version = "1.70.1" | ||
| 179 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 180 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" | ||
| 181 | |||
| 182 | [[package]] | ||
| 183 | name = "libc" | ||
| 184 | version = "0.2.174" | ||
| 185 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 186 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" | ||
| 187 | |||
| 188 | [[package]] | ||
| 189 | name = "once_cell_polyfill" | ||
| 190 | version = "1.70.1" | ||
| 191 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 192 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" | ||
| 193 | |||
| 194 | [[package]] | ||
| 195 | name = "proc-macro2" | ||
| 196 | version = "1.0.95" | ||
| 197 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 198 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" | ||
| 199 | dependencies = [ | ||
| 200 | "unicode-ident", | ||
| 201 | ] | ||
| 202 | |||
| 203 | [[package]] | ||
| 204 | name = "quote" | ||
| 205 | version = "1.0.40" | ||
| 206 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 207 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" | ||
| 208 | dependencies = [ | ||
| 209 | "proc-macro2", | ||
| 210 | ] | ||
| 211 | |||
| 212 | [[package]] | ||
| 213 | name = "sha2" | ||
| 214 | version = "0.10.9" | ||
| 215 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 216 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | ||
| 217 | dependencies = [ | ||
| 218 | "cfg-if", | ||
| 219 | "cpufeatures", | ||
| 220 | "digest", | ||
| 221 | ] | ||
| 222 | |||
| 223 | [[package]] | ||
| 224 | name = "slotmap" | ||
| 225 | version = "1.0.7" | ||
| 226 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 227 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" | ||
| 228 | dependencies = [ | ||
| 229 | "version_check", | ||
| 230 | ] | ||
| 231 | |||
| 232 | [[package]] | ||
| 233 | name = "strsim" | ||
| 234 | version = "0.11.1" | ||
| 235 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 236 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | ||
| 237 | |||
| 238 | [[package]] | ||
| 239 | name = "syn" | ||
| 240 | version = "2.0.104" | ||
| 241 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 242 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" | ||
| 243 | dependencies = [ | ||
| 244 | "proc-macro2", | ||
| 245 | "quote", | ||
| 246 | "unicode-ident", | ||
| 247 | ] | ||
| 248 | |||
| 249 | [[package]] | ||
| 250 | name = "typenum" | ||
| 251 | version = "1.18.0" | ||
| 252 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 253 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" | ||
| 254 | |||
| 255 | [[package]] | ||
| 256 | name = "unicode-ident" | ||
| 257 | version = "1.0.18" | ||
| 258 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 259 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" | ||
| 260 | |||
| 261 | [[package]] | ||
| 262 | name = "utf8parse" | ||
| 263 | version = "0.2.2" | ||
| 264 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 265 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | ||
| 266 | |||
| 267 | [[package]] | ||
| 268 | name = "version_check" | ||
| 269 | version = "0.9.5" | ||
| 270 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 271 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | ||
| 272 | |||
| 273 | [[package]] | ||
| 274 | name = "windows-link" | ||
| 275 | version = "0.1.3" | ||
| 276 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 277 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" | ||
| 278 | |||
| 279 | [[package]] | ||
| 280 | name = "windows-sys" | ||
| 281 | version = "0.60.2" | ||
| 282 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 283 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | ||
| 284 | dependencies = [ | ||
| 285 | "windows-targets", | ||
| 286 | ] | ||
| 287 | |||
| 288 | [[package]] | ||
| 289 | name = "windows-targets" | ||
| 290 | version = "0.53.3" | ||
| 291 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 292 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" | ||
| 293 | dependencies = [ | ||
| 294 | "windows-link", | ||
| 295 | "windows_aarch64_gnullvm", | ||
| 296 | "windows_aarch64_msvc", | ||
| 297 | "windows_i686_gnu", | ||
| 298 | "windows_i686_gnullvm", | ||
| 299 | "windows_i686_msvc", | ||
| 300 | "windows_x86_64_gnu", | ||
| 301 | "windows_x86_64_gnullvm", | ||
| 302 | "windows_x86_64_msvc", | ||
| 303 | ] | ||
| 304 | |||
| 305 | [[package]] | ||
| 306 | name = "windows_aarch64_gnullvm" | ||
| 307 | version = "0.53.0" | ||
| 308 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 309 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" | ||
| 310 | |||
| 311 | [[package]] | ||
| 312 | name = "windows_aarch64_msvc" | ||
| 313 | version = "0.53.0" | ||
| 314 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 315 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" | ||
| 316 | |||
| 317 | [[package]] | ||
| 318 | name = "windows_i686_gnu" | ||
| 319 | version = "0.53.0" | ||
| 320 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 321 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" | ||
| 322 | |||
| 323 | [[package]] | ||
| 324 | name = "windows_i686_gnullvm" | ||
| 325 | version = "0.53.0" | ||
| 326 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 327 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" | ||
| 328 | |||
| 329 | [[package]] | ||
| 330 | name = "windows_i686_msvc" | ||
| 331 | version = "0.53.0" | ||
| 332 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 333 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" | ||
| 334 | |||
| 335 | [[package]] | ||
| 336 | name = "windows_x86_64_gnu" | ||
| 337 | version = "0.53.0" | ||
| 338 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 339 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" | ||
| 340 | |||
| 341 | [[package]] | ||
| 342 | name = "windows_x86_64_gnullvm" | ||
| 343 | version = "0.53.0" | ||
| 344 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 345 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" | ||
| 346 | |||
| 347 | [[package]] | ||
| 348 | name = "windows_x86_64_msvc" | ||
| 349 | version = "0.53.0" | ||
| 350 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 351 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" | ||
diff --git a/fctdrive/Cargo.toml b/fctdrive/Cargo.toml new file mode 100644 index 0000000..f2ffa1d --- /dev/null +++ b/fctdrive/Cargo.toml | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | [package] | ||
| 2 | name = "fctdrive" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | |||
| 6 | [dependencies] | ||
| 7 | clap = { version = "4.5.43", features = ["derive", "env"] } | ||
| 8 | hex = { git = "https://git.d464.sh/hex-rs", version = "1.1.0" } | ||
| 9 | sha2 = "0.10.9" | ||
| 10 | slotmap = "1.0.7" | ||
| 11 | |||
| 12 | [profile.release] | ||
| 13 | debug = true | ||
diff --git a/fctdrive/src/main.rs b/fctdrive/src/main.rs new file mode 100644 index 0000000..0f7c83c --- /dev/null +++ b/fctdrive/src/main.rs | |||
| @@ -0,0 +1,1025 @@ | |||
| 1 | use std::{ | ||
| 2 | collections::{HashMap, VecDeque}, | ||
| 3 | fmt::Display, | ||
| 4 | fs::File, | ||
| 5 | io::{BufWriter, Read as _, Write}, | ||
| 6 | path::{Path, PathBuf}, | ||
| 7 | str::FromStr, | ||
| 8 | sync::{Arc, Mutex}, | ||
| 9 | time::SystemTime, | ||
| 10 | }; | ||
| 11 | |||
| 12 | use clap::{Args, Parser, Subcommand}; | ||
| 13 | use sha2::Digest; | ||
| 14 | use slotmap::SlotMap; | ||
| 15 | |||
| 16 | const BLOB_STORE_HIERARCHY_DEPTH: usize = 4; | ||
| 17 | |||
| 18 | #[derive(Debug)] | ||
| 19 | pub struct InvalidBlobId; | ||
| 20 | |||
| 21 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 22 | pub struct BlobId([u8; 32]); | ||
| 23 | |||
| 24 | impl BlobId { | ||
| 25 | pub fn hash(&self, data: &[u8]) -> BlobId { | ||
| 26 | let mut hasher = sha2::Sha256::default(); | ||
| 27 | hasher.write_all(data).unwrap(); | ||
| 28 | Self(hasher.finalize().try_into().unwrap()) | ||
| 29 | } | ||
| 30 | |||
| 31 | pub fn as_bytes(&self) -> &[u8; 32] { | ||
| 32 | &self.0 | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | impl Display for BlobId { | ||
| 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 38 | hex::display(&self.0).fmt(f) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | impl FromStr for BlobId { | ||
| 43 | type Err = InvalidBlobId; | ||
| 44 | |||
| 45 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 46 | let decoded = hex::decode_hex(s).map_err(|_| InvalidBlobId)?; | ||
| 47 | Ok(Self(decoded.try_into().map_err(|_| InvalidBlobId)?)) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | #[derive(Debug, Clone)] | ||
| 52 | pub struct BlobStore(PathBuf); | ||
| 53 | |||
| 54 | impl BlobStore { | ||
| 55 | pub fn new(path: impl Into<PathBuf>) -> Self { | ||
| 56 | Self(path.into()) | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | pub fn blob_path(store: &BlobStore, blob_id: &BlobId) -> PathBuf { | ||
| 61 | let encoded = hex::encode_hex(blob_id.as_bytes()); | ||
| 62 | let mut path = store.0.clone(); | ||
| 63 | for depth in 0..BLOB_STORE_HIERARCHY_DEPTH { | ||
| 64 | let base_idx = depth * 2; | ||
| 65 | path.push(&encoded[base_idx..base_idx + 2]); | ||
| 66 | } | ||
| 67 | path.push(encoded); | ||
| 68 | path | ||
| 69 | } | ||
| 70 | |||
| 71 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 72 | pub enum ImportMode { | ||
| 73 | Move, | ||
| 74 | Copy, | ||
| 75 | HardLink, | ||
| 76 | } | ||
| 77 | |||
| 78 | pub fn blob_import_file( | ||
| 79 | store: &BlobStore, | ||
| 80 | mode: ImportMode, | ||
| 81 | blob_id: &BlobId, | ||
| 82 | file_path: &Path, | ||
| 83 | ) -> std::io::Result<()> { | ||
| 84 | let blob_path = blob_path(store, blob_id); | ||
| 85 | if blob_path.exists() { | ||
| 86 | return Ok(()); | ||
| 87 | } | ||
| 88 | |||
| 89 | if let Some(parent) = blob_path.parent() { | ||
| 90 | std::fs::create_dir_all(parent)?; | ||
| 91 | } | ||
| 92 | match mode { | ||
| 93 | ImportMode::Move => { | ||
| 94 | std::fs::rename(file_path, blob_path)?; | ||
| 95 | } | ||
| 96 | ImportMode::Copy => { | ||
| 97 | todo!() | ||
| 98 | } | ||
| 99 | ImportMode::HardLink => match std::fs::hard_link(file_path, blob_path) { | ||
| 100 | Ok(()) => {} | ||
| 101 | Err(err) => { | ||
| 102 | if err.kind() != std::io::ErrorKind::AlreadyExists { | ||
| 103 | panic!("{err}"); | ||
| 104 | } | ||
| 105 | } | ||
| 106 | }, | ||
| 107 | } | ||
| 108 | Ok(()) | ||
| 109 | } | ||
| 110 | |||
| 111 | pub fn blob_hash_file(file_path: &Path) -> std::io::Result<BlobId> { | ||
| 112 | let mut file = std::fs::File::open(file_path)?; | ||
| 113 | let mut buf = vec![0u8; 1 * 1024 * 1024]; | ||
| 114 | let mut hasher = sha2::Sha256::default(); | ||
| 115 | loop { | ||
| 116 | let n = file.read(&mut buf)?; | ||
| 117 | if n == 0 { | ||
| 118 | break; | ||
| 119 | } | ||
| 120 | hasher.write_all(&buf[..n])?; | ||
| 121 | } | ||
| 122 | Ok(BlobId(hasher.finalize().try_into().unwrap())) | ||
| 123 | } | ||
| 124 | |||
| 125 | const OPERATION_KIND_CREATE_FILE: &'static str = "create_file"; | ||
| 126 | const OPERATION_KIND_CREATE_DIR: &'static str = "create_dir"; | ||
| 127 | const OPERATION_KIND_REMOVE: &'static str = "remove"; | ||
| 128 | const OPERATION_KIND_RENAME: &'static str = "rename"; | ||
| 129 | |||
| 130 | #[derive(Debug)] | ||
| 131 | pub struct InvalidOperation; | ||
| 132 | |||
| 133 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 134 | pub struct Operation { | ||
| 135 | pub header: OperationHeader, | ||
| 136 | pub data: OperationData, | ||
| 137 | } | ||
| 138 | |||
| 139 | impl FromStr for Operation { | ||
| 140 | type Err = InvalidOperation; | ||
| 141 | |||
| 142 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 143 | let err = || InvalidOperation; | ||
| 144 | let mut iter = s.split('\t'); | ||
| 145 | |||
| 146 | let timestamp_str = iter.next().ok_or_else(err)?; | ||
| 147 | let revision_str = iter.next().ok_or_else(err)?; | ||
| 148 | let email_str = iter.next().ok_or_else(err)?; | ||
| 149 | let kind_str = iter.next().ok_or_else(err)?; | ||
| 150 | |||
| 151 | let timestamp = timestamp_str.parse().map_err(|_| err())?; | ||
| 152 | let revision = revision_str.parse().map_err(|_| err())?; | ||
| 153 | let email = email_str.to_string(); | ||
| 154 | let data = match kind_str { | ||
| 155 | OPERATION_KIND_CREATE_FILE => { | ||
| 156 | let path_str = iter.next().ok_or_else(err)?; | ||
| 157 | let blob_str = iter.next().ok_or_else(err)?; | ||
| 158 | |||
| 159 | let path = path_str.parse().map_err(|_| err())?; | ||
| 160 | let blob = blob_str.parse().map_err(|_| err())?; | ||
| 161 | |||
| 162 | OperationData::CreateFile(OperationCreateFile { path, blob }) | ||
| 163 | } | ||
| 164 | OPERATION_KIND_CREATE_DIR => { | ||
| 165 | let path_str = iter.next().ok_or_else(err)?; | ||
| 166 | |||
| 167 | let path = path_str.parse().map_err(|_| err())?; | ||
| 168 | |||
| 169 | OperationData::CreateDir(OperationCreateDir { path }) | ||
| 170 | } | ||
| 171 | OPERATION_KIND_RENAME => { | ||
| 172 | let old_str = iter.next().ok_or_else(err)?; | ||
| 173 | let new_str = iter.next().ok_or_else(err)?; | ||
| 174 | |||
| 175 | let old = old_str.parse().map_err(|_| err())?; | ||
| 176 | let new = new_str.parse().map_err(|_| err())?; | ||
| 177 | |||
| 178 | OperationData::Rename(OperationRename { old, new }) | ||
| 179 | } | ||
| 180 | OPERATION_KIND_REMOVE => { | ||
| 181 | let path_str = iter.next().ok_or_else(err)?; | ||
| 182 | |||
| 183 | let path = path_str.parse().map_err(|_| err())?; | ||
| 184 | |||
| 185 | OperationData::Remove(OperationRemove { path }) | ||
| 186 | } | ||
| 187 | _ => return Err(err()), | ||
| 188 | }; | ||
| 189 | |||
| 190 | Ok(Self { | ||
| 191 | header: OperationHeader { | ||
| 192 | timestamp, | ||
| 193 | revision, | ||
| 194 | email, | ||
| 195 | }, | ||
| 196 | data, | ||
| 197 | }) | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | impl Display for Operation { | ||
| 202 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 203 | write!( | ||
| 204 | f, | ||
| 205 | "{}\t{}\t{}\t", | ||
| 206 | self.header.timestamp, self.header.revision, self.header.email | ||
| 207 | )?; | ||
| 208 | match &self.data { | ||
| 209 | OperationData::CreateFile(data) => write!( | ||
| 210 | f, | ||
| 211 | "{}\t{}\t{}", | ||
| 212 | OPERATION_KIND_CREATE_FILE, data.path, data.blob | ||
| 213 | ), | ||
| 214 | OperationData::CreateDir(data) => { | ||
| 215 | write!(f, "{}\t{}", OPERATION_KIND_CREATE_DIR, data.path) | ||
| 216 | } | ||
| 217 | OperationData::Remove(data) => write!(f, "{}\t{}", OPERATION_KIND_REMOVE, data.path), | ||
| 218 | OperationData::Rename(data) => { | ||
| 219 | write!(f, "{}\t{}\t{}", OPERATION_KIND_RENAME, data.old, data.new) | ||
| 220 | } | ||
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | |||
| 225 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 226 | pub struct OperationHeader { | ||
| 227 | pub timestamp: u64, | ||
| 228 | pub revision: u64, | ||
| 229 | pub email: String, | ||
| 230 | } | ||
| 231 | |||
| 232 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 233 | pub enum OperationData { | ||
| 234 | CreateFile(OperationCreateFile), | ||
| 235 | CreateDir(OperationCreateDir), | ||
| 236 | Remove(OperationRemove), | ||
| 237 | Rename(OperationRename), | ||
| 238 | } | ||
| 239 | |||
| 240 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 241 | pub struct OperationCreateFile { | ||
| 242 | pub path: DrivePath, | ||
| 243 | pub blob: BlobId, | ||
| 244 | } | ||
| 245 | |||
| 246 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 247 | pub struct OperationCreateDir { | ||
| 248 | pub path: DrivePath, | ||
| 249 | } | ||
| 250 | |||
| 251 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 252 | pub struct OperationRemove { | ||
| 253 | pub path: DrivePath, | ||
| 254 | } | ||
| 255 | |||
| 256 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 257 | pub struct OperationRename { | ||
| 258 | pub old: DrivePath, | ||
| 259 | pub new: DrivePath, | ||
| 260 | } | ||
| 261 | |||
| 262 | #[derive(Debug)] | ||
| 263 | pub struct InvalidDrivePath(String); | ||
| 264 | |||
| 265 | impl std::fmt::Display for InvalidDrivePath { | ||
| 266 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 267 | write!(f, "invalid path: {}", self.0) | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | impl std::error::Error for InvalidDrivePath {} | ||
| 272 | |||
| 273 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] | ||
| 274 | pub struct DrivePath(Vec<DrivePathComponent>); | ||
| 275 | |||
| 276 | impl Display for DrivePath { | ||
| 277 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 278 | if self.is_root() { | ||
| 279 | f.write_str("/")?; | ||
| 280 | } else { | ||
| 281 | for comp in self.components() { | ||
| 282 | f.write_str("/")?; | ||
| 283 | comp.fmt(f)?; | ||
| 284 | } | ||
| 285 | } | ||
| 286 | Ok(()) | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | impl DrivePath { | ||
| 291 | pub fn is_root(&self) -> bool { | ||
| 292 | self.0.is_empty() | ||
| 293 | } | ||
| 294 | |||
| 295 | pub fn push(&self, component: DrivePathComponent) -> DrivePath { | ||
| 296 | let mut components = self.0.clone(); | ||
| 297 | components.push(component); | ||
| 298 | Self(components) | ||
| 299 | } | ||
| 300 | |||
| 301 | pub fn components(&self) -> &[DrivePathComponent] { | ||
| 302 | self.0.as_slice() | ||
| 303 | } | ||
| 304 | |||
| 305 | pub fn parent(&self) -> DrivePath { | ||
| 306 | if self.0.is_empty() { | ||
| 307 | Self(Default::default()) | ||
| 308 | } else { | ||
| 309 | let slice = self.0.as_slice(); | ||
| 310 | Self(slice[..slice.len() - 1].to_owned()) | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | pub fn last(&self) -> Option<DrivePathComponent> { | ||
| 315 | self.0.last().cloned() | ||
| 316 | } | ||
| 317 | |||
| 318 | pub fn split(&self) -> Option<(DrivePath, DrivePathComponent)> { | ||
| 319 | if self.0.is_empty() { | ||
| 320 | None | ||
| 321 | } else { | ||
| 322 | Some(( | ||
| 323 | Self(self.0[..self.0.len() - 1].to_owned()), | ||
| 324 | self.0[self.0.len() - 1].clone(), | ||
| 325 | )) | ||
| 326 | } | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | impl FromStr for DrivePath { | ||
| 331 | type Err = InvalidDrivePath; | ||
| 332 | |||
| 333 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 334 | let mut components = Vec::default(); | ||
| 335 | for unchecked_component in s.trim().trim_matches('/').split('/') { | ||
| 336 | match unchecked_component.parse() { | ||
| 337 | Ok(component) => components.push(component), | ||
| 338 | Err(err) => { | ||
| 339 | return Err(InvalidDrivePath(format!( | ||
| 340 | "path contained invalid component '{unchecked_component}': {err}" | ||
| 341 | ))); | ||
| 342 | } | ||
| 343 | }; | ||
| 344 | } | ||
| 345 | Ok(Self(components)) | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | #[derive(Debug)] | ||
| 350 | pub struct InvalidDrivePathComponent(&'static str); | ||
| 351 | |||
| 352 | impl Display for InvalidDrivePathComponent { | ||
| 353 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 354 | self.0.fmt(f) | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | impl InvalidDrivePathComponent { | ||
| 359 | pub const fn new(msg: &'static str) -> Self { | ||
| 360 | Self(msg) | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 365 | pub struct DrivePathComponent(String); | ||
| 366 | |||
| 367 | impl Display for DrivePathComponent { | ||
| 368 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 369 | self.0.fmt(f) | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 373 | impl DrivePathComponent { | ||
| 374 | pub fn as_str(&self) -> &str { | ||
| 375 | &self.0 | ||
| 376 | } | ||
| 377 | } | ||
| 378 | |||
| 379 | impl FromStr for DrivePathComponent { | ||
| 380 | type Err = InvalidDrivePathComponent; | ||
| 381 | |||
| 382 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 383 | if s.is_empty() { | ||
| 384 | return Err(InvalidDrivePathComponent::new( | ||
| 385 | "path component cannot be empty", | ||
| 386 | )); | ||
| 387 | } | ||
| 388 | |||
| 389 | if s.contains('\t') { | ||
| 390 | return Err(InvalidDrivePathComponent::new( | ||
| 391 | "path component cannot contain tabs", | ||
| 392 | )); | ||
| 393 | } | ||
| 394 | |||
| 395 | if s == "." || s == ".." { | ||
| 396 | return Err(InvalidDrivePathComponent::new( | ||
| 397 | "path component cannot be '.' or '..'", | ||
| 398 | )); | ||
| 399 | } | ||
| 400 | |||
| 401 | if s.contains('/') { | ||
| 402 | return Err(InvalidDrivePathComponent::new( | ||
| 403 | "path component cannot contain '/'", | ||
| 404 | )); | ||
| 405 | } | ||
| 406 | |||
| 407 | if s.len() > 256 { | ||
| 408 | return Err(InvalidDrivePathComponent::new("path component is too long")); | ||
| 409 | } | ||
| 410 | |||
| 411 | Ok(Self(s.to_string())) | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | #[derive(Debug)] | ||
| 416 | pub struct FsError(String); | ||
| 417 | |||
| 418 | impl From<String> for FsError { | ||
| 419 | fn from(value: String) -> Self { | ||
| 420 | Self(value) | ||
| 421 | } | ||
| 422 | } | ||
| 423 | |||
| 424 | impl From<&str> for FsError { | ||
| 425 | fn from(value: &str) -> Self { | ||
| 426 | Self(value.to_string()) | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 431 | pub enum FsNodeKind { | ||
| 432 | File, | ||
| 433 | Directory, | ||
| 434 | } | ||
| 435 | |||
| 436 | pub struct FsNode { | ||
| 437 | pub kind: FsNodeKind, | ||
| 438 | pub author: String, | ||
| 439 | pub lastmod: u64, | ||
| 440 | pub children: HashMap<DrivePathComponent, FsNodeId>, | ||
| 441 | pub blob: BlobId, | ||
| 442 | } | ||
| 443 | |||
| 444 | slotmap::new_key_type! { pub struct FsNodeId; } | ||
| 445 | |||
| 446 | pub struct Fs { | ||
| 447 | pub root: FsNodeId, | ||
| 448 | pub nodes: SlotMap<FsNodeId, FsNode>, | ||
| 449 | } | ||
| 450 | |||
| 451 | impl Default for Fs { | ||
| 452 | fn default() -> Self { | ||
| 453 | let mut nodes: SlotMap<FsNodeId, FsNode> = Default::default(); | ||
| 454 | let root = nodes.insert(FsNode { | ||
| 455 | kind: FsNodeKind::Directory, | ||
| 456 | author: "system".to_string(), | ||
| 457 | lastmod: 0, | ||
| 458 | children: Default::default(), | ||
| 459 | blob: Default::default(), | ||
| 460 | }); | ||
| 461 | Self { root, nodes } | ||
| 462 | } | ||
| 463 | } | ||
| 464 | |||
| 465 | pub fn apply(fs: &mut Fs, op: &Operation) -> Result<(), FsError> { | ||
| 466 | match &op.data { | ||
| 467 | OperationData::CreateFile(data) => apply_create_file(fs, &op.header, &data), | ||
| 468 | OperationData::CreateDir(data) => apply_create_dir(fs, &op.header, &data), | ||
| 469 | OperationData::Remove(data) => apply_remove(fs, &op.header, &data), | ||
| 470 | OperationData::Rename(data) => apply_rename(fs, &op.header, &data), | ||
| 471 | } | ||
| 472 | } | ||
| 473 | |||
| 474 | pub fn apply_create_file( | ||
| 475 | fs: &mut Fs, | ||
| 476 | header: &OperationHeader, | ||
| 477 | data: &OperationCreateFile, | ||
| 478 | ) -> Result<(), FsError> { | ||
| 479 | if data.path.is_root() { | ||
| 480 | return Err(FsError::from("cannot create file at root")); | ||
| 481 | } | ||
| 482 | |||
| 483 | let mut parent_id = fs.root; | ||
| 484 | for comp in data.path.parent().components() { | ||
| 485 | let node = &mut fs.nodes[parent_id]; | ||
| 486 | if node.kind != FsNodeKind::Directory { | ||
| 487 | return Err(FsError::from("cannot create file under another file")); | ||
| 488 | } | ||
| 489 | parent_id = match node.children.get(comp) { | ||
| 490 | Some(id) => *id, | ||
| 491 | None => { | ||
| 492 | let id = fs.nodes.insert(FsNode { | ||
| 493 | kind: FsNodeKind::Directory, | ||
| 494 | author: header.email.clone(), | ||
| 495 | lastmod: header.timestamp, | ||
| 496 | children: Default::default(), | ||
| 497 | blob: Default::default(), | ||
| 498 | }); | ||
| 499 | fs.nodes[parent_id].children.insert(comp.clone(), id); | ||
| 500 | id | ||
| 501 | } | ||
| 502 | }; | ||
| 503 | } | ||
| 504 | |||
| 505 | let name = data.path.last().unwrap(); | ||
| 506 | let parent = &mut fs.nodes[parent_id]; | ||
| 507 | |||
| 508 | if parent.kind != FsNodeKind::Directory { | ||
| 509 | return Err(FsError::from("cannot create file under another file")); | ||
| 510 | } | ||
| 511 | |||
| 512 | match parent.children.get(&name).copied() { | ||
| 513 | Some(node_id) => { | ||
| 514 | let node = &mut fs.nodes[node_id]; | ||
| 515 | if node.kind == FsNodeKind::Directory { | ||
| 516 | return Err(FsError::from( | ||
| 517 | "node at path already exists but is a directory", | ||
| 518 | )); | ||
| 519 | } | ||
| 520 | node.author = header.email.clone(); | ||
| 521 | node.lastmod = header.timestamp; | ||
| 522 | node.blob = data.blob.clone(); | ||
| 523 | } | ||
| 524 | None => { | ||
| 525 | let id = fs.nodes.insert(FsNode { | ||
| 526 | kind: FsNodeKind::File, | ||
| 527 | author: header.email.clone(), | ||
| 528 | lastmod: header.timestamp, | ||
| 529 | children: Default::default(), | ||
| 530 | blob: data.blob.clone(), | ||
| 531 | }); | ||
| 532 | fs.nodes[parent_id].children.insert(name, id); | ||
| 533 | } | ||
| 534 | } | ||
| 535 | Ok(()) | ||
| 536 | } | ||
| 537 | |||
| 538 | pub fn apply_create_dir( | ||
| 539 | fs: &mut Fs, | ||
| 540 | header: &OperationHeader, | ||
| 541 | data: &OperationCreateDir, | ||
| 542 | ) -> Result<(), FsError> { | ||
| 543 | let (parent, name) = data | ||
| 544 | .path | ||
| 545 | .split() | ||
| 546 | .ok_or_else(|| FsError::from("cannot recreate root directory"))?; | ||
| 547 | |||
| 548 | let mut parent_id = fs.root; | ||
| 549 | for comp in parent.components() { | ||
| 550 | let node = &fs.nodes[parent_id]; | ||
| 551 | parent_id = match node.children.get(comp) { | ||
| 552 | Some(&child_id) => { | ||
| 553 | let child = &fs.nodes[child_id]; | ||
| 554 | if child.kind == FsNodeKind::File { | ||
| 555 | return Err(FsError::from("cannot create directory under file")); | ||
| 556 | } | ||
| 557 | child_id | ||
| 558 | } | ||
| 559 | None => { | ||
| 560 | let id = fs.nodes.insert(FsNode { | ||
| 561 | kind: FsNodeKind::Directory, | ||
| 562 | author: header.email.clone(), | ||
| 563 | lastmod: header.timestamp, | ||
| 564 | children: Default::default(), | ||
| 565 | blob: Default::default(), | ||
| 566 | }); | ||
| 567 | fs.nodes[parent_id].children.insert(comp.clone(), id); | ||
| 568 | id | ||
| 569 | } | ||
| 570 | }; | ||
| 571 | } | ||
| 572 | |||
| 573 | let parent = &fs.nodes[parent_id]; | ||
| 574 | match parent.children.get(&name).copied() { | ||
| 575 | Some(child_id) => { | ||
| 576 | let child = &fs.nodes[child_id]; | ||
| 577 | if child.kind != FsNodeKind::Directory { | ||
| 578 | return Err(FsError::from( | ||
| 579 | "cannot create directory, the given path is already a file", | ||
| 580 | )); | ||
| 581 | } | ||
| 582 | } | ||
| 583 | None => { | ||
| 584 | let id = fs.nodes.insert(FsNode { | ||
| 585 | kind: FsNodeKind::Directory, | ||
| 586 | author: header.email.clone(), | ||
| 587 | lastmod: header.timestamp, | ||
| 588 | children: Default::default(), | ||
| 589 | blob: Default::default(), | ||
| 590 | }); | ||
| 591 | |||
| 592 | let parent = &mut fs.nodes[parent_id]; | ||
| 593 | parent.children.insert(name, id); | ||
| 594 | } | ||
| 595 | } | ||
| 596 | |||
| 597 | Ok(()) | ||
| 598 | } | ||
| 599 | |||
| 600 | pub fn apply_remove( | ||
| 601 | fs: &mut Fs, | ||
| 602 | _header: &OperationHeader, | ||
| 603 | data: &OperationRemove, | ||
| 604 | ) -> Result<(), FsError> { | ||
| 605 | let (parent, name) = data | ||
| 606 | .path | ||
| 607 | .split() | ||
| 608 | .ok_or_else(|| FsError::from("cannot remove root directory"))?; | ||
| 609 | |||
| 610 | let parent_id = match find_node(fs, &parent) { | ||
| 611 | Some(id) => id, | ||
| 612 | None => return Ok(()), | ||
| 613 | }; | ||
| 614 | |||
| 615 | let parent = &mut fs.nodes[parent_id]; | ||
| 616 | parent.children.remove(&name); | ||
| 617 | |||
| 618 | Ok(()) | ||
| 619 | } | ||
| 620 | |||
| 621 | fn find_node(fs: &Fs, path: &DrivePath) -> Option<FsNodeId> { | ||
| 622 | let mut node_id = fs.root; | ||
| 623 | for comp in path.components() { | ||
| 624 | let node = &fs.nodes[node_id]; | ||
| 625 | node_id = *node.children.get(comp)?; | ||
| 626 | } | ||
| 627 | Some(node_id) | ||
| 628 | } | ||
| 629 | |||
| 630 | pub fn apply_rename( | ||
| 631 | fs: &mut Fs, | ||
| 632 | _header: &OperationHeader, | ||
| 633 | data: &OperationRename, | ||
| 634 | ) -> Result<(), FsError> { | ||
| 635 | let (old_parent, old_name) = data | ||
| 636 | .old | ||
| 637 | .split() | ||
| 638 | .ok_or_else(|| FsError::from("cannot move root directory"))?; | ||
| 639 | |||
| 640 | let (new_parent, new_name) = data | ||
| 641 | .new | ||
| 642 | .split() | ||
| 643 | .ok_or_else(|| FsError::from("move destination cannot be root directory"))?; | ||
| 644 | |||
| 645 | let old_parent_id = find_node(fs, &old_parent).unwrap(); | ||
| 646 | let old_parent = &mut fs.nodes[old_parent_id]; | ||
| 647 | let node_id = old_parent.children.remove(&old_name).unwrap(); | ||
| 648 | |||
| 649 | let new_parent_id = find_node(fs, &new_parent).unwrap(); | ||
| 650 | let new_parent = &mut fs.nodes[new_parent_id]; | ||
| 651 | new_parent.children.insert(new_name, node_id); | ||
| 652 | |||
| 653 | Ok(()) | ||
| 654 | } | ||
| 655 | |||
| 656 | #[derive(Debug, Parser)] | ||
| 657 | struct Cli { | ||
| 658 | #[clap(subcommand)] | ||
| 659 | cmd: Cmd, | ||
| 660 | } | ||
| 661 | |||
| 662 | #[derive(Debug, Subcommand)] | ||
| 663 | enum Cmd { | ||
| 664 | CreateFile(CreateFileArgs), | ||
| 665 | CreateDir(CreateDirArgs), | ||
| 666 | Remove(RemoveArgs), | ||
| 667 | Rename(RenameArgs), | ||
| 668 | View(ViewArgs), | ||
| 669 | Import(ImportArgs), | ||
| 670 | } | ||
| 671 | |||
| 672 | #[derive(Debug, Args)] | ||
| 673 | struct CreateFileArgs { | ||
| 674 | #[clap(long)] | ||
| 675 | timestamp: Option<u64>, | ||
| 676 | |||
| 677 | #[clap(long)] | ||
| 678 | email: String, | ||
| 679 | |||
| 680 | #[clap(long)] | ||
| 681 | path: DrivePath, | ||
| 682 | |||
| 683 | #[clap(long)] | ||
| 684 | file: std::path::PathBuf, | ||
| 685 | } | ||
| 686 | |||
| 687 | #[derive(Debug, Args)] | ||
| 688 | struct CreateDirArgs { | ||
| 689 | #[clap(long)] | ||
| 690 | timestamp: Option<u64>, | ||
| 691 | |||
| 692 | #[clap(long)] | ||
| 693 | email: String, | ||
| 694 | |||
| 695 | #[clap(long)] | ||
| 696 | path: DrivePath, | ||
| 697 | } | ||
| 698 | |||
| 699 | #[derive(Debug, Args)] | ||
| 700 | struct RemoveArgs { | ||
| 701 | #[clap(long)] | ||
| 702 | timestamp: Option<u64>, | ||
| 703 | |||
| 704 | #[clap(long)] | ||
| 705 | email: String, | ||
| 706 | |||
| 707 | #[clap(long)] | ||
| 708 | path: DrivePath, | ||
| 709 | } | ||
| 710 | |||
| 711 | #[derive(Debug, Args)] | ||
| 712 | struct RenameArgs { | ||
| 713 | #[clap(long)] | ||
| 714 | timestamp: Option<u64>, | ||
| 715 | |||
| 716 | #[clap(long)] | ||
| 717 | email: String, | ||
| 718 | |||
| 719 | #[clap(long)] | ||
| 720 | old: DrivePath, | ||
| 721 | |||
| 722 | #[clap(long)] | ||
| 723 | new: DrivePath, | ||
| 724 | } | ||
| 725 | |||
| 726 | #[derive(Debug, Args)] | ||
| 727 | struct ViewArgs {} | ||
| 728 | |||
| 729 | #[derive(Debug, Args)] | ||
| 730 | struct ImportArgs { | ||
| 731 | #[clap(long)] | ||
| 732 | timestamp: Option<u64>, | ||
| 733 | |||
| 734 | #[clap(long)] | ||
| 735 | email: String, | ||
| 736 | |||
| 737 | path: PathBuf, | ||
| 738 | } | ||
| 739 | |||
| 740 | fn main() { | ||
| 741 | let cli = Cli::parse(); | ||
| 742 | |||
| 743 | match cli.cmd { | ||
| 744 | Cmd::CreateFile(args) => cmd_create_file(args), | ||
| 745 | Cmd::CreateDir(args) => cmd_create_dir(args), | ||
| 746 | Cmd::Remove(args) => cmd_remove(args), | ||
| 747 | Cmd::Rename(args) => cmd_rename(args), | ||
| 748 | Cmd::View(args) => cmd_view(args), | ||
| 749 | Cmd::Import(args) => cmd_import(args), | ||
| 750 | } | ||
| 751 | } | ||
| 752 | |||
| 753 | fn cmd_create_file(args: CreateFileArgs) { | ||
| 754 | let file_blob_id = blob_hash_file(&args.file).unwrap(); | ||
| 755 | let _lock = write_lock(); | ||
| 756 | |||
| 757 | let mut fs = Fs::default(); | ||
| 758 | let mut ops = read_log_file(); | ||
| 759 | let store = BlobStore::new("blobs"); | ||
| 760 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 761 | |||
| 762 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 763 | let revision = get_next_revision(&ops); | ||
| 764 | blob_import_file(&store, ImportMode::HardLink, &file_blob_id, &args.file).unwrap(); | ||
| 765 | |||
| 766 | let new_op = Operation { | ||
| 767 | header: OperationHeader { | ||
| 768 | timestamp, | ||
| 769 | revision, | ||
| 770 | email: args.email, | ||
| 771 | }, | ||
| 772 | data: OperationData::CreateFile(OperationCreateFile { | ||
| 773 | path: args.path, | ||
| 774 | blob: file_blob_id, | ||
| 775 | }), | ||
| 776 | }; | ||
| 777 | apply(&mut fs, &new_op).unwrap(); | ||
| 778 | ops.push(new_op); | ||
| 779 | write_log_file(&ops); | ||
| 780 | } | ||
| 781 | |||
| 782 | fn cmd_create_dir(args: CreateDirArgs) { | ||
| 783 | let _lock = write_lock(); | ||
| 784 | let mut fs = Fs::default(); | ||
| 785 | let mut ops = read_log_file(); | ||
| 786 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 787 | |||
| 788 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 789 | let revision = get_next_revision(&ops); | ||
| 790 | |||
| 791 | let new_op = Operation { | ||
| 792 | header: OperationHeader { | ||
| 793 | timestamp, | ||
| 794 | revision, | ||
| 795 | email: args.email, | ||
| 796 | }, | ||
| 797 | data: OperationData::CreateDir(OperationCreateDir { path: args.path }), | ||
| 798 | }; | ||
| 799 | apply(&mut fs, &new_op).unwrap(); | ||
| 800 | ops.push(new_op); | ||
| 801 | write_log_file(&ops); | ||
| 802 | } | ||
| 803 | |||
| 804 | fn cmd_remove(args: RemoveArgs) { | ||
| 805 | let _lock = write_lock(); | ||
| 806 | let mut fs = Fs::default(); | ||
| 807 | let mut ops = read_log_file(); | ||
| 808 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 809 | |||
| 810 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 811 | let revision = get_next_revision(&ops); | ||
| 812 | |||
| 813 | let new_op = Operation { | ||
| 814 | header: OperationHeader { | ||
| 815 | timestamp, | ||
| 816 | revision, | ||
| 817 | email: args.email, | ||
| 818 | }, | ||
| 819 | data: OperationData::Remove(OperationRemove { path: args.path }), | ||
| 820 | }; | ||
| 821 | apply(&mut fs, &new_op).unwrap(); | ||
| 822 | ops.push(new_op); | ||
| 823 | write_log_file(&ops); | ||
| 824 | } | ||
| 825 | |||
| 826 | fn cmd_rename(args: RenameArgs) { | ||
| 827 | let _lock = write_lock(); | ||
| 828 | let mut fs = Fs::default(); | ||
| 829 | let mut ops = read_log_file(); | ||
| 830 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 831 | |||
| 832 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 833 | let revision = get_next_revision(&ops); | ||
| 834 | |||
| 835 | let new_op = Operation { | ||
| 836 | header: OperationHeader { | ||
| 837 | timestamp, | ||
| 838 | revision, | ||
| 839 | email: args.email, | ||
| 840 | }, | ||
| 841 | data: OperationData::Rename(OperationRename { | ||
| 842 | old: args.old, | ||
| 843 | new: args.new, | ||
| 844 | }), | ||
| 845 | }; | ||
| 846 | |||
| 847 | apply(&mut fs, &new_op).unwrap(); | ||
| 848 | ops.push(new_op); | ||
| 849 | write_log_file(&ops); | ||
| 850 | } | ||
| 851 | |||
| 852 | fn cmd_view(args: ViewArgs) { | ||
| 853 | let mut fs = Fs::default(); | ||
| 854 | let ops = read_log_file(); | ||
| 855 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 856 | write_node(&fs, fs.root, Default::default()); | ||
| 857 | } | ||
| 858 | |||
| 859 | #[derive(Debug, Clone)] | ||
| 860 | struct Queue<T>(Arc<Mutex<VecDeque<T>>>); | ||
| 861 | |||
| 862 | impl<T> Default for Queue<T> { | ||
| 863 | fn default() -> Self { | ||
| 864 | Self(Arc::new(Mutex::new(Default::default()))) | ||
| 865 | } | ||
| 866 | } | ||
| 867 | |||
| 868 | impl<T> From<Vec<T>> for Queue<T> { | ||
| 869 | fn from(value: Vec<T>) -> Self { | ||
| 870 | Self(Arc::new(Mutex::new(value.into()))) | ||
| 871 | } | ||
| 872 | } | ||
| 873 | |||
| 874 | impl<T> Queue<T> { | ||
| 875 | pub fn push(&self, v: T) { | ||
| 876 | self.0.lock().unwrap().push_back(v); | ||
| 877 | } | ||
| 878 | |||
| 879 | pub fn pop(&self) -> Option<T> { | ||
| 880 | self.0.lock().unwrap().pop_front() | ||
| 881 | } | ||
| 882 | } | ||
| 883 | |||
| 884 | fn cmd_import(args: ImportArgs) { | ||
| 885 | let _lock = write_lock(); | ||
| 886 | |||
| 887 | let mut ops = read_log_file(); | ||
| 888 | |||
| 889 | let store = BlobStore::new("blobs"); | ||
| 890 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 891 | let root = args.path.canonicalize().unwrap(); | ||
| 892 | |||
| 893 | let files = Queue::from(collect_all_file_paths(&root)); | ||
| 894 | let num_threads = std::thread::available_parallelism().unwrap().get(); | ||
| 895 | let mut handles = Vec::default(); | ||
| 896 | for _ in 0..num_threads { | ||
| 897 | let root = root.clone(); | ||
| 898 | let email = args.email.clone(); | ||
| 899 | let files = files.clone(); | ||
| 900 | let store = store.clone(); | ||
| 901 | let handle = std::thread::spawn(move || { | ||
| 902 | let mut ops = Vec::default(); | ||
| 903 | while let Some(file) = files.pop() { | ||
| 904 | let rel = file.strip_prefix(&root).unwrap(); | ||
| 905 | let drive_path = rel.to_str().unwrap().parse::<DrivePath>().unwrap(); | ||
| 906 | let blob_id = blob_hash_file(&file).unwrap(); | ||
| 907 | blob_import_file(&store, ImportMode::HardLink, &blob_id, &file).unwrap(); | ||
| 908 | let op = Operation { | ||
| 909 | header: OperationHeader { | ||
| 910 | timestamp, | ||
| 911 | revision: 0, | ||
| 912 | email: email.clone(), | ||
| 913 | }, | ||
| 914 | data: OperationData::CreateFile(OperationCreateFile { | ||
| 915 | path: drive_path, | ||
| 916 | blob: blob_id, | ||
| 917 | }), | ||
| 918 | }; | ||
| 919 | ops.push(op); | ||
| 920 | } | ||
| 921 | ops | ||
| 922 | }); | ||
| 923 | handles.push(handle); | ||
| 924 | } | ||
| 925 | |||
| 926 | let mut fs = Fs::default(); | ||
| 927 | let mut next_rev = get_next_revision(&ops); | ||
| 928 | for handle in handles { | ||
| 929 | let mut task_ops = handle.join().unwrap(); | ||
| 930 | for op in &mut task_ops { | ||
| 931 | op.header.revision = next_rev; | ||
| 932 | next_rev += 1; | ||
| 933 | } | ||
| 934 | ops.extend(task_ops); | ||
| 935 | } | ||
| 936 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 937 | write_log_file(&ops); | ||
| 938 | } | ||
| 939 | |||
| 940 | fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> { | ||
| 941 | let mut queue = vec![root.to_path_buf()]; | ||
| 942 | let mut files = vec![]; | ||
| 943 | while let Some(path) = queue.pop() { | ||
| 944 | if path.is_dir() { | ||
| 945 | let mut read = path.read_dir().unwrap(); | ||
| 946 | while let Some(entry) = read.next() { | ||
| 947 | let entry = entry.unwrap(); | ||
| 948 | queue.push(entry.path()); | ||
| 949 | } | ||
| 950 | } else { | ||
| 951 | files.push(path); | ||
| 952 | } | ||
| 953 | } | ||
| 954 | files | ||
| 955 | } | ||
| 956 | |||
| 957 | fn write_node(fs: &Fs, node_id: FsNodeId, path: DrivePath) { | ||
| 958 | let node = &fs.nodes[node_id]; | ||
| 959 | match node.kind { | ||
| 960 | FsNodeKind::File => println!( | ||
| 961 | "{}\tfile\t{}\t{}\t{}", | ||
| 962 | path, node.lastmod, node.blob, node.author | ||
| 963 | ), | ||
| 964 | FsNodeKind::Directory => { | ||
| 965 | println!("{}\tdir\t{}\t-\t{}", path, node.lastmod, node.author); | ||
| 966 | for (child_comp, child_id) in node.children.iter() { | ||
| 967 | let child_path = path.push(child_comp.clone()); | ||
| 968 | write_node(fs, *child_id, child_path); | ||
| 969 | } | ||
| 970 | } | ||
| 971 | } | ||
| 972 | } | ||
| 973 | |||
| 974 | fn read_log_file() -> Vec<Operation> { | ||
| 975 | let mut operations = Vec::default(); | ||
| 976 | if std::fs::exists("log.txt").unwrap() { | ||
| 977 | let contents = std::fs::read_to_string("log.txt").unwrap(); | ||
| 978 | for line in contents.lines() { | ||
| 979 | let operation = line.parse().unwrap(); | ||
| 980 | operations.push(operation); | ||
| 981 | } | ||
| 982 | } | ||
| 983 | operations | ||
| 984 | } | ||
| 985 | |||
| 986 | fn write_log_file(ops: &[Operation]) { | ||
| 987 | { | ||
| 988 | let file = File::options() | ||
| 989 | .create(true) | ||
| 990 | .write(true) | ||
| 991 | .truncate(true) | ||
| 992 | .open("log.txt.tmp") | ||
| 993 | .unwrap(); | ||
| 994 | let mut writer = BufWriter::new(file); | ||
| 995 | for op in ops { | ||
| 996 | writeln!(writer, "{op}").unwrap(); | ||
| 997 | } | ||
| 998 | writer.flush().unwrap(); | ||
| 999 | } | ||
| 1000 | std::fs::rename("log.txt.tmp", "log.txt").unwrap(); | ||
| 1001 | } | ||
| 1002 | |||
| 1003 | fn get_timestamp() -> u64 { | ||
| 1004 | SystemTime::now() | ||
| 1005 | .duration_since(SystemTime::UNIX_EPOCH) | ||
| 1006 | .unwrap() | ||
| 1007 | .as_secs() | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | fn get_next_revision(ops: &[Operation]) -> u64 { | ||
| 1011 | match ops.last() { | ||
| 1012 | Some(op) => op.header.revision + 1, | ||
| 1013 | None => 0, | ||
| 1014 | } | ||
| 1015 | } | ||
| 1016 | |||
| 1017 | fn write_lock() -> File { | ||
| 1018 | let file = std::fs::OpenOptions::new() | ||
| 1019 | .write(true) | ||
| 1020 | .create(true) | ||
| 1021 | .open("write.lock") | ||
| 1022 | .unwrap(); | ||
| 1023 | file.lock().unwrap(); | ||
| 1024 | file | ||
| 1025 | } | ||
diff --git a/filesystem.go b/filesystem.go deleted file mode 100644 index 236f704..0000000 --- a/filesystem.go +++ /dev/null | |||
| @@ -1,244 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import "fmt" | ||
| 4 | |||
| 5 | type Filesystem_Ent_Type string | ||
| 6 | |||
| 7 | const ( | ||
| 8 | Filesystem_Ent_Type_Dir Filesystem_Ent_Type = "dir" | ||
| 9 | Filesystem_Ent_Type_File Filesystem_Ent_Type = "file" | ||
| 10 | ) | ||
| 11 | |||
| 12 | type Filesystem struct { | ||
| 13 | Root *Filesystem_Ent | ||
| 14 | } | ||
| 15 | |||
| 16 | func NewFilesystem() *Filesystem { | ||
| 17 | return &Filesystem{ | ||
| 18 | Root: &Filesystem_Ent{ | ||
| 19 | Type: Filesystem_Ent_Type_Dir, | ||
| 20 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 21 | }, | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | type Filesystem_Ent struct { | ||
| 26 | Type Filesystem_Ent_Type | ||
| 27 | Children map[PathComponent]*Filesystem_Ent | ||
| 28 | Blob string | ||
| 29 | } | ||
| 30 | |||
| 31 | func (fs *Filesystem) GetEntry(path Path) *Filesystem_Ent { | ||
| 32 | current := fs.Root | ||
| 33 | for _, component := range path.Components() { | ||
| 34 | if current == nil || current.Type != Filesystem_Ent_Type_Dir { | ||
| 35 | return nil | ||
| 36 | } | ||
| 37 | current = current.Children[component] | ||
| 38 | } | ||
| 39 | return current | ||
| 40 | } | ||
| 41 | |||
| 42 | func (fs *Filesystem) EnsureParentDirs(path Path) error { | ||
| 43 | if path.IsRoot() { | ||
| 44 | return nil | ||
| 45 | } | ||
| 46 | |||
| 47 | parentPath := path.Parent() | ||
| 48 | current := fs.Root | ||
| 49 | |||
| 50 | for _, component := range parentPath.Components() { | ||
| 51 | if current.Children == nil { | ||
| 52 | current.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 53 | } | ||
| 54 | |||
| 55 | if next, exists := current.Children[component]; exists { | ||
| 56 | if next.Type != Filesystem_Ent_Type_Dir { | ||
| 57 | return fmt.Errorf("path component '%s' is a file, cannot create directory", component) | ||
| 58 | } | ||
| 59 | current = next | ||
| 60 | } else { | ||
| 61 | newDir := &Filesystem_Ent{ | ||
| 62 | Type: Filesystem_Ent_Type_Dir, | ||
| 63 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 64 | } | ||
| 65 | current.Children[component] = newDir | ||
| 66 | current = newDir | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | return nil | ||
| 71 | } | ||
| 72 | |||
| 73 | func (fs *Filesystem) ApplyOperation(op *Operation) error { | ||
| 74 | switch op.Type { | ||
| 75 | case Operation_Type_CreateFile: | ||
| 76 | return fs.ApplyOperation_CreateFile(op) | ||
| 77 | case Operation_Type_CreateDir: | ||
| 78 | return fs.ApplyOperation_CreateDir(op) | ||
| 79 | case Operation_Type_Remove: | ||
| 80 | return fs.ApplyOperation_Remove(op) | ||
| 81 | case Operation_Type_Rename: | ||
| 82 | return fs.ApplyOperation_Rename(op) | ||
| 83 | default: | ||
| 84 | return fmt.Errorf("unhandled operation type: %s", op.Type) | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | func (fs *Filesystem) ApplyOperation_CreateFile(op *Operation) error { | ||
| 89 | params := op.GetCreateFileParams() | ||
| 90 | |||
| 91 | path, err := ParsePath(params.Path) | ||
| 92 | if err != nil { | ||
| 93 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 94 | } | ||
| 95 | |||
| 96 | if path.IsRoot() { | ||
| 97 | return fmt.Errorf("cannot create file at root path") | ||
| 98 | } | ||
| 99 | |||
| 100 | if err := fs.EnsureParentDirs(path); err != nil { | ||
| 101 | return fmt.Errorf("failed to create parent directories: %w", err) | ||
| 102 | } | ||
| 103 | |||
| 104 | parentPath := path.Parent() | ||
| 105 | parent := fs.GetEntry(parentPath) | ||
| 106 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 107 | return fmt.Errorf("parent is not a directory") | ||
| 108 | } | ||
| 109 | |||
| 110 | if parent.Children == nil { | ||
| 111 | parent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 112 | } | ||
| 113 | |||
| 114 | filename := path.Components()[len(path.Components())-1] | ||
| 115 | parent.Children[filename] = &Filesystem_Ent{ | ||
| 116 | Type: Filesystem_Ent_Type_File, | ||
| 117 | Blob: params.Blob, | ||
| 118 | } | ||
| 119 | |||
| 120 | return nil | ||
| 121 | } | ||
| 122 | |||
| 123 | func (fs *Filesystem) ApplyOperation_CreateDir(op *Operation) error { | ||
| 124 | params := op.GetCreateDirParams() | ||
| 125 | |||
| 126 | path, err := ParsePath(params.Path) | ||
| 127 | if err != nil { | ||
| 128 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 129 | } | ||
| 130 | |||
| 131 | if path.IsRoot() { | ||
| 132 | return nil | ||
| 133 | } | ||
| 134 | |||
| 135 | existing := fs.GetEntry(path) | ||
| 136 | if existing != nil { | ||
| 137 | if existing.Type == Filesystem_Ent_Type_Dir { | ||
| 138 | return nil | ||
| 139 | } | ||
| 140 | return fmt.Errorf("cannot create directory '%s': file already exists", params.Path) | ||
| 141 | } | ||
| 142 | |||
| 143 | if err := fs.EnsureParentDirs(path); err != nil { | ||
| 144 | return fmt.Errorf("failed to create parent directories: %w", err) | ||
| 145 | } | ||
| 146 | |||
| 147 | parentPath := path.Parent() | ||
| 148 | parent := fs.GetEntry(parentPath) | ||
| 149 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 150 | return fmt.Errorf("parent is not a directory") | ||
| 151 | } | ||
| 152 | |||
| 153 | if parent.Children == nil { | ||
| 154 | parent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 155 | } | ||
| 156 | |||
| 157 | dirname := path.Components()[len(path.Components())-1] | ||
| 158 | parent.Children[dirname] = &Filesystem_Ent{ | ||
| 159 | Type: Filesystem_Ent_Type_Dir, | ||
| 160 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 161 | } | ||
| 162 | |||
| 163 | return nil | ||
| 164 | } | ||
| 165 | |||
| 166 | func (fs *Filesystem) ApplyOperation_Remove(op *Operation) error { | ||
| 167 | params := op.GetRemoveParams() | ||
| 168 | |||
| 169 | path, err := ParsePath(params.Path) | ||
| 170 | if err != nil { | ||
| 171 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 172 | } | ||
| 173 | |||
| 174 | if path.IsRoot() { | ||
| 175 | return fmt.Errorf("cannot remove root directory") | ||
| 176 | } | ||
| 177 | |||
| 178 | existing := fs.GetEntry(path) | ||
| 179 | if existing == nil { | ||
| 180 | return nil | ||
| 181 | } | ||
| 182 | |||
| 183 | parentPath := path.Parent() | ||
| 184 | parent := fs.GetEntry(parentPath) | ||
| 185 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 186 | return fmt.Errorf("parent is not a directory") | ||
| 187 | } | ||
| 188 | |||
| 189 | filename := path.Components()[len(path.Components())-1] | ||
| 190 | delete(parent.Children, filename) | ||
| 191 | |||
| 192 | return nil | ||
| 193 | } | ||
| 194 | |||
| 195 | func (fs *Filesystem) ApplyOperation_Rename(op *Operation) error { | ||
| 196 | params := op.GetRenameParams() | ||
| 197 | |||
| 198 | oldPath, err := ParsePath(params.Old) | ||
| 199 | if err != nil { | ||
| 200 | return fmt.Errorf("invalid old path '%s': %w", params.Old, err) | ||
| 201 | } | ||
| 202 | |||
| 203 | newPath, err := ParsePath(params.New) | ||
| 204 | if err != nil { | ||
| 205 | return fmt.Errorf("invalid new path '%s': %w", params.New, err) | ||
| 206 | } | ||
| 207 | |||
| 208 | if oldPath.IsRoot() || newPath.IsRoot() { | ||
| 209 | return fmt.Errorf("cannot rename root directory") | ||
| 210 | } | ||
| 211 | |||
| 212 | existing := fs.GetEntry(oldPath) | ||
| 213 | if existing == nil { | ||
| 214 | return nil | ||
| 215 | } | ||
| 216 | |||
| 217 | if err := fs.EnsureParentDirs(newPath); err != nil { | ||
| 218 | return fmt.Errorf("failed to create parent directories for new path: %w", err) | ||
| 219 | } | ||
| 220 | |||
| 221 | oldParentPath := oldPath.Parent() | ||
| 222 | oldParent := fs.GetEntry(oldParentPath) | ||
| 223 | if oldParent == nil || oldParent.Type != Filesystem_Ent_Type_Dir { | ||
| 224 | return fmt.Errorf("old parent is not a directory") | ||
| 225 | } | ||
| 226 | |||
| 227 | newParentPath := newPath.Parent() | ||
| 228 | newParent := fs.GetEntry(newParentPath) | ||
| 229 | if newParent == nil || newParent.Type != Filesystem_Ent_Type_Dir { | ||
| 230 | return fmt.Errorf("new parent is not a directory") | ||
| 231 | } | ||
| 232 | |||
| 233 | if newParent.Children == nil { | ||
| 234 | newParent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 235 | } | ||
| 236 | |||
| 237 | oldFilename := oldPath.Components()[len(oldPath.Components())-1] | ||
| 238 | newFilename := newPath.Components()[len(newPath.Components())-1] | ||
| 239 | |||
| 240 | newParent.Children[newFilename] = existing | ||
| 241 | delete(oldParent.Children, oldFilename) | ||
| 242 | |||
| 243 | return nil | ||
| 244 | } | ||
| @@ -1,8 +0,0 @@ | |||
| 1 | module git.d464.sh/fctdrive | ||
| 2 | |||
| 3 | go 1.24.5 | ||
| 4 | |||
| 5 | require ( | ||
| 6 | github.com/go-chi/chi/v5 v5.2.2 | ||
| 7 | github.com/pkg/errors v0.9.1 | ||
| 8 | ) | ||
| @@ -1,4 +0,0 @@ | |||
| 1 | github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= | ||
| 2 | github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= | ||
| 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
| 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
diff --git a/index.html b/index.html deleted file mode 100644 index 169ebc0..0000000 --- a/index.html +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <meta charset="UTF-8"> | ||
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 7 | <title>FCT Drive</title> | ||
| 8 | </head> | ||
| 9 | |||
| 10 | <body> | ||
| 11 | <form enctype="multipart/form-data" method="post" action="/upload"> | ||
| 12 | <input type="file" name="file"> | ||
| 13 | <input type="submit" name="submit" value="Upload"> | ||
| 14 | </form> | ||
| 15 | </body> | ||
| 16 | |||
| 17 | </html> | ||
diff --git a/main.go b/main.go deleted file mode 100644 index 1be1067..0000000 --- a/main.go +++ /dev/null | |||
| @@ -1,75 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "net/http" | ||
| 6 | "os" | ||
| 7 | |||
| 8 | "github.com/go-chi/chi/v5" | ||
| 9 | ) | ||
| 10 | |||
| 11 | type App struct { | ||
| 12 | Operations []*Operation | ||
| 13 | Filesystem *Filesystem | ||
| 14 | } | ||
| 15 | |||
| 16 | func main() { | ||
| 17 | router := chi.NewRouter() | ||
| 18 | |||
| 19 | app := &App{ | ||
| 20 | Operations: []*Operation{}, | ||
| 21 | Filesystem: NewFilesystem(), | ||
| 22 | } | ||
| 23 | |||
| 24 | router.Get("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 25 | w.Header().Add("Content-Type", "text/html") | ||
| 26 | file, _ := os.ReadFile("index.html") | ||
| 27 | w.Write(file) | ||
| 28 | }) | ||
| 29 | router.Route("/api", func(r chi.Router) { | ||
| 30 | r.Get("/view", view) | ||
| 31 | r.Get("/filesystem/*", h(app, filesystemGet)) | ||
| 32 | r.Post("/filesystem/*", h(app, filesystemPost)) | ||
| 33 | r.Delete("/filesystem/*", h(app, filesystemDelete)) | ||
| 34 | }) | ||
| 35 | |||
| 36 | if err := http.ListenAndServe("0.0.0.0:5000", router); err != nil { | ||
| 37 | panic(err) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | func h(app *App, f func(*App, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { | ||
| 42 | return func(w http.ResponseWriter, r *http.Request) { | ||
| 43 | f(app, w, r) | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | func view(w http.ResponseWriter, r *http.Request) { | ||
| 48 | w.Write([]byte("Hi")) | ||
| 49 | } | ||
| 50 | |||
| 51 | func filesystemGet(app *App, w http.ResponseWriter, r *http.Request) { | ||
| 52 | path, err := ParsePath("/" + chi.URLParam(r, "*")) | ||
| 53 | if err != nil { | ||
| 54 | panic(err) | ||
| 55 | } | ||
| 56 | |||
| 57 | // response := map[string]interface{}{ | ||
| 58 | // "path": path, | ||
| 59 | // "message": "Filesystem path requested", | ||
| 60 | // } | ||
| 61 | |||
| 62 | entry := app.Filesystem.GetEntry(path) | ||
| 63 | |||
| 64 | w.Header().Set("Content-Type", "application/json") | ||
| 65 | json.NewEncoder(w).Encode(entry) | ||
| 66 | } | ||
| 67 | |||
| 68 | func filesystemPost(app *App, w http.ResponseWriter, r *http.Request) { | ||
| 69 | if err := r.ParseMultipartForm(1024 * 1024); err != nil { | ||
| 70 | panic(err) | ||
| 71 | } | ||
| 72 | |||
| 73 | } | ||
| 74 | |||
| 75 | func filesystemDelete(app *App, w http.ResponseWriter, r *http.Request) {} | ||
diff --git a/path.go b/path.go deleted file mode 100644 index bb81229..0000000 --- a/path.go +++ /dev/null | |||
| @@ -1,75 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type PathComponent string | ||
| 9 | |||
| 10 | type Path []PathComponent | ||
| 11 | |||
| 12 | func NewPathComponent(s string) (PathComponent, error) { | ||
| 13 | if s != strings.TrimSpace(s) { | ||
| 14 | return PathComponent(""), fmt.Errorf("path component cannot contain leading or trailing spaces") | ||
| 15 | } | ||
| 16 | if strings.Contains(s, "/") { | ||
| 17 | return PathComponent(""), fmt.Errorf("path component cannot contain '/'") | ||
| 18 | } | ||
| 19 | if len(s) == 0 { | ||
| 20 | return PathComponent(""), fmt.Errorf("path component cannot be empty") | ||
| 21 | } | ||
| 22 | return PathComponent(s), nil | ||
| 23 | } | ||
| 24 | |||
| 25 | func (c PathComponent) String() string { | ||
| 26 | return string(c) | ||
| 27 | } | ||
| 28 | |||
| 29 | func (p Path) Components() []PathComponent { | ||
| 30 | return []PathComponent(p) | ||
| 31 | } | ||
| 32 | |||
| 33 | func (p Path) Parent() Path { | ||
| 34 | components := []PathComponent(p) | ||
| 35 | if len(components) == 0 { | ||
| 36 | return Path(components) | ||
| 37 | } else { | ||
| 38 | return Path(components[:len(components)-1]) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | func (p Path) IsRoot() bool { | ||
| 43 | return len(p.Components()) == 0 | ||
| 44 | } | ||
| 45 | |||
| 46 | func ParsePath(pathStr string) (Path, error) { | ||
| 47 | if pathStr == "" || pathStr == "/" { | ||
| 48 | return Path{}, nil | ||
| 49 | } | ||
| 50 | |||
| 51 | if !strings.HasPrefix(pathStr, "/") { | ||
| 52 | return nil, fmt.Errorf("path must be absolute, got: %s", pathStr) | ||
| 53 | } | ||
| 54 | |||
| 55 | pathStr = strings.TrimPrefix(pathStr, "/") | ||
| 56 | if len(pathStr) == 0 { | ||
| 57 | return Path{}, nil | ||
| 58 | } | ||
| 59 | |||
| 60 | parts := strings.Split(pathStr, "/") | ||
| 61 | components := make([]PathComponent, 0, len(parts)) | ||
| 62 | |||
| 63 | for _, part := range parts { | ||
| 64 | if len(part) == 0 { | ||
| 65 | return nil, fmt.Errorf("empty path component in: %s", pathStr) | ||
| 66 | } | ||
| 67 | component, err := NewPathComponent(part) | ||
| 68 | if err != nil { | ||
| 69 | return nil, fmt.Errorf("invalid path component '%s': %w", part, err) | ||
| 70 | } | ||
| 71 | components = append(components, component) | ||
| 72 | } | ||
| 73 | |||
| 74 | return Path(components), nil | ||
| 75 | } | ||
diff --git a/service.yaml b/service.yaml deleted file mode 100644 index e69de29..0000000 --- a/service.yaml +++ /dev/null | |||
