Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

add pull and serve to cli

+555 -5
cli/Cargo.lock
··· 174 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 175 176 [[package]] 177 name = "backtrace" 178 version = "0.3.76" 179 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 346 version = "3.19.0" 347 source = "registry+https://github.com/rust-lang/crates.io-index" 348 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 349 350 [[package]] 351 name = "bytes" ··· 549 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 550 551 [[package]] 552 name = "core-foundation" 553 version = "0.9.4" 554 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 751 ] 752 753 [[package]] 754 name = "digest" 755 version = "0.10.7" 756 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 956 ] 957 958 [[package]] 959 name = "futures-channel" 960 version = "0.3.31" 961 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 989 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 990 991 [[package]] 992 name = "futures-macro" 993 version = "0.3.31" 994 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1027 "pin-project-lite", 1028 "pin-utils", 1029 "slab", 1030 ] 1031 1032 [[package]] ··· 1274 ] 1275 1276 [[package]] 1277 name = "httparse" 1278 version = "1.10.1" 1279 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1299 "http", 1300 "http-body", 1301 "httparse", 1302 "itoa", 1303 "pin-project-lite", 1304 "pin-utils", ··· 1362 "js-sys", 1363 "log", 1364 "wasm-bindgen", 1365 - "windows-core", 1366 ] 1367 1368 [[package]] ··· 1635 "bon", 1636 "bytes", 1637 "chrono", 1638 "cid", 1639 "getrandom 0.2.16", 1640 "getrandom 0.3.4", 1641 "http", ··· 1645 "miette", 1646 "multibase", 1647 "multihash", 1648 "ouroboros", 1649 "p256", 1650 "rand 0.9.2", ··· 1658 "smol_str", 1659 "thiserror 2.0.17", 1660 "tokio", 1661 "tokio-util", 1662 "trait-variant", 1663 "url", ··· 1856 source = "registry+https://github.com/rust-lang/crates.io-index" 1857 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1858 dependencies = [ 1859 - "spin", 1860 ] 1861 1862 [[package]] ··· 1916 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1917 1918 [[package]] 1919 name = "lru-cache" 1920 version = "0.1.2" 1921 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1974 ] 1975 1976 [[package]] 1977 name = "memchr" 1978 version = "2.7.6" 1979 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2108 ] 2109 2110 [[package]] 2111 name = "ndk-context" 2112 version = "0.1.1" 2113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2130 ] 2131 2132 [[package]] 2133 name = "num-bigint-dig" 2134 version = "0.8.5" 2135 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2247 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 2248 2249 [[package]] 2250 name = "option-ext" 2251 version = "0.2.0" 2252 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2305 ] 2306 2307 [[package]] 2308 name = "parking_lot" 2309 version = "0.12.5" 2310 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2378 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2379 dependencies = [ 2380 "siphasher", 2381 ] 2382 2383 [[package]] ··· 2752 "tokio", 2753 "tokio-rustls", 2754 "tokio-util", 2755 - "tower", 2756 - "tower-http", 2757 "tower-service", 2758 "url", 2759 "wasm-bindgen", ··· 2877 ] 2878 2879 [[package]] 2880 name = "rustls-pki-types" 2881 version = "1.13.0" 2882 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2925 ] 2926 2927 [[package]] 2928 name = "schemars" 2929 version = "0.9.0" 2930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2949 ] 2950 2951 [[package]] 2952 name = "scopeguard" 2953 version = "1.2.0" 2954 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2969 ] 2970 2971 [[package]] 2972 name = "serde" 2973 version = "1.0.228" 2974 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3047 ] 3048 3049 [[package]] 3050 name = "serde_repr" 3051 version = "0.1.20" 3052 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3101 ] 3102 3103 [[package]] 3104 name = "sha1_smol" 3105 version = "1.0.1" 3106 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3118 ] 3119 3120 [[package]] 3121 name = "shellexpand" 3122 version = "3.1.1" 3123 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3210 version = "0.9.8" 3211 source = "registry+https://github.com/rust-lang/crates.io-index" 3212 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3213 3214 [[package]] 3215 name = "spki" ··· 3465 ] 3466 3467 [[package]] 3468 name = "threadpool" 3469 version = "1.8.1" 3470 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3582 ] 3583 3584 [[package]] 3585 name = "tokio-util" 3586 version = "0.7.16" 3587 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3590 "bytes", 3591 "futures-core", 3592 "futures-sink", 3593 "pin-project-lite", 3594 "tokio", 3595 ] 3596 3597 [[package]] 3598 name = "tower" 3599 version = "0.5.2" 3600 source = "registry+https://github.com/rust-lang/crates.io-index" 3601 checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" ··· 3607 "tokio", 3608 "tower-layer", 3609 "tower-service", 3610 ] 3611 3612 [[package]] ··· 3622 "http-body", 3623 "iri-string", 3624 "pin-project-lite", 3625 - "tower", 3626 "tower-layer", 3627 "tower-service", 3628 ] ··· 3645 source = "registry+https://github.com/rust-lang/crates.io-index" 3646 checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 3647 dependencies = [ 3648 "pin-project-lite", 3649 "tracing-attributes", 3650 "tracing-core", ··· 3668 checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 3669 dependencies = [ 3670 "once_cell", 3671 ] 3672 3673 [[package]] ··· 3694 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 3695 3696 [[package]] 3697 name = "twoway" 3698 version = "0.1.8" 3699 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3745 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" 3746 3747 [[package]] 3748 name = "unsigned-varint" 3749 version = "0.8.0" 3750 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3791 version = "0.2.2" 3792 source = "registry+https://github.com/rust-lang/crates.io-index" 3793 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3794 3795 [[package]] 3796 name = "version_check" ··· 3976 ] 3977 3978 [[package]] 3979 name = "windows-core" 3980 version = "0.62.2" 3981 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3989 ] 3990 3991 [[package]] 3992 name = "windows-implement" 3993 version = "0.60.2" 3994 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4023 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4024 4025 [[package]] 4026 name = "windows-registry" 4027 version = "0.5.3" 4028 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4175 "windows_x86_64_gnu 0.53.1", 4176 "windows_x86_64_gnullvm 0.53.1", 4177 "windows_x86_64_msvc 0.53.1", 4178 ] 4179 4180 [[package]] ··· 4371 name = "wisp-cli" 4372 version = "0.1.0" 4373 dependencies = [ 4374 "base64 0.22.1", 4375 "bytes", 4376 "clap", 4377 "flate2", 4378 "futures", ··· 4387 "mime_guess", 4388 "multibase", 4389 "multihash", 4390 "reqwest", 4391 "rustversion", 4392 "serde", ··· 4394 "sha2", 4395 "shellexpand", 4396 "tokio", 4397 "walkdir", 4398 ] 4399
··· 174 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 175 176 [[package]] 177 + name = "axum" 178 + version = "0.7.9" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 181 + dependencies = [ 182 + "async-trait", 183 + "axum-core", 184 + "bytes", 185 + "futures-util", 186 + "http", 187 + "http-body", 188 + "http-body-util", 189 + "hyper", 190 + "hyper-util", 191 + "itoa", 192 + "matchit", 193 + "memchr", 194 + "mime", 195 + "percent-encoding", 196 + "pin-project-lite", 197 + "rustversion", 198 + "serde", 199 + "serde_json", 200 + "serde_path_to_error", 201 + "serde_urlencoded", 202 + "sync_wrapper", 203 + "tokio", 204 + "tower 0.5.2", 205 + "tower-layer", 206 + "tower-service", 207 + "tracing", 208 + ] 209 + 210 + [[package]] 211 + name = "axum-core" 212 + version = "0.4.5" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 215 + dependencies = [ 216 + "async-trait", 217 + "bytes", 218 + "futures-util", 219 + "http", 220 + "http-body", 221 + "http-body-util", 222 + "mime", 223 + "pin-project-lite", 224 + "rustversion", 225 + "sync_wrapper", 226 + "tower-layer", 227 + "tower-service", 228 + "tracing", 229 + ] 230 + 231 + [[package]] 232 name = "backtrace" 233 version = "0.3.76" 234 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 401 version = "3.19.0" 402 source = "registry+https://github.com/rust-lang/crates.io-index" 403 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 404 + 405 + [[package]] 406 + name = "byteorder" 407 + version = "1.5.0" 408 + source = "registry+https://github.com/rust-lang/crates.io-index" 409 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 410 411 [[package]] 412 name = "bytes" ··· 610 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 611 612 [[package]] 613 + name = "cordyceps" 614 + version = "0.3.4" 615 + source = "registry+https://github.com/rust-lang/crates.io-index" 616 + checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" 617 + dependencies = [ 618 + "loom", 619 + "tracing", 620 + ] 621 + 622 + [[package]] 623 name = "core-foundation" 624 version = "0.9.4" 625 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 822 ] 823 824 [[package]] 825 + name = "derive_more" 826 + version = "1.0.0" 827 + source = "registry+https://github.com/rust-lang/crates.io-index" 828 + checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 829 + dependencies = [ 830 + "derive_more-impl", 831 + ] 832 + 833 + [[package]] 834 + name = "derive_more-impl" 835 + version = "1.0.0" 836 + source = "registry+https://github.com/rust-lang/crates.io-index" 837 + checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 838 + dependencies = [ 839 + "proc-macro2", 840 + "quote", 841 + "syn 2.0.108", 842 + "unicode-xid", 843 + ] 844 + 845 + [[package]] 846 + name = "diatomic-waker" 847 + version = "0.2.3" 848 + source = "registry+https://github.com/rust-lang/crates.io-index" 849 + checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" 850 + 851 + [[package]] 852 name = "digest" 853 version = "0.10.7" 854 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1054 ] 1055 1056 [[package]] 1057 + name = "futures-buffered" 1058 + version = "0.2.12" 1059 + source = "registry+https://github.com/rust-lang/crates.io-index" 1060 + checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" 1061 + dependencies = [ 1062 + "cordyceps", 1063 + "diatomic-waker", 1064 + "futures-core", 1065 + "pin-project-lite", 1066 + "spin 0.10.0", 1067 + ] 1068 + 1069 + [[package]] 1070 name = "futures-channel" 1071 version = "0.3.31" 1072 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1100 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1101 1102 [[package]] 1103 + name = "futures-lite" 1104 + version = "2.6.1" 1105 + source = "registry+https://github.com/rust-lang/crates.io-index" 1106 + checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" 1107 + dependencies = [ 1108 + "fastrand", 1109 + "futures-core", 1110 + "futures-io", 1111 + "parking", 1112 + "pin-project-lite", 1113 + ] 1114 + 1115 + [[package]] 1116 name = "futures-macro" 1117 version = "0.3.31" 1118 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1151 "pin-project-lite", 1152 "pin-utils", 1153 "slab", 1154 + ] 1155 + 1156 + [[package]] 1157 + name = "generator" 1158 + version = "0.8.7" 1159 + source = "registry+https://github.com/rust-lang/crates.io-index" 1160 + checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" 1161 + dependencies = [ 1162 + "cc", 1163 + "cfg-if", 1164 + "libc", 1165 + "log", 1166 + "rustversion", 1167 + "windows", 1168 ] 1169 1170 [[package]] ··· 1412 ] 1413 1414 [[package]] 1415 + name = "http-range-header" 1416 + version = "0.4.2" 1417 + source = "registry+https://github.com/rust-lang/crates.io-index" 1418 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 1419 + 1420 + [[package]] 1421 name = "httparse" 1422 version = "1.10.1" 1423 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1443 "http", 1444 "http-body", 1445 "httparse", 1446 + "httpdate", 1447 "itoa", 1448 "pin-project-lite", 1449 "pin-utils", ··· 1507 "js-sys", 1508 "log", 1509 "wasm-bindgen", 1510 + "windows-core 0.62.2", 1511 ] 1512 1513 [[package]] ··· 1780 "bon", 1781 "bytes", 1782 "chrono", 1783 + "ciborium", 1784 "cid", 1785 + "futures", 1786 "getrandom 0.2.16", 1787 "getrandom 0.3.4", 1788 "http", ··· 1792 "miette", 1793 "multibase", 1794 "multihash", 1795 + "n0-future", 1796 "ouroboros", 1797 "p256", 1798 "rand 0.9.2", ··· 1806 "smol_str", 1807 "thiserror 2.0.17", 1808 "tokio", 1809 + "tokio-tungstenite-wasm", 1810 "tokio-util", 1811 "trait-variant", 1812 "url", ··· 2005 source = "registry+https://github.com/rust-lang/crates.io-index" 2006 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2007 dependencies = [ 2008 + "spin 0.9.8", 2009 ] 2010 2011 [[package]] ··· 2065 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 2066 2067 [[package]] 2068 + name = "loom" 2069 + version = "0.7.2" 2070 + source = "registry+https://github.com/rust-lang/crates.io-index" 2071 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 2072 + dependencies = [ 2073 + "cfg-if", 2074 + "generator", 2075 + "scoped-tls", 2076 + "tracing", 2077 + "tracing-subscriber", 2078 + ] 2079 + 2080 + [[package]] 2081 name = "lru-cache" 2082 version = "0.1.2" 2083 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2136 ] 2137 2138 [[package]] 2139 + name = "matchers" 2140 + version = "0.2.0" 2141 + source = "registry+https://github.com/rust-lang/crates.io-index" 2142 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 2143 + dependencies = [ 2144 + "regex-automata", 2145 + ] 2146 + 2147 + [[package]] 2148 + name = "matchit" 2149 + version = "0.7.3" 2150 + source = "registry+https://github.com/rust-lang/crates.io-index" 2151 + checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 2152 + 2153 + [[package]] 2154 name = "memchr" 2155 version = "2.7.6" 2156 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2285 ] 2286 2287 [[package]] 2288 + name = "n0-future" 2289 + version = "0.1.3" 2290 + source = "registry+https://github.com/rust-lang/crates.io-index" 2291 + checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" 2292 + dependencies = [ 2293 + "cfg_aliases", 2294 + "derive_more", 2295 + "futures-buffered", 2296 + "futures-lite", 2297 + "futures-util", 2298 + "js-sys", 2299 + "pin-project", 2300 + "send_wrapper", 2301 + "tokio", 2302 + "tokio-util", 2303 + "wasm-bindgen", 2304 + "wasm-bindgen-futures", 2305 + "web-time", 2306 + ] 2307 + 2308 + [[package]] 2309 name = "ndk-context" 2310 version = "0.1.1" 2311 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2328 ] 2329 2330 [[package]] 2331 + name = "nu-ansi-term" 2332 + version = "0.50.3" 2333 + source = "registry+https://github.com/rust-lang/crates.io-index" 2334 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 2335 + dependencies = [ 2336 + "windows-sys 0.61.2", 2337 + ] 2338 + 2339 + [[package]] 2340 name = "num-bigint-dig" 2341 version = "0.8.5" 2342 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2454 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 2455 2456 [[package]] 2457 + name = "openssl-probe" 2458 + version = "0.1.6" 2459 + source = "registry+https://github.com/rust-lang/crates.io-index" 2460 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2461 + 2462 + [[package]] 2463 name = "option-ext" 2464 version = "0.2.0" 2465 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2518 ] 2519 2520 [[package]] 2521 + name = "parking" 2522 + version = "2.2.1" 2523 + source = "registry+https://github.com/rust-lang/crates.io-index" 2524 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2525 + 2526 + [[package]] 2527 name = "parking_lot" 2528 version = "0.12.5" 2529 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2597 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2598 dependencies = [ 2599 "siphasher", 2600 + ] 2601 + 2602 + [[package]] 2603 + name = "pin-project" 2604 + version = "1.1.10" 2605 + source = "registry+https://github.com/rust-lang/crates.io-index" 2606 + checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 2607 + dependencies = [ 2608 + "pin-project-internal", 2609 + ] 2610 + 2611 + [[package]] 2612 + name = "pin-project-internal" 2613 + version = "1.1.10" 2614 + source = "registry+https://github.com/rust-lang/crates.io-index" 2615 + checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 2616 + dependencies = [ 2617 + "proc-macro2", 2618 + "quote", 2619 + "syn 2.0.108", 2620 ] 2621 2622 [[package]] ··· 2991 "tokio", 2992 "tokio-rustls", 2993 "tokio-util", 2994 + "tower 0.5.2", 2995 + "tower-http 0.6.6", 2996 "tower-service", 2997 "url", 2998 "wasm-bindgen", ··· 3116 ] 3117 3118 [[package]] 3119 + name = "rustls-native-certs" 3120 + version = "0.8.2" 3121 + source = "registry+https://github.com/rust-lang/crates.io-index" 3122 + checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" 3123 + dependencies = [ 3124 + "openssl-probe", 3125 + "rustls-pki-types", 3126 + "schannel", 3127 + "security-framework", 3128 + ] 3129 + 3130 + [[package]] 3131 name = "rustls-pki-types" 3132 version = "1.13.0" 3133 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3176 ] 3177 3178 [[package]] 3179 + name = "schannel" 3180 + version = "0.1.28" 3181 + source = "registry+https://github.com/rust-lang/crates.io-index" 3182 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 3183 + dependencies = [ 3184 + "windows-sys 0.61.2", 3185 + ] 3186 + 3187 + [[package]] 3188 name = "schemars" 3189 version = "0.9.0" 3190 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3209 ] 3210 3211 [[package]] 3212 + name = "scoped-tls" 3213 + version = "1.0.1" 3214 + source = "registry+https://github.com/rust-lang/crates.io-index" 3215 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 3216 + 3217 + [[package]] 3218 name = "scopeguard" 3219 version = "1.2.0" 3220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3235 ] 3236 3237 [[package]] 3238 + name = "security-framework" 3239 + version = "3.5.1" 3240 + source = "registry+https://github.com/rust-lang/crates.io-index" 3241 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 3242 + dependencies = [ 3243 + "bitflags", 3244 + "core-foundation 0.10.1", 3245 + "core-foundation-sys", 3246 + "libc", 3247 + "security-framework-sys", 3248 + ] 3249 + 3250 + [[package]] 3251 + name = "security-framework-sys" 3252 + version = "2.15.0" 3253 + source = "registry+https://github.com/rust-lang/crates.io-index" 3254 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 3255 + dependencies = [ 3256 + "core-foundation-sys", 3257 + "libc", 3258 + ] 3259 + 3260 + [[package]] 3261 + name = "send_wrapper" 3262 + version = "0.6.0" 3263 + source = "registry+https://github.com/rust-lang/crates.io-index" 3264 + checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" 3265 + 3266 + [[package]] 3267 name = "serde" 3268 version = "1.0.228" 3269 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3342 ] 3343 3344 [[package]] 3345 + name = "serde_path_to_error" 3346 + version = "0.1.20" 3347 + source = "registry+https://github.com/rust-lang/crates.io-index" 3348 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 3349 + dependencies = [ 3350 + "itoa", 3351 + "serde", 3352 + "serde_core", 3353 + ] 3354 + 3355 + [[package]] 3356 name = "serde_repr" 3357 version = "0.1.20" 3358 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3407 ] 3408 3409 [[package]] 3410 + name = "sha1" 3411 + version = "0.10.6" 3412 + source = "registry+https://github.com/rust-lang/crates.io-index" 3413 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 3414 + dependencies = [ 3415 + "cfg-if", 3416 + "cpufeatures", 3417 + "digest", 3418 + ] 3419 + 3420 + [[package]] 3421 name = "sha1_smol" 3422 version = "1.0.1" 3423 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3435 ] 3436 3437 [[package]] 3438 + name = "sharded-slab" 3439 + version = "0.1.7" 3440 + source = "registry+https://github.com/rust-lang/crates.io-index" 3441 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 3442 + dependencies = [ 3443 + "lazy_static", 3444 + ] 3445 + 3446 + [[package]] 3447 name = "shellexpand" 3448 version = "3.1.1" 3449 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3536 version = "0.9.8" 3537 source = "registry+https://github.com/rust-lang/crates.io-index" 3538 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3539 + 3540 + [[package]] 3541 + name = "spin" 3542 + version = "0.10.0" 3543 + source = "registry+https://github.com/rust-lang/crates.io-index" 3544 + checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" 3545 3546 [[package]] 3547 name = "spki" ··· 3797 ] 3798 3799 [[package]] 3800 + name = "thread_local" 3801 + version = "1.1.9" 3802 + source = "registry+https://github.com/rust-lang/crates.io-index" 3803 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 3804 + dependencies = [ 3805 + "cfg-if", 3806 + ] 3807 + 3808 + [[package]] 3809 name = "threadpool" 3810 version = "1.8.1" 3811 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3923 ] 3924 3925 [[package]] 3926 + name = "tokio-tungstenite" 3927 + version = "0.24.0" 3928 + source = "registry+https://github.com/rust-lang/crates.io-index" 3929 + checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" 3930 + dependencies = [ 3931 + "futures-util", 3932 + "log", 3933 + "rustls", 3934 + "rustls-native-certs", 3935 + "rustls-pki-types", 3936 + "tokio", 3937 + "tokio-rustls", 3938 + "tungstenite", 3939 + ] 3940 + 3941 + [[package]] 3942 + name = "tokio-tungstenite-wasm" 3943 + version = "0.4.0" 3944 + source = "registry+https://github.com/rust-lang/crates.io-index" 3945 + checksum = "e21a5c399399c3db9f08d8297ac12b500e86bca82e930253fdc62eaf9c0de6ae" 3946 + dependencies = [ 3947 + "futures-channel", 3948 + "futures-util", 3949 + "http", 3950 + "httparse", 3951 + "js-sys", 3952 + "rustls", 3953 + "thiserror 1.0.69", 3954 + "tokio", 3955 + "tokio-tungstenite", 3956 + "wasm-bindgen", 3957 + "web-sys", 3958 + ] 3959 + 3960 + [[package]] 3961 name = "tokio-util" 3962 version = "0.7.16" 3963 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3966 "bytes", 3967 "futures-core", 3968 "futures-sink", 3969 + "futures-util", 3970 "pin-project-lite", 3971 "tokio", 3972 ] 3973 3974 [[package]] 3975 name = "tower" 3976 + version = "0.4.13" 3977 + source = "registry+https://github.com/rust-lang/crates.io-index" 3978 + checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 3979 + dependencies = [ 3980 + "tower-layer", 3981 + "tower-service", 3982 + "tracing", 3983 + ] 3984 + 3985 + [[package]] 3986 + name = "tower" 3987 version = "0.5.2" 3988 source = "registry+https://github.com/rust-lang/crates.io-index" 3989 checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" ··· 3995 "tokio", 3996 "tower-layer", 3997 "tower-service", 3998 + "tracing", 3999 + ] 4000 + 4001 + [[package]] 4002 + name = "tower-http" 4003 + version = "0.5.2" 4004 + source = "registry+https://github.com/rust-lang/crates.io-index" 4005 + checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" 4006 + dependencies = [ 4007 + "async-compression", 4008 + "bitflags", 4009 + "bytes", 4010 + "futures-core", 4011 + "futures-util", 4012 + "http", 4013 + "http-body", 4014 + "http-body-util", 4015 + "http-range-header", 4016 + "httpdate", 4017 + "mime", 4018 + "mime_guess", 4019 + "percent-encoding", 4020 + "pin-project-lite", 4021 + "tokio", 4022 + "tokio-util", 4023 + "tower-layer", 4024 + "tower-service", 4025 + "tracing", 4026 ] 4027 4028 [[package]] ··· 4038 "http-body", 4039 "iri-string", 4040 "pin-project-lite", 4041 + "tower 0.5.2", 4042 "tower-layer", 4043 "tower-service", 4044 ] ··· 4061 source = "registry+https://github.com/rust-lang/crates.io-index" 4062 checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 4063 dependencies = [ 4064 + "log", 4065 "pin-project-lite", 4066 "tracing-attributes", 4067 "tracing-core", ··· 4085 checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 4086 dependencies = [ 4087 "once_cell", 4088 + "valuable", 4089 + ] 4090 + 4091 + [[package]] 4092 + name = "tracing-log" 4093 + version = "0.2.0" 4094 + source = "registry+https://github.com/rust-lang/crates.io-index" 4095 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 4096 + dependencies = [ 4097 + "log", 4098 + "once_cell", 4099 + "tracing-core", 4100 + ] 4101 + 4102 + [[package]] 4103 + name = "tracing-subscriber" 4104 + version = "0.3.20" 4105 + source = "registry+https://github.com/rust-lang/crates.io-index" 4106 + checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 4107 + dependencies = [ 4108 + "matchers", 4109 + "nu-ansi-term", 4110 + "once_cell", 4111 + "regex-automata", 4112 + "sharded-slab", 4113 + "smallvec", 4114 + "thread_local", 4115 + "tracing", 4116 + "tracing-core", 4117 + "tracing-log", 4118 ] 4119 4120 [[package]] ··· 4141 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 4142 4143 [[package]] 4144 + name = "tungstenite" 4145 + version = "0.24.0" 4146 + source = "registry+https://github.com/rust-lang/crates.io-index" 4147 + checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" 4148 + dependencies = [ 4149 + "byteorder", 4150 + "bytes", 4151 + "data-encoding", 4152 + "http", 4153 + "httparse", 4154 + "log", 4155 + "rand 0.8.5", 4156 + "rustls", 4157 + "rustls-pki-types", 4158 + "sha1", 4159 + "thiserror 1.0.69", 4160 + "utf-8", 4161 + ] 4162 + 4163 + [[package]] 4164 name = "twoway" 4165 version = "0.1.8" 4166 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4212 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" 4213 4214 [[package]] 4215 + name = "unicode-xid" 4216 + version = "0.2.6" 4217 + source = "registry+https://github.com/rust-lang/crates.io-index" 4218 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 4219 + 4220 + [[package]] 4221 name = "unsigned-varint" 4222 version = "0.8.0" 4223 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4264 version = "0.2.2" 4265 source = "registry+https://github.com/rust-lang/crates.io-index" 4266 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 4267 + 4268 + [[package]] 4269 + name = "valuable" 4270 + version = "0.1.1" 4271 + source = "registry+https://github.com/rust-lang/crates.io-index" 4272 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 4273 4274 [[package]] 4275 name = "version_check" ··· 4455 ] 4456 4457 [[package]] 4458 + name = "windows" 4459 + version = "0.61.3" 4460 + source = "registry+https://github.com/rust-lang/crates.io-index" 4461 + checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 4462 + dependencies = [ 4463 + "windows-collections", 4464 + "windows-core 0.61.2", 4465 + "windows-future", 4466 + "windows-link 0.1.3", 4467 + "windows-numerics", 4468 + ] 4469 + 4470 + [[package]] 4471 + name = "windows-collections" 4472 + version = "0.2.0" 4473 + source = "registry+https://github.com/rust-lang/crates.io-index" 4474 + checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 4475 + dependencies = [ 4476 + "windows-core 0.61.2", 4477 + ] 4478 + 4479 + [[package]] 4480 + name = "windows-core" 4481 + version = "0.61.2" 4482 + source = "registry+https://github.com/rust-lang/crates.io-index" 4483 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 4484 + dependencies = [ 4485 + "windows-implement", 4486 + "windows-interface", 4487 + "windows-link 0.1.3", 4488 + "windows-result 0.3.4", 4489 + "windows-strings 0.4.2", 4490 + ] 4491 + 4492 + [[package]] 4493 name = "windows-core" 4494 version = "0.62.2" 4495 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4503 ] 4504 4505 [[package]] 4506 + name = "windows-future" 4507 + version = "0.2.1" 4508 + source = "registry+https://github.com/rust-lang/crates.io-index" 4509 + checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 4510 + dependencies = [ 4511 + "windows-core 0.61.2", 4512 + "windows-link 0.1.3", 4513 + "windows-threading", 4514 + ] 4515 + 4516 + [[package]] 4517 name = "windows-implement" 4518 version = "0.60.2" 4519 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4548 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4549 4550 [[package]] 4551 + name = "windows-numerics" 4552 + version = "0.2.0" 4553 + source = "registry+https://github.com/rust-lang/crates.io-index" 4554 + checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 4555 + dependencies = [ 4556 + "windows-core 0.61.2", 4557 + "windows-link 0.1.3", 4558 + ] 4559 + 4560 + [[package]] 4561 name = "windows-registry" 4562 version = "0.5.3" 4563 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4710 "windows_x86_64_gnu 0.53.1", 4711 "windows_x86_64_gnullvm 0.53.1", 4712 "windows_x86_64_msvc 0.53.1", 4713 + ] 4714 + 4715 + [[package]] 4716 + name = "windows-threading" 4717 + version = "0.1.0" 4718 + source = "registry+https://github.com/rust-lang/crates.io-index" 4719 + checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 4720 + dependencies = [ 4721 + "windows-link 0.1.3", 4722 ] 4723 4724 [[package]] ··· 4915 name = "wisp-cli" 4916 version = "0.1.0" 4917 dependencies = [ 4918 + "axum", 4919 "base64 0.22.1", 4920 "bytes", 4921 + "chrono", 4922 "clap", 4923 "flate2", 4924 "futures", ··· 4933 "mime_guess", 4934 "multibase", 4935 "multihash", 4936 + "n0-future", 4937 "reqwest", 4938 "rustversion", 4939 "serde", ··· 4941 "sha2", 4942 "shellexpand", 4943 "tokio", 4944 + "tower 0.4.13", 4945 + "tower-http 0.5.2", 4946 + "url", 4947 "walkdir", 4948 ] 4949
+7 -1
cli/Cargo.toml
··· 11 jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["loopback"] } 12 jacquard-oauth = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 13 jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 14 - jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 15 jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["dns"] } 16 jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 17 jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard" } ··· 33 multihash = "0.19.3" 34 multibase = "0.9" 35 sha2 = "0.10"
··· 11 jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["loopback"] } 12 jacquard-oauth = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 13 jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 14 + jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["websocket"] } 15 jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["dns"] } 16 jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 17 jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard" } ··· 33 multihash = "0.19.3" 34 multibase = "0.9" 35 sha2 = "0.10" 36 + axum = "0.7" 37 + tower-http = { version = "0.5", features = ["fs", "compression-gzip"] } 38 + tower = "0.4" 39 + n0-future = "0.1" 40 + chrono = "0.4" 41 + url = "2.5"
+71
cli/src/download.rs
···
··· 1 + use base64::Engine; 2 + use bytes::Bytes; 3 + use flate2::read::GzDecoder; 4 + use jacquard_common::types::blob::BlobRef; 5 + use miette::IntoDiagnostic; 6 + use std::io::Read; 7 + use url::Url; 8 + 9 + /// Download a blob from the PDS 10 + pub async fn download_blob(pds_url: &Url, blob_ref: &BlobRef<'_>, did: &str) -> miette::Result<Bytes> { 11 + // Extract CID from blob ref 12 + let cid = blob_ref.blob().r#ref.to_string(); 13 + 14 + // Construct blob download URL 15 + // The correct endpoint is: /xrpc/com.atproto.sync.getBlob?did={did}&cid={cid} 16 + let blob_url = pds_url 17 + .join(&format!("/xrpc/com.atproto.sync.getBlob?did={}&cid={}", did, cid)) 18 + .into_diagnostic()?; 19 + 20 + let client = reqwest::Client::new(); 21 + let response = client 22 + .get(blob_url) 23 + .send() 24 + .await 25 + .into_diagnostic()?; 26 + 27 + if !response.status().is_success() { 28 + return Err(miette::miette!( 29 + "Failed to download blob: {}", 30 + response.status() 31 + )); 32 + } 33 + 34 + let bytes = response.bytes().await.into_diagnostic()?; 35 + Ok(bytes) 36 + } 37 + 38 + /// Decompress and decode a blob (base64 + gzip) 39 + pub fn decompress_blob(data: &[u8], is_base64: bool, is_gzipped: bool) -> miette::Result<Vec<u8>> { 40 + let mut current_data = data.to_vec(); 41 + 42 + // First, decode base64 if needed 43 + if is_base64 { 44 + current_data = base64::prelude::BASE64_STANDARD 45 + .decode(&current_data) 46 + .into_diagnostic()?; 47 + } 48 + 49 + // Then, decompress gzip if needed 50 + if is_gzipped { 51 + let mut decoder = GzDecoder::new(&current_data[..]); 52 + let mut decompressed = Vec::new(); 53 + decoder.read_to_end(&mut decompressed).into_diagnostic()?; 54 + current_data = decompressed; 55 + } 56 + 57 + Ok(current_data) 58 + } 59 + 60 + /// Download and decompress a blob 61 + pub async fn download_and_decompress_blob( 62 + pds_url: &Url, 63 + blob_ref: &BlobRef<'_>, 64 + did: &str, 65 + is_base64: bool, 66 + is_gzipped: bool, 67 + ) -> miette::Result<Vec<u8>> { 68 + let data = download_blob(pds_url, blob_ref, did).await?; 69 + decompress_blob(&data, is_base64, is_gzipped) 70 + } 71 +
+109 -16
cli/src/main.rs
··· 2 mod place_wisp; 3 mod cid; 4 mod blob_map; 5 6 - use clap::Parser; 7 use jacquard::CowStr; 8 use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession, AgentSession}; 9 use jacquard::oauth::client::OAuthClient; ··· 23 use place_wisp::fs::*; 24 25 #[derive(Parser, Debug)] 26 - #[command(author, version, about = "Deploy a static site to wisp.place")] 27 struct Args { 28 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 29 - input: CowStr<'static>, 30 31 /// Path to the directory containing your static site 32 - #[arg(short, long, default_value = ".")] 33 - path: PathBuf, 34 35 /// Site name (defaults to directory name) 36 - #[arg(short, long)] 37 site: Option<String>, 38 39 - /// Path to auth store file (will be created if missing, only used with OAuth) 40 - #[arg(long, default_value = "/tmp/wisp-oauth-session.json")] 41 - store: String, 42 43 - /// App Password for authentication (alternative to OAuth) 44 - #[arg(long)] 45 password: Option<CowStr<'static>>, 46 } 47 48 #[tokio::main] 49 async fn main() -> miette::Result<()> { 50 let args = Args::parse(); 51 52 - // Dispatch to appropriate authentication method 53 - if let Some(password) = args.password { 54 - run_with_app_password(args.input, password, args.path, args.site).await 55 - } else { 56 - run_with_oauth(args.input, args.store, args.path, args.site).await 57 } 58 } 59
··· 2 mod place_wisp; 3 mod cid; 4 mod blob_map; 5 + mod metadata; 6 + mod download; 7 + mod pull; 8 + mod serve; 9 10 + use clap::{Parser, Subcommand}; 11 use jacquard::CowStr; 12 use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession, AgentSession}; 13 use jacquard::oauth::client::OAuthClient; ··· 27 use place_wisp::fs::*; 28 29 #[derive(Parser, Debug)] 30 + #[command(author, version, about = "wisp.place CLI tool")] 31 struct Args { 32 + #[command(subcommand)] 33 + command: Option<Commands>, 34 + 35 + // Deploy arguments (when no subcommand is specified) 36 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 37 + #[arg(global = true, conflicts_with = "command")] 38 + input: Option<CowStr<'static>>, 39 40 /// Path to the directory containing your static site 41 + #[arg(short, long, global = true, conflicts_with = "command")] 42 + path: Option<PathBuf>, 43 44 /// Site name (defaults to directory name) 45 + #[arg(short, long, global = true, conflicts_with = "command")] 46 site: Option<String>, 47 48 + /// Path to auth store file 49 + #[arg(long, global = true, conflicts_with = "command")] 50 + store: Option<String>, 51 52 + /// App Password for authentication 53 + #[arg(long, global = true, conflicts_with = "command")] 54 password: Option<CowStr<'static>>, 55 } 56 57 + #[derive(Subcommand, Debug)] 58 + enum Commands { 59 + /// Deploy a static site to wisp.place (default command) 60 + Deploy { 61 + /// Handle (e.g., alice.bsky.social), DID, or PDS URL 62 + input: CowStr<'static>, 63 + 64 + /// Path to the directory containing your static site 65 + #[arg(short, long, default_value = ".")] 66 + path: PathBuf, 67 + 68 + /// Site name (defaults to directory name) 69 + #[arg(short, long)] 70 + site: Option<String>, 71 + 72 + /// Path to auth store file (will be created if missing, only used with OAuth) 73 + #[arg(long, default_value = "/tmp/wisp-oauth-session.json")] 74 + store: String, 75 + 76 + /// App Password for authentication (alternative to OAuth) 77 + #[arg(long)] 78 + password: Option<CowStr<'static>>, 79 + }, 80 + /// Pull a site from the PDS to a local directory 81 + Pull { 82 + /// Handle (e.g., alice.bsky.social) or DID 83 + input: CowStr<'static>, 84 + 85 + /// Site name (record key) 86 + #[arg(short, long)] 87 + site: String, 88 + 89 + /// Output directory for the downloaded site 90 + #[arg(short, long, default_value = ".")] 91 + output: PathBuf, 92 + }, 93 + /// Serve a site locally with real-time firehose updates 94 + Serve { 95 + /// Handle (e.g., alice.bsky.social) or DID 96 + input: CowStr<'static>, 97 + 98 + /// Site name (record key) 99 + #[arg(short, long)] 100 + site: String, 101 + 102 + /// Output directory for the site files 103 + #[arg(short, long, default_value = ".")] 104 + output: PathBuf, 105 + 106 + /// Port to serve on 107 + #[arg(short, long, default_value = "8080")] 108 + port: u16, 109 + }, 110 + } 111 + 112 #[tokio::main] 113 async fn main() -> miette::Result<()> { 114 let args = Args::parse(); 115 116 + match args.command { 117 + Some(Commands::Deploy { input, path, site, store, password }) => { 118 + // Dispatch to appropriate authentication method 119 + if let Some(password) = password { 120 + run_with_app_password(input, password, path, site).await 121 + } else { 122 + run_with_oauth(input, store, path, site).await 123 + } 124 + } 125 + Some(Commands::Pull { input, site, output }) => { 126 + pull::pull_site(input, CowStr::from(site), output).await 127 + } 128 + Some(Commands::Serve { input, site, output, port }) => { 129 + serve::serve_site(input, CowStr::from(site), output, port).await 130 + } 131 + None => { 132 + // Legacy mode: if input is provided, assume deploy command 133 + if let Some(input) = args.input { 134 + let path = args.path.unwrap_or_else(|| PathBuf::from(".")); 135 + let store = args.store.unwrap_or_else(|| "/tmp/wisp-oauth-session.json".to_string()); 136 + 137 + // Dispatch to appropriate authentication method 138 + if let Some(password) = args.password { 139 + run_with_app_password(input, password, path, args.site).await 140 + } else { 141 + run_with_oauth(input, store, path, args.site).await 142 + } 143 + } else { 144 + // No command and no input, show help 145 + use clap::CommandFactory; 146 + Args::command().print_help().into_diagnostic()?; 147 + Ok(()) 148 + } 149 + } 150 } 151 } 152
+46
cli/src/metadata.rs
···
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::collections::HashMap; 3 + use std::path::Path; 4 + use miette::IntoDiagnostic; 5 + 6 + /// Metadata tracking file CIDs for incremental updates 7 + #[derive(Debug, Clone, Serialize, Deserialize)] 8 + pub struct SiteMetadata { 9 + /// Record CID from the PDS 10 + pub record_cid: String, 11 + /// Map of file paths to their blob CIDs 12 + pub file_cids: HashMap<String, String>, 13 + /// Timestamp when the site was last synced 14 + pub last_sync: i64, 15 + } 16 + 17 + impl SiteMetadata { 18 + pub fn new(record_cid: String, file_cids: HashMap<String, String>) -> Self { 19 + Self { 20 + record_cid, 21 + file_cids, 22 + last_sync: chrono::Utc::now().timestamp(), 23 + } 24 + } 25 + 26 + /// Load metadata from a directory 27 + pub fn load(dir: &Path) -> miette::Result<Option<Self>> { 28 + let metadata_path = dir.join(".wisp-metadata.json"); 29 + if !metadata_path.exists() { 30 + return Ok(None); 31 + } 32 + 33 + let contents = std::fs::read_to_string(&metadata_path).into_diagnostic()?; 34 + let metadata: SiteMetadata = serde_json::from_str(&contents).into_diagnostic()?; 35 + Ok(Some(metadata)) 36 + } 37 + 38 + /// Save metadata to a directory 39 + pub fn save(&self, dir: &Path) -> miette::Result<()> { 40 + let metadata_path = dir.join(".wisp-metadata.json"); 41 + let contents = serde_json::to_string_pretty(self).into_diagnostic()?; 42 + std::fs::write(&metadata_path, contents).into_diagnostic()?; 43 + Ok(()) 44 + } 45 + } 46 +
+305
cli/src/pull.rs
···
··· 1 + use crate::blob_map; 2 + use crate::download; 3 + use crate::metadata::SiteMetadata; 4 + use crate::place_wisp::fs::*; 5 + use jacquard::CowStr; 6 + use jacquard::prelude::IdentityResolver; 7 + use jacquard_common::types::string::Did; 8 + use jacquard_common::xrpc::XrpcExt; 9 + use jacquard_identity::PublicResolver; 10 + use miette::IntoDiagnostic; 11 + use std::collections::HashMap; 12 + use std::path::{Path, PathBuf}; 13 + use url::Url; 14 + 15 + /// Pull a site from the PDS to a local directory 16 + pub async fn pull_site( 17 + input: CowStr<'static>, 18 + rkey: CowStr<'static>, 19 + output_dir: PathBuf, 20 + ) -> miette::Result<()> { 21 + println!("Pulling site {} from {}...", rkey, input); 22 + 23 + // Resolve handle to DID if needed 24 + let resolver = PublicResolver::default(); 25 + let did = if input.starts_with("did:") { 26 + Did::new(&input).into_diagnostic()? 27 + } else { 28 + // It's a handle, resolve it 29 + let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?; 30 + resolver.resolve_handle(&handle).await.into_diagnostic()? 31 + }; 32 + 33 + // Resolve PDS endpoint for the DID 34 + let pds_url = resolver.pds_for_did(&did).await.into_diagnostic()?; 35 + println!("Resolved PDS: {}", pds_url); 36 + 37 + // Fetch the place.wisp.fs record 38 + 39 + println!("Fetching record from PDS..."); 40 + let client = reqwest::Client::new(); 41 + 42 + // Use com.atproto.repo.getRecord 43 + use jacquard::api::com_atproto::repo::get_record::GetRecord; 44 + use jacquard_common::types::string::Rkey as RkeyType; 45 + let rkey_parsed = RkeyType::new(&rkey).into_diagnostic()?; 46 + 47 + use jacquard_common::types::ident::AtIdentifier; 48 + use jacquard_common::types::string::RecordKey; 49 + let request = GetRecord::new() 50 + .repo(AtIdentifier::Did(did.clone())) 51 + .collection(CowStr::from("place.wisp.fs")) 52 + .rkey(RecordKey::from(rkey_parsed)) 53 + .build(); 54 + 55 + let response = client 56 + .xrpc(pds_url.clone()) 57 + .send(&request) 58 + .await 59 + .into_diagnostic()?; 60 + 61 + let record_output = response.into_output().into_diagnostic()?; 62 + let record_cid = record_output.cid.as_ref().map(|c| c.to_string()).unwrap_or_default(); 63 + 64 + // Parse the record value as Fs 65 + use jacquard_common::types::value::from_data; 66 + let fs_record: Fs = from_data(&record_output.value).into_diagnostic()?; 67 + 68 + let file_count = fs_record.file_count.map(|c| c.to_string()).unwrap_or_else(|| "?".to_string()); 69 + println!("Found site '{}' with {} files", fs_record.site, file_count); 70 + 71 + // Load existing metadata for incremental updates 72 + let existing_metadata = SiteMetadata::load(&output_dir)?; 73 + let existing_file_cids = existing_metadata 74 + .as_ref() 75 + .map(|m| m.file_cids.clone()) 76 + .unwrap_or_default(); 77 + 78 + // Extract blob map from the new manifest 79 + let new_blob_map = blob_map::extract_blob_map(&fs_record.root); 80 + let new_file_cids: HashMap<String, String> = new_blob_map 81 + .iter() 82 + .map(|(path, (_blob_ref, cid))| (path.clone(), cid.clone())) 83 + .collect(); 84 + 85 + // Clean up any leftover temp directories from previous failed attempts 86 + let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new(".")); 87 + let output_name = output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy(); 88 + let temp_prefix = format!(".tmp-{}-", output_name); 89 + 90 + if let Ok(entries) = parent.read_dir() { 91 + for entry in entries.flatten() { 92 + let name = entry.file_name(); 93 + if name.to_string_lossy().starts_with(&temp_prefix) { 94 + let _ = std::fs::remove_dir_all(entry.path()); 95 + } 96 + } 97 + } 98 + 99 + // Check if we need to update (but only if output directory actually exists with files) 100 + if let Some(metadata) = &existing_metadata { 101 + if metadata.record_cid == record_cid { 102 + // Verify that the output directory actually exists and has content 103 + let has_content = output_dir.exists() && 104 + output_dir.read_dir() 105 + .map(|mut entries| entries.any(|e| { 106 + if let Ok(entry) = e { 107 + !entry.file_name().to_string_lossy().starts_with(".wisp-metadata") 108 + } else { 109 + false 110 + } 111 + })) 112 + .unwrap_or(false); 113 + 114 + if has_content { 115 + println!("Site is already up to date!"); 116 + return Ok(()); 117 + } 118 + } 119 + } 120 + 121 + // Create temporary directory for atomic update 122 + // Place temp dir in parent directory to avoid issues with non-existent output_dir 123 + let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new(".")); 124 + let temp_dir_name = format!( 125 + ".tmp-{}-{}", 126 + output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy(), 127 + chrono::Utc::now().timestamp() 128 + ); 129 + let temp_dir = parent.join(temp_dir_name); 130 + std::fs::create_dir_all(&temp_dir).into_diagnostic()?; 131 + 132 + println!("Downloading files..."); 133 + let mut downloaded = 0; 134 + let mut reused = 0; 135 + 136 + // Download files recursively 137 + let download_result = download_directory( 138 + &fs_record.root, 139 + &temp_dir, 140 + &pds_url, 141 + did.as_str(), 142 + &new_blob_map, 143 + &existing_file_cids, 144 + &output_dir, 145 + String::new(), 146 + &mut downloaded, 147 + &mut reused, 148 + ) 149 + .await; 150 + 151 + // If download failed, clean up temp directory 152 + if let Err(e) = download_result { 153 + let _ = std::fs::remove_dir_all(&temp_dir); 154 + return Err(e); 155 + } 156 + 157 + println!( 158 + "Downloaded {} files, reused {} files", 159 + downloaded, reused 160 + ); 161 + 162 + // Save metadata 163 + let metadata = SiteMetadata::new(record_cid, new_file_cids); 164 + metadata.save(&temp_dir)?; 165 + 166 + // Move files from temp to output directory 167 + let output_abs = std::fs::canonicalize(&output_dir).unwrap_or_else(|_| output_dir.clone()); 168 + let current_dir = std::env::current_dir().into_diagnostic()?; 169 + 170 + // Special handling for pulling to current directory 171 + if output_abs == current_dir { 172 + // Move files from temp to current directory 173 + for entry in std::fs::read_dir(&temp_dir).into_diagnostic()? { 174 + let entry = entry.into_diagnostic()?; 175 + let dest = current_dir.join(entry.file_name()); 176 + 177 + // Remove existing file/dir if it exists 178 + if dest.exists() { 179 + if dest.is_dir() { 180 + std::fs::remove_dir_all(&dest).into_diagnostic()?; 181 + } else { 182 + std::fs::remove_file(&dest).into_diagnostic()?; 183 + } 184 + } 185 + 186 + // Move from temp to current dir 187 + std::fs::rename(entry.path(), dest).into_diagnostic()?; 188 + } 189 + 190 + // Clean up temp directory 191 + std::fs::remove_dir_all(&temp_dir).into_diagnostic()?; 192 + } else { 193 + // If output directory exists and has content, remove it first 194 + if output_dir.exists() { 195 + std::fs::remove_dir_all(&output_dir).into_diagnostic()?; 196 + } 197 + 198 + // Ensure parent directory exists 199 + if let Some(parent) = output_dir.parent() { 200 + if !parent.as_os_str().is_empty() && !parent.exists() { 201 + std::fs::create_dir_all(parent).into_diagnostic()?; 202 + } 203 + } 204 + 205 + // Rename temp to final location 206 + match std::fs::rename(&temp_dir, &output_dir) { 207 + Ok(_) => {}, 208 + Err(e) => { 209 + // Clean up temp directory on failure 210 + let _ = std::fs::remove_dir_all(&temp_dir); 211 + return Err(miette::miette!("Failed to move temp directory: {}", e)); 212 + } 213 + } 214 + } 215 + 216 + println!("✓ Site pulled successfully to {}", output_dir.display()); 217 + 218 + Ok(()) 219 + } 220 + 221 + /// Recursively download a directory 222 + fn download_directory<'a>( 223 + dir: &'a Directory<'_>, 224 + output_dir: &'a Path, 225 + pds_url: &'a Url, 226 + did: &'a str, 227 + new_blob_map: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 228 + existing_file_cids: &'a HashMap<String, String>, 229 + existing_output_dir: &'a Path, 230 + path_prefix: String, 231 + downloaded: &'a mut usize, 232 + reused: &'a mut usize, 233 + ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send + 'a>> { 234 + Box::pin(async move { 235 + for entry in &dir.entries { 236 + let entry_name = entry.name.as_str(); 237 + let current_path = if path_prefix.is_empty() { 238 + entry_name.to_string() 239 + } else { 240 + format!("{}/{}", path_prefix, entry_name) 241 + }; 242 + 243 + match &entry.node { 244 + EntryNode::File(file) => { 245 + let output_path = output_dir.join(entry_name); 246 + 247 + // Check if file CID matches existing 248 + if let Some((_blob_ref, new_cid)) = new_blob_map.get(&current_path) { 249 + if let Some(existing_cid) = existing_file_cids.get(&current_path) { 250 + if existing_cid == new_cid { 251 + // File unchanged, copy from existing directory 252 + let existing_path = existing_output_dir.join(&current_path); 253 + if existing_path.exists() { 254 + std::fs::copy(&existing_path, &output_path).into_diagnostic()?; 255 + *reused += 1; 256 + println!(" ✓ Reused {}", current_path); 257 + continue; 258 + } 259 + } 260 + } 261 + } 262 + 263 + // File is new or changed, download it 264 + println!(" ↓ Downloading {}", current_path); 265 + let data = download::download_and_decompress_blob( 266 + pds_url, 267 + &file.blob, 268 + did, 269 + file.base64.unwrap_or(false), 270 + file.encoding.as_ref().map(|e| e.as_str() == "gzip").unwrap_or(false), 271 + ) 272 + .await?; 273 + 274 + std::fs::write(&output_path, data).into_diagnostic()?; 275 + *downloaded += 1; 276 + } 277 + EntryNode::Directory(subdir) => { 278 + let subdir_path = output_dir.join(entry_name); 279 + std::fs::create_dir_all(&subdir_path).into_diagnostic()?; 280 + 281 + download_directory( 282 + subdir, 283 + &subdir_path, 284 + pds_url, 285 + did, 286 + new_blob_map, 287 + existing_file_cids, 288 + existing_output_dir, 289 + current_path, 290 + downloaded, 291 + reused, 292 + ) 293 + .await?; 294 + } 295 + EntryNode::Unknown(_) => { 296 + // Skip unknown node types 297 + println!(" ⚠ Skipping unknown node type for {}", current_path); 298 + } 299 + } 300 + } 301 + 302 + Ok(()) 303 + }) 304 + } 305 +
+202
cli/src/serve.rs
···
··· 1 + use crate::pull::pull_site; 2 + use axum::Router; 3 + use jacquard::CowStr; 4 + use jacquard_common::jetstream::{CommitOperation, JetstreamMessage, JetstreamParams}; 5 + use jacquard_common::types::string::Did; 6 + use jacquard_common::xrpc::{SubscriptionClient, TungsteniteSubscriptionClient}; 7 + use miette::IntoDiagnostic; 8 + use n0_future::StreamExt; 9 + use std::path::PathBuf; 10 + use std::sync::Arc; 11 + use tokio::sync::RwLock; 12 + use tower_http::compression::CompressionLayer; 13 + use tower_http::services::ServeDir; 14 + use url::Url; 15 + 16 + /// Shared state for the server 17 + #[derive(Clone)] 18 + struct ServerState { 19 + did: CowStr<'static>, 20 + rkey: CowStr<'static>, 21 + output_dir: PathBuf, 22 + last_cid: Arc<RwLock<Option<String>>>, 23 + } 24 + 25 + /// Serve a site locally with real-time firehose updates 26 + pub async fn serve_site( 27 + input: CowStr<'static>, 28 + rkey: CowStr<'static>, 29 + output_dir: PathBuf, 30 + port: u16, 31 + ) -> miette::Result<()> { 32 + println!("Serving site {} from {} on port {}...", rkey, input, port); 33 + 34 + // Resolve handle to DID if needed 35 + use jacquard_identity::PublicResolver; 36 + use jacquard::prelude::IdentityResolver; 37 + 38 + let resolver = PublicResolver::default(); 39 + let did = if input.starts_with("did:") { 40 + Did::new(&input).into_diagnostic()? 41 + } else { 42 + // It's a handle, resolve it 43 + let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?; 44 + resolver.resolve_handle(&handle).await.into_diagnostic()? 45 + }; 46 + 47 + println!("Resolved to DID: {}", did.as_str()); 48 + 49 + // Create output directory if it doesn't exist 50 + std::fs::create_dir_all(&output_dir).into_diagnostic()?; 51 + 52 + // Initial pull of the site 53 + println!("Performing initial pull..."); 54 + let did_str = CowStr::from(did.as_str().to_string()); 55 + pull_site(did_str.clone(), rkey.clone(), output_dir.clone()).await?; 56 + 57 + // Create shared state 58 + let state = ServerState { 59 + did: did_str.clone(), 60 + rkey: rkey.clone(), 61 + output_dir: output_dir.clone(), 62 + last_cid: Arc::new(RwLock::new(None)), 63 + }; 64 + 65 + // Start firehose listener in background 66 + let firehose_state = state.clone(); 67 + tokio::spawn(async move { 68 + if let Err(e) = watch_firehose(firehose_state).await { 69 + eprintln!("Firehose error: {}", e); 70 + } 71 + }); 72 + 73 + // Create HTTP server with gzip compression 74 + let app = Router::new() 75 + .fallback_service( 76 + ServeDir::new(&output_dir) 77 + .precompressed_gzip() 78 + ) 79 + .layer(CompressionLayer::new()) 80 + .with_state(state); 81 + 82 + let addr = format!("0.0.0.0:{}", port); 83 + let listener = tokio::net::TcpListener::bind(&addr) 84 + .await 85 + .into_diagnostic()?; 86 + 87 + println!("\n✓ Server running at http://localhost:{}", port); 88 + println!(" Watching for updates on the firehose...\n"); 89 + 90 + axum::serve(listener, app).await.into_diagnostic()?; 91 + 92 + Ok(()) 93 + } 94 + 95 + /// Watch the firehose for updates to the specific site 96 + fn watch_firehose(state: ServerState) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send>> { 97 + Box::pin(async move { 98 + let jetstream_url = Url::parse("wss://jetstream1.us-east.fire.hose.cam") 99 + .into_diagnostic()?; 100 + 101 + println!("[Firehose] Connecting to Jetstream..."); 102 + 103 + // Create subscription client 104 + let client = TungsteniteSubscriptionClient::from_base_uri(jetstream_url); 105 + 106 + // Subscribe with no filters (we'll filter manually) 107 + // Jetstream doesn't support filtering by collection in the params builder 108 + let params = JetstreamParams::new().build(); 109 + 110 + let stream = client.subscribe(&params).await.into_diagnostic()?; 111 + println!("[Firehose] Connected! Watching for updates..."); 112 + 113 + // Convert to typed message stream 114 + let (_sink, mut messages) = stream.into_stream(); 115 + 116 + loop { 117 + match messages.next().await { 118 + Some(Ok(msg)) => { 119 + if let Err(e) = handle_firehose_message(&state, msg).await { 120 + eprintln!("[Firehose] Error handling message: {}", e); 121 + } 122 + } 123 + Some(Err(e)) => { 124 + eprintln!("[Firehose] Stream error: {}", e); 125 + // Try to reconnect after a delay 126 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 127 + return Box::pin(watch_firehose(state)).await; 128 + } 129 + None => { 130 + println!("[Firehose] Stream ended, reconnecting..."); 131 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 132 + return Box::pin(watch_firehose(state)).await; 133 + } 134 + } 135 + } 136 + }) 137 + } 138 + 139 + /// Handle a firehose message 140 + async fn handle_firehose_message( 141 + state: &ServerState, 142 + msg: JetstreamMessage<'_>, 143 + ) -> miette::Result<()> { 144 + match msg { 145 + JetstreamMessage::Commit { 146 + did, 147 + commit, 148 + .. 149 + } => { 150 + // Check if this is our site 151 + if did.as_str() == state.did.as_str() 152 + && commit.collection.as_str() == "place.wisp.fs" 153 + && commit.rkey.as_str() == state.rkey.as_str() 154 + { 155 + match commit.operation { 156 + CommitOperation::Create | CommitOperation::Update => { 157 + let new_cid = commit.cid.as_ref().map(|c| c.to_string()); 158 + 159 + // Check if CID changed 160 + let should_update = { 161 + let last_cid = state.last_cid.read().await; 162 + new_cid != *last_cid 163 + }; 164 + 165 + if should_update { 166 + println!("\n[Update] Detected change to site {} (CID: {:?})", state.rkey, new_cid); 167 + println!("[Update] Pulling latest version..."); 168 + 169 + // Pull the updated site 170 + match pull_site( 171 + state.did.clone(), 172 + state.rkey.clone(), 173 + state.output_dir.clone(), 174 + ) 175 + .await 176 + { 177 + Ok(_) => { 178 + // Update last CID 179 + let mut last_cid = state.last_cid.write().await; 180 + *last_cid = new_cid; 181 + println!("[Update] ✓ Site updated successfully!\n"); 182 + } 183 + Err(e) => { 184 + eprintln!("[Update] Failed to pull site: {}", e); 185 + } 186 + } 187 + } 188 + } 189 + CommitOperation::Delete => { 190 + println!("\n[Update] Site {} was deleted", state.rkey); 191 + } 192 + } 193 + } 194 + } 195 + _ => { 196 + // Ignore identity and account messages 197 + } 198 + } 199 + 200 + Ok(()) 201 + } 202 +