diff --git a/.github/actions/test/action.yaml b/.github/actions/test/action.yaml index a1680185..cdc2d5dd 100644 --- a/.github/actions/test/action.yaml +++ b/.github/actions/test/action.yaml @@ -14,7 +14,7 @@ runs: run: | $commandPath = Join-Path . ... $tagValues = & { - $tags = 'nofuse', 'noipfs' + $tags = 'nofuse', 'noipfs', 'nonfs' $combinations = @() $allCombinations = [Math]::Pow(2, $tags.Length) for ($i = 0; $i -lt $allCombinations; $i++) { @@ -31,7 +31,7 @@ runs: return $combinations } $tagValues | ForEach-Object { - "go vet -tags=$_ ${commandPath}" + go vet -tags="$_" "${commandPath}" } go test -cover ${commandPath} go test -race ${commandPath} diff --git a/cmd/build/main.go b/cmd/build/main.go index 0f8d3040..7ab5befc 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -110,7 +110,8 @@ func parseFlags() (mode buildMode, tags string, output string) { tagsUsage = "a comma-separated list of build tags" + "\nsupported in addition to Go's standard tags:" + "\nnofuse - build without FUSE host support" + - "\nnoipfs - build without IPFS guest support" + "\nnoipfs - build without IPFS guest support" + + "\nnonfs - build without NFS host & guest support" ) flagSet.StringVar(&tags, tagName, "", tagsUsage) const ( diff --git a/go.mod b/go.mod index 8a1c4f83..8ab8afea 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,36 @@ module github.com/djdv/go-filesystem-utils -go 1.20 +go 1.21 require ( github.com/adrg/xdg v0.4.0 github.com/charmbracelet/glamour v0.6.0 github.com/djdv/p9 v0.2.1-0.20230601152255-7d74b92b80b0 - github.com/hashicorp/golang-lru/v2 v2.0.2 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 github.com/ipfs/boxo v0.10.2-0.20230629143123-2d3edc552442 + github.com/ipfs/go-block-format v0.1.2 github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-ipfs-cmds v0.9.0 github.com/ipfs/go-ipld-cbor v0.0.6 github.com/ipfs/go-ipld-format v0.5.0 + github.com/ipfs/go-unixfsnode v1.7.1 github.com/ipfs/kubo v0.21.0 + github.com/ipld/go-codec-dagpb v1.6.0 github.com/jaevor/go-nanoid v1.3.0 + github.com/lxn/win v0.0.0-20210218163916-a377121e959e github.com/mattn/go-colorable v0.1.4 github.com/muesli/termenv v0.15.1 github.com/multiformats/go-multiaddr v0.9.0 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/multiformats/go-multibase v0.2.0 github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 + github.com/willscott/go-nfs v0.0.2 + github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 - golang.org/x/sys v0.9.0 - golang.org/x/term v0.9.0 + golang.org/x/sys v0.16.0 + golang.org/x/term v0.12.0 ) require ( @@ -37,28 +44,24 @@ require ( github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-bitfield v1.1.0 // indirect - github.com/ipfs/go-block-format v0.1.2 - github.com/ipfs/go-blockservice v0.5.2 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-ipfs-util v0.0.3 // indirect github.com/ipfs/go-ipld-legacy v0.2.1 // indirect github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/ipfs/go-metrics-interface v0.0.1 // indirect - github.com/ipfs/go-unixfsnode v1.7.3 - github.com/ipld/go-codec-dagpb v1.6.0 github.com/ipld/go-ipld-prime v0.20.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-libp2p v0.27.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/lxn/win v0.0.0-20210218163916-a377121e959e github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/microcosm-cc/bluemonday v1.0.21 // indirect @@ -77,6 +80,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/polydawn/refmt v0.89.0 // indirect + github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect @@ -89,13 +93,12 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.10.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.15.0 // indirect golang.org/x/sync v0.2.0 // indirect golang.org/x/tools v0.9.1 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index f175d778..8e3bee92 100644 --- a/go.sum +++ b/go.sum @@ -12,20 +12,26 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= +github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/djdv/p9 v0.2.1-0.20230601152255-7d74b92b80b0 h1:TmRbQZzEz+AbtudHs+4OtcggEd6mgbcf1UA3DdUMg/M= @@ -33,41 +39,58 @@ github.com/djdv/p9 v0.2.1-0.20230601152255-7d74b92b80b0/go.mod h1:TGzUXNk2SONYuJ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A= +github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= -github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= +github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= github.com/ipfs/boxo v0.10.2-0.20230629143123-2d3edc552442 h1:SGbw381zt6c1VFf3QCBaJ+eVJ4AwD9fPaFKFp9U9Apk= @@ -77,8 +100,8 @@ github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= github.com/ipfs/go-block-format v0.1.2 h1:GAjkfhVx1f4YTODS6Esrj1wt2HhrtwTnhEr+DyPUaJo= github.com/ipfs/go-block-format v0.1.2/go.mod h1:mACVcrxarQKstUU3Yf/RdwbC4DzPV6++rO2a3d+a/KE= -github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= -github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= +github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY= +github.com/ipfs/go-blockservice v0.5.0/go.mod h1:W6brZ5k20AehbmERplmERn8o2Ni3ZZubvAxaIUeaT6w= github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= @@ -87,16 +110,25 @@ github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LK github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ipfs-blockstore v1.3.0 h1:m2EXaWgwTzAfsmt5UdJ7Is6l4gJcaM/A12XwJyvYvMM= +github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= +github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= +github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= github.com/ipfs/go-ipfs-cmds v0.9.0 h1:K0VcXg1l1k6aY6sHnoxYcyimyJQbcV1ueXuWgThmK9Q= github.com/ipfs/go-ipfs-cmds v0.9.0/go.mod h1:SBFHK8WNwC416QWH9Vz1Ql42SSMAOqKpaHUMBu3jpLo= github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= +github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q= +github.com/ipfs/go-ipfs-ds-help v1.1.0/go.mod h1:YR5+6EaebOhfcqVCyqemItCLthrpVNot+rsOU/5IatU= github.com/ipfs/go-ipfs-exchange-interface v0.2.0 h1:8lMSJmKogZYNo2jjhUs0izT+dck05pqUw4mWNW9Pw6Y= +github.com/ipfs/go-ipfs-exchange-interface v0.2.0/go.mod h1:z6+RhJuDQbqKguVyslSOuVDhqF9JtTrO3eptSAiW2/Y= github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= +github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= +github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4= github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= @@ -113,25 +145,32 @@ github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Ax github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipfs/go-merkledag v0.10.0 h1:IUQhj/kzTZfam4e+LnaEpoiZ9vZF6ldimVlby+6OXL4= +github.com/ipfs/go-merkledag v0.10.0/go.mod h1:zkVav8KiYlmbzUzNM6kENzkdP5+qR7+2mCwxkQ6GIj8= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= +github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU= github.com/ipfs/go-unixfs v0.4.5 h1:wj8JhxvV1G6CD7swACwSKYa+NgtdWC1RUit+gFnymDU= -github.com/ipfs/go-unixfsnode v1.7.3 h1:giAxFq7CxAm2Z8h8yFAD7TOQUpf5XG7a2xrR143ci4Y= -github.com/ipfs/go-unixfsnode v1.7.3/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk= +github.com/ipfs/go-unixfs v0.4.5/go.mod h1:BIznJNvt/gEx/ooRMI4Us9K8+qeGO7vx1ohnbk8gjFg= +github.com/ipfs/go-unixfsnode v1.7.1 h1:RRxO2b6CSr5UQ/kxnGzaChTjp5LWTdf3Y4n8ANZgB/s= +github.com/ipfs/go-unixfsnode v1.7.1/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk= github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs= +github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU= github.com/ipfs/kubo v0.21.0 h1:1+XKokeyatfI2Mri5iBn8Eplxf2F5ud0K5zLDg4tSSc= github.com/ipfs/kubo v0.21.0/go.mod h1:LuK5ANXz/UhHp8jdt4mRwE16AHhbRqY68g/uyc5/VqU= github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d h1:22g+x1tgWSXK34i25qjs+afr7basaneEkHaglBshd2g= +github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d/go.mod h1:SH2pi/NgfGBsV/CGBAQPxMfghIgwzbh5lQ2N+6dNRI8= github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg= github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -141,26 +180,36 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-libp2p v0.27.7 h1:nhMs03CRxslKkkK2uLuN8f72uwNkE6RJS1JFb3H9UIQ= github.com/libp2p/go-libp2p v0.27.7/go.mod h1:oMfQGTb9CHnrOuSM6yMmyK2lXz3qIhnkn2+oK3B1Y2g= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= @@ -177,6 +226,7 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -211,6 +261,7 @@ github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= @@ -233,10 +284,13 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= +github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -247,19 +301,31 @@ github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXx github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= github.com/quic-go/webtransport-go v0.5.3 h1:5XMlzemqB4qmOlgIus5zB45AcZ2kCgCy2EptUrfOPWU= +github.com/quic-go/webtransport-go v0.5.3/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -277,18 +343,26 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U= +github.com/warpfork/go-testmark v0.11.0/go.mod h1:jhEf8FVxd+F17juRubpmut64NEG6I2rgkUhlcqqXwE0= github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= +github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI= github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa h1:EyA027ZAkuaCLoxVX4r1TZMPy1d31fM6hbfQ4OU4I5o= github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= +github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee0= +github.com/willscott/go-nfs v0.0.2/go.mod h1:SvullWeHxr/924WQNbUaZqtluBt2vuZ61g6yAV+xj7w= +github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o= +github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5 h1:jxZvjx8Ve5sOXorZG0KzTxbp0Cr1n3FEegfmyd9br1k= github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -310,6 +384,7 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -325,8 +400,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -344,8 +419,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -370,16 +445,17 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -399,6 +475,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -410,9 +487,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/internal/commands/flag.go b/internal/commands/flag.go index 796685c8..6f5a82db 100644 --- a/internal/commands/flag.go +++ b/internal/commands/flag.go @@ -14,6 +14,7 @@ import ( "unicode/utf8" "unsafe" + "github.com/djdv/go-filesystem-utils/internal/filesystem" "github.com/djdv/go-filesystem-utils/internal/generic" "github.com/djdv/p9/p9" "github.com/multiformats/go-multiaddr" @@ -496,7 +497,7 @@ func flagSetFunc[ // `bool` flags don't require a value and this // must be conveyed to the [flag] package. if _, ok := any(setter).(func(bool, *ST) error); ok { - boolFunc(flagSet, name, usage, func(parameter string) error { + flagSet.BoolFunc(name, usage, func(parameter string) error { return parseAndSet(parameter, options, setter) }) return @@ -550,6 +551,12 @@ func parseFlag[V any](parameter string) (value V, err error) { *typed, err = parseID[p9.UID](parameter) case *p9.GID: *typed, err = parseID[p9.GID](parameter) + case *uint: + var temp uint64 + temp, err = strconv.ParseUint(parameter, 0, 64) + *typed = uint(temp) + case *uint64: + *typed, err = strconv.ParseUint(parameter, 0, 64) case *uint32: var temp uint64 temp, err = strconv.ParseUint(parameter, 0, 32) @@ -579,3 +586,7 @@ func parseMultiaddrList(parameter string) ([]multiaddr.Multiaddr, error) { } return maddrs, nil } + +func prefixIDFlag(system filesystem.ID) string { + return strings.ToLower(string(system)) + "-" +} diff --git a/internal/commands/flag_20.go b/internal/commands/flag_20.go deleted file mode 100644 index ee1b2b8a..00000000 --- a/internal/commands/flag_20.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !go1.21 - -package commands - -import "flag" - -type boolFuncValue func(string) error - -func (f boolFuncValue) Set(s string) error { return f(s) } - -func (f boolFuncValue) String() string { return "" } - -func (f boolFuncValue) IsBoolFlag() bool { return true } - -func boolFunc(flagSet *flag.FlagSet, name, usage string, fn func(string) error) { - flagSet.Var(boolFuncValue(fn), name, usage) -} diff --git a/internal/commands/flag_21.go b/internal/commands/flag_21.go deleted file mode 100644 index cdc975fc..00000000 --- a/internal/commands/flag_21.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build go1.21 - -package commands - -import "flag" - -func boolFunc(flagSet *flag.FlagSet, name, usage string, fn func(string) error) { - flagSet.BoolFunc(name, usage, fn) -} diff --git a/internal/commands/mount.go b/internal/commands/mount.go index 4629d06f..141835ba 100644 --- a/internal/commands/mount.go +++ b/internal/commands/mount.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io/fs" + "slices" "strings" "github.com/djdv/go-filesystem-utils/internal/command" @@ -272,6 +273,7 @@ func makeHostCommands() []command.Command { var ( commandMakers = []makeCommand{ makeFUSECommand, + makeNFSCommand, } commands = make([]command.Command, 0, len(commandMakers)) ) @@ -292,6 +294,9 @@ func makeGuestCommands[ ](host filesystem.Host, ) []command.Command { guests := makeIPFSCommands[HC, HM](host) + if nfsGuest := makeNFSGuestCommand[HC, HM](host); nfsGuest != nil { + guests = append(guests, nfsGuest) + } sortCommands(guests) return guests } @@ -423,3 +428,15 @@ func newMountFile(idRoot p9.File, } return targetFile.Close() } + +func sortCommands(commands []command.Command) { + slices.SortFunc( + commands, + func(a, b command.Command) int { + return strings.Compare( + a.Name(), + b.Name(), + ) + }, + ) +} diff --git a/internal/commands/mountpoint.go b/internal/commands/mountpoint.go index ba3ec88a..50235001 100644 --- a/internal/commands/mountpoint.go +++ b/internal/commands/mountpoint.go @@ -90,6 +90,7 @@ func makeMountPointHosts(path ninePath, autoUnlink bool) mountPointHosts { var ( hostMakers = []makeHostsFunc{ makeFUSEHost, + makeNFSHost, } hosts = make(mountPointHosts, len(hostMakers)) ) @@ -142,6 +143,7 @@ func makeMountPointGuests[ ) mountPointGuests { guests := make(mountPointGuests) makeIPFSGuests[HC](guests, path) + makeNFSGuest[HC](guests, path) return guests } diff --git a/internal/commands/mountpoint_ipfs.go b/internal/commands/mountpoint_ipfs.go index 9561207a..fdd2380f 100644 --- a/internal/commands/mountpoint_ipfs.go +++ b/internal/commands/mountpoint_ipfs.go @@ -71,10 +71,6 @@ func guestOverlayText(overlay, overlaid filesystem.ID) string { return string(overlay) + " is an " + string(overlaid) + " overlay" } -func prefixIDFlag(system filesystem.ID) string { - return strings.ToLower(string(system)) + "-" -} - func (*ipfsOptions) usage(filesystem.Host) string { return string(ipfs.IPFSID) + " provides an empty root directory." + "\nChild paths are forwarded to the IPFS API." @@ -112,11 +108,8 @@ func (io *ipfsOptions) bindFlagsVarient(system filesystem.ID, flagSet *flag.Flag return nil }) nodeCacheName := flagPrefix + "node-cache" - const ( - defaultCacheCount = 64 - nodeCacheUsage = "number of nodes to keep in the cache" + - "\nnegative values disable node caching" - ) + const nodeCacheUsage = "number of nodes to keep in the cache" + + "\nnegative values disable node caching" flagSetFunc(flagSet, nodeCacheName, nodeCacheUsage, io, func(value int, settings *ipfsSettings) error { settings.NodeCacheCount = value @@ -187,7 +180,10 @@ func (po *pinFSOptions) BindFlags(flagSet *flag.FlagSet) { } func (po pinFSOptions) make() (pinFSSettings, error) { - return makeWithOptions(po...) + settings := pinFSSettings{ + CacheExpiry: pinfsExpiryDefault, + } + return settings, generic.ApplyOptions(&settings, po...) } func (set pinFSSettings) marshal(string) ([]byte, error) { diff --git a/internal/commands/mountpoint_nfs.go b/internal/commands/mountpoint_nfs.go new file mode 100644 index 00000000..6e83b355 --- /dev/null +++ b/internal/commands/mountpoint_nfs.go @@ -0,0 +1,208 @@ +//go:build !nonfs + +package commands + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "os" + + "github.com/djdv/go-filesystem-utils/internal/command" + "github.com/djdv/go-filesystem-utils/internal/filesystem" + p9fs "github.com/djdv/go-filesystem-utils/internal/filesystem/9p" + "github.com/djdv/go-filesystem-utils/internal/filesystem/nfs" + "github.com/djdv/go-filesystem-utils/internal/generic" + "github.com/multiformats/go-multiaddr" +) + +type ( + nfsHostSettings nfs.Host + nfsHostOption func(*nfsHostSettings) error + nfsHostOptions []nfsHostOption + nfsGuestSettings struct { + nfs.Guest + defaultUID, defaultGID, + defaultHostname, defaultDirpath bool + } + nfsGuestOption func(*nfsGuestSettings) error + nfsGuestOptions []nfsGuestOption +) + +const nfsServerFlagName = "server" + +func (ns nfsGuestSettings) marshal(string) ([]byte, error) { + return json.Marshal(ns.Guest) +} + +func makeNFSCommand() command.Command { + return makeMountSubcommand( + nfs.HostID, + makeGuestCommands[nfsHostOptions, nfsHostSettings](nfs.HostID), + ) +} + +func makeNFSHost(path ninePath, autoUnlink bool) (filesystem.Host, p9fs.MakeGuestFunc) { + guests := makeMountPointGuests[nfs.Host](path) + return nfs.HostID, newMakeGuestFunc(guests, path, autoUnlink) +} + +func (on *nfsHostOptions) BindFlags(flagSet *flag.FlagSet) { /* NOOP */ } + +func (on nfsHostOptions) make() (nfsHostSettings, error) { + return makeWithOptions(on...) +} + +func (*nfsHostOptions) usage(guest filesystem.ID) string { + return string(nfs.HostID) + " hosts " + + string(guest) + " as an NFS server" +} + +func (set nfsHostSettings) marshal(arg string) ([]byte, error) { + if arg == "" { + err := command.UsageError{ + Err: generic.ConstError( + "expected server multiaddr", + ), + } + return nil, err + } + maddr, err := multiaddr.NewMultiaddr(arg) + if err != nil { + return nil, err + } + set.Maddr = maddr + return json.Marshal(set) +} + +func unmarshalNFS() (filesystem.Host, decodeFunc) { + return nfs.HostID, func(b []byte) (string, error) { + var host nfs.Host + if err := json.Unmarshal(b, &host); err != nil { + return "", err + } + if maddr := host.Maddr; maddr != nil { + return host.Maddr.String(), nil + } + return "", errors.New("NFS host address was not present in the mountpoint data") + } +} + +func makeNFSGuestCommand[ + HC mountCmdHost[HT, HM], + HM marshaller, + HT any, +](host filesystem.Host, +) command.Command { + return makeMountCommand[HC, HM, nfsGuestOptions, nfsGuestSettings](host, nfs.GuestID) +} + +func makeNFSGuest[ + HC mountPointHost[T], + T any, +](guests mountPointGuests, path ninePath, +) { + guests[nfs.GuestID] = newMountPointFunc[HC, nfs.Guest](path) +} + +func (*nfsGuestOptions) usage(filesystem.Host) string { + return string(nfs.GuestID) + " attaches to an NFS file server" +} + +func (no *nfsGuestOptions) BindFlags(flagSet *flag.FlagSet) { + var ( + flagPrefix = prefixIDFlag(nfs.GuestID) + srvName = flagPrefix + nfsServerFlagName + ) + const srvUsage = "NFS server `maddr`" + flagSetFunc(flagSet, srvName, srvUsage, no, + func(value multiaddr.Multiaddr, settings *nfsGuestSettings) error { + settings.Maddr = value + return nil + }) + hostnameName := flagPrefix + "hostname" + const hostnameUsage = "client's `hostname`" + flagSetFunc(flagSet, hostnameName, hostnameUsage, no, + func(value string, settings *nfsGuestSettings) error { + settings.Hostname = value + return nil + }) + flagSet.Lookup(hostnameName). + DefValue = "caller's hostname" + dirpathName := flagPrefix + "dirpath" + const dirpathUsage = "`dirpath` used when mounting the server" + flagSetFunc(flagSet, dirpathName, dirpathUsage, no, + func(value string, settings *nfsGuestSettings) error { + settings.Dirpath = value + return nil + }) + flagSet.Lookup(dirpathName). + DefValue = "/" + linkSepName := flagPrefix + "link-separator" + const linkSepUsage = "`separator` character to replace with `/` when parsing relative symlinks" + flagSetFunc(flagSet, linkSepName, linkSepUsage, no, + func(value string, settings *nfsGuestSettings) error { + settings.LinkSeparator = value + return nil + }) + linkLimitName := flagPrefix + "link-limit" + const linkLimitUsage = "sets the maximum amount of times a symbolic link will be resolved in a link chain" + flagSetFunc(flagSet, linkLimitName, linkLimitUsage, no, + func(value uint, settings *nfsGuestSettings) error { + settings.LinkLimit = value + return nil + }) + uidName := flagPrefix + "uid" + const uidUsage = "client's `uid`" + flagSetFunc(flagSet, uidName, uidUsage, no, + func(value uint32, settings *nfsGuestSettings) error { + settings.UID = value + return nil + }) + flagSet.Lookup(uidName). + DefValue = "caller's uid" + gidName := flagPrefix + "gid" + const gidUsage = "client's `gid`" + flagSetFunc(flagSet, gidName, gidUsage, no, + func(value uint32, settings *nfsGuestSettings) error { + settings.GID = value + return nil + }) + flagSet.Lookup(gidName). + DefValue = "caller's gid" +} + +func (no nfsGuestOptions) make() (nfsGuestSettings, error) { + settings, err := makeWithOptions(no...) + if err != nil { + return nfsGuestSettings{}, err + } + if settings.Maddr == nil { + var ( + flagPrefix = prefixIDFlag(nfs.GuestID) + srvName = flagPrefix + nfsServerFlagName + ) + return nfsGuestSettings{}, fmt.Errorf( + "flag `-%s` must be provided for NFS guests", + srvName, + ) + } + if settings.Hostname == "" { + hostname, err := os.Hostname() + if err != nil { + return nfsGuestSettings{}, err + } + settings.Hostname = hostname + } + if settings.defaultUID { + settings.UID = uint32(os.Getuid()) + } + if settings.defaultGID { + settings.GID = uint32(os.Getgid()) + } + if settings.Dirpath == "" { + settings.Dirpath = "/" + } + return settings, nil +} diff --git a/internal/commands/mountpoint_nonfs.go b/internal/commands/mountpoint_nonfs.go new file mode 100644 index 00000000..ee2aed73 --- /dev/null +++ b/internal/commands/mountpoint_nonfs.go @@ -0,0 +1,38 @@ +//go:build nonfs + +package commands + +import ( + "github.com/djdv/go-filesystem-utils/internal/command" + "github.com/djdv/go-filesystem-utils/internal/filesystem" + p9fs "github.com/djdv/go-filesystem-utils/internal/filesystem/9p" +) + +const nfsHost = filesystem.Host("") + +func makeNFSCommand() command.Command { + return nil +} + +func makeNFSHost(ninePath, bool) (filesystem.Host, p9fs.MakeGuestFunc) { + return nfsHost, nil +} + +func unmarshalNFS() (filesystem.Host, decodeFunc) { + return nfsHost, nil +} + +func makeNFSGuestCommand[ + HC mountCmdHost[HT, HM], + HM marshaller, + HT any, +](filesystem.Host, +) command.Command { + return nil +} + +func makeNFSGuest[ + HC mountPointHost[T], + T any, +](mountPointGuests, ninePath, +) { /*NOOP*/ } diff --git a/internal/commands/slices_20.go b/internal/commands/slices_20.go deleted file mode 100644 index d054d1e7..00000000 --- a/internal/commands/slices_20.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !go1.21 - -package commands - -import ( - "github.com/djdv/go-filesystem-utils/internal/command" - "golang.org/x/exp/slices" -) - -func sortCommands(commands []command.Command) { - slices.SortFunc(commands, func(a, b command.Command) bool { - return a.Name() < b.Name() - }) -} diff --git a/internal/commands/slices_21.go b/internal/commands/slices_21.go deleted file mode 100644 index 0865dfbb..00000000 --- a/internal/commands/slices_21.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build go1.21 - -package commands - -import ( - "slices" - "strings" - - "github.com/djdv/go-filesystem-utils/internal/command" -) - -func sortCommands(commands []command.Command) { - slices.SortFunc( - commands, - func(a, b command.Command) int { - return strings.Compare( - a.Name(), - b.Name(), - ) - }, - ) -} diff --git a/internal/commands/unmount.go b/internal/commands/unmount.go index 834eb651..edef57e4 100644 --- a/internal/commands/unmount.go +++ b/internal/commands/unmount.go @@ -146,6 +146,7 @@ func newDecodeTargetFunc() p9fs.DecodeTargetFunc { var ( decoderMakers = []makeDecoderFunc{ unmarshalFUSE, + unmarshalNFS, } decoders = make(decoders, len(decoderMakers)) ) diff --git a/internal/filesystem/cgofuse/file.go b/internal/filesystem/cgofuse/file.go index 5f69c25f..4ce8d795 100644 --- a/internal/filesystem/cgofuse/file.go +++ b/internal/filesystem/cgofuse/file.go @@ -250,19 +250,11 @@ func readFile(file fs.File, buff []byte, ofst int64) (int, error) { } func seekFile(file fs.File, ofst int64) (errNo, error) { - const noSeekFmt = "file %T does not support seeking" - seeker, ok := file.(seekerFile) - if !ok { - return -fuse.ESPIPE, fmt.Errorf(noSeekFmt, file) - } - if _, err := seeker.Seek(ofst, io.SeekStart); err != nil { - // HACK: This is probably a bad idea actually. - // TODO: Re-evaluate putting the onus - // back on the implementation, rather than us. - if errors.Is(err, fserrors.ErrUnsupported) { - return -fuse.ESPIPE, fmt.Errorf(noSeekFmt, file) + if _, err := filesystem.Seek(file, ofst, io.SeekStart); err != nil { + if errors.Is(err, errors.ErrUnsupported) { + return -fuse.ESPIPE, err } - return -fuse.EIO, fmt.Errorf("offset seek error: %w", err) + return -fuse.EIO, err } return operationSuccess, nil } diff --git a/internal/filesystem/cgofuse/fuse.go b/internal/filesystem/cgofuse/fuse.go index 0284d917..c3cc7b55 100644 --- a/internal/filesystem/cgofuse/fuse.go +++ b/internal/filesystem/cgofuse/fuse.go @@ -32,11 +32,6 @@ type ( // (So that cross API locks can be possible. E.g. FUSE+9P accessing the same `fs.File`) ioMu sync.Mutex } - seekerFile interface { - fs.File - io.Seeker - } - fillFunc = func(name string, stat *fuse.Stat_t, ofst int64) bool ) @@ -161,7 +156,7 @@ func (gw *goWrapper) Unlink(path string) errNo { func (gw *goWrapper) Symlink(target, newpath string) errNo { defer gw.systemLock.CreateOrDelete(newpath)() - if linker, ok := gw.FS.(filesystem.SymlinkFS); ok { + if linker, ok := gw.FS.(filesystem.LinkMaker); ok { goTarget, goNewPath, err := fuseToGoPair(target, newpath) if err != nil { gw.logError(newpath+"->"+target, err) @@ -184,7 +179,7 @@ func (gw *goWrapper) Readlink(path string) (errNo, string) { case "": return -fuse.ENOENT, "" default: - if extractor, ok := gw.FS.(filesystem.SymlinkFS); ok { + if extractor, ok := gw.FS.(filesystem.LinkReader); ok { goPath, err := fuseToGo(path) if err != nil { gw.logError(path, err) diff --git a/internal/filesystem/cgofuse/fuse_windows.go b/internal/filesystem/cgofuse/fuse_windows.go index ca52398d..3f30119c 100644 --- a/internal/filesystem/cgofuse/fuse_windows.go +++ b/internal/filesystem/cgofuse/fuse_windows.go @@ -11,7 +11,7 @@ import ( const ( cgoDepPanic = "cgofuse: cannot find winfsp" cgoDepMessage = "WinFSP(http://www.secfs.net/winfsp/) is required" + - "to mount on this platform, but it was not found" + " to mount on this platform, but it was not found" systemNameOpt = "FileSystemName=" volNameOpt = "volname=" diff --git a/internal/filesystem/cgofuse/stat.go b/internal/filesystem/cgofuse/stat.go index bef2e098..49c256e1 100644 --- a/internal/filesystem/cgofuse/stat.go +++ b/internal/filesystem/cgofuse/stat.go @@ -1,8 +1,10 @@ package cgofuse import ( + "errors" "io/fs" + "github.com/djdv/go-filesystem-utils/internal/filesystem" "github.com/winfsp/cgofuse/fuse" ) @@ -56,6 +58,11 @@ func (gw *goWrapper) infoFromPath(path string) (fs.FileInfo, error) { if err != nil { return nil, err } + if stat, err := filesystem.Lstat(gw.FS, goPath); err == nil { + return stat, nil + } else if !errors.Is(err, errors.ErrUnsupported) { + return nil, err + } return fs.Stat(gw.FS, goPath) } diff --git a/internal/filesystem/cgofuse/translate.go b/internal/filesystem/cgofuse/translate.go index c5d8b16f..e117bf74 100644 --- a/internal/filesystem/cgofuse/translate.go +++ b/internal/filesystem/cgofuse/translate.go @@ -11,10 +11,7 @@ import ( "github.com/winfsp/cgofuse/fuse" ) -const ( - goRoot = "." - errEmptyPath = generic.ConstError("path argument is empty") -) +const errEmptyPath = generic.ConstError("path argument is empty") // fuseToGo converts a FUSE absolute path // to a relative [fs.FS] name. @@ -30,7 +27,7 @@ func fuseToGo(path string) (string, error) { Kind: fserrors.InvalidItem, } case posixRoot: - return goRoot, nil + return filesystem.Root, nil } // TODO: does fuse guarantee slash prefixed paths? @@ -128,6 +125,8 @@ var ( fserrors.IsDir: -fuse.EISDIR, fserrors.NotDir: -fuse.ENOTDIR, fserrors.NotEmpty: -fuse.ENOTEMPTY, + fserrors.Recursion: -fuse.ELOOP, + fserrors.Closed: -fuse.EBADF, } ) diff --git a/internal/filesystem/errors/errors.go b/internal/filesystem/errors/errors.go index 61b2a23e..1e9a4efa 100644 --- a/internal/filesystem/errors/errors.go +++ b/internal/filesystem/errors/errors.go @@ -27,6 +27,8 @@ const ( NotDir // Item is not a directory. NotEmpty // Directory not empty. ReadOnly // File system has no modification capabilities. + Recursion // Item has recurred too many times. E.g. an infinite symlink loop. + Closed // Item was never opened or has already been closed. ) func (e *Error) Unwrap() error { return &e.PathError } diff --git a/internal/filesystem/errors/unsupported.go b/internal/filesystem/errors/unsupported.go deleted file mode 100644 index a8e72b3f..00000000 --- a/internal/filesystem/errors/unsupported.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !go1.21 - -package errors - -import "github.com/djdv/go-filesystem-utils/internal/generic" - -const ErrUnsupported = generic.ConstError("unsupported operation") diff --git a/internal/filesystem/errors/unsupported_21.go b/internal/filesystem/errors/unsupported_21.go deleted file mode 100644 index 9679f37a..00000000 --- a/internal/filesystem/errors/unsupported_21.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build go1.21 - -package errors - -import "errors" - -var ErrUnsupported = errors.ErrUnsupported diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go index 09be51b1..221656fd 100644 --- a/internal/filesystem/filesystem.go +++ b/internal/filesystem/filesystem.go @@ -9,6 +9,7 @@ import ( "os" "time" + fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" "github.com/djdv/go-filesystem-utils/internal/generic" ) @@ -36,9 +37,16 @@ type ( fs.FS Remove(name string) error } - SymlinkFS interface { + LinkStater interface { + fs.FS + Lstat(name string) (fs.FileInfo, error) + } + LinkMaker interface { fs.FS Symlink(oldname, newname string) error + } + LinkReader interface { + fs.FS Readlink(name string) (string, error) } RenameFS interface { @@ -105,18 +113,25 @@ const ( ExecuteUser WriteUser ReadUser +) +const ( Root = "." - ErrPath = generic.ConstError("path not valid") - ErrNotFound = generic.ConstError("file not found") - ErrNotOpen = generic.ConstError("file is not open") ErrIsDir = generic.ConstError("file is a directory") ErrIsNotDir = generic.ConstError("file is not a directory") ) func (dw dirEntryWrapper) Error() error { return dw.error } +func FSID(fsys fs.FS) (ID, error) { + if fsys, ok := fsys.(IDFS); ok { + return fsys.ID(), nil + } + const op = "id" + return "", unsupportedOpErrAnonymous(op, fsys) +} + func OpenFile(fsys fs.FS, name string, flag int, perm fs.FileMode) (fs.File, error) { if fsys, ok := fsys.(OpenFileFS); ok { return fsys.OpenFile(name, flag, perm) @@ -124,7 +139,56 @@ func OpenFile(fsys fs.FS, name string, flag int, perm fs.FileMode) (fs.File, err if flag == os.O_RDONLY { return fsys.Open(name) } - return nil, fmt.Errorf(`open "%s": operation not supported`, name) + const op = "open" + return nil, unsupportedOpErr(op, name) +} + +func CreateFile(fsys fs.FS, name string) (fs.File, error) { + if fsys, ok := fsys.(CreateFileFS); ok { + return fsys.CreateFile(name) + } + const op = "createfile" + return nil, unsupportedOpErr(op, name) +} + +func Remove(fsys fs.FS, name string) error { + if fsys, ok := fsys.(RemoveFS); ok { + return fsys.Remove(name) + } + const op = "remove" + return unsupportedOpErr(op, name) +} + +func Lstat(fsys fs.FS, name string) (fs.FileInfo, error) { + if fsys, ok := fsys.(LinkStater); ok { + return fsys.Lstat(name) + } + const op = "lstat" + return nil, unsupportedOpErr(op, name) +} + +func Symlink(fsys fs.FS, oldname, newname string) error { + if fsys, ok := fsys.(LinkMaker); ok { + return fsys.Symlink(oldname, newname) + } + const op = "symlink" + return unsupportedOpErr2(op, oldname, newname) +} + +func Readlink(fsys fs.FS, name string) (string, error) { + if fsys, ok := fsys.(LinkReader); ok { + return fsys.Readlink(name) + } + const op = "readlink" + return "", unsupportedOpErr(op, name) +} + +func Rename(fsys fs.FS, oldName, newName string) error { + if fsys, ok := fsys.(RenameFS); ok { + return fsys.Rename(oldName, newName) + } + const op = "rename" + return unsupportedOpErr2(op, oldName, newName) } func Truncate(fsys fs.FS, name string, size int64) error { @@ -132,19 +196,27 @@ func Truncate(fsys fs.FS, name string, size int64) error { if err != nil { return err } - truncater, ok := file.(TruncateFile) - if !ok { + if fsys, ok := file.(TruncateFile); ok { return errors.Join( - fmt.Errorf(`truncate "%s": operation not supported`, name), + fsys.Truncate(size), file.Close(), ) } + const op = "truncate" return errors.Join( - truncater.Truncate(size), + unsupportedOpErr(op, name), file.Close(), ) } +func Mkdir(fsys fs.FS, name string, perm fs.FileMode) error { + if fsys, ok := fsys.(MkdirFS); ok { + return fsys.Mkdir(name, perm) + } + const op = "mkdir" + return unsupportedOpErr(op, name) +} + // StreamDir reads the directory // and returns a channel of directory entry results. // @@ -190,3 +262,28 @@ func StreamDir(ctx context.Context, count int, directory fs.ReadDirFile) <-chan }() return stream } + +func Seek(file fs.File, offset int64, whence int) (int64, error) { + if seeker, ok := file.(io.Seeker); ok { + return seeker.Seek(offset, whence) + } + const op = "seek" + return -1, unsupportedOpErrAnonymous(op, file) +} + +func unsupportedOpErr(op, name string) error { + return fserrors.New(op, name, errors.ErrUnsupported, fserrors.InvalidOperation) +} + +func unsupportedOpErr2(op, name1, name2 string) error { + name := fmt.Sprintf( + `"%s" -> "%s"`, + name1, name2, + ) + return fserrors.New(op, name, errors.ErrUnsupported, fserrors.InvalidOperation) +} + +func unsupportedOpErrAnonymous(op string, subject any) error { + name := fmt.Sprintf("%T", subject) + return unsupportedOpErr(op, name) +} diff --git a/internal/filesystem/filesystem_test.go b/internal/filesystem/filesystem_test.go index 0071110f..05eb871d 100644 --- a/internal/filesystem/filesystem_test.go +++ b/internal/filesystem/filesystem_test.go @@ -205,8 +205,7 @@ func streamDirCancels(t *testing.T, testFS fstest.MapFS) { func openRoot(t *testing.T, fsys fs.FS) fs.File { t.Helper() - const fsRoot = "." - root, err := fsys.Open(fsRoot) + root, err := fsys.Open(filesystem.Root) if err != nil { t.Fatal(err) } diff --git a/internal/filesystem/ipfs/ipfs.go b/internal/filesystem/ipfs/ipfs.go index 9c11717c..e647320f 100644 --- a/internal/filesystem/ipfs/ipfs.go +++ b/internal/filesystem/ipfs/ipfs.go @@ -10,10 +10,11 @@ import ( "github.com/djdv/go-filesystem-utils/internal/filesystem" fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" "github.com/djdv/go-filesystem-utils/internal/generic" - lru "github.com/hashicorp/golang-lru/v2" + "github.com/hashicorp/golang-lru/arc/v2" coreiface "github.com/ipfs/boxo/coreiface" coreoptions "github.com/ipfs/boxo/coreiface/options" corepath "github.com/ipfs/boxo/coreiface/path" + files "github.com/ipfs/boxo/files" ipath "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" "github.com/ipfs/go-cid" @@ -26,8 +27,8 @@ type ( ipld.Node *nodeInfo } - ipfsNodeCache = lru.ARCCache[cid.Cid, ipfsRecord] - ipfsDirCache = lru.ARCCache[cid.Cid, []filesystem.StreamDirEntry] + ipfsNodeCache = arc.ARCCache[cid.Cid, ipfsRecord] + ipfsDirCache = arc.ARCCache[cid.Cid, []filesystem.StreamDirEntry] IPFS struct { ctx context.Context cancel context.CancelFunc @@ -37,6 +38,7 @@ type ( dirCache *ipfsDirCache info nodeInfo nodeTimeout time.Duration + linkLimit uint } ipfsSettings struct { *IPFS @@ -65,6 +67,7 @@ func NewIPFS(core coreiface.CoreAPI, options ...IPFSOption) (*IPFS, error) { }, core: core, nodeTimeout: 1 * time.Minute, + linkLimit: 40, // Arbitrary. } settings = ipfsSettings{ IPFS: fsys, @@ -102,7 +105,7 @@ func (settings *ipfsSettings) fillInDefaults() error { } func (settings *ipfsSettings) initNodeCache(count int) error { - nodeCache, err := lru.NewARC[cid.Cid, ipfsRecord](count) + nodeCache, err := arc.NewARC[cid.Cid, ipfsRecord](count) if err != nil { return err } @@ -111,7 +114,7 @@ func (settings *ipfsSettings) initNodeCache(count int) error { } func (settings *ipfsSettings) initDirectoryCache(count int) error { - dirCache, err := lru.NewARC[cid.Cid, []filesystem.StreamDirEntry](count) + dirCache, err := arc.NewARC[cid.Cid, []filesystem.StreamDirEntry](count) if err != nil { return err } @@ -145,23 +148,20 @@ func WithDirectoryCacheCount(cacheCount int) IPFSOption { } } -// WithNodeTimeout sets a timeout duration to use -// when communicating with the IPFS API/node. -// If <= 0, operations will not time out, -// and will remain pending until the file system is closed. -func WithNodeTimeout(duration time.Duration) IPFSOption { - return func(ifs *ipfsSettings) error { - ifs.nodeTimeout = duration - return nil - } -} - func (*IPFS) ID() filesystem.ID { return IPFSID } func (fsys *IPFS) setContext(ctx context.Context) { fsys.ctx, fsys.cancel = context.WithCancel(ctx) } +func (fsys *IPFS) setNodeTimeout(timeout time.Duration) { + fsys.nodeTimeout = timeout +} + +func (fsys *IPFS) setLinkLimit(limit uint) { + fsys.linkLimit = limit +} + func (fsys *IPFS) setPermissions(permissions fs.FileMode) { fsys.info.mode = fsys.info.mode.Type() | permissions.Perm() } @@ -171,20 +171,91 @@ func (fsys *IPFS) Close() error { return nil } -func (fsys *IPFS) Stat(name string) (fs.FileInfo, error) { - const op = "stat" +func (fsys *IPFS) Lstat(name string) (fs.FileInfo, error) { + const op = "lstat" + info, _, err := fsys.lstat(op, name) + return info, err +} + +func (fsys *IPFS) lstat(op, name string) (fs.FileInfo, cid.Cid, error) { if name == filesystem.Root { - return &fsys.info, nil + return &fsys.info, cid.Cid{}, nil } cid, err := fsys.toCID(op, name) if err != nil { - return nil, err + return nil, cid, err } info, err := fsys.getInfo(name, cid) if err != nil { - return nil, fserrors.New(op, name, err, fserrors.IO) + const kind = fserrors.IO + return nil, cid, fserrors.New(op, name, err, kind) + } + return info, cid, nil +} + +func (fsys *IPFS) Stat(name string) (fs.FileInfo, error) { + const depth = 0 + return fsys.stat(name, depth) +} + +func (fsys *IPFS) stat(name string, depth uint) (fs.FileInfo, error) { + const op = "stat" + info, cid, err := fsys.lstat(op, name) + if err != nil { + return nil, err + } + if isLink := info.Mode()&fs.ModeSymlink != 0; !isLink { + return info, nil + } + if depth++; depth >= fsys.linkLimit { + return nil, linkLimitError(op, name, fsys.linkLimit) + } + target, err := fsys.resolveCIDSymlink(op, name, cid) + if err != nil { + return nil, err + } + return fsys.stat(target, depth) +} + +func (fsys *IPFS) Readlink(name string) (string, error) { + const op = "readlink" + if name == filesystem.Root { + const kind = fserrors.InvalidItem + return "", fserrors.New(op, name, errRootLink, kind) + } + cid, err := fsys.toCID(op, name) + if err != nil { + return "", err } - return info, nil + return fsys.resolveCIDSymlink(op, name, cid) +} + +func readNodeLink(op, name string, node files.Node) (string, error) { + link, ok := node.(*files.Symlink) + if !ok { + const kind = fserrors.InvalidItem + err := fmt.Errorf( + "expected node type: %T but got: %T", + link, node, + ) + return "", fserrors.New(op, name, err, kind) + } + target := link.Target + if len(target) == 0 { + const kind = fserrors.InvalidItem + return "", fserrors.New(op, name, errEmptyLink, kind) + } + return target, nil +} + +func (fsys *IPFS) resolveCIDSymlink(op, name string, cid cid.Cid) (string, error) { + var ( + ufs = fsys.core.Unixfs() + ctx, cancel = fsys.nodeContext() + ) + defer cancel() + const allowedPrefix = "/ipfs/" + return getUnixFSLink(ctx, op, name, ufs, cid, allowedPrefix) } func (fsys *IPFS) toCID(op, goPath string) (cid.Cid, error) { @@ -314,34 +385,41 @@ func (fsys *IPFS) resolvePath(goPath string) (cid.Cid, error) { } func (fsys *IPFS) Open(name string) (fs.File, error) { + const depth = 0 + return fsys.open(name, depth) +} + +func (fsys *IPFS) open(name string, depth uint) (fs.File, error) { if name == filesystem.Root { return emptyRoot{info: &fsys.info}, nil } const op = "open" - if !fs.ValidPath(name) { - return nil, fserrors.New(op, name, filesystem.ErrPath, fserrors.InvalidItem) + if err := validatePath(op, name); err != nil { + return nil, err } cid, err := fsys.toCID(op, name) if err != nil { return nil, err } - file, err := fsys.openCid(name, cid) - if err != nil { - return nil, fserrors.New(op, name, err, fserrors.IO) - } - return file, nil -} - -func (fsys *IPFS) openCid(name string, cid cid.Cid) (fs.File, error) { info, err := fsys.getInfo(name, cid) if err != nil { - return nil, err + const kind = fserrors.IO + return nil, fserrors.New(op, name, err, kind) } switch typ := info.mode.Type(); typ { case fs.FileMode(0): return fsys.openFile(cid, info) case fs.ModeDir: return fsys.openDir(cid, info) + case fs.ModeSymlink: + if depth++; depth >= fsys.linkLimit { + return nil, linkLimitError(op, name, fsys.linkLimit) + } + target, err := fsys.resolveCIDSymlink(op, name, cid) + if err != nil { + return nil, err + } + return fsys.open(target, depth) default: return nil, fmt.Errorf( "%w got: \"%s\" want: regular file or directory", @@ -476,17 +554,15 @@ func (id *ipfsDirectory) Read([]byte) (int, error) { func (id *ipfsDirectory) StreamDir() <-chan filesystem.StreamDirEntry { const op = "streamdir" - stream := id.stream - if stream == nil { - errs := make(chan filesystem.StreamDirEntry, 1) - // TODO: We don't have an error kind - // that translates into EBADF - errs <- newErrorEntry( - fserrors.New(op, id.info.name, filesystem.ErrNotOpen, fserrors.IO), - ) - return errs + if stream := id.stream; stream != nil { + return stream.ch } - return stream.ch + errs := make(chan filesystem.StreamDirEntry, 1) + errs <- newErrorEntry( + fserrors.New(op, id.info.name, fs.ErrClosed, fserrors.Closed), + ) + close(errs) + return errs } func (id *ipfsDirectory) ReadDir(count int) ([]fs.DirEntry, error) { @@ -496,9 +572,7 @@ func (id *ipfsDirectory) ReadDir(count int) ([]fs.DirEntry, error) { } stream := id.stream if stream == nil { - // TODO: We don't have an error kind - // that translates into EBADF - return nil, fserrors.New(op, id.info.name, filesystem.ErrNotOpen, fserrors.IO) + return nil, fserrors.New(op, id.info.name, fs.ErrClosed, fserrors.Closed) } var ( ctx = stream.Context @@ -519,5 +593,14 @@ func (id *ipfsDirectory) Close() error { id.stream = nil return nil } - return fserrors.New(op, id.info.name, filesystem.ErrNotOpen, fserrors.InvalidItem) + return fserrors.New(op, id.info.name, fs.ErrClosed, fserrors.Closed) +} + +func linkLimitError(op, name string, limit uint) error { + const kind = fserrors.Recursion + err := fmt.Errorf( + "reached symbolic link resolution limit (%d) during operation", + limit, + ) + return fserrors.New(op, name, err, kind) } diff --git a/internal/filesystem/ipfs/ipfs_internal_test.go b/internal/filesystem/ipfs/ipfs_internal_test.go index 46bbfe1c..141088d5 100644 --- a/internal/filesystem/ipfs/ipfs_internal_test.go +++ b/internal/filesystem/ipfs/ipfs_internal_test.go @@ -12,6 +12,7 @@ var ( _ fs.FS = (*IPFS)(nil) _ fs.StatFS = (*IPFS)(nil) _ filesystem.IDFS = (*IPFS)(nil) + _ symlinkRFS = (*IPFS)(nil) _ fs.File = (*ipfsDirectory)(nil) _ fs.ReadDirFile = (*ipfsDirectory)(nil) _ filesystem.StreamDirFile = (*ipfsDirectory)(nil) @@ -30,5 +31,7 @@ func testIPFSOptions(t *testing.T) { nil, WithContext[IPFSOption](context.Background()), WithPermissions[IPFSOption](0), + WithNodeTimeout[IPFSOption](0), + WithLinkLimit[IPFSOption](0), ) } diff --git a/internal/filesystem/ipfs/ipns.go b/internal/filesystem/ipfs/ipns.go index 38d19f36..a9d0f4b9 100644 --- a/internal/filesystem/ipfs/ipns.go +++ b/internal/filesystem/ipfs/ipns.go @@ -12,7 +12,7 @@ import ( "github.com/djdv/go-filesystem-utils/internal/filesystem" fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" - lru "github.com/hashicorp/golang-lru/v2" + "github.com/hashicorp/golang-lru/arc/v2" coreiface "github.com/ipfs/boxo/coreiface" corepath "github.com/ipfs/boxo/coreiface/path" ipath "github.com/ipfs/boxo/path" @@ -26,7 +26,7 @@ type ( *cid.Cid *time.Time } - ipnsRootCache = lru.ARCCache[string, ipnsRecord] + ipnsRootCache = arc.ARCCache[string, ipnsRecord] IPNS struct { ctx context.Context core coreiface.CoreAPI @@ -37,6 +37,7 @@ type ( info nodeInfo nodeTimeout time.Duration expiry time.Duration + linkLimit uint } ipnsSettings struct { *IPNS @@ -63,6 +64,7 @@ func NewIPNS(core coreiface.CoreAPI, ipfs fs.FS, options ...IPNSOption) (*IPNS, readAll | executeAll, }, nodeTimeout: 1 * time.Minute, + linkLimit: 40, // Arbitrary. } settings = ipnsSettings{ IPNS: fsys, @@ -99,7 +101,7 @@ func (settings *ipnsSettings) fillInDefaults() error { } func (settings *ipnsSettings) initRootCache(cacheSize int) error { - rootCache, err := lru.NewARC[string, ipnsRecord](cacheSize) + rootCache, err := arc.NewARC[string, ipnsRecord](cacheSize) if err != nil { return err } @@ -137,6 +139,10 @@ func (fsys *IPNS) setContext(ctx context.Context) { fsys.ctx, fsys.cancel = context.WithCancel(ctx) } +func (fsys *IPNS) setLinkLimit(limit uint) { + fsys.linkLimit = limit +} + func (fsys *IPNS) setPermissions(permissions fs.FileMode) { fsys.info.mode = fsys.info.mode.Type() | permissions.Perm() } @@ -149,8 +155,17 @@ func (fsys *IPNS) Close() error { return nil } +func (fsys *IPNS) Lstat(name string) (fs.FileInfo, error) { + const op = "lstat" + return fsys.stat(op, name, filesystem.Lstat) +} + func (fsys *IPNS) Stat(name string) (fs.FileInfo, error) { const op = "stat" + return fsys.stat(op, name, fs.Stat) +} + +func (fsys *IPNS) stat(op, name string, statFn statFunc) (fs.FileInfo, error) { if name == filesystem.Root { return &fsys.info, nil } @@ -158,7 +173,7 @@ func (fsys *IPNS) Stat(name string) (fs.FileInfo, error) { if err != nil { return nil, err } - return fs.Stat(fsys.ipfs, cid.String()) + return statFn(fsys.ipfs, cid.String()) } func (fsys *IPNS) toCID(op, goPath string) (cid.Cid, error) { @@ -229,14 +244,11 @@ func (fsys *IPNS) resolvePath(goPath string) (cid.Cid, error) { } func (fsys *IPNS) nodeContext() (context.Context, context.CancelFunc) { - var ( - ctx = fsys.ctx - timeout = fsys.nodeTimeout - ) - if timeout <= 0 { - return context.WithCancel(ctx) + ctx := fsys.ctx + if timeout := fsys.nodeTimeout; timeout > 0 { + return context.WithTimeout(ctx, timeout) } - return context.WithTimeout(ctx, timeout) + return context.WithCancel(ctx) } func (fsys *IPNS) fetchCID(ctx context.Context, goPath string) (cid.Cid, error) { @@ -251,22 +263,64 @@ func (fsys *IPNS) fetchCID(ctx context.Context, goPath string) (cid.Cid, error) return resolved.Cid(), nil } +func (fsys *IPNS) Readlink(name string) (string, error) { + const op = "readlink" + if name == filesystem.Root { + const kind = fserrors.InvalidItem + return "", fserrors.New(op, name, errRootLink, kind) + } + cid, err := fsys.toCID(op, name) + if err != nil { + return "", err + } + return filesystem.Readlink(fsys.ipfs, cid.String()) +} + +func (fsys *IPNS) resolveCIDSymlink(op, name string, cid cid.Cid) (string, error) { + var ( + ufs = fsys.core.Unixfs() + ctx, cancel = fsys.nodeContext() + ) + defer cancel() + const allowedPrefix = "/ipns/" + return getUnixFSLink(ctx, op, name, ufs, cid, allowedPrefix) +} + func (fsys *IPNS) Open(name string) (fs.File, error) { + const depth = 0 + return fsys.open(name, depth) +} + +func (fsys *IPNS) open(name string, depth uint) (fs.File, error) { if name == filesystem.Root { return emptyRoot{info: &fsys.info}, nil } const op = "open" - if !fs.ValidPath(name) { - return nil, fserrors.New(op, name, filesystem.ErrPath, fserrors.InvalidItem) + if err := validatePath(op, name); err != nil { + return nil, err } cid, err := fsys.toCID(op, name) if err != nil { return nil, err } ipfs := fsys.ipfs + info, err := filesystem.Lstat(ipfs, cid.String()) + if err != nil { + return nil, err + } + if info.Mode().Type() == fs.ModeSymlink { + if depth++; depth >= fsys.linkLimit { + return nil, linkLimitError(op, name, fsys.linkLimit) + } + target, err := fsys.resolveCIDSymlink(op, name, cid) + if err != nil { + return nil, err + } + return fsys.open(target, depth) + } file, err := ipfs.Open(cid.String()) if err != nil { - return nil, fserrors.New(op, name, err, fserrors.IO) + return nil, err } nFile := ipnsFile{ file: file, @@ -398,7 +452,7 @@ func (nf *ipnsFile) Seek(offset int64, whence int) (int64, error) { if seeker, ok := nf.file.(io.Seeker); ok { return seeker.Seek(offset, whence) } - return 0, fserrors.ErrUnsupported + return 0, errors.ErrUnsupported } func (nf *ipnsFile) Read(b []byte) (int, error) { @@ -412,19 +466,18 @@ func (nf *ipnsFile) ReadDir(count int) ([]fs.DirEntry, error) { if err := nf.refreshFn(); err != nil { return nil, err } - // TODO: these kinds of things should - // use the new [errors.ErrUnsupported] value too. file := nf.file if directory, ok := file.(fs.ReadDirFile); ok { return directory.ReadDir(count) } var ( name string - err error = filesystem.ErrIsDir - kind = fserrors.NotDir + err error = errors.ErrUnsupported + kind fserrors.Kind ) if info, sErr := file.Stat(); sErr == nil { name = info.Name() + kind = fserrors.InvalidOperation } else { err = errors.Join(err, sErr) kind = fserrors.IO diff --git a/internal/filesystem/ipfs/ipns_internal_test.go b/internal/filesystem/ipfs/ipns_internal_test.go index 1377b7a7..5bcc53cc 100644 --- a/internal/filesystem/ipfs/ipns_internal_test.go +++ b/internal/filesystem/ipfs/ipns_internal_test.go @@ -13,6 +13,7 @@ var ( _ fs.FS = (*IPNS)(nil) _ fs.StatFS = (*IPNS)(nil) _ filesystem.IDFS = (*IPNS)(nil) + _ symlinkRFS = (*IPNS)(nil) _ fs.File = (*ipnsFile)(nil) _ fs.ReadDirFile = (*ipnsFile)(nil) _ io.Seeker = (*ipnsFile)(nil) @@ -31,5 +32,6 @@ func testIPNSOptions(t *testing.T) { nil, nil, WithContext[IPNSOption](context.Background()), WithPermissions[IPNSOption](0), + WithLinkLimit[IPNSOption](0), ) } diff --git a/internal/filesystem/ipfs/keyfs.go b/internal/filesystem/ipfs/keyfs.go index 69f5353b..39c18b30 100644 --- a/internal/filesystem/ipfs/keyfs.go +++ b/internal/filesystem/ipfs/keyfs.go @@ -2,25 +2,42 @@ package ipfs import ( "context" + "errors" "fmt" "io" "io/fs" "path" "strings" + "sync" "time" "github.com/djdv/go-filesystem-utils/internal/filesystem" fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" + "github.com/djdv/go-filesystem-utils/internal/generic" coreiface "github.com/ipfs/boxo/coreiface" + coreoptions "github.com/ipfs/boxo/coreiface/options" + corepath "github.com/ipfs/boxo/coreiface/path" ) type ( + keyfsCacheEntry struct { + last time.Time + snapshot []coreiface.Key + } KeyFS struct { keyAPI coreiface.KeyAPI + dag coreiface.APIDagService + names coreiface.NameAPI + pins coreiface.PinAPI ipns fs.FS ctx context.Context cancel context.CancelFunc - permissions fs.FileMode + info nodeInfo + nodeTimeout time.Duration + cache keyfsCacheEntry + cacheMu sync.Mutex + linkLimit uint + expiry time.Duration } KeyFSOption func(*KeyFS) error keyDirectory struct { @@ -46,10 +63,34 @@ func WithIPNS(ipns fs.FS) KeyFSOption { return func(ka *KeyFS) error { ka.ipns = ipns; return nil } } +func WithNameService(names coreiface.NameAPI) KeyFSOption { + return func(ka *KeyFS) error { ka.names = names; return nil } +} + +func WithPinService(pins coreiface.PinAPI) KeyFSOption { + return func(ka *KeyFS) error { ka.pins = pins; return nil } +} + +// CacheKeysFor will cache responses from the node and consider +// them valid for the duration. Negative values retain the +// cache forever. A 0 value disables caching. +func CacheKeysFor(duration time.Duration) KeyFSOption { + return func(kfs *KeyFS) error { + kfs.expiry = duration + return nil + } +} + func NewKeyFS(core coreiface.KeyAPI, options ...KeyFSOption) (*KeyFS, error) { + const permissions = readAll | executeAll fsys := &KeyFS{ - permissions: readAll | executeAll, - keyAPI: core, + info: nodeInfo{ + modTime: time.Now(), + name: filesystem.Root, + mode: fs.ModeDir | permissions, + }, + keyAPI: core, + linkLimit: 40, // Arbitrary. } for _, setter := range options { if err := setter(fsys); err != nil { @@ -68,8 +109,21 @@ func (fsys *KeyFS) setContext(ctx context.Context) { fsys.ctx, fsys.cancel = context.WithCancel(ctx) } +func (fsys *KeyFS) setNodeTimeout(timeout time.Duration) { + fsys.nodeTimeout = timeout +} + +func (fsys *KeyFS) setLinkLimit(limit uint) { + fsys.linkLimit = limit +} + func (fsys *KeyFS) setPermissions(permissions fs.FileMode) { - fsys.permissions = permissions.Perm() + typ := fsys.info.mode.Type() + fsys.info.mode = typ | permissions.Perm() +} + +func (kfs *KeyFS) setDag(dag coreiface.APIDagService) { + kfs.dag = dag } func (ki *KeyFS) Close() error { @@ -77,61 +131,199 @@ func (ki *KeyFS) Close() error { return nil } -// TODO: probably inefficient. Review. -// TODO: deceptive name. This may translate the name. -// but it won't if we don't have such a key -// (which is fine for non-named IPNS paths). -func (ki *KeyFS) translateName(name string) (string, error) { - keys, err := ki.keyAPI.List(ki.ctx) +func (ki *KeyFS) nodeContext() (context.Context, context.CancelFunc) { + var ( + ctx = ki.ctx + timeout = ki.nodeTimeout + ) + if timeout <= 0 { + return context.WithCancel(ctx) + } + return context.WithTimeout(ctx, timeout) +} + +func (ki *KeyFS) getKeys() ([]coreiface.Key, error) { + expiry := ki.expiry + if cacheDisabled := expiry == 0; cacheDisabled { + return ki.fetchKeys() + } + ki.cacheMu.Lock() + defer ki.cacheMu.Unlock() + if cache := ki.cache; time.Since(cache.last) < expiry { + return cache.snapshot, nil + } + keys, err := ki.fetchKeys() + if err != nil { + return nil, err + } + ki.cache = keyfsCacheEntry{ + snapshot: keys, + last: time.Now(), + } + return keys, nil +} + +func (ki *KeyFS) fetchKeys() ([]coreiface.Key, error) { + ctx, cancel := ki.nodeContext() + defer cancel() + return ki.keyAPI.List(ctx) +} + +// maybeTranslateName will translate the first component +// of `name` if the component is the name of a key that we have. +// Otherwise `name` is returned unchanged. +func (ki *KeyFS) maybeTranslateName(name string) (string, error) { + keys, err := ki.getKeys() if err != nil { return "", err } + const separator = "/" var ( - components = strings.Split(name, "/") + components = strings.Split(name, separator) keyName = components[0] ) for _, key := range keys { - if key.Name() == keyName { - keyName = pathWithoutNamespace(key) - break + if key.Name() != keyName { + continue } + tail := components[1:] + components = append( + []string{pathWithoutNamespace(key)}, + tail..., + ) + return strings.Join(components, separator), nil } - components = append([]string{keyName}, components[1:]...) - keyName = strings.Join(components, "/") - return keyName, nil + return name, nil +} + +func (kfs *KeyFS) Lstat(name string) (fs.FileInfo, error) { + const op = "lstat" + return kfs.stat(op, name, filesystem.Lstat) } func (kfs *KeyFS) Stat(name string) (fs.FileInfo, error) { const op = "stat" + return kfs.stat(op, name, filesystem.Lstat) +} + +func (kfs *KeyFS) stat(op, name string, statFn statFunc) (fs.FileInfo, error) { if name == filesystem.Root { - return &keyDirectory{ - mode: fs.ModeDir | kfs.permissions, - ipns: kfs.ipns, - }, nil + return &kfs.info, nil + } + ipns := kfs.ipns + if ipns == nil { + return nil, fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) + } + translated, err := kfs.maybeTranslateName(name) + if err != nil { + return nil, fserrors.New(op, name, err, fserrors.IO) + } + return statFn(ipns, translated) +} + +func (kfs *KeyFS) Symlink(oldname, newname string) error { + const op = "symlink" + dag, names, err := kfs.symlinkAPIs() + if err != nil { + return fserrors.New(op, newname, err, fserrors.InvalidOperation) + } + var ( + api = kfs.keyAPI + ctx, cancel = kfs.nodeContext() + ) + defer cancel() + key, err := api.Generate(ctx, newname) + if err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + linkCid, err := makeAndAddLink(ctx, oldname, dag) + if err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + path := corepath.IpfsPath(linkCid) + if pins := kfs.pins; pins != nil { + if err := pins.Add(ctx, path); err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + } + if _, err := names.Publish(ctx, path, + coreoptions.Name.Key(key.Name()), + coreoptions.Name.AllowOffline(true), + ); err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + return nil +} + +func (kfs *KeyFS) symlinkAPIs() (coreiface.APIDagService, coreiface.NameAPI, error) { + var ( + dag = kfs.dag + names = kfs.names + errs []error + ) + if dag == nil { + const err = generic.ConstError("system created without dag service option") + errs = append(errs, err) + } + if names == nil { + const err = generic.ConstError("system created without name service option") + errs = append(errs, err) + } + if errs == nil { + return dag, names, nil + } + errs = append([]error{errors.ErrUnsupported}, errs...) + return nil, nil, errors.Join(errs...) +} + +func (kfs *KeyFS) Readlink(name string) (string, error) { + const op = "readlink" + if name == filesystem.Root { + const kind = fserrors.InvalidItem + return "", fserrors.New(op, name, errRootLink, kind) } if subsys := kfs.ipns; subsys != nil { - return fs.Stat(subsys, name) + return filesystem.Readlink(subsys, name) } - return nil, fserrors.New(op, name, filesystem.ErrNotFound, fserrors.NotExist) + return "", fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) } func (kfs *KeyFS) Open(name string) (fs.File, error) { + const depth = 0 + return kfs.open(name, depth) +} + +func (kfs *KeyFS) open(name string, depth uint) (fs.File, error) { const op = "open" if name == filesystem.Root { - file, err := kfs.openRoot() - if err != nil { - return nil, err - } - return file, nil + return kfs.openRoot() + } + if err := validatePath(op, name); err != nil { + return nil, err + } + ipns := kfs.ipns + if ipns == nil { + return nil, fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) } - translated, err := kfs.translateName(name) + translated, err := kfs.maybeTranslateName(name) if err != nil { return nil, fserrors.New(op, name, err, fserrors.IO) } - if subsys := kfs.ipns; subsys != nil { - return subsys.Open(translated) + info, err := kfs.Lstat(translated) + if err != nil { + return nil, err + } + if info.Mode().Type() == fs.ModeSymlink { + if depth++; depth >= kfs.linkLimit { + return nil, linkLimitError(op, name, kfs.linkLimit) + } + target, err := filesystem.Readlink(ipns, translated) + if err != nil { + return nil, err + } + return kfs.open(target, depth) } - return nil, fserrors.New(op, name, filesystem.ErrNotFound, fserrors.NotExist) + return ipns.Open(translated) } func (kfs *KeyFS) openRoot() (fs.ReadDirFile, error) { @@ -145,24 +337,31 @@ func (kfs *KeyFS) openRoot() (fs.ReadDirFile, error) { } var ( dirCtx, dirCancel = context.WithCancel(rootCtx) - entries = make(chan filesystem.StreamDirEntry) + entries = make(chan filesystem.StreamDirEntry, 1) + permissions = kfs.info.mode.Perm() ) go func() { - keys, err := kfs.keyAPI.List(dirCtx) + defer close(entries) + keys, err := kfs.getKeys() if err != nil { dirCancel() + entries <- errorEntry{error: err} + return } for _, key := range keys { - entries <- &keyDirEntry{ - permissions: kfs.permissions, + select { + case entries <- &keyDirEntry{ + permissions: permissions, Key: key, ipns: kfs.ipns, + }: + case <-dirCtx.Done(): + return } } - close(entries) }() return &keyDirectory{ - mode: fs.ModeDir | kfs.permissions, + mode: fs.ModeDir | permissions, ipns: kfs.ipns, stream: &entryStream{ Context: dirCtx, CancelFunc: dirCancel, @@ -192,7 +391,7 @@ func (kd *keyDirectory) ReadDir(count int) ([]fs.DirEntry, error) { } stream := kd.stream if stream == nil { - return nil, fserrors.New(op, filesystem.Root, filesystem.ErrNotOpen, fserrors.IO) + return nil, fserrors.New(op, filesystem.Root, fs.ErrClosed, fserrors.Closed) } var ( ctx = stream.Context @@ -216,7 +415,7 @@ func (kd *keyDirectory) Close() error { kd.stream = nil return nil } - return fserrors.New(op, filesystem.Root, filesystem.ErrNotOpen, fserrors.InvalidItem) + return fserrors.New(op, filesystem.Root, fs.ErrClosed, fserrors.Closed) } func pathWithoutNamespace(key coreiface.Key) string { @@ -244,7 +443,7 @@ func (ke *keyDirEntry) Type() fs.FileMode { if err != nil { return fs.ModeIrregular } - return info.Mode() & fs.ModeType + return info.Mode().Type() } func (ke *keyDirEntry) IsDir() bool { return ke.Type()&fs.ModeDir != 0 } diff --git a/internal/filesystem/ipfs/keyfs_internal_test.go b/internal/filesystem/ipfs/keyfs_internal_test.go index f1dc67ee..56bf4ec4 100644 --- a/internal/filesystem/ipfs/keyfs_internal_test.go +++ b/internal/filesystem/ipfs/keyfs_internal_test.go @@ -12,6 +12,7 @@ var ( _ fs.FS = (*KeyFS)(nil) _ fs.StatFS = (*KeyFS)(nil) _ filesystem.IDFS = (*KeyFS)(nil) + _ symlinkFS = (*KeyFS)(nil) _ fs.File = (*keyDirectory)(nil) _ fs.ReadDirFile = (*keyDirectory)(nil) ) @@ -29,5 +30,7 @@ func testKeyFSOptions(t *testing.T) { nil, WithContext[KeyFSOption](context.Background()), WithPermissions[KeyFSOption](0), + WithDagService[KeyFSOption](nil), + WithLinkLimit[KeyFSOption](0), ) } diff --git a/internal/filesystem/ipfs/mountpoint.go b/internal/filesystem/ipfs/mountpoint.go index 955c3430..60e83bc6 100644 --- a/internal/filesystem/ipfs/mountpoint.go +++ b/internal/filesystem/ipfs/mountpoint.go @@ -9,6 +9,7 @@ import ( "github.com/djdv/go-filesystem-utils/internal/filesystem" p9fs "github.com/djdv/go-filesystem-utils/internal/filesystem/9p" + maddrc "github.com/djdv/go-filesystem-utils/internal/multiaddr" coreiface "github.com/ipfs/boxo/coreiface" "github.com/multiformats/go-multiaddr" ) @@ -36,7 +37,7 @@ func (*IPFSGuest) GuestID() filesystem.ID { return IPFSID } func (ig *IPFSGuest) UnmarshalJSON(b []byte) error { // multiformats/go-multiaddr issue #100 var maddrWorkaround struct { - APIMaddr multiaddrContainer `json:"apiMaddr,omitempty"` + APIMaddr maddrc.Multiaddr `json:"apiMaddr,omitempty"` } if err := json.Unmarshal(b, &maddrWorkaround); err != nil { return err @@ -203,6 +204,7 @@ func (pg *PinFSGuest) MakeFS() (fs.FS, error) { client.Pin(), WithIPFS(ipfsFS), CachePinsFor(pg.CacheExpiry), + WithDagService[PinFSOption](client.Dag()), ) } @@ -240,12 +242,18 @@ func (kg *KeyFSGuest) MakeFS() (fs.FS, error) { if err != nil { return nil, err } - // TODO: options - ipnsFS, err := NewIPNS(client, ipfs) + var ipnsOptions []IPNSOption + if expiry := kg.IPNSGuest.NodeExpiry; expiry != 0 { + ipnsOptions = []IPNSOption{CacheNodesFor(expiry)} + } + ipnsFS, err := NewIPNS(client, ipfs, ipnsOptions...) if err != nil { return nil, err } return NewKeyFS(client.Key(), WithIPNS(ipnsFS), + WithDagService[KeyFSOption](client.Dag()), + WithNameService(client.Name()), + WithPinService(client.Pin()), ) } diff --git a/internal/filesystem/ipfs/multiaddr.go b/internal/filesystem/ipfs/multiaddr.go deleted file mode 100644 index a3ca383b..00000000 --- a/internal/filesystem/ipfs/multiaddr.go +++ /dev/null @@ -1,53 +0,0 @@ -package ipfs - -import ( - "encoding/json" - - "github.com/multiformats/go-multiaddr" -) - -type multiaddrContainer struct{ multiaddr.Multiaddr } - -func (mc *multiaddrContainer) MarshalBinary() ([]byte, error) { - if maddr := mc.Multiaddr; maddr != nil { - return maddr.MarshalBinary() - } - return []byte{}, nil -} - -func (mc *multiaddrContainer) UnmarshalBinary(b []byte) (err error) { - mc.Multiaddr, err = multiaddr.NewMultiaddrBytes(b) - return -} - -func (mc *multiaddrContainer) MarshalText() ([]byte, error) { - if maddr := mc.Multiaddr; maddr != nil { - return maddr.MarshalText() - } - return []byte{}, nil -} - -func (mc *multiaddrContainer) UnmarshalText(b []byte) (err error) { - mc.Multiaddr, err = multiaddr.NewMultiaddr(string(b)) - return -} - -func (mc *multiaddrContainer) MarshalJSON() ([]byte, error) { - if maddr := mc.Multiaddr; maddr != nil { - return maddr.MarshalJSON() - } - return []byte("null"), nil -} - -func (mc *multiaddrContainer) UnmarshalJSON(b []byte) error { - var mcStr string - if err := json.Unmarshal(b, &mcStr); err != nil { - return err - } - maddr, err := multiaddr.NewMultiaddr(mcStr) - if err != nil { - return err - } - mc.Multiaddr = maddr - return nil -} diff --git a/internal/filesystem/ipfs/pinfs.go b/internal/filesystem/ipfs/pinfs.go index 71852f68..10e299e2 100644 --- a/internal/filesystem/ipfs/pinfs.go +++ b/internal/filesystem/ipfs/pinfs.go @@ -2,6 +2,8 @@ package ipfs import ( "context" + "errors" + "fmt" "io/fs" "sync" "sync/atomic" @@ -12,6 +14,7 @@ import ( "github.com/djdv/go-filesystem-utils/internal/generic" coreiface "github.com/ipfs/boxo/coreiface" coreoptions "github.com/ipfs/boxo/coreiface/options" + corepath "github.com/ipfs/boxo/coreiface/path" ) type ( @@ -21,6 +24,7 @@ type ( } pinShared struct { api coreiface.PinAPI + dag coreiface.APIDagService ipfs fs.FS info pinDirectoryInfo } @@ -45,6 +49,7 @@ type ( size int64 } PinFSOption func(*PinFS) error + statFunc func(fs.FS, string) (fs.FileInfo, error) ) const PinFSID filesystem.ID = "PinFS" @@ -86,6 +91,10 @@ func (pfs *PinFS) setPermissions(permissions fs.FileMode) { pfs.info.permissions = permissions.Perm() } +func (pfs *PinFS) setDag(dag coreiface.APIDagService) { + pfs.dag = dag +} + func (pfs *PinFS) initStatFunc() { var ( ipfs = pfs.ipfs @@ -112,6 +121,9 @@ func (pfs *PinFS) initStatFunc() { } } +// CachePinsFor will cache responses from the node and consider +// them valid for the duration. Negative values retain the +// cache forever. A 0 value disables caching. func CachePinsFor(duration time.Duration) PinFSOption { return func(pfs *PinFS) error { pfs.expiry = duration @@ -121,15 +133,66 @@ func CachePinsFor(duration time.Duration) PinFSOption { func (*PinFS) ID() filesystem.ID { return PinFSID } +func (pfs *PinFS) Lstat(name string) (fs.FileInfo, error) { + const op = "lstat" + return pfs.stat(op, name, filesystem.Lstat) +} + func (pfs *PinFS) Stat(name string) (fs.FileInfo, error) { const op = "stat" + return pfs.stat(op, name, fs.Stat) +} + +func (pfs *PinFS) stat(op, name string, statFn statFunc) (fs.FileInfo, error) { if name == filesystem.Root { return &pfs.info, nil } if subsys := pfs.ipfs; subsys != nil { - return fs.Stat(subsys, name) + return statFn(subsys, name) } - return nil, fserrors.New(op, name, filesystem.ErrNotFound, fserrors.NotExist) + return nil, fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) +} + +func (pfs *PinFS) Symlink(oldname, newname string) error { + const op = "symlink" + pfs.cacheMu.Lock() + defer pfs.cacheMu.Unlock() + var ( + dag = pfs.dag + ctx, cancel = context.WithCancel(pfs.ctx) + ) + defer cancel() + if dag == nil { + err := fmt.Errorf("%w - system created without dag service option", + errors.ErrUnsupported, + ) + return fserrors.New(op, newname, err, fserrors.InvalidOperation) + } + linkCid, err := makeAndAddLink(ctx, oldname, dag) + if err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + path := corepath.IpfsPath(linkCid) + if err := pfs.api.Add(ctx, path); err != nil { + return fserrors.New(op, newname, err, fserrors.IO) + } + if cacheEnabled := pfs.expiry > 0; cacheEnabled { + // We modified the pinset; invalidate the cache. + pfs.info.modTime.Store(new(time.Time)) + } + return nil +} + +func (pfs *PinFS) Readlink(name string) (string, error) { + const op = "readlink" + if name == filesystem.Root { + const kind = fserrors.InvalidItem + return "", fserrors.New(op, name, errRootLink, kind) + } + if subsys := pfs.ipfs; subsys != nil { + return filesystem.Readlink(subsys, name) + } + return "", fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) } func (pfs *PinFS) Open(name string) (fs.File, error) { @@ -140,7 +203,7 @@ func (pfs *PinFS) Open(name string) (fs.File, error) { if subsys := pfs.ipfs; subsys != nil { return subsys.Open(name) } - return nil, fserrors.New(op, name, filesystem.ErrNotFound, fserrors.NotExist) + return nil, fserrors.New(op, name, fs.ErrNotExist, fserrors.NotExist) } func (pfs *PinFS) openRoot() (fs.ReadDirFile, error) { @@ -162,8 +225,7 @@ func (pfs *PinFS) openRoot() (fs.ReadDirFile, error) { } func (pfs *PinFS) getEntries(ctx context.Context) (<-chan filesystem.StreamDirEntry, error) { - cacheDisabled := pfs.expiry == 0 - if cacheDisabled { + if cacheDisabled := pfs.expiry == 0; cacheDisabled { return pfs.fetchEntries(ctx) } pfs.cacheMu.Lock() @@ -283,9 +345,7 @@ func (pd *pinDirectory) ReadDir(count int) ([]fs.DirEntry, error) { } stream := pd.stream if stream == nil { - // TODO: We don't have an error kind - // that translates into EBADF - return nil, fserrors.New(op, filesystem.Root, filesystem.ErrNotOpen, fserrors.IO) + return nil, fserrors.New(op, filesystem.Root, fs.ErrClosed, fserrors.Closed) } var ( ctx = stream.Context @@ -301,17 +361,15 @@ func (pd *pinDirectory) ReadDir(count int) ([]fs.DirEntry, error) { func (pd *pinDirectory) StreamDir() <-chan filesystem.StreamDirEntry { const op = "streamdir" - stream := pd.stream - if stream == nil { - errs := make(chan filesystem.StreamDirEntry, 1) - // TODO: We don't have an error kind - // that translates into EBADF - errs <- newErrorEntry( - fserrors.New(op, filesystem.Root, filesystem.ErrNotOpen, fserrors.IO), - ) - return errs + if stream := pd.stream; stream != nil { + return stream.ch } - return stream.ch + errs := make(chan filesystem.StreamDirEntry, 1) + errs <- newErrorEntry( + fserrors.New(op, filesystem.Root, fs.ErrClosed, fserrors.Closed), + ) + close(errs) + return errs } func (pd *pinDirectory) Close() error { @@ -321,9 +379,7 @@ func (pd *pinDirectory) Close() error { pd.stream = nil return nil } - // TODO: We don't have an error kind - // that translates into EBADF - return fserrors.New(op, filesystem.Root, filesystem.ErrNotOpen, fserrors.IO) + return fserrors.New(op, filesystem.Root, fs.ErrClosed, fserrors.Closed) } func (pe *pinDirEntry) Name() string { diff --git a/internal/filesystem/ipfs/pinfs_internal_test.go b/internal/filesystem/ipfs/pinfs_internal_test.go index 2edde1fb..b970ec18 100644 --- a/internal/filesystem/ipfs/pinfs_internal_test.go +++ b/internal/filesystem/ipfs/pinfs_internal_test.go @@ -12,6 +12,7 @@ var ( _ fs.FS = (*PinFS)(nil) _ fs.StatFS = (*PinFS)(nil) _ filesystem.IDFS = (*PinFS)(nil) + _ symlinkFS = (*PinFS)(nil) _ fs.File = (*pinDirectory)(nil) _ fs.ReadDirFile = (*pinDirectory)(nil) _ filesystem.StreamDirFile = (*pinDirectory)(nil) @@ -30,5 +31,6 @@ func testPinFSOptions(t *testing.T) { nil, WithContext[PinFSOption](context.Background()), WithPermissions[PinFSOption](0), + WithDagService[PinFSOption](nil), ) } diff --git a/internal/filesystem/ipfs/resolve.go b/internal/filesystem/ipfs/resolve.go index 66d9d7b8..16f414e6 100644 --- a/internal/filesystem/ipfs/resolve.go +++ b/internal/filesystem/ipfs/resolve.go @@ -2,8 +2,8 @@ package ipfs import ( "context" + "errors" - fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" "github.com/ipfs/boxo/blockservice" "github.com/ipfs/boxo/exchange" bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice" @@ -64,19 +64,19 @@ func (getNodeFn fnBlockStore) GetSize(ctx context.Context, c cid.Cid) (int, erro func (fnBlockStore) HashOnRead(bool) {} func (fnBlockStore) Put(context.Context, blocks.Block) error { - return fserrors.ErrUnsupported + return errors.ErrUnsupported } func (fnBlockStore) PutMany(context.Context, []blocks.Block) error { - return fserrors.ErrUnsupported + return errors.ErrUnsupported } func (fnBlockStore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { - return nil, fserrors.ErrUnsupported + return nil, errors.ErrUnsupported } func (fnBlockStore) DeleteBlock(context.Context, cid.Cid) error { - return fserrors.ErrUnsupported + return errors.ErrUnsupported } func (getNodeFn fnBlockFetcher) GetBlock(_ context.Context, c cid.Cid) (blocks.Block, error) { diff --git a/internal/filesystem/ipfs/shared.go b/internal/filesystem/ipfs/shared.go index 4e6f40e6..f8bdfe6d 100644 --- a/internal/filesystem/ipfs/shared.go +++ b/internal/filesystem/ipfs/shared.go @@ -3,8 +3,10 @@ package ipfs import ( "context" "errors" + "fmt" "io" "io/fs" + "path" "strings" "time" @@ -12,10 +14,13 @@ import ( fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" "github.com/djdv/go-filesystem-utils/internal/generic" coreiface "github.com/ipfs/boxo/coreiface" - dag "github.com/ipfs/boxo/ipld/merkledag" + corepath "github.com/ipfs/boxo/coreiface/path" + files "github.com/ipfs/boxo/files" + mdag "github.com/ipfs/boxo/ipld/merkledag" "github.com/ipfs/boxo/ipld/unixfs" unixpb "github.com/ipfs/boxo/ipld/unixfs/pb" "github.com/ipfs/boxo/path/resolver" + "github.com/ipfs/go-cid" ipfscmds "github.com/ipfs/go-ipfs-cmds" cbor "github.com/ipfs/go-ipld-cbor" ipld "github.com/ipfs/go-ipld-format" @@ -27,10 +32,22 @@ type ( *T setContext(context.Context) } + nodeTimeoutSetter[T any] interface { + *T + setNodeTimeout(time.Duration) + } + linkLimitSetter[T any] interface { + *T + setLinkLimit(uint) + } permissionSetter[T any] interface { *T setPermissions(fs.FileMode) } + dagSetter[T any] interface { + *T + setDag(coreiface.APIDagService) + } nodeInfo struct { modTime time.Time name string @@ -53,10 +70,20 @@ type ( modTime time.Time permissions fs.FileMode } + symlinkRFS interface { + filesystem.LinkStater + filesystem.LinkReader + } + symlinkFS interface { + symlinkRFS + filesystem.LinkMaker + } ) const ( errUnexpectedType = generic.ConstError("unexpected type") + errEmptyLink = generic.ConstError("empty link target") + errRootLink = generic.ConstError("root is not a symlink") executeAll = filesystem.ExecuteUser | filesystem.ExecuteGroup | filesystem.ExecuteOther readAll = filesystem.ReadUser | filesystem.ReadGroup | filesystem.ReadOther ) @@ -81,6 +108,37 @@ func WithContext[ } } +// WithNodeTimeout sets a timeout duration to use +// when communicating with the IPFS API/node. +// If <= 0, operations will not time out, +// and will remain pending until the file system is closed. +func WithNodeTimeout[ + OT generic.OptionFunc[T], + T any, + I nodeTimeoutSetter[T], +](timeout time.Duration, +) OT { + return func(settings *T) error { + any(settings).(I).setNodeTimeout(timeout) + return nil + } +} + +// WithLinkLimit sets the maximum amount of times an +// operation will resolve a symbolic link chain, +// before it returns a recursion error. +func WithLinkLimit[ + OT generic.OptionFunc[T], + T any, + I linkLimitSetter[T], +](limit uint, +) OT { + return func(settings *T) error { + any(settings).(I).setLinkLimit(limit) + return nil + } +} + func WithPermissions[ OT generic.OptionFunc[T], T any, @@ -93,6 +151,20 @@ func WithPermissions[ } } +// WithDagService supplies a dag service to +// use to add support for various write operations. +func WithDagService[ + OT generic.OptionFunc[T], + T any, + I dagSetter[T], +](dag coreiface.APIDagService, +) OT { + return func(mode *T) error { + any(mode).(I).setDag(dag) + return nil + } +} + func (ni *nodeInfo) Name() string { return ni.name } func (ni *nodeInfo) Size() int64 { return ni.size } func (ni *nodeInfo) Mode() fs.FileMode { return ni.mode } @@ -135,9 +207,16 @@ func (emptyRoot) ReadDir(count int) ([]fs.DirEntry, error) { return nil, nil } +func validatePath(op, name string) error { + if fs.ValidPath(name) { + return nil + } + return fserrors.New(op, name, fs.ErrInvalid, fserrors.InvalidItem) +} + func statNode(node ipld.Node, info *nodeInfo) error { switch typedNode := node.(type) { - case *dag.ProtoNode: + case *mdag.ProtoNode: return statProto(typedNode, info) case *cbor.Node: return statCbor(typedNode, info) @@ -146,7 +225,7 @@ func statNode(node ipld.Node, info *nodeInfo) error { } } -func statProto(node *dag.ProtoNode, info *nodeInfo) error { +func statProto(node *mdag.ProtoNode, info *nodeInfo) error { ufsNode, err := unixfs.ExtractFSNode(node) if err != nil { return err @@ -392,3 +471,68 @@ func fsTypeName(mode fs.FileMode) string { return "irregular" } } + +func makeAndAddLink(ctx context.Context, target string, dag coreiface.APIDagService) (cid.Cid, error) { + dagData, err := unixfs.SymlinkData(target) + if err != nil { + return cid.Cid{}, err + } + dagNode := mdag.NodeWithData(dagData) + if err := dag.Add(ctx, dagNode); err != nil { + return cid.Cid{}, err + } + return dagNode.Cid(), nil +} + +func getUnixFSLink(ctx context.Context, + op, name string, + ufs coreiface.UnixfsAPI, cid cid.Cid, + allowedPrefix string, +) (string, error) { + cPath := corepath.IpfsPath(cid) + link, err := ufs.Get(ctx, cPath) + if err != nil { + const kind = fserrors.IO + return "", fserrors.New(op, name, err, kind) + } + return resolveNodeLink(op, name, link, allowedPrefix) +} + +func resolveNodeLink(op, name string, node files.Node, prefix string) (string, error) { + target, err := readNodeLink(op, name, node) + if err != nil { + return "", err + } + // We allow 2 kinds of absolute links: + // 1) File system's root + // 2) Paths matching an explicitly allowed prefix + if strings.HasPrefix(target, prefix) { + target = strings.TrimPrefix(target, prefix) + return path.Clean(target), nil + } + switch target { + case "/": + return filesystem.Root, nil + case "..": + name = path.Dir(name) + fallthrough + case ".": + return path.Dir(name), nil + } + if target[0] == '/' { + const ( + err = generic.ConstError("link target must be relative") + kind = fserrors.InvalidItem + ) + pair := fmt.Sprintf( + `%s -> %s`, + name, target, + ) + return "", fserrors.New(op, pair, err, kind) + } + if target = path.Join("/"+name, target); target == "/" { + target = filesystem.Root + } + target = strings.TrimPrefix(target, "/") + return target, nil +} diff --git a/internal/filesystem/nfs/client.go b/internal/filesystem/nfs/client.go new file mode 100644 index 00000000..0546fa5e --- /dev/null +++ b/internal/filesystem/nfs/client.go @@ -0,0 +1,611 @@ +package nfs + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "strings" + "sync" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" + fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" + "github.com/djdv/go-filesystem-utils/internal/generic" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/willscott/go-nfs-client/nfs" + "github.com/willscott/go-nfs-client/nfs/rpc" +) + +type ( + goFS struct { + target *nfs.Target + linkSeparator string + linkLimit uint + // NOTE [2024.01.08]: The NFS server library is able to handle multiple requests concurrently + // but the client library is not intended to handle multiple outstanding operations. + // As a result, we lock on each operation. + // If this changes upstream, we can drop the operation mutex. + opMu sync.Mutex + } + goShared struct { + opMu *sync.Mutex + netName string + } + goFile struct { + file *nfs.File + goShared + } + goDirectory struct { + goShared + target *nfs.Target + entries []*nfs.EntryPlus + } + goEnt struct { + *nfs.EntryPlus + } + // goStatWrapper wraps the [fs.FileInfo] returned from + // [nfs.target.Getattr] (which returns an empty name + // in its `.Name` method). + goStatWrapper struct { + fs.FileInfo + name string + } + clientSettings struct { + *goFS + hostname, dirpath string + uid, gid uint32 + defaultUID, defaultGID bool + } + ClientOption func(*clientSettings) error +) + +const errStale = generic.ConstError("handle became stale") + +func NewNFSGuest(maddr multiaddr.Multiaddr, options ...ClientOption) (*goFS, error) { + var ( + fsys = goFS{ + linkLimit: 40, // Arbitrary; Linux's default. + } + settings = makeClientSettings(&fsys) + ) + if err := generic.ApplyOptions(&settings, options...); err != nil { + return nil, err + } + if err := settings.fillDefaults(); err != nil { + return nil, err + } + naddr, err := manet.ToNetAddr(maddr) + if err != nil { + return nil, err + } + const ( + network = "tcp" + privileged = false + ) + client, err := rpc.DialTCP(network, naddr.String(), privileged) + if err != nil { + return nil, err + } + var ( + machinename = settings.hostname + uid = settings.uid + gid = settings.gid + dirpath = settings.dirpath + auth = rpc.NewAuthUnix(machinename, uid, gid) + mounter = nfs.Mount{Client: client} + ) + target, err := mounter.Mount(dirpath, auth.Auth()) + if err != nil { + mounter.Close() + return nil, err + } + fsys.target = target + return &fsys, nil +} + +func makeClientSettings(fsys *goFS) clientSettings { + return clientSettings{ + goFS: fsys, + defaultUID: true, + defaultGID: true, + } +} + +// WithUID overrides the default NFS `uid` value. +// Used in the `AUTH_UNIX` authentication "flavor". +func WithUID(uid uint32) ClientOption { + return func(set *clientSettings) error { + set.uid = uid + set.defaultUID = false + return nil + } +} + +// WithGID overrides the default NFS `gid` value. +// Used in the `AUTH_UNIX` authentication "flavor". +func WithGID(gid uint32) ClientOption { + return func(set *clientSettings) error { + set.gid = gid + set.defaultGID = false + return nil + } +} + +// WithDirpath overrides the default NFS `dirpath` value. +// Specifies the path on the NFS server to be mounted. +func WithDirpath(path string) ClientOption { + return func(set *clientSettings) error { + set.dirpath = path + return nil + } +} + +// WithHostname overrides the default NFS `hostname` value. +// Used in the `AUTH_UNIX` authentication "flavor". +func WithHostname(hostname string) ClientOption { + return func(set *clientSettings) error { + set.hostname = hostname + return nil + } +} + +// WithLinkSeparator sets a string to be used when normalizing +// symbolic link targets during internal file system operations +// (ReadLink is unaffected). +// E.g. consider a link target `target\with slash`, by default the system +// interprets that as a single file whose name contains a `\`. +// If the link separator is set to `\`, then the link is converted to +// `target/with slash`, where `name` is now internally considered a directory. +// You'd want to use this if the NFS server is hosting links with relative +// targets that are formatted in the DOS/NT (or other) convention. +func WithLinkSeparator(separator string) ClientOption { + return func(set *clientSettings) error { + set.linkSeparator = separator + return nil + } +} + +// WithLinkLimit sets the maximum amount of times an +// operation will resolve a symbolic link chain, +// before it returns a recursion error. +func WithLinkLimit(limit uint) ClientOption { + return func(set *clientSettings) error { + set.linkLimit = limit + return nil + } +} + +func (set *clientSettings) fillDefaults() error { + if set.hostname == "" { + hostname, err := os.Hostname() + if err != nil { + return err + } + set.hostname = hostname + } + if set.defaultUID { + set.uid = uint32(os.Getuid()) + } + if set.defaultGID { + set.gid = uint32(os.Getgid()) + } + if set.dirpath == "" { + set.dirpath = "/" + } + return nil +} + +func (*goFS) ID() filesystem.ID { return GuestID } + +func (fsys *goFS) Lstat(name string) (fs.FileInfo, error) { + fsys.opMu.Lock() + defer fsys.opMu.Unlock() + const op = "lstat" + return getattr(op, fsys.target, name) +} + +func getattr(op string, target *nfs.Target, name string) (fs.FileInfo, error) { + info, err := target.Getattr(name) + if err != nil { + return nil, nfsToFsErr(op, name, err) + } + return goStatWrapper{ + name: path.Base(name), + FileInfo: info, + }, nil +} + +func (fsys *goFS) Stat(name string) (fs.FileInfo, error) { + fsys.opMu.Lock() + defer fsys.opMu.Unlock() + const ( + op = "stat" + depth = 0 + ) + return fsys.statLocked(op, name, depth) +} + +func (fsys *goFS) statLocked(op, name string, depth uint) (fs.FileInfo, error) { + target := fsys.target + info, err := getattr(op, target, name) + if err != nil { + return nil, err + } + if isLink := info.Mode().Type()&fs.ModeSymlink != 0; !isLink { + return info, nil + } + if depth++; depth >= fsys.linkLimit { + return nil, linkLimitError(op, name, fsys.linkLimit) + } + resolved, err := fsys.resolveLinkLocked(op, name) + if err != nil { + return nil, err + } + return fsys.statLocked(op, resolved, depth) +} + +func (fsys *goFS) resolveLinkLocked(op, name string) (string, error) { + link, err := fsys.target.Open(name) + if err != nil { + return "", nfsToFsErr(op, name, err) + } + target, err := link.Readlink() + if err != nil { + return "", nfsToFsErr(op, name, err) + } + if targetIsInvalid(target) { + const ( + err = generic.ConstError("link target must be relative") + kind = fserrors.InvalidItem + ) + pair := fmt.Sprintf( + `%s -> %s`, + name, target, + ) + return "", fserrors.New(op, pair, err, kind) + } + if sep := fsys.linkSeparator; sep != "" { + target = strings.ReplaceAll(target, sep, "/") + } + return path.Join( + path.Dir(name), + target, + ), nil +} + +func (in goStatWrapper) Name() string { return in.name } + +func (fsys *goFS) CreateFile(name string) (fs.File, error) { + fsys.opMu.Lock() + defer fsys.opMu.Unlock() + const perm = 0o666 + file, err := fsys.target.OpenFile(name, perm) + if err != nil { + const op = "create" + return nil, nfsToFsErr(op, name, err) + } + return &goFile{ + goShared: goShared{ + netName: name, + opMu: &fsys.opMu, + }, + file: file, + }, nil +} + +func (fsys *goFS) Open(name string) (fs.File, error) { + fsys.opMu.Lock() + defer fsys.opMu.Unlock() + const depth = 0 + return fsys.openLocked(name, depth) +} + +func (fsys *goFS) openLocked(name string, depth uint) (fs.File, error) { + const op = "open" + if !fs.ValidPath(name) { + return nil, fserrors.New(op, name, fs.ErrInvalid, fserrors.InvalidItem) + } + var ( + target = fsys.target + info, err = target.Getattr(name) + ) + if err != nil { + return nil, nfsToFsErr(op, name, err) + } + shared := goShared{ + netName: name, + opMu: &fsys.opMu, + } + switch typ := info.Mode().Type(); { + case typ.IsRegular(): + file, err := target.Open(name) + if err != nil { + return nil, nfsToFsErr(op, name, err) + } + return &goFile{ + goShared: shared, + file: file, + }, nil + case typ.IsDir(): + return &goDirectory{ + goShared: shared, + target: fsys.target, + }, nil + case typ&fs.ModeSymlink != 0: + if depth++; depth >= fsys.linkLimit { + return nil, linkLimitError(op, name, fsys.linkLimit) + } + resolved, err := fsys.resolveLinkLocked(op, name) + if err != nil { + return nil, err + } + return fsys.openLocked(resolved, depth) + default: + return nil, fmt.Errorf( + `open "%s": file type "%v" %w`, + name, typ, errors.ErrUnsupported, + ) + } +} + +func (fsys *goFS) Symlink(oldname, newname string) error { + if err := fsys.target.Symlink(newname, oldname); err != nil { + return &os.LinkError{ + Op: "symlink", + Old: oldname, + New: newname, + Err: err, + } + } + return nil +} + +func (fsys *goFS) Readlink(name string) (string, error) { + fsys.opMu.Lock() + defer fsys.opMu.Unlock() + link, err := fsys.target.Open(name) + if err != nil { + const op = "readlink" + return "", nfsToFsErr(op, name, err) + } + return link.Readlink() +} + +func (f *goFile) refreshLocked() error { + var ( + target = f.file.Target + name = f.netName + ) + info, err := target.Getattr(name) + if err != nil { + return err + } + typ := info.Mode().Type() + if !typ.IsRegular() { + return fmt.Errorf( + `refresh "%s": file type changed from regular to %v`, + name, fsTypeName(typ), + ) + } + cur, err := f.file.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + file, err := target.Open(name) + if err != nil { + return err + } + sought, err := file.Seek(cur, io.SeekStart) + if err != nil { + return err + } + if err := compareOffsets(sought, cur); err != nil { + return err + } + f.file = file + return nil +} + +func fsTypeName(typ fs.FileMode) string { + switch typ { + case fs.FileMode(0): + return "regular" + case fs.ModeDir: + return "directory" + case fs.ModeSymlink: + return "symbolic link" + case fs.ModeNamedPipe: + return "named pipe" + case fs.ModeSocket: + return "socket" + case fs.ModeDevice: + return "device" + case fs.ModeCharDevice: + return "character device" + default: + return "irregular" + } +} + +func (f *goFile) Stat() (fs.FileInfo, error) { + f.opMu.Lock() + defer f.opMu.Unlock() + const op = "stat" + return getattr(op, f.file.Target, f.netName) +} + +func (f *goFile) Read(p []byte) (int, error) { + f.opMu.Lock() + defer f.opMu.Unlock() + return f.readLocked(p) +} + +func (f *goFile) readLocked(p []byte) (int, error) { + n, err := f.file.Read(p) + if err != nil { + if errors.Is(err, io.EOF) { + return n, err + } + const op = "read" + err = nfsToFsErr(op, f.netName, err) + if !errors.Is(err, errStale) { + return n, err + } + if err := f.refreshLocked(); err != nil { + return n, nfsToFsErr(op, f.netName, err) + } + return f.readLocked(p) + } + return n, nil +} + +func (f *goFile) Write(p []byte) (int, error) { + f.opMu.Lock() + defer f.opMu.Unlock() + return f.writeLocked(p) +} + +func (f *goFile) writeLocked(p []byte) (int, error) { + n, err := f.file.Write(p) + if err != nil { + const op = "write" + err = nfsToFsErr(op, f.netName, err) + if !errors.Is(err, errStale) { + return n, err + } + if err := f.refreshLocked(); err != nil { + return n, nfsToFsErr(op, f.netName, err) + } + return f.writeLocked(p) + } + return n, nil +} + +func (f *goFile) Seek(offset int64, whence int) (int64, error) { + f.opMu.Lock() + defer f.opMu.Unlock() + off, err := f.file.Seek(offset, whence) + if err != nil { + const op = "seek" + return off, nfsToFsErr(op, f.netName, err) + } + return off, nil +} + +func (f *goFile) Close() error { + f.opMu.Lock() + defer f.opMu.Unlock() + if err := f.file.Close(); err != nil { + const op = "close" + err = nfsToFsErr(op, f.netName, err) + if !errors.Is(err, errStale) { + return err + } + } + return nil +} + +func (dir *goDirectory) Read([]byte) (int, error) { + return -1, errors.ErrUnsupported +} + +func (dir *goDirectory) ReadDir(count int) ([]fs.DirEntry, error) { + dir.opMu.Lock() + defer dir.opMu.Unlock() + entries := dir.entries + if entries == nil { + var ( + err error + target = dir.target + name = dir.netName + ) + if entries, err = target.ReadDirPlus(name); err != nil { + const op = "readdir" + return nil, nfsToFsErr(op, dir.netName, err) + } + dir.entries = entries + } + entriesLeft := len(entries) + if entriesLeft == 0 && count > 0 { + return nil, io.EOF + } + if count > 0 && entriesLeft > count { + entriesLeft = count + } + list := make([]fs.DirEntry, entriesLeft) + for i, ent := range entries[:entriesLeft] { + list[i] = goEnt{EntryPlus: ent} + } + dir.entries = entries[entriesLeft:] + return list, nil +} + +func (dir *goDirectory) Stat() (fs.FileInfo, error) { + dir.opMu.Lock() + defer dir.opMu.Unlock() + const op = "stat" + return getattr(op, dir.target, dir.netName) +} + +func (*goDirectory) Close() error { return nil } + +func (ent goEnt) Info() (fs.FileInfo, error) { return &ent.Attr.Attr, nil } + +func (ent goEnt) Type() fs.FileMode { return ent.Mode() } + +func nfsToFsErr(op, name string, err error) error { + var kind fserrors.Kind + switch { + case errors.Is(err, fs.ErrPermission): + kind = fserrors.Permission + case errors.Is(err, fs.ErrExist): + kind = fserrors.Exist + case errors.Is(err, fs.ErrNotExist): + kind = fserrors.NotExist + default: + const NFS3ERR_JUKEBOX = 10008 + var nfsError *nfs.Error + if errors.As(err, &nfsError) { + switch nfsError.ErrorNum { + case nfs.NFS3ErrStale: + return errStale + case nfs.NFS3ErrInval, nfs.NFS3ErrNameTooLong, + nfs.NFS3ErrRemote, nfs.NFS3ErrBadType: + kind = fserrors.InvalidItem + case nfs.NFS3ErrPerm, nfs.NFS3ErrAcces: + kind = fserrors.Permission + case nfs.NFS3ErrIO, nfs.NFS3ErrNXIO, + nfs.NFS3ErrXDev, nfs.NFS3ErrNoDev, + nfs.NFS3ErrFBig, nfs.NFS3ErrNoSpc, + nfs.NFS3ErrMLink, nfs.NFS3ErrDQuot, + nfs.NFS3ErrBadHandle, nfs.NFS3ErrNotSync, + nfs.NFS3ErrBadCookie, nfs.NFS3ErrTooSmall, + nfs.NFS3ErrServerFault, NFS3ERR_JUKEBOX: + // NOTE: Jukebox is technically a temporary error + // but we have no analog for those yet. + kind = fserrors.IO + case nfs.NFS3ErrIsDir: + kind = fserrors.IsDir + case nfs.NFS3ErrNotDir: + kind = fserrors.NotDir + case nfs.NFS3ErrNotEmpty: + kind = fserrors.NotEmpty + case nfs.NFS3ErrROFS: + kind = fserrors.ReadOnly + } + } + } + return fserrors.New(op, name, err, kind) +} + +func linkLimitError(op, name string, limit uint) error { + const kind = fserrors.Recursion + err := fmt.Errorf( + "reached symbolic link resolution limit (%d) during operation", + limit, + ) + return fserrors.New(op, name, err, kind) +} diff --git a/internal/filesystem/nfs/client_internal_test.go b/internal/filesystem/nfs/client_internal_test.go new file mode 100644 index 00000000..01c57ec0 --- /dev/null +++ b/internal/filesystem/nfs/client_internal_test.go @@ -0,0 +1,25 @@ +package nfs + +import ( + "io" + "io/fs" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" +) + +var ( + _ fs.FS = (*goFS)(nil) + _ fs.StatFS = (*goFS)(nil) + _ filesystem.IDFS = (*goFS)(nil) + _ filesystem.CreateFileFS = (*goFS)(nil) + _ interface { + filesystem.LinkStater + filesystem.LinkReader + filesystem.LinkMaker + } = (*goFS)(nil) + _ fs.File = (*goFile)(nil) + _ io.Seeker = (*goFile)(nil) + _ io.Writer = (*goFile)(nil) + _ fs.ReadDirFile = (*goDirectory)(nil) + _ fs.DirEntry = (*goEnt)(nil) +) diff --git a/internal/filesystem/nfs/doc.go b/internal/filesystem/nfs/doc.go new file mode 100644 index 00000000..25e40fc4 --- /dev/null +++ b/internal/filesystem/nfs/doc.go @@ -0,0 +1,3 @@ +// Package nfs implements server and client file systems +// for the Network File System protocol. +package nfs diff --git a/internal/filesystem/nfs/mountpoint.go b/internal/filesystem/nfs/mountpoint.go new file mode 100644 index 00000000..be2c31b1 --- /dev/null +++ b/internal/filesystem/nfs/mountpoint.go @@ -0,0 +1,130 @@ +package nfs + +import ( + "encoding/json" + "errors" + "io" + "io/fs" + "net" + "os" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" + "github.com/djdv/go-filesystem-utils/internal/generic" + maddrc "github.com/djdv/go-filesystem-utils/internal/multiaddr" + "github.com/go-git/go-billy/v5/helper/polyfill" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/willscott/go-nfs" + nfshelper "github.com/willscott/go-nfs/helpers" +) + +type ( + // Host holds metadata required to host + // a file system as an NFS server. + Host struct { + Maddr multiaddr.Multiaddr `json:"maddr,omitempty"` + } + // Guest holds metadata required to establish + // a client connection to an NFS server. + Guest struct { + Maddr multiaddr.Multiaddr `json:"maddr,omitempty"` + Hostname string `json:"hostname,omitempty"` + Dirpath string `json:"dirpath,omitempty"` + LinkSeparator string `json:"linkSeparator,omitempty"` + LinkLimit uint `json:"linkLimit,omitempty"` + UID uint32 `json:"uid,omitempty"` + GID uint32 `json:"gid,omitempty"` + } +) + +const ( + HostID filesystem.Host = "NFS" + GuestID filesystem.ID = "NFS" +) + +func (*Host) HostID() filesystem.Host { return HostID } + +func (nh *Host) UnmarshalJSON(b []byte) error { + // multiformats/go-multiaddr issue #100 + var maddrWorkaround struct { + Maddr maddrc.Multiaddr `json:"maddr,omitempty"` + } + if err := json.Unmarshal(b, &maddrWorkaround); err != nil { + return err + } + nh.Maddr = maddrWorkaround.Maddr.Multiaddr + return nil +} + +func (nh *Host) Mount(fsys fs.FS) (io.Closer, error) { + listener, err := manet.Listen(nh.Maddr) + if err != nil { + return nil, err + } + const cacheLimit = 1024 + var ( + netFsys = &netFS{fsys: fsys} + billyFsys = polyfill.New(netFsys) + nfsHandler = nfshelper.NewNullAuthHandler(billyFsys) + cachedHandler = nfshelper.NewCachingHandler(nfsHandler, cacheLimit) + goListener = manet.NetListener(listener) + errsCh = make(chan error, 1) + closerFn generic.Closer = func() error { + if err := listener.Close(); err != nil { + return err + } + if err := <-errsCh; !errors.Is(err, net.ErrClosed) { + return err + } + return nil + } + ) + // The NFS library has verbose logging by default. + // If the operator has not specified a log level, + // override the library's default level. + // (Primarily to suppress `ENOENT` errors in the console.) + const nfslibEnvKey = "LOG_LEVEL" + if _, set := os.LookupEnv(nfslibEnvKey); !set { + nfs.Log.SetLevel(nfs.PanicLevel) + } + go func() { errsCh <- nfs.Serve(goListener, cachedHandler) }() + return closerFn, nil +} + +func (*Guest) GuestID() filesystem.ID { return GuestID } +func (gn *Guest) UnmarshalJSON(b []byte) error { + // multiformats/go-multiaddr issue #100 + var maddrWorkaround struct { + Maddr maddrc.Multiaddr `json:"maddr,omitempty"` + } + if err := json.Unmarshal(b, &maddrWorkaround); err != nil { + return err + } + gn.Maddr = maddrWorkaround.Maddr.Multiaddr + return json.Unmarshal(b, &struct { + Hostname *string `json:"hostname,omitempty"` + Dirpath *string `json:"dirpath,omitempty"` + LinkSeparator *string `json:"linkSeparator,omitempty"` + LinkLimit *uint `json:"linkLimit,omitempty"` + UID *uint32 `json:"uid,omitempty"` + GID *uint32 `json:"gid,omitempty"` + }{ + Hostname: &gn.Hostname, + Dirpath: &gn.Dirpath, + LinkSeparator: &gn.LinkSeparator, + LinkLimit: &gn.LinkLimit, + UID: &gn.UID, + GID: &gn.GID, + }) +} + +func (gn *Guest) MakeFS() (fs.FS, error) { + return NewNFSGuest(gn.Maddr, + WithHostname(gn.Hostname), + WithDirpath(gn.Dirpath), + WithLinkSeparator(gn.LinkSeparator), + WithLinkLimit(gn.LinkLimit), + WithUID(gn.UID), + WithGID(gn.GID), + ) +} diff --git a/internal/filesystem/nfs/path.go b/internal/filesystem/nfs/path.go new file mode 100644 index 00000000..0c98c3a4 --- /dev/null +++ b/internal/filesystem/nfs/path.go @@ -0,0 +1,66 @@ +package nfs + +const ( + posixSeparator = '/' + dosSeparator = '\\' +) + +func targetIsInvalid(path string) bool { + if len(path) >= 1 && path[0] == posixSeparator { + return true + } + if isVolume(path) { + return true + } + return false +} + +// isVolume is a modification of [filepath.VolumeName] +// (and its callees) for non-Windows GOOS systems. +func isVolume(path string) bool { + const ( + driveLetterSize = 2 + uncSlashCount = 2 + uncPrefix = `\\.\UNC` + ) + switch { + case len(path) >= driveLetterSize && path[1] == ':': + return true + case pathHasPrefixFold(path, uncPrefix): + return true + case len(path) >= uncSlashCount && isSlash(path[1]): + return true + default: + return false + } +} + +func isSlash(c uint8) bool { + return c == dosSeparator || c == posixSeparator +} + +func pathHasPrefixFold(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if isSlash(prefix[i]) { + if !isSlash(s[i]) { + return false + } + } else if toUpper(prefix[i]) != toUpper(s[i]) { + return false + } + } + if len(s) > len(prefix) && !isSlash(s[len(prefix)]) { + return false + } + return true +} + +func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') + } + return c +} diff --git a/internal/filesystem/nfs/path_test.go b/internal/filesystem/nfs/path_test.go new file mode 100644 index 00000000..b5675b6c --- /dev/null +++ b/internal/filesystem/nfs/path_test.go @@ -0,0 +1,76 @@ +package nfs + +import ( + "testing" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" +) + +func TestPath(t *testing.T) { + t.Parallel() + t.Run("link target invalid", _isInvalidLink) +} + +func _isInvalidLink(t *testing.T) { + t.Parallel() + for _, test := range []struct { + path string + invalid bool + }{ + {path: ""}, + {path: filesystem.Root}, + {path: ".."}, + { + path: "/POSIX/absolute", + invalid: true, + }, + {path: "POSIX/relative"}, + {path: "../POSIX/relative"}, + { + path: "C:", + invalid: true, + }, + { + path: `C:\`, + invalid: true, + }, + { + path: `C:\DOS`, + invalid: true, + }, + {path: `\`}, + { + path: `\\server\share`, + invalid: true, + }, + { + path: `\\server\share\file`, + invalid: true, + }, + { + path: `//server/share`, + invalid: true, + }, + { + path: `//server/share/file`, + invalid: true, + }, + } { + var ( + path = test.path + want = test.invalid + ) + t.Run(path, func(t *testing.T) { + t.Parallel() + got := targetIsInvalid(path) + if got != want { + t.Errorf( + "path is-absolute mismatch"+ + "\n\tgot: %t"+ + "\n\twant:%t\n", + got, want, + ) + } + }) + } +} diff --git a/internal/filesystem/nfs/server.go b/internal/filesystem/nfs/server.go new file mode 100644 index 00000000..800091dc --- /dev/null +++ b/internal/filesystem/nfs/server.go @@ -0,0 +1,270 @@ +package nfs + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "strings" + "sync" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" + "github.com/go-git/go-billy/v5" +) + +type ( + netFS struct { + fsys fs.FS + } + netFile struct { + file fs.File + billyName string // "… as presented to Open." -v5 docs. + } + // netFileEx extends operation support + // of basic [fs.File]s. + netFileEx struct { + netFile + curMu sync.Mutex + } +) + +func (ns *netFS) Capabilities() billy.Capability { + return billy.WriteCapability | + billy.ReadCapability | + billy.ReadAndWriteCapability | + billy.SeekCapability | + billy.TruncateCapability +} + +func toGoPath(filename string) string { + if filename == "/" { + return filesystem.Root + } + return filename +} + +func (ns *netFS) Create(filename string) (billy.File, error) { + const ( + flag = os.O_RDWR | os.O_CREATE | os.O_TRUNC + perm = 0o666 + ) + return ns.OpenFile(filename, flag, perm) +} + +func (ns *netFS) Open(filename string) (billy.File, error) { + const ( + flag = os.O_RDONLY + perm = 0 + ) + return ns.OpenFile(filename, flag, perm) +} + +func (ns *netFS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + name := toGoPath(filename) + if !fs.ValidPath(name) { + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: fs.ErrInvalid, + } + } + file, err := filesystem.OpenFile(ns.fsys, name, flag, perm) + if err != nil { + return nil, err + } + netFile := netFile{ + billyName: filename, + file: file, + } + if _, ok := file.(io.ReaderAt); ok { + return &netFile, nil + } + return &netFileEx{ + netFile: netFile, + }, nil +} + +func (ns *netFS) Stat(filename string) (fs.FileInfo, error) { + return fs.Stat(ns.fsys, toGoPath(filename)) +} + +func (ns *netFS) Rename(oldpath, newpath string) error { + var ( + oldName = toGoPath(oldpath) + newName = toGoPath(newpath) + ) + return filesystem.Rename(ns.fsys, oldName, newName) +} + +func (ns *netFS) Remove(filename string) error { + return filesystem.Remove(ns.fsys, toGoPath(filename)) +} + +func (ns *netFS) Join(elem ...string) string { + if len(elem) == 0 { + return "/" + } + return path.Join(elem...) +} + +func (ns *netFS) ReadDir(path string) ([]fs.FileInfo, error) { + ents, err := fs.ReadDir(ns.fsys, toGoPath(path)) + if err != nil { + return nil, err + } + infos := make([]fs.FileInfo, len(ents)) + for i, ent := range ents { + info, err := ent.Info() + if err != nil { + return nil, err + } + infos[i] = info + } + return infos, nil +} + +func (ns *netFS) MkdirAll(filename string, perm os.FileMode) error { + const goDelimiter = "/" + var ( + name = toGoPath(filename) + fsys = ns.fsys + components = strings.Split(name, goDelimiter) + ) + for i, dir := range components { + var ( + fragment = append(components[:i], dir) + name = path.Join(fragment...) + ) + if err := filesystem.Mkdir(fsys, name, perm); err != nil { + if !errors.Is(err, fs.ErrExist) { + return err + } + } + } + return nil +} + +func (ns *netFS) Lstat(filename string) (fs.FileInfo, error) { + return filesystem.Lstat(ns.fsys, toGoPath(filename)) +} + +func (ns *netFS) Symlink(oldpath, newpath string) error { + var ( + oldName = toGoPath(oldpath) + newName = toGoPath(newpath) + ) + return filesystem.Symlink(ns.fsys, oldName, newName) +} + +func (ns *netFS) Readlink(filename string) (string, error) { + return filesystem.Readlink(ns.fsys, toGoPath(filename)) +} + +func (nf *netFile) Name() string { return nf.billyName } + +func (nf *netFile) Write(p []byte) (n int, err error) { + if writer, ok := nf.file.(io.Writer); ok { + return writer.Write(p) + } + const op = "write" + return -1, unsupportedOpErr(op, nf.billyName) +} + +func (nf *netFile) Read(p []byte) (int, error) { + return nf.file.Read(p) +} + +func (nf *netFile) ReadAt(p []byte, off int64) (int, error) { + // NOTE: interface checked during [Open]. + return nf.file.(io.ReaderAt).ReadAt(p, off) +} + +func (nf *netFileEx) Read(p []byte) (int, error) { + nf.curMu.Lock() + defer nf.curMu.Unlock() + return nf.file.Read(p) +} + +func (nf *netFileEx) ReadAt(p []byte, off int64) (int, error) { + nf.curMu.Lock() + defer nf.curMu.Unlock() + readSeeker, ok := nf.file.(io.ReadSeeker) + if !ok { + const op = "readat" + return -1, unsupportedOpErr(op, nf.billyName) + } + return readAtLocked(readSeeker, p, off) +} + +func readAtLocked(rs io.ReadSeeker, p []byte, off int64) (int, error) { + const errno = -1 + were, err := rs.Seek(0, io.SeekCurrent) + if err != nil { + return errno, err + } + sought, err := rs.Seek(off, io.SeekStart) + if err != nil { + return errno, err + } + if err := compareOffsets(sought, off); err != nil { + return errno, err + } + n, rErr := rs.Read(p) + where, err := rs.Seek(were, io.SeekStart) + if err != nil { + return errno, errors.Join(err, rErr) + } + if err := compareOffsets(where, were); err != nil { + return errno, errors.Join(err, rErr) + } + return n, rErr +} + +func (nf *netFile) Seek(offset int64, whence int) (int64, error) { + if seeker, ok := nf.file.(io.Seeker); ok { + return seeker.Seek(offset, whence) + } + const op = "seek" + return -1, unsupportedOpErr(op, nf.billyName) +} + +func (nf *netFile) Close() error { + return nf.file.Close() +} + +func (nf *netFile) Lock() error { + const op = "lock" + return unsupportedOpErr(op, nf.billyName) +} + +func (nf *netFile) Unlock() error { + const op = "unlock" + return unsupportedOpErr(op, nf.billyName) +} + +func (nf *netFile) Truncate(size int64) error { + if truncater, ok := nf.file.(filesystem.TruncateFile); ok { + return truncater.Truncate(size) + } + const op = "truncate" + return unsupportedOpErr(op, nf.billyName) +} + +func unsupportedOpErr(op, name string) error { + return fmt.Errorf( + op+` "%s": %w`, + name, errors.ErrUnsupported, + ) +} + +func compareOffsets(got, want int64) (err error) { + if got == want { + return nil + } + return fmt.Errorf( + "offset mismatch got %d expected %d", + got, want, + ) +} diff --git a/internal/filesystem/nfs/server_internal_test.go b/internal/filesystem/nfs/server_internal_test.go new file mode 100644 index 00000000..bf9d73b4 --- /dev/null +++ b/internal/filesystem/nfs/server_internal_test.go @@ -0,0 +1,9 @@ +package nfs + +import "github.com/go-git/go-billy/v5" + +var ( + _ billy.Basic = (*netFS)(nil) + _ billy.Symlink = (*netFS)(nil) + _ billy.File = (*netFile)(nil) +) diff --git a/internal/multiaddr/multiaddr.go b/internal/multiaddr/multiaddr.go new file mode 100644 index 00000000..c1b8621a --- /dev/null +++ b/internal/multiaddr/multiaddr.go @@ -0,0 +1,63 @@ +package multiaddr + +import ( + "encoding/json" + + "github.com/multiformats/go-multiaddr" +) + +// Multiaddr wraps the reference Multiaddr library +// adding deserialization support. +type Multiaddr struct{ multiaddr.Multiaddr } + +func (ma *Multiaddr) MarshalBinary() ([]byte, error) { + if maddr := ma.Multiaddr; maddr != nil { + return maddr.MarshalBinary() + } + return nil, nil +} + +func (ma *Multiaddr) UnmarshalBinary(b []byte) error { + maddr, err := multiaddr.NewMultiaddrBytes(b) + if err != nil { + return err + } + ma.Multiaddr = maddr + return nil +} + +func (ma *Multiaddr) MarshalText() ([]byte, error) { + if maddr := ma.Multiaddr; maddr != nil { + return maddr.MarshalText() + } + return []byte{}, nil +} + +func (ma *Multiaddr) UnmarshalText(b []byte) error { + maddr, err := multiaddr.NewMultiaddr(string(b)) + if err != nil { + return err + } + ma.Multiaddr = maddr + return nil +} + +func (ma *Multiaddr) MarshalJSON() ([]byte, error) { + if maddr := ma.Multiaddr; maddr != nil { + return maddr.MarshalJSON() + } + return []byte("null"), nil +} + +func (ma *Multiaddr) UnmarshalJSON(b []byte) error { + var maddrString string + if err := json.Unmarshal(b, &maddrString); err != nil { + return err + } + maddr, err := multiaddr.NewMultiaddr(maddrString) + if err != nil { + return err + } + ma.Multiaddr = maddr + return nil +}