feat(rust-provider): Add Rust-backed provider with XFS-safe durability via IPC bridge, TypeScript provider, tests and docs

This commit is contained in:
2026-03-05 19:36:11 +00:00
parent 61e3f3a0b6
commit 5283247bea
24 changed files with 14453 additions and 1248 deletions

757
rust/Cargo.lock generated Normal file
View File

@@ -0,0 +1,757 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inotify"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libredox"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
"bitflags 2.11.0",
"libc",
"plain",
"redox_syscall 0.7.3",
]
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "notify"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
dependencies = [
"bitflags 2.11.0",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.52.0",
]
[[package]]
name = "notify-types"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
dependencies = [
"instant",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.18",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "regex-lite"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smartfs-bin"
version = "0.1.0"
dependencies = [
"base64",
"clap",
"serde",
"serde_json",
"smartfs-core",
"smartfs-protocol",
"tokio",
]
[[package]]
name = "smartfs-core"
version = "0.1.0"
dependencies = [
"base64",
"filetime",
"libc",
"notify",
"regex-lite",
"serde",
"serde_json",
"smartfs-protocol",
]
[[package]]
name = "smartfs-protocol"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

20
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[workspace]
resolver = "2"
members = [
"crates/smartfs-protocol",
"crates/smartfs-core",
"crates/smartfs-bin",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
clap = { version = "4", features = ["derive"] }
notify = "7"
base64 = "0.22"

View File

@@ -0,0 +1,18 @@
[package]
name = "smartfs-bin"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "smartfs-bin"
path = "src/main.rs"
[dependencies]
smartfs-protocol = { path = "../smartfs-protocol" }
smartfs-core = { path = "../smartfs-core" }
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
clap.workspace = true
base64.workspace = true

View File

@@ -0,0 +1,419 @@
use base64::{Engine as _, engine::general_purpose::STANDARD};
use clap::Parser;
use smartfs_core::{FsOps, WatchManager};
use smartfs_protocol::*;
use std::collections::HashMap;
use std::io::{self, BufRead, BufWriter, Write as IoWrite};
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt;
#[derive(Parser)]
#[command(name = "smartfs-bin", about = "SmartFS Rust filesystem backend")]
struct Cli {
/// Run in management/IPC mode (JSON over stdin/stdout)
#[arg(long)]
management: bool,
}
fn main() {
let cli = Cli::parse();
if cli.management {
run_management_mode();
} else {
eprintln!("smartfs-bin: use --management flag for IPC mode");
std::process::exit(1);
}
}
/// State for open write streams
struct WriteStreamState {
writer: BufWriter<std::fs::File>,
final_path: PathBuf,
temp_path: Option<PathBuf>,
mode: Option<u32>,
}
fn run_management_mode() {
// Send ready event
let ready = IpcEvent {
event: "ready".to_string(),
data: serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"provider": "rust"
}),
};
send_json(&ready);
let watch_manager = WatchManager::new();
let mut write_streams: HashMap<String, WriteStreamState> = HashMap::new();
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
if line.trim().is_empty() {
continue;
}
let request: IpcRequest = match serde_json::from_str(&line) {
Ok(r) => r,
Err(e) => {
eprintln!("smartfs-bin: invalid JSON: {}", e);
continue;
}
};
let response = dispatch_command(&request, &watch_manager, &mut write_streams);
send_json(&response);
}
}
fn dispatch_command(
req: &IpcRequest,
watch_manager: &WatchManager,
write_streams: &mut HashMap<String, WriteStreamState>,
) -> IpcResponse {
match req.method.as_str() {
"readFile" => {
match serde_json::from_value::<ReadFileParams>(req.params.clone()) {
Ok(params) => match FsOps::read_file(&params) {
Ok(result) => IpcResponse::ok(req.id.clone(), result),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"writeFile" => {
match serde_json::from_value::<WriteFileParams>(req.params.clone()) {
Ok(params) => match FsOps::write_file(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"appendFile" => {
match serde_json::from_value::<AppendFileParams>(req.params.clone()) {
Ok(params) => match FsOps::append_file(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"deleteFile" => {
match serde_json::from_value::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::delete_file(Path::new(&params.path)) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"copyFile" => {
match serde_json::from_value::<CopyMoveParams>(req.params.clone()) {
Ok(params) => match FsOps::copy_file(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"moveFile" => {
match serde_json::from_value::<CopyMoveParams>(req.params.clone()) {
Ok(params) => match FsOps::move_file(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"fileExists" => {
match serde_json::from_value::<PathParams>(req.params.clone()) {
Ok(params) => {
let exists = FsOps::file_exists(Path::new(&params.path));
IpcResponse::ok(req.id.clone(), serde_json::json!(exists))
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"fileStat" => {
match serde_json::from_value::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::file_stat(Path::new(&params.path)) {
Ok(stats) => {
IpcResponse::ok(req.id.clone(), serde_json::to_value(&stats).unwrap())
}
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"listDirectory" => {
match serde_json::from_value::<ListDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::list_directory(&params) {
Ok(entries) => {
IpcResponse::ok(req.id.clone(), serde_json::to_value(&entries).unwrap())
}
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"createDirectory" => {
match serde_json::from_value::<CreateDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::create_directory(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"deleteDirectory" => {
match serde_json::from_value::<DeleteDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::delete_directory(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"directoryExists" => {
match serde_json::from_value::<PathParams>(req.params.clone()) {
Ok(params) => {
let exists = FsOps::directory_exists(Path::new(&params.path));
IpcResponse::ok(req.id.clone(), serde_json::json!(exists))
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"directoryStat" => {
match serde_json::from_value::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::directory_stat(Path::new(&params.path)) {
Ok(stats) => {
IpcResponse::ok(req.id.clone(), serde_json::to_value(&stats).unwrap())
}
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"watch" => {
match serde_json::from_value::<WatchParams>(req.params.clone()) {
Ok(params) => {
match watch_manager.add_watch(
params.id,
&params.path,
params.recursive.unwrap_or(false),
) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
}
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"unwatchAll" => {
match watch_manager.remove_all() {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
}
}
"batch" => {
match serde_json::from_value::<BatchParams>(req.params.clone()) {
Ok(params) => {
let results = FsOps::batch(&params);
IpcResponse::ok(req.id.clone(), serde_json::to_value(&results).unwrap())
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"executeTransaction" => {
match serde_json::from_value::<TransactionParams>(req.params.clone()) {
Ok(params) => match FsOps::execute_transaction(&params) {
Ok(()) => IpcResponse::ok_void(req.id.clone()),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"normalizePath" => {
match serde_json::from_value::<NormalizePathParams>(req.params.clone()) {
Ok(params) => {
let result = FsOps::normalize_path(&params.path);
IpcResponse::ok(req.id.clone(), serde_json::json!(result))
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"joinPath" => {
match serde_json::from_value::<JoinPathParams>(req.params.clone()) {
Ok(params) => {
let result = FsOps::join_path(&params.segments);
IpcResponse::ok(req.id.clone(), serde_json::json!(result))
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"readFileStream" => {
match serde_json::from_value::<ReadFileStreamParams>(req.params.clone()) {
Ok(params) => match FsOps::read_file_stream(&req.id, &params) {
Ok(total) => IpcResponse::ok(req.id.clone(), serde_json::json!({ "totalBytes": total })),
Err(e) => IpcResponse::err(req.id.clone(), e),
},
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"writeStreamBegin" => {
match serde_json::from_value::<WriteStreamBeginParams>(req.params.clone()) {
Ok(params) => {
let final_path = PathBuf::from(&params.path);
// Ensure parent directory exists
if let Some(parent) = final_path.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
return IpcResponse::err(req.id.clone(), format!("writeStreamBegin mkdir: {}", e));
}
}
}
let (write_path, temp_path) = if params.atomic.unwrap_or(false) {
let temp = final_path.with_extension(format!(
"tmp.{}",
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
(temp.clone(), Some(temp))
} else {
(final_path.clone(), None)
};
match std::fs::File::create(&write_path) {
Ok(file) => {
let stream_id = format!("ws_{}", req.id);
write_streams.insert(stream_id.clone(), WriteStreamState {
writer: BufWriter::new(file),
final_path,
temp_path,
mode: params.mode,
});
IpcResponse::ok(req.id.clone(), serde_json::json!({ "streamId": stream_id }))
}
Err(e) => IpcResponse::err(req.id.clone(), format!("writeStreamBegin create: {}", e)),
}
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"writeStreamChunk" => {
match serde_json::from_value::<WriteStreamChunkParams>(req.params.clone()) {
Ok(params) => {
let stream = match write_streams.get_mut(&params.stream_id) {
Some(s) => s,
None => return IpcResponse::err(req.id.clone(), format!("unknown streamId: {}", params.stream_id)),
};
// Write data if non-empty
if !params.data.is_empty() {
match STANDARD.decode(&params.data) {
Ok(bytes) => {
if let Err(e) = stream.writer.write_all(&bytes) {
write_streams.remove(&params.stream_id);
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk write: {}", e));
}
}
Err(e) => {
write_streams.remove(&params.stream_id);
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk decode: {}", e));
}
}
}
if params.last {
// Finalize: flush, fsync, set mode, rename if atomic, fsync parent
let state = write_streams.remove(&params.stream_id).unwrap();
let mut writer = state.writer;
if let Err(e) = writer.flush() {
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk flush: {}", e));
}
// Get inner file for fsync
let file = match writer.into_inner() {
Ok(f) => f,
Err(e) => {
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk into_inner: {}", e.error()));
}
};
if let Err(e) = file.sync_all() {
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk fsync: {}", e));
}
drop(file);
// Set mode if requested
if let Some(mode) = state.mode {
let write_path = state.temp_path.as_ref().unwrap_or(&state.final_path);
let _ = std::fs::set_permissions(write_path, std::fs::Permissions::from_mode(mode));
}
// Rename if atomic
if let Some(ref temp_path) = state.temp_path {
if let Err(e) = std::fs::rename(temp_path, &state.final_path) {
let _ = std::fs::remove_file(temp_path);
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk rename: {}", e));
}
}
// Fsync parent
if let Some(parent) = state.final_path.parent() {
let _ = std::fs::File::open(parent).and_then(|f| f.sync_all());
}
}
IpcResponse::ok_void(req.id.clone())
}
Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)),
}
}
"ping" => IpcResponse::ok(req.id.clone(), serde_json::json!({ "pong": true })),
other => IpcResponse::err(req.id.clone(), format!("unknown method: {}", other)),
}
}
fn send_json<T: serde::Serialize>(value: &T) {
if let Ok(json) = serde_json::to_string(value) {
let stdout = io::stdout();
let mut out = stdout.lock();
let _ = writeln!(out, "{}", json);
let _ = out.flush();
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "smartfs-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
smartfs-protocol = { path = "../smartfs-protocol" }
serde.workspace = true
serde_json.workspace = true
notify.workspace = true
libc = "0.2"
regex-lite = "0.1"
filetime = "0.2"
base64.workspace = true

View File

@@ -0,0 +1,5 @@
mod ops;
mod watch;
pub use ops::FsOps;
pub use watch::WatchManager;

View File

@@ -0,0 +1,649 @@
use base64::{Engine as _, engine::general_purpose::STANDARD};
use smartfs_protocol::*;
use std::fs;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
/// Filesystem operations with XFS-safe fsync after metadata changes.
pub struct FsOps;
impl FsOps {
// ── Safety primitive ────────────────────────────────────────────────
/// Fsync a parent directory to ensure metadata durability on XFS.
/// This is the key operation that Node.js cannot do.
fn fsync_parent(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
let dir = fs::File::open(parent)?;
dir.sync_all()?;
}
Ok(())
}
/// Fsync a specific directory.
fn fsync_dir(path: &Path) -> io::Result<()> {
let dir = fs::File::open(path)?;
dir.sync_all()?;
Ok(())
}
// ── File operations ─────────────────────────────────────────────────
pub fn read_file(params: &ReadFileParams) -> Result<serde_json::Value, String> {
let path = Path::new(&params.path);
let bytes = fs::read(path).map_err(|e| format!("read_file: {}", e))?;
let encoding = params.encoding.as_deref().unwrap_or("utf8");
match encoding {
"base64" => {
let encoded = STANDARD.encode(&bytes);
Ok(serde_json::json!({ "content": encoded }))
}
"hex" => {
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
Ok(serde_json::json!({ "content": hex }))
}
"buffer" => {
let encoded = STANDARD.encode(&bytes);
Ok(serde_json::json!({ "content": encoded, "isBuffer": true }))
}
_ => {
// utf8, utf-8, ascii
let content = String::from_utf8_lossy(&bytes).into_owned();
Ok(serde_json::json!({ "content": content }))
}
}
}
pub fn write_file(params: &WriteFileParams) -> Result<(), String> {
let path = Path::new(&params.path);
let content: Vec<u8> = if params.encoding.as_deref() == Some("base64") {
STANDARD.decode(&params.content).map_err(|e| format!("write_file base64 decode: {}", e))?
} else {
params.content.as_bytes().to_vec()
};
// Ensure parent directory exists
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| format!("write_file mkdir: {}", e))?;
Self::fsync_parent(parent).ok();
}
}
if params.atomic.unwrap_or(false) {
// Atomic write: write to temp → fsync file → rename → fsync parent
let temp_path = path.with_extension(format!(
"tmp.{}",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
// Write to temp file
fs::write(&temp_path, content).map_err(|e| format!("write_file temp: {}", e))?;
// Fsync the temp file data
let f = fs::File::open(&temp_path).map_err(|e| format!("write_file open temp: {}", e))?;
f.sync_all().map_err(|e| format!("write_file fsync temp: {}", e))?;
drop(f);
// Set mode if requested
if let Some(mode) = params.mode {
fs::set_permissions(&temp_path, fs::Permissions::from_mode(mode))
.map_err(|e| format!("write_file chmod: {}", e))?;
}
// Rename (atomic on same filesystem)
fs::rename(&temp_path, path).map_err(|e| {
// Clean up temp on failure
let _ = fs::remove_file(&temp_path);
format!("write_file rename: {}", e)
})?;
// Fsync parent to ensure the rename is durable
Self::fsync_parent(path).map_err(|e| format!("write_file fsync parent: {}", e))?;
} else {
fs::write(path, content).map_err(|e| format!("write_file: {}", e))?;
// Fsync the file
let f = fs::File::open(path).map_err(|e| format!("write_file open: {}", e))?;
f.sync_all().map_err(|e| format!("write_file fsync: {}", e))?;
drop(f);
if let Some(mode) = params.mode {
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.map_err(|e| format!("write_file chmod: {}", e))?;
}
// Fsync parent for new file creation
Self::fsync_parent(path).map_err(|e| format!("write_file fsync parent: {}", e))?;
}
Ok(())
}
pub fn append_file(params: &AppendFileParams) -> Result<(), String> {
use std::io::Write;
let path = Path::new(&params.path);
let content: Vec<u8> = if params.encoding.as_deref() == Some("base64") {
STANDARD.decode(&params.content).map_err(|e| format!("append_file base64 decode: {}", e))?
} else {
params.content.as_bytes().to_vec()
};
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| format!("append_file: {}", e))?;
file.write_all(&content)
.map_err(|e| format!("append_file write: {}", e))?;
file.sync_all()
.map_err(|e| format!("append_file fsync: {}", e))?;
// Fsync parent in case this created the file
Self::fsync_parent(path).ok();
Ok(())
}
pub fn delete_file(path: &Path) -> Result<(), String> {
fs::remove_file(path).map_err(|e| format!("delete_file: {}", e))?;
Self::fsync_parent(path).map_err(|e| format!("delete_file fsync parent: {}", e))?;
Ok(())
}
pub fn copy_file(params: &CopyMoveParams) -> Result<(), String> {
let from = Path::new(&params.from);
let to = Path::new(&params.to);
if !params.overwrite.unwrap_or(true) && to.exists() {
return Err("copy_file: destination already exists".to_string());
}
fs::copy(from, to).map_err(|e| format!("copy_file: {}", e))?;
if params.preserve_timestamps.unwrap_or(false) {
// Copy timestamps
let metadata = fs::metadata(from).map_err(|e| format!("copy_file stat: {}", e))?;
let atime = filetime::FileTime::from_last_access_time(&metadata);
let mtime = filetime::FileTime::from_last_modification_time(&metadata);
filetime::set_file_times(to, atime, mtime).ok();
}
// Fsync parent after creating new file entry
Self::fsync_parent(to).map_err(|e| format!("copy_file fsync parent: {}", e))?;
Ok(())
}
pub fn move_file(params: &CopyMoveParams) -> Result<(), String> {
let from = Path::new(&params.from);
let to = Path::new(&params.to);
if !params.overwrite.unwrap_or(true) && to.exists() {
return Err("move_file: destination already exists".to_string());
}
match fs::rename(from, to) {
Ok(()) => {
// Fsync both parent directories (source and dest may differ)
Self::fsync_parent(from).ok();
Self::fsync_parent(to).map_err(|e| format!("move_file fsync parent: {}", e))?;
}
Err(e) if e.raw_os_error() == Some(libc::EXDEV) => {
// Cross-device: copy then delete
Self::copy_file(params)?;
Self::delete_file(from)?;
}
Err(e) => return Err(format!("move_file: {}", e)),
}
Ok(())
}
pub fn file_exists(path: &Path) -> bool {
path.exists() && path.is_file()
}
pub fn file_stat(path: &Path) -> Result<FileStats, String> {
Self::stat_path(path)
}
// ── Directory operations ────────────────────────────────────────────
pub fn list_directory(params: &ListDirectoryParams) -> Result<Vec<DirectoryEntry>, String> {
let path = Path::new(&params.path);
let mut entries = Vec::new();
if params.recursive.unwrap_or(false) {
Self::list_directory_recursive(path, &mut entries, params)?;
} else {
let dir_entries = fs::read_dir(path)
.map_err(|e| format!("list_directory: {}", e))?;
for entry_result in dir_entries {
let entry = entry_result.map_err(|e| format!("list_directory entry: {}", e))?;
let dir_entry = Self::to_directory_entry(&entry, params)?;
if let Some(filter) = &params.filter {
if !Self::matches_filter(&dir_entry.name, filter) {
continue;
}
}
entries.push(dir_entry);
}
}
Ok(entries)
}
fn list_directory_recursive(
path: &Path,
entries: &mut Vec<DirectoryEntry>,
params: &ListDirectoryParams,
) -> Result<(), String> {
let dir_entries = fs::read_dir(path)
.map_err(|e| format!("list_directory_recursive: {}", e))?;
for entry_result in dir_entries {
let entry = entry_result.map_err(|e| format!("list_directory entry: {}", e))?;
let dir_entry = Self::to_directory_entry(&entry, params)?;
let matches = if let Some(filter) = &params.filter {
Self::matches_filter(&dir_entry.name, filter)
} else {
true
};
if matches {
entries.push(dir_entry.clone());
}
if dir_entry.is_directory {
Self::list_directory_recursive(&entry.path(), entries, params)?;
}
}
Ok(())
}
pub fn create_directory(params: &CreateDirectoryParams) -> Result<(), String> {
let path = Path::new(&params.path);
if params.recursive.unwrap_or(true) {
fs::create_dir_all(path).map_err(|e| format!("create_directory: {}", e))?;
} else {
fs::create_dir(path).map_err(|e| format!("create_directory: {}", e))?;
}
if let Some(mode) = params.mode {
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.map_err(|e| format!("create_directory chmod: {}", e))?;
}
// Fsync parent to ensure directory entry is durable
Self::fsync_parent(path).map_err(|e| format!("create_directory fsync: {}", e))?;
Ok(())
}
pub fn delete_directory(params: &DeleteDirectoryParams) -> Result<(), String> {
let path = Path::new(&params.path);
if params.recursive.unwrap_or(true) {
fs::remove_dir_all(path).map_err(|e| format!("delete_directory: {}", e))?;
} else {
fs::remove_dir(path).map_err(|e| format!("delete_directory: {}", e))?;
}
Self::fsync_parent(path).map_err(|e| format!("delete_directory fsync: {}", e))?;
Ok(())
}
pub fn directory_exists(path: &Path) -> bool {
path.exists() && path.is_dir()
}
pub fn directory_stat(path: &Path) -> Result<FileStats, String> {
Self::stat_path(path)
}
// ── Batch operations ────────────────────────────────────────────────
/// Execute multiple operations, collecting parent dirs for a single fsync pass at the end.
pub fn batch(params: &BatchParams) -> Vec<BatchResult> {
let mut results = Vec::with_capacity(params.operations.len());
let mut dirs_to_sync: Vec<PathBuf> = Vec::new();
for (index, op) in params.operations.iter().enumerate() {
let result = Self::execute_batch_op(op, &mut dirs_to_sync);
results.push(BatchResult {
index,
success: result.is_ok(),
error: result.err(),
});
}
// Batch fsync all affected parent directories
dirs_to_sync.sort();
dirs_to_sync.dedup();
for dir in &dirs_to_sync {
Self::fsync_dir(dir).ok();
}
results
}
fn execute_batch_op(op: &BatchOp, dirs_to_sync: &mut Vec<PathBuf>) -> Result<(), String> {
let path = Path::new(&op.path);
match op.op_type.as_str() {
"write" => {
let content = op.content.as_deref().unwrap_or("");
fs::write(path, content.as_bytes()).map_err(|e| e.to_string())?;
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"append" => {
use std::io::Write;
let content = op.content.as_deref().unwrap_or("");
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| e.to_string())?;
file.write_all(content.as_bytes()).map_err(|e| e.to_string())?;
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"delete" => {
fs::remove_file(path).map_err(|e| e.to_string())?;
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"copy" => {
let to = Path::new(op.target_path.as_deref().ok_or("copy: missing targetPath")?);
fs::copy(path, to).map_err(|e| e.to_string())?;
if let Some(parent) = to.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"move" => {
let to = Path::new(op.target_path.as_deref().ok_or("move: missing targetPath")?);
fs::rename(path, to).map_err(|e| e.to_string())?;
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
if let Some(parent) = to.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"mkdir" => {
if op.recursive.unwrap_or(true) {
fs::create_dir_all(path).map_err(|e| e.to_string())?;
} else {
fs::create_dir(path).map_err(|e| e.to_string())?;
}
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
"rmdir" => {
if op.recursive.unwrap_or(true) {
fs::remove_dir_all(path).map_err(|e| e.to_string())?;
} else {
fs::remove_dir(path).map_err(|e| e.to_string())?;
}
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
other => {
return Err(format!("unknown batch op type: {}", other));
}
}
Ok(())
}
// ── Transaction operations ──────────────────────────────────────────
pub fn execute_transaction(params: &TransactionParams) -> Result<(), String> {
// Phase 1: Prepare backups
let mut backups: Vec<(usize, Option<Vec<u8>>)> = Vec::new();
for (i, op) in params.operations.iter().enumerate() {
let path = Path::new(&op.path);
let backup = if path.exists() && path.is_file() {
Some(fs::read(path).map_err(|e| format!("transaction backup {}: {}", i, e))?)
} else {
None
};
backups.push((i, backup));
}
// Phase 2: Execute operations
let mut completed = 0;
let mut dirs_to_sync: Vec<PathBuf> = Vec::new();
for (i, op) in params.operations.iter().enumerate() {
let path = Path::new(&op.path);
let result = match op.op_type.as_str() {
"write" => {
let content = op.content.as_deref().unwrap_or("");
fs::write(path, content.as_bytes()).map_err(|e| e.to_string())
}
"append" => {
use std::io::Write;
let content = op.content.as_deref().unwrap_or("");
fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut f| f.write_all(content.as_bytes()))
.map_err(|e| e.to_string())
}
"delete" => fs::remove_file(path).map_err(|e| e.to_string()),
"copy" => {
let to = op.target_path.as_deref().ok_or("copy: missing targetPath")?;
fs::copy(path, to).map(|_| ()).map_err(|e| e.to_string())
}
"move" => {
let to = op.target_path.as_deref().ok_or("move: missing targetPath")?;
fs::rename(path, to).map_err(|e| e.to_string())
}
other => Err(format!("unknown transaction op: {}", other)),
};
match result {
Ok(()) => {
completed = i + 1;
if let Some(parent) = path.parent() {
dirs_to_sync.push(parent.to_path_buf());
}
if let Some(tp) = &op.target_path {
if let Some(parent) = Path::new(tp).parent() {
dirs_to_sync.push(parent.to_path_buf());
}
}
}
Err(e) => {
// Rollback completed operations in reverse order
for j in (0..completed).rev() {
let (_, ref backup) = backups[j];
let rollback_path = Path::new(&params.operations[j].path);
if let Some(data) = backup {
let _ = fs::write(rollback_path, data);
} else {
let _ = fs::remove_file(rollback_path);
}
}
return Err(format!("transaction failed at op {}: {}", i, e));
}
}
}
// Phase 3: Batch fsync all affected directories
dirs_to_sync.sort();
dirs_to_sync.dedup();
for dir in &dirs_to_sync {
Self::fsync_dir(dir).ok();
}
Ok(())
}
// ── Path operations ─────────────────────────────────────────────────
pub fn normalize_path(path: &str) -> String {
let p = Path::new(path);
// Use canonicalize if the path exists, otherwise just clean it
match p.canonicalize() {
Ok(canonical) => canonical.to_string_lossy().into_owned(),
Err(_) => {
// Manual normalization for non-existent paths
let mut components = Vec::new();
for component in p.components() {
match component {
std::path::Component::ParentDir => { components.pop(); }
std::path::Component::CurDir => {}
_ => components.push(component),
}
}
let result: PathBuf = components.into_iter().collect();
result.to_string_lossy().into_owned()
}
}
}
pub fn join_path(segments: &[String]) -> String {
let mut result = PathBuf::new();
for seg in segments {
result.push(seg);
}
result.to_string_lossy().into_owned()
}
// ── Streaming operations ─────────────────────────────────────────────
/// Read a file in chunks, writing IpcStreamChunk messages to stdout.
/// Returns the total number of bytes read.
pub fn read_file_stream(
request_id: &str,
params: &ReadFileStreamParams,
) -> Result<u64, String> {
use std::io::{Read, Write};
let path = Path::new(&params.path);
let chunk_size = params.chunk_size.unwrap_or(65536); // 64KB default
let mut file = fs::File::open(path)
.map_err(|e| format!("read_file_stream: {}", e))?;
let mut total_bytes: u64 = 0;
let mut buf = vec![0u8; chunk_size];
loop {
let n = file.read(&mut buf).map_err(|e| format!("read_file_stream read: {}", e))?;
if n == 0 {
break;
}
total_bytes += n as u64;
let encoded = STANDARD.encode(&buf[..n]);
let chunk = IpcStreamChunk {
id: request_id.to_string(),
stream: true,
data: serde_json::json!(encoded),
};
if let Ok(json) = serde_json::to_string(&chunk) {
let stdout = io::stdout();
let mut out = stdout.lock();
let _ = writeln!(out, "{}", json);
let _ = out.flush();
}
}
Ok(total_bytes)
}
// ── Helpers ─────────────────────────────────────────────────────────
pub fn stat_path(path: &Path) -> Result<FileStats, String> {
let metadata = fs::symlink_metadata(path).map_err(|e| format!("stat: {}", e))?;
let file_type = metadata.file_type();
Ok(FileStats {
size: metadata.len(),
birthtime: system_time_to_iso(metadata.created().ok()),
mtime: system_time_to_iso(metadata.modified().ok()),
atime: system_time_to_iso(metadata.accessed().ok()),
is_file: file_type.is_file(),
is_directory: file_type.is_dir(),
is_symbolic_link: file_type.is_symlink(),
mode: metadata.permissions().mode(),
})
}
fn to_directory_entry(
entry: &fs::DirEntry,
params: &ListDirectoryParams,
) -> Result<DirectoryEntry, String> {
let file_type = entry.file_type().map_err(|e| format!("dir entry type: {}", e))?;
let path = entry.path();
let stats = if params.include_stats.unwrap_or(false) {
Self::stat_path(&path).ok()
} else {
None
};
Ok(DirectoryEntry {
name: entry.file_name().to_string_lossy().into_owned(),
path: path.to_string_lossy().into_owned(),
is_file: file_type.is_file(),
is_directory: file_type.is_dir(),
is_symbolic_link: file_type.is_symlink(),
stats,
})
}
fn matches_filter(name: &str, filter: &str) -> bool {
if let Some(regex_pattern) = filter.strip_prefix("regex:") {
// Raw regex pattern from TypeScript RegExp
if let Ok(regex) = regex_lite::Regex::new(regex_pattern) {
return regex.is_match(name);
}
return name.contains(regex_pattern);
}
// Simple glob matching: * matches any sequence
let pattern = filter.replace('.', "\\.").replace('*', ".*");
if let Ok(regex) = regex_lite::Regex::new(&format!("^{}$", pattern)) {
regex.is_match(name)
} else {
name.contains(filter)
}
}
}
fn system_time_to_iso(time: Option<SystemTime>) -> String {
match time {
Some(t) => {
let duration = t
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let millis = duration.subsec_millis();
// Simple ISO-ish format: unix timestamp as ISO string
// Full ISO formatting without chrono
format!("{}.{:03}Z", secs, millis)
}
None => "0.000Z".to_string(),
}
}

View File

@@ -0,0 +1,109 @@
use crate::FsOps;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use smartfs_protocol::{IpcEvent, WatchEvent};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
/// Manages file watchers, emitting events as IPC events to stdout.
pub struct WatchManager {
watchers: Arc<Mutex<HashMap<String, RecommendedWatcher>>>,
}
impl WatchManager {
pub fn new() -> Self {
Self {
watchers: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn add_watch(
&self,
id: String,
path: &str,
recursive: bool,
) -> Result<(), String> {
let watch_id = id.clone();
let mode = if recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
};
let (tx, rx) = std::sync::mpsc::channel::<notify::Result<notify::Event>>();
let mut watcher = RecommendedWatcher::new(tx, Config::default())
.map_err(|e| format!("watch create: {}", e))?;
watcher
.watch(Path::new(path), mode)
.map_err(|e| format!("watch path: {}", e))?;
// Spawn a thread to read events and write IPC events to stdout
let watch_id_clone = watch_id.clone();
std::thread::spawn(move || {
for event in rx {
match event {
Ok(ev) => {
let event_type = match ev.kind {
EventKind::Create(_) => "add",
EventKind::Modify(_) => "change",
EventKind::Remove(_) => "delete",
_ => continue,
};
for ev_path in &ev.paths {
let stats = if event_type != "delete" {
FsOps::stat_path(ev_path).ok()
} else {
None
};
let watch_event = WatchEvent {
event_type: event_type.to_string(),
path: ev_path.to_string_lossy().into_owned(),
timestamp: {
let d = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
format!("{}.{:03}Z", d.as_secs(), d.subsec_millis())
},
stats,
};
let ipc_event = IpcEvent {
event: format!("watch:{}", watch_id_clone),
data: serde_json::to_value(&watch_event).unwrap_or_default(),
};
if let Ok(json) = serde_json::to_string(&ipc_event) {
// Write to stdout (IPC channel)
println!("{}", json);
}
}
}
Err(e) => {
eprintln!("watch error: {}", e);
}
}
}
});
// Store the watcher to keep it alive
self.watchers
.lock()
.map_err(|e| format!("lock: {}", e))?
.insert(id, watcher);
Ok(())
}
pub fn remove_all(&self) -> Result<(), String> {
self.watchers
.lock()
.map_err(|e| format!("lock: {}", e))?
.clear();
Ok(())
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "smartfs-protocol"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,243 @@
use serde::{Deserialize, Serialize};
// ── IPC envelope types ──────────────────────────────────────────────────────
/// Request from TypeScript (via stdin)
#[derive(Debug, Deserialize)]
pub struct IpcRequest {
pub id: String,
pub method: String,
pub params: serde_json::Value,
}
/// Response to TypeScript (via stdout)
#[derive(Debug, Serialize)]
pub struct IpcResponse {
pub id: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl IpcResponse {
pub fn ok(id: String, result: serde_json::Value) -> Self {
Self { id, success: true, result: Some(result), error: None }
}
pub fn ok_void(id: String) -> Self {
Self { id, success: true, result: None, error: None }
}
pub fn err(id: String, error: String) -> Self {
Self { id, success: false, result: None, error: Some(error) }
}
}
/// Stream chunk (Rust → TS, before final response)
#[derive(Debug, Serialize)]
pub struct IpcStreamChunk {
pub id: String,
pub stream: bool,
pub data: serde_json::Value,
}
/// Unsolicited event (Rust → TS)
#[derive(Debug, Serialize)]
pub struct IpcEvent {
pub event: String,
pub data: serde_json::Value,
}
// ── Filesystem domain types ─────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileStats {
pub size: u64,
pub birthtime: String,
pub mtime: String,
pub atime: String,
#[serde(rename = "isFile")]
pub is_file: bool,
#[serde(rename = "isDirectory")]
pub is_directory: bool,
#[serde(rename = "isSymbolicLink")]
pub is_symbolic_link: bool,
pub mode: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DirectoryEntry {
pub name: String,
pub path: String,
#[serde(rename = "isFile")]
pub is_file: bool,
#[serde(rename = "isDirectory")]
pub is_directory: bool,
#[serde(rename = "isSymbolicLink")]
pub is_symbolic_link: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stats: Option<FileStats>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WatchEvent {
#[serde(rename = "type")]
pub event_type: String,
pub path: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stats: Option<FileStats>,
}
// ── Command parameter types ─────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct ReadFileParams {
pub path: String,
pub encoding: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct WriteFileParams {
pub path: String,
pub content: String,
pub atomic: Option<bool>,
pub mode: Option<u32>,
pub encoding: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AppendFileParams {
pub path: String,
pub content: String,
pub encoding: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PathParams {
pub path: String,
}
#[derive(Debug, Deserialize)]
pub struct CopyMoveParams {
pub from: String,
pub to: String,
pub overwrite: Option<bool>,
#[serde(rename = "preserveTimestamps")]
pub preserve_timestamps: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct ListDirectoryParams {
pub path: String,
pub recursive: Option<bool>,
#[serde(rename = "includeStats")]
pub include_stats: Option<bool>,
pub filter: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CreateDirectoryParams {
pub path: String,
pub recursive: Option<bool>,
pub mode: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct DeleteDirectoryParams {
pub path: String,
pub recursive: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct WatchParams {
pub path: String,
pub id: String,
pub recursive: Option<bool>,
}
// ── Batch operations ────────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct BatchOp {
#[serde(rename = "type")]
pub op_type: String,
pub path: String,
#[serde(rename = "targetPath")]
pub target_path: Option<String>,
pub content: Option<String>,
pub encoding: Option<String>,
pub atomic: Option<bool>,
pub mode: Option<u32>,
pub overwrite: Option<bool>,
pub recursive: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct BatchResult {
pub index: usize,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct BatchParams {
pub operations: Vec<BatchOp>,
}
// ── Transaction operations ──────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct TransactionOp {
#[serde(rename = "type")]
pub op_type: String,
pub path: String,
#[serde(rename = "targetPath")]
pub target_path: Option<String>,
pub content: Option<String>,
pub encoding: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TransactionParams {
pub operations: Vec<TransactionOp>,
}
// ── Path operations ─────────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct NormalizePathParams {
pub path: String,
}
#[derive(Debug, Deserialize)]
pub struct JoinPathParams {
pub segments: Vec<String>,
}
// ── Streaming operations ────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct ReadFileStreamParams {
pub path: String,
#[serde(rename = "chunkSize")]
pub chunk_size: Option<usize>,
}
#[derive(Debug, Deserialize)]
pub struct WriteStreamBeginParams {
pub path: String,
pub atomic: Option<bool>,
pub mode: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct WriteStreamChunkParams {
#[serde(rename = "streamId")]
pub stream_id: String,
pub data: String,
pub last: bool,
}