diff --git a/.gitignore b/.gitignore index fcc7b9b..5bbfe5f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ httpfs_vfsdata.go bin/ coverage.out bin -build \ No newline at end of file +build +deploy-debug.sh \ No newline at end of file diff --git a/.gqlgen.yml b/.gqlgen.yml index 9f3f595..7363cf7 100644 --- a/.gqlgen.yml +++ b/.gqlgen.yml @@ -42,3 +42,7 @@ models: extraFields: F: type: "*github.com/anacrolix/torrent.PeerConn" + # TorrentProgress: + # fields: + # torrent: + # resolver: true diff --git a/.vscode/launch.json b/.vscode/launch.json index 4b9ec0e..bc0b8c4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,13 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}" + }, { "name": "Launch Package", "type": "go", diff --git a/Makefile b/Makefile index beef8f0..028fa69 100644 --- a/Makefile +++ b/Makefile @@ -34,11 +34,7 @@ go-generate: @echo " > Generating code files..." go generate ./... -.PHONY: help -all: help -help: Makefile - @echo - @echo " Choose a command run in "$(PROJECTNAME)":" - @echo - @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' - @echo +generate-graphql: src/delivery/graph/generated.go + +src/delivery/graph/generated.go: .gqlgen.yml graphql/* graphql/types/* cmd/generate-graphql/* + go run cmd/generate-graphql/main.go diff --git a/cmd/generate-graphlq/main.go b/cmd/generate-graphql/main.go similarity index 88% rename from cmd/generate-graphlq/main.go rename to cmd/generate-graphql/main.go index 97ef427..ea2d6aa 100644 --- a/cmd/generate-graphlq/main.go +++ b/cmd/generate-graphql/main.go @@ -10,14 +10,14 @@ import ( "github.com/99designs/gqlgen/codegen/config" ) -type plugin_ struct { +type fieldDirectiveFix struct { } -func (plugin_) Name() string { +func (fieldDirectiveFix) Name() string { return "Fix Directive hook called with wrong object" } -func (plugin_) GenerateCode(cfg *codegen.Data) error { +func (fieldDirectiveFix) GenerateCode(cfg *codegen.Data) error { for _, input := range cfg.Inputs { for _, field := range input.Fields { if field.GoFieldType == codegen.GoFieldVariable { @@ -56,7 +56,7 @@ func main() { } err = api.Generate(cfg, - api.AddPlugin(&plugin_{}), + api.AddPlugin(&fieldDirectiveFix{}), ) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go index fe63a17..3b4dce0 100644 --- a/cmd/tstor/main.go +++ b/cmd/tstor/main.go @@ -15,7 +15,7 @@ import ( "git.kmsign.ru/royalcat/tstor/src/config" "git.kmsign.ru/royalcat/tstor/src/host" - "git.kmsign.ru/royalcat/tstor/src/host/filestorage" + "git.kmsign.ru/royalcat/tstor/src/host/datastorage" "git.kmsign.ru/royalcat/tstor/src/host/service" "git.kmsign.ru/royalcat/tstor/src/host/store" "git.kmsign.ru/royalcat/tstor/src/host/vfs" @@ -90,13 +90,13 @@ func run(configPath string) error { return fmt.Errorf("error creating node ID: %w", err) } - st, _, err := filestorage.Setup(conf.TorrentClient) + st, _, err := datastorage.Setup(conf.TorrentClient) if err != nil { return err } defer st.Close() - excludedFilesStore, err := store.NewExcludedFiles(conf.TorrentClient.MetadataFolder, st) + excludedFilesStore, err := store.NewFileMappings(conf.TorrentClient.MetadataFolder, st) if err != nil { return err } @@ -238,5 +238,5 @@ func run(configPath string) error { signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) <-sigChan - return nil + return ts.Close() } diff --git a/go.mod b/go.mod index eb52aa8..047396f 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,14 @@ module git.kmsign.ru/royalcat/tstor -go 1.21 +go 1.22.1 require ( github.com/99designs/gqlgen v0.17.43 - github.com/anacrolix/dht/v2 v2.21.0 + github.com/RoaringBitmap/roaring v1.2.3 + github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 github.com/anacrolix/missinggo/v2 v2.7.3 - github.com/anacrolix/torrent v1.54.0 + github.com/anacrolix/torrent v1.55.0 github.com/billziss-gh/cgofuse v1.5.0 github.com/bodgit/sevenzip v1.4.5 github.com/dgraph-io/badger/v4 v4.2.0 @@ -15,6 +16,10 @@ require ( github.com/gin-contrib/pprof v1.4.0 github.com/gin-gonic/gin v1.9.1 github.com/go-git/go-billy/v5 v5.5.0 + github.com/gofrs/uuid/v5 v5.0.0 + github.com/google/uuid v1.5.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/providers/file v0.1.0 @@ -22,27 +27,24 @@ require ( github.com/knadh/koanf/v2 v2.0.1 github.com/lmittmann/tint v1.0.4 github.com/nwaples/rardecode/v2 v2.0.0-beta.2 - github.com/philippgille/gokv v0.6.0 - github.com/philippgille/gokv/badgerdb v0.6.0 - github.com/philippgille/gokv/encoding v0.6.0 + github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73 github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.27.0 github.com/vektah/gqlparser/v2 v2.5.11 github.com/willscott/go-nfs v0.0.2 + go.uber.org/multierr v1.11.0 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b golang.org/x/net v0.19.0 ) require ( - github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect - github.com/RoaringBitmap/roaring v1.2.3 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/anacrolix/chansync v0.3.0 // indirect github.com/anacrolix/envpprof v1.3.0 // indirect - github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 // indirect + github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect github.com/anacrolix/go-libutp v1.3.1 // indirect github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo/perf v1.0.0 // indirect @@ -60,12 +62,11 @@ require ( github.com/bodgit/windows v1.0.1 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/badger v1.6.0 // indirect - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -74,24 +75,21 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect github.com/go-llsqlite/crawshaw v0.4.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.0.0 // indirect + github.com/golang/glog v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.3 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/flatbuffers v1.12.1 // indirect - github.com/google/uuid v1.5.0 // indirect + github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.4 // indirect @@ -106,7 +104,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 // indirect github.com/pierrec/lz4/v4 v4.1.19 // indirect github.com/pion/datachannel v1.5.2 // indirect github.com/pion/dtls/v2 v2.2.4 // indirect @@ -141,7 +138,7 @@ require ( github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.etcd.io/bbolt v1.3.6 // indirect - go.opencensus.io v0.22.5 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.8.0 // indirect go.opentelemetry.io/otel/trace v1.8.0 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect diff --git a/go.sum b/go.sum index 9475909..29df124 100644 --- a/go.sum +++ b/go.sum @@ -21,11 +21,10 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuFCPo= github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= -github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -49,15 +48,15 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U= github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= -github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs= -github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk= github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0= -github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg= -github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so= +github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0= github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o= github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= @@ -96,8 +95,8 @@ github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= -github.com/anacrolix/torrent v1.54.0 h1:sl+2J1pHjJWq6+5G861+Yc74k2XTc/m8ijaMQR/8+2k= -github.com/anacrolix/torrent v1.54.0/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk= +github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8= +github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs= github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= @@ -109,7 +108,6 @@ github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= @@ -137,6 +135,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -147,18 +147,13 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -176,7 +171,9 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -215,8 +212,8 @@ github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYz github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -233,18 +230,18 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= @@ -262,25 +259,30 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/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= @@ -290,6 +292,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -311,7 +314,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -322,7 +324,6 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -368,15 +369,12 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -406,24 +404,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/philippgille/gokv v0.0.0-20191001201555-5ac9a20de634/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc= -github.com/philippgille/gokv v0.5.1-0.20191011213304-eb77f15b9c61/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc= -github.com/philippgille/gokv v0.6.0 h1:fNEx/tSwV73nzlYd3iRYB8F+SEVJNNFzH1gsaT8SK2c= -github.com/philippgille/gokv v0.6.0/go.mod h1:tjXRFw9xDHgxLS8WJdfYotKGWp8TWqu4RdXjMDG/XBo= -github.com/philippgille/gokv/badgerdb v0.6.0 h1:4Qigf2SpyXLF8KaM5nA5/D/0aD/bZevuAnrW4ZsDsjA= -github.com/philippgille/gokv/badgerdb v0.6.0/go.mod h1:3u2avs8gtmCc0R0Bw4jKV8aaDfLb5V9JToSASyhpFGM= -github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:SjxSrCoeYrYn85oTtroyG1ePY8aE72nvLQlw8IYwAN8= -github.com/philippgille/gokv/encoding v0.6.0 h1:P1TN+Aulpd6Qd7qcLqgPwoxzOQ42UHBXOovWvFxJRI8= -github.com/philippgille/gokv/encoding v0.6.0/go.mod h1:/yKvq2BKJlKJsH7KMDrhDlEw2Pt3V1nKyFhs4iOqz5U= -github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61 h1:4tVyBgfpK0NSqu7tNZTwYfC/pbyWUR2y+O7mxEg5BTQ= -github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:EUc+s9ONc1+VOr9NUEd8S0YbGRrQd/gz/p+2tvwt12s= -github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 h1:ril/jI0JgXNjPWwDkvcRxlZ09kgHXV2349xChjbsQ4o= -github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:2dBhsJgY/yVIkjY5V3AnDUxUbEPzT6uQ3LvoVT8TR20= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4= github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -505,9 +489,12 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7 h1:fmBTD0RaTvWbd6KrgLVXSDUJ4dWTPOFUdkdHp+kYvRM= +github.com/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A= +github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73 h1:zeFE8Nx11oD6In+f+VDYwGH72t7NV6L5dqaNbDIhB1E= +github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= @@ -526,12 +513,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:X github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4= github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -558,7 +541,6 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= @@ -574,7 +556,6 @@ github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee 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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -588,19 +569,20 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg= go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM= go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY= go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -660,13 +642,13 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -703,7 +685,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -712,11 +693,9 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -831,20 +810,26 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/graphql/mutation.graphql b/graphql/mutation.graphql index 2d95198..80830ed 100644 --- a/graphql/mutation.graphql +++ b/graphql/mutation.graphql @@ -1,10 +1,20 @@ type Mutation { validateTorrents(filter: TorrentFilter!): Boolean! cleanupTorrents(files: Boolean, dryRun: Boolean!): Int! + downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse } + input TorrentFilter @oneOf { everything: Boolean infohash: String # pathGlob: String! +} + +type DownloadTorrentResponse { + task: Task +} + +type Task { + id: ID! } \ No newline at end of file diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 7942192..9342a90 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -1,4 +1,5 @@ directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION +directive @stream on FIELD_DEFINITION scalar DateTime @@ -6,4 +7,3 @@ type Schema { query: Query mutation: Mutation } - diff --git a/graphql/subscription.graphql b/graphql/subscription.graphql new file mode 100644 index 0000000..9df09ee --- /dev/null +++ b/graphql/subscription.graphql @@ -0,0 +1,16 @@ +type Subscription { + taskProgress(taskID: ID!): Progress + torrentDownloadUpdates: TorrentProgress +} + + +type TorrentProgress implements Progress { + torrent: Torrent! + current: Int! + total: Int! +} + +interface Progress { + current: Int! + total: Int! +} \ No newline at end of file diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go new file mode 100644 index 0000000..923ef0e --- /dev/null +++ b/pkg/uuid/uuid.go @@ -0,0 +1,102 @@ +package uuid + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "time" + + fuuid "github.com/gofrs/uuid/v5" +) + +var Nil = UUID{} + +type UUIDList = []UUID + +type UUID struct { + fuuid.UUID +} + +func New() UUID { + return UUID{fuuid.Must(fuuid.NewV7())} +} + +func NewFromTime(t time.Time) UUID { + gen := fuuid.NewGenWithOptions( + fuuid.WithEpochFunc(func() time.Time { return t }), + ) + return UUID{fuuid.Must(gen.NewV7())} +} + +func NewP() *UUID { + return &UUID{fuuid.Must(fuuid.NewV7())} +} + +func FromString(text string) (UUID, error) { + u, err := fuuid.FromString(text) + if err != nil { + return Nil, err + } + + return UUID{u}, nil +} + +func MustFromString(text string) UUID { + u, err := fuuid.FromString(text) + if err != nil { + panic(err) + } + + return UUID{u} +} + +func FromBytes(input []byte) (UUID, error) { + u, err := fuuid.FromBytes(input) + if err != nil { + return Nil, err + } + + return UUID{u}, nil +} + +func (a *UUID) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + if s == "" { + a.UUID = fuuid.Nil + return nil + } + + return a.UUID.Parse(s) +} + +func (a UUID) MarshalJSON() ([]byte, error) { + if a.IsNil() { + return json.Marshal("") + } + + return json.Marshal(a.UUID) +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface +func (u *UUID) UnmarshalGQL(v interface{}) error { + id, ok := v.(string) + if !ok { + return fmt.Errorf("uuid must be a string") + } + + return u.Parse(id) +} + +// MarshalGQL implements the graphql.Marshaler interface +func (u UUID) MarshalGQL(w io.Writer) { + b := []byte(strconv.Quote(u.String())) + _, err := w.Write(b) + if err != nil { + panic(err) + } +} diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go index e91e331..a4f51df 100644 --- a/src/delivery/graphql/generated.go +++ b/src/delivery/graphql/generated.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "io" "strconv" "sync" "sync/atomic" @@ -41,16 +42,23 @@ type Config struct { type ResolverRoot interface { Mutation() MutationResolver Query() QueryResolver + Subscription() SubscriptionResolver Torrent() TorrentResolver } type DirectiveRoot struct { - OneOf func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) + OneOf func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) + Stream func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) } type ComplexityRoot struct { + DownloadTorrentResponse struct { + Task func(childComplexity int) int + } + Mutation struct { CleanupTorrents func(childComplexity int, files *bool, dryRun bool) int + DownloadTorrent func(childComplexity int, infohash string, file *string) int ValidateTorrents func(childComplexity int, filter model.TorrentFilter) int } @@ -63,6 +71,15 @@ type ComplexityRoot struct { Query func(childComplexity int) int } + Subscription struct { + TaskProgress func(childComplexity int, taskID string) int + TorrentDownloadUpdates func(childComplexity int) int + } + + Task struct { + ID func(childComplexity int) int + } + Torrent struct { BytesCompleted func(childComplexity int) int BytesMissing func(childComplexity int) int @@ -87,15 +104,26 @@ type ComplexityRoot struct { IP func(childComplexity int) int Port func(childComplexity int) int } + + TorrentProgress struct { + Current func(childComplexity int) int + Torrent func(childComplexity int) int + Total func(childComplexity int) int + } } type MutationResolver interface { ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error) CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error) + DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error) } type QueryResolver interface { Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error) } +type SubscriptionResolver interface { + TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error) + TorrentDownloadUpdates(ctx context.Context) (<-chan *model.TorrentProgress, error) +} type TorrentResolver interface { Name(ctx context.Context, obj *model.Torrent) (string, error) @@ -123,6 +151,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "DownloadTorrentResponse.task": + if e.complexity.DownloadTorrentResponse.Task == nil { + break + } + + return e.complexity.DownloadTorrentResponse.Task(childComplexity), true + case "Mutation.cleanupTorrents": if e.complexity.Mutation.CleanupTorrents == nil { break @@ -135,6 +170,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CleanupTorrents(childComplexity, args["files"].(*bool), args["dryRun"].(bool)), true + case "Mutation.downloadTorrent": + if e.complexity.Mutation.DownloadTorrent == nil { + break + } + + args, err := ec.field_Mutation_downloadTorrent_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DownloadTorrent(childComplexity, args["infohash"].(string), args["file"].(*string)), true + case "Mutation.validateTorrents": if e.complexity.Mutation.ValidateTorrents == nil { break @@ -173,6 +220,32 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Schema.Query(childComplexity), true + case "Subscription.taskProgress": + if e.complexity.Subscription.TaskProgress == nil { + break + } + + args, err := ec.field_Subscription_taskProgress_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Subscription.TaskProgress(childComplexity, args["taskID"].(string)), true + + case "Subscription.torrentDownloadUpdates": + if e.complexity.Subscription.TorrentDownloadUpdates == nil { + break + } + + return e.complexity.Subscription.TorrentDownloadUpdates(childComplexity), true + + case "Task.id": + if e.complexity.Task.ID == nil { + break + } + + return e.complexity.Task.ID(childComplexity), true + case "Torrent.bytesCompleted": if e.complexity.Torrent.BytesCompleted == nil { break @@ -285,6 +358,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TorrentPeer.Port(childComplexity), true + case "TorrentProgress.current": + if e.complexity.TorrentProgress.Current == nil { + break + } + + return e.complexity.TorrentProgress.Current(childComplexity), true + + case "TorrentProgress.torrent": + if e.complexity.TorrentProgress.Torrent == nil { + break + } + + return e.complexity.TorrentProgress.Torrent(childComplexity), true + + case "TorrentProgress.total": + if e.complexity.TorrentProgress.Total == nil { + break + } + + return e.complexity.TorrentProgress.Total(childComplexity), true + } return 0, false } @@ -345,6 +439,23 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { var buf bytes.Buffer data.MarshalGQL(&buf) + return &graphql.Response{ + Data: buf.Bytes(), + } + } + case ast.Subscription: + next := ec._Subscription(ctx, rc.Operation.SelectionSet) + + var buf bytes.Buffer + return func(ctx context.Context) *graphql.Response { + buf.Reset() + data := next(ctx) + + if data == nil { + return nil + } + data.MarshalGQL(&buf) + return &graphql.Response{ Data: buf.Bytes(), } @@ -400,12 +511,22 @@ var sources = []*ast.Source{ {Name: "../../../graphql/mutation.graphql", Input: `type Mutation { validateTorrents(filter: TorrentFilter!): Boolean! cleanupTorrents(files: Boolean, dryRun: Boolean!): Int! + downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse } + input TorrentFilter @oneOf { everything: Boolean infohash: String # pathGlob: String! +} + +type DownloadTorrentResponse { + task: Task +} + +type Task { + id: ID! }`, BuiltIn: false}, {Name: "../../../graphql/query.graphql", Input: `type Query { torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]! @@ -451,6 +572,7 @@ input BooleanFilter @oneOf { eq: Boolean }`, BuiltIn: false}, {Name: "../../../graphql/schema.graphql", Input: `directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION +directive @stream on FIELD_DEFINITION scalar DateTime @@ -460,6 +582,22 @@ type Schema { } `, BuiltIn: false}, + {Name: "../../../graphql/subscription.graphql", Input: `type Subscription { + taskProgress(taskID: ID!): Progress + torrentDownloadUpdates: TorrentProgress +} + + +type TorrentProgress implements Progress { + torrent: Torrent! + current: Int! + total: Int! +} + +interface Progress { + current: Int! + total: Int! +}`, BuiltIn: false}, {Name: "../../../graphql/types/torrent.graphql", Input: `type Torrent { name: String! infohash: String! @@ -515,6 +653,30 @@ func (ec *executionContext) field_Mutation_cleanupTorrents_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_downloadTorrent_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["infohash"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("infohash")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["infohash"] = arg0 + var arg1 *string + if tmp, ok := rawArgs["file"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file")) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["file"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_validateTorrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -569,6 +731,21 @@ func (ec *executionContext) field_Query_torrents_args(ctx context.Context, rawAr return args, nil } +func (ec *executionContext) field_Subscription_taskProgress_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["taskID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("taskID")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["taskID"] = arg0 + return args, nil +} + func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -607,6 +784,51 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _DownloadTorrentResponse_task(ctx context.Context, field graphql.CollectedField, obj *model.DownloadTorrentResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DownloadTorrentResponse_task(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Task, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Task) + fc.Result = res + return ec.marshalOTask2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTask(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DownloadTorrentResponse_task(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DownloadTorrentResponse", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Task_id(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Task", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_validateTorrents(ctx, field) if err != nil { @@ -717,6 +939,62 @@ func (ec *executionContext) fieldContext_Mutation_cleanupTorrents(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_downloadTorrent(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_downloadTorrent(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DownloadTorrent(rctx, fc.Args["infohash"].(string), fc.Args["file"].(*string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.DownloadTorrentResponse) + fc.Result = res + return ec.marshalODownloadTorrentResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDownloadTorrentResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_downloadTorrent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "task": + return ec.fieldContext_DownloadTorrentResponse_task(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DownloadTorrentResponse", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_downloadTorrent_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_torrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_torrents(ctx, field) if err != nil { @@ -986,6 +1264,8 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi return ec.fieldContext_Mutation_validateTorrents(ctx, field) case "cleanupTorrents": return ec.fieldContext_Mutation_cleanupTorrents(ctx, field) + case "downloadTorrent": + return ec.fieldContext_Mutation_downloadTorrent(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Mutation", field.Name) }, @@ -993,6 +1273,179 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _Subscription_taskProgress(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) { + fc, err := ec.fieldContext_Subscription_taskProgress(ctx, field) + if err != nil { + return nil + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Subscription().TaskProgress(rctx, fc.Args["taskID"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return nil + } + if resTmp == nil { + return nil + } + return func(ctx context.Context) graphql.Marshaler { + select { + case res, ok := <-resTmp.(<-chan model.Progress): + if !ok { + return nil + } + return graphql.WriterFunc(func(w io.Writer) { + w.Write([]byte{'{'}) + graphql.MarshalString(field.Alias).MarshalGQL(w) + w.Write([]byte{':'}) + ec.marshalOProgress2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐProgress(ctx, field.Selections, res).MarshalGQL(w) + w.Write([]byte{'}'}) + }) + case <-ctx.Done(): + return nil + } + } +} + +func (ec *executionContext) fieldContext_Subscription_taskProgress(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Subscription", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Subscription_taskProgress_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Subscription_torrentDownloadUpdates(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) { + fc, err := ec.fieldContext_Subscription_torrentDownloadUpdates(ctx, field) + if err != nil { + return nil + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Subscription().TorrentDownloadUpdates(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return nil + } + if resTmp == nil { + return nil + } + return func(ctx context.Context) graphql.Marshaler { + select { + case res, ok := <-resTmp.(<-chan *model.TorrentProgress): + if !ok { + return nil + } + return graphql.WriterFunc(func(w io.Writer) { + w.Write([]byte{'{'}) + graphql.MarshalString(field.Alias).MarshalGQL(w) + w.Write([]byte{':'}) + ec.marshalOTorrentProgress2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentProgress(ctx, field.Selections, res).MarshalGQL(w) + w.Write([]byte{'}'}) + }) + case <-ctx.Done(): + return nil + } + } +} + +func (ec *executionContext) fieldContext_Subscription_torrentDownloadUpdates(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Subscription", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "torrent": + return ec.fieldContext_TorrentProgress_torrent(ctx, field) + case "current": + return ec.fieldContext_TorrentProgress_current(ctx, field) + case "total": + return ec.fieldContext_TorrentProgress_total(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TorrentProgress", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *model.Task) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Task_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Task_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Task", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Torrent_name(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Torrent_name(ctx, field) if err != nil { @@ -1725,6 +2178,156 @@ func (ec *executionContext) fieldContext_TorrentPeer_clientName(ctx context.Cont return fc, nil } +func (ec *executionContext) _TorrentProgress_torrent(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentProgress_torrent(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Torrent, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Torrent) + fc.Result = res + return ec.marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrent(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TorrentProgress_torrent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentProgress", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_Torrent_name(ctx, field) + case "infohash": + return ec.fieldContext_Torrent_infohash(ctx, field) + case "bytesCompleted": + return ec.fieldContext_Torrent_bytesCompleted(ctx, field) + case "torrentFilePath": + return ec.fieldContext_Torrent_torrentFilePath(ctx, field) + case "bytesMissing": + return ec.fieldContext_Torrent_bytesMissing(ctx, field) + case "files": + return ec.fieldContext_Torrent_files(ctx, field) + case "excludedFiles": + return ec.fieldContext_Torrent_excludedFiles(ctx, field) + case "peers": + return ec.fieldContext_Torrent_peers(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Torrent", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _TorrentProgress_current(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentProgress_current(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Current, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int64) + fc.Result = res + return ec.marshalNInt2int64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TorrentProgress_current(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentProgress", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _TorrentProgress_total(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentProgress_total(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Total, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int64) + fc.Result = res + return ec.marshalNInt2int64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TorrentProgress_total(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentProgress", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_name(ctx, field) if err != nil { @@ -4058,10 +4661,62 @@ func (ec *executionContext) unmarshalInputTorrentsFilter(ctx context.Context, ob // region ************************** interface.gotpl *************************** +func (ec *executionContext) _Progress(ctx context.Context, sel ast.SelectionSet, obj model.Progress) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.TorrentProgress: + return ec._TorrentProgress(ctx, sel, &obj) + case *model.TorrentProgress: + if obj == nil { + return graphql.Null + } + return ec._TorrentProgress(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + // endregion ************************** interface.gotpl *************************** // region **************************** object.gotpl **************************** +var downloadTorrentResponseImplementors = []string{"DownloadTorrentResponse"} + +func (ec *executionContext) _DownloadTorrentResponse(ctx context.Context, sel ast.SelectionSet, obj *model.DownloadTorrentResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, downloadTorrentResponseImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DownloadTorrentResponse") + case "task": + out.Values[i] = ec._DownloadTorrentResponse_task(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -4095,6 +4750,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "downloadTorrent": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_downloadTorrent(ctx, field) + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4228,6 +4887,67 @@ func (ec *executionContext) _Schema(ctx context.Context, sel ast.SelectionSet, o return out } +var subscriptionImplementors = []string{"Subscription"} + +func (ec *executionContext) _Subscription(ctx context.Context, sel ast.SelectionSet) func(ctx context.Context) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, subscriptionImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Subscription", + }) + if len(fields) != 1 { + ec.Errorf(ctx, "must subscribe to exactly one stream") + return nil + } + + switch fields[0].Name { + case "taskProgress": + return ec._Subscription_taskProgress(ctx, fields[0]) + case "torrentDownloadUpdates": + return ec._Subscription_torrentDownloadUpdates(ctx, fields[0]) + default: + panic("unknown field " + strconv.Quote(fields[0].Name)) + } +} + +var taskImplementors = []string{"Task"} + +func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *model.Task) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, taskImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Task") + case "id": + out.Values[i] = ec._Task_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var torrentImplementors = []string{"Torrent"} func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet, obj *model.Torrent) graphql.Marshaler { @@ -4534,6 +5254,55 @@ func (ec *executionContext) _TorrentPeer(ctx context.Context, sel ast.SelectionS return out } +var torrentProgressImplementors = []string{"TorrentProgress", "Progress"} + +func (ec *executionContext) _TorrentProgress(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentProgress) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, torrentProgressImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TorrentProgress") + case "torrent": + out.Values[i] = ec._TorrentProgress_torrent(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "current": + out.Values[i] = ec._TorrentProgress_current(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "total": + out.Values[i] = ec._TorrentProgress_total(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -4890,6 +5659,21 @@ func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.S return graphql.WrapContextMarshaler(ctx, res) } +func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { + res, err := graphql.UnmarshalID(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalID(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) unmarshalNInt2int64(ctx context.Context, v interface{}) (int64, error) { res, err := graphql.UnmarshalInt64(v) return res, graphql.ErrorOnPath(ctx, err) @@ -5382,6 +6166,13 @@ func (ec *executionContext) marshalODateTime2ᚖtimeᚐTime(ctx context.Context, return res } +func (ec *executionContext) marshalODownloadTorrentResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDownloadTorrentResponse(ctx context.Context, sel ast.SelectionSet, v *model.DownloadTorrentResponse) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DownloadTorrentResponse(ctx, sel, v) +} + func (ec *executionContext) unmarshalOInt2ᚕint64ᚄ(ctx context.Context, v interface{}) ([]int64, error) { if v == nil { return nil, nil @@ -5459,6 +6250,13 @@ func (ec *executionContext) unmarshalOPagination2ᚖgitᚗkmsignᚗruᚋroyalcat return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOProgress2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐProgress(ctx context.Context, sel ast.SelectionSet, v model.Progress) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Progress(ctx, sel, v) +} + func (ec *executionContext) marshalOQuery2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQuery(ctx context.Context, sel ast.SelectionSet, v *model.Query) graphql.Marshaler { if v == nil { return graphql.Null @@ -5528,6 +6326,20 @@ func (ec *executionContext) unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalc return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOTask2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTask(ctx context.Context, sel ast.SelectionSet, v *model.Task) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Task(ctx, sel, v) +} + +func (ec *executionContext) marshalOTorrentProgress2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentProgress(ctx context.Context, sel ast.SelectionSet, v *model.TorrentProgress) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._TorrentProgress(ctx, sel, v) +} + func (ec *executionContext) unmarshalOTorrentsFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentsFilter(ctx context.Context, v interface{}) (*model.TorrentsFilter, error) { if v == nil { return nil, nil diff --git a/src/delivery/graphql/model/mappers.go b/src/delivery/graphql/model/mappers.go index 1523130..e2227e0 100644 --- a/src/delivery/graphql/model/mappers.go +++ b/src/delivery/graphql/model/mappers.go @@ -1,6 +1,9 @@ package model -import "github.com/anacrolix/torrent" +import ( + "git.kmsign.ru/royalcat/tstor/src/host/controller" + "github.com/anacrolix/torrent" +) func MapPeerSource(source torrent.PeerSource) string { switch source { @@ -22,3 +25,13 @@ func MapPeerSource(source torrent.PeerSource) string { return "Unknown" } } + +func MapTorrent(t *controller.Torrent) *Torrent { + return &Torrent{ + Infohash: t.InfoHash(), + Name: t.Name(), + BytesCompleted: t.BytesCompleted(), + BytesMissing: t.BytesMissing(), + T: t, + } +} diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go index fec0890..deb4b6d 100644 --- a/src/delivery/graphql/model/models_gen.go +++ b/src/delivery/graphql/model/models_gen.go @@ -9,6 +9,12 @@ import ( "github.com/anacrolix/torrent" ) +type Progress interface { + IsProgress() + GetCurrent() int64 + GetTotal() int64 +} + type BooleanFilter struct { Eq *bool `json:"eq,omitempty"` } @@ -21,6 +27,10 @@ type DateTimeFilter struct { Lte *time.Time `json:"lte,omitempty"` } +type DownloadTorrentResponse struct { + Task *Task `json:"task,omitempty"` +} + type IntFilter struct { Eq *int64 `json:"eq,omitempty"` Gt *int64 `json:"gt,omitempty"` @@ -52,6 +62,13 @@ type StringFilter struct { In []string `json:"in,omitempty"` } +type Subscription struct { +} + +type Task struct { + ID string `json:"id"` +} + type Torrent struct { Name string `json:"name"` Infohash string `json:"infohash"` @@ -85,6 +102,16 @@ type TorrentPeer struct { F *torrent.PeerConn `json:"-"` } +type TorrentProgress struct { + Torrent *Torrent `json:"torrent"` + Current int64 `json:"current"` + Total int64 `json:"total"` +} + +func (TorrentProgress) IsProgress() {} +func (this TorrentProgress) GetCurrent() int64 { return this.Current } +func (this TorrentProgress) GetTotal() int64 { return this.Total } + type TorrentsFilter struct { Name *StringFilter `json:"name,omitempty"` BytesCompleted *IntFilter `json:"bytesCompleted,omitempty"` diff --git a/src/delivery/graphql/resolver/mutation.resolvers.go b/src/delivery/graphql/resolver/mutation.resolvers.go index e6f16c6..210b7a6 100644 --- a/src/delivery/graphql/resolver/mutation.resolvers.go +++ b/src/delivery/graphql/resolver/mutation.resolvers.go @@ -7,8 +7,11 @@ package resolver import ( "context" + "git.kmsign.ru/royalcat/tstor/pkg/uuid" graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" + "git.kmsign.ru/royalcat/tstor/src/host/service" + aih "github.com/anacrolix/torrent/types/infohash" ) // ValidateTorrents is the resolver for the validateTorrents field. @@ -58,6 +61,25 @@ func (r *mutationResolver) CleanupTorrents(ctx context.Context, files *bool, dry } } +// DownloadTorrent is the resolver for the downloadTorrent field. +func (r *mutationResolver) DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error) { + f := "" + if file != nil { + f = *file + } + + err := r.Service.Download(ctx, &service.TorrentDownloadTask{ + ID: uuid.New(), + InfoHash: aih.FromHexString(infohash), + File: f, + }) + if err != nil { + return nil, err + } + + return &model.DownloadTorrentResponse{}, nil +} + // Mutation returns graph.MutationResolver implementation. func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} } diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go index 9aecef7..b4687a7 100644 --- a/src/delivery/graphql/resolver/query.resolvers.go +++ b/src/delivery/graphql/resolver/query.resolvers.go @@ -52,13 +52,7 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt tr := []*model.Torrent{} for _, t := range torrents { - d := &model.Torrent{ - Infohash: t.InfoHash(), - Name: t.Name(), - BytesCompleted: t.BytesCompleted(), - BytesMissing: t.BytesMissing(), - T: t, - } + d := model.MapTorrent(t) if !filterFunc(d) { continue diff --git a/src/delivery/graphql/resolver/subscription.resolvers.go b/src/delivery/graphql/resolver/subscription.resolvers.go new file mode 100644 index 0000000..f30af50 --- /dev/null +++ b/src/delivery/graphql/resolver/subscription.resolvers.go @@ -0,0 +1,54 @@ +package resolver + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.43 + +import ( + "context" + "fmt" + + graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" + "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" +) + +// TaskProgress is the resolver for the taskProgress field. +func (r *subscriptionResolver) TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error) { + panic(fmt.Errorf("not implemented: TaskProgress - taskProgress")) +} + +// TorrentDownloadUpdates is the resolver for the torrentDownloadUpdates field. +func (r *subscriptionResolver) TorrentDownloadUpdates(ctx context.Context) (<-chan *model.TorrentProgress, error) { + out := make(chan *model.TorrentProgress) + progress, err := r.Service.DownloadProgress(ctx) + if err != nil { + return nil, err + } + + go func() { + defer close(out) + for p := range progress { + if p.Torrent == nil { + fmt.Println("nil torrent") + continue + } + po := &model.TorrentProgress{ + Torrent: model.MapTorrent(p.Torrent), + Current: p.Current, + Total: p.Total, + } + select { + case <-ctx.Done(): + return + case out <- po: + } + } + }() + + return out, nil +} + +// Subscription returns graph.SubscriptionResolver implementation. +func (r *Resolver) Subscription() graph.SubscriptionResolver { return &subscriptionResolver{r} } + +type subscriptionResolver struct{ *Resolver } diff --git a/src/delivery/graphql/resolver/torrent.resolvers.go b/src/delivery/graphql/resolver/torrent.resolvers.go index 778fbf6..ef231ad 100644 --- a/src/delivery/graphql/resolver/torrent.resolvers.go +++ b/src/delivery/graphql/resolver/torrent.resolvers.go @@ -19,7 +19,7 @@ func (r *torrentResolver) Name(ctx context.Context, obj *model.Torrent) (string, // Files is the resolver for the files field. func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) { out := []*model.TorrentFile{} - files, err := obj.T.Files() + files, err := obj.T.Files(ctx) if err != nil { return nil, err } @@ -37,17 +37,17 @@ func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*mod // ExcludedFiles is the resolver for the excludedFiles field. func (r *torrentResolver) ExcludedFiles(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) { out := []*model.TorrentFile{} - files, err := obj.T.ExcludedFiles() - if err != nil { - return nil, err - } - for _, f := range files { - out = append(out, &model.TorrentFile{ - Filename: f.DisplayPath(), - Size: f.Length(), - F: f, - }) - } + // files, err := obj.T.ExcludedFiles() + // if err != nil { + // return nil, err + // } + // for _, f := range files { + // out = append(out, &model.TorrentFile{ + // Filename: f.DisplayPath(), + // Size: f.Length(), + // F: f, + // }) + // } return out, nil } diff --git a/src/export/nfs/cache.go b/src/export/nfs/cache.go new file mode 100644 index 0000000..926bbda --- /dev/null +++ b/src/export/nfs/cache.go @@ -0,0 +1,197 @@ +package nfs + +import ( + "crypto/sha256" + "encoding/binary" + "io/fs" + "reflect" + "slices" + + "github.com/willscott/go-nfs" + + "github.com/go-git/go-billy/v5" + "github.com/google/uuid" + lru "github.com/hashicorp/golang-lru/v2" +) + +// NewCachingHandler wraps a handler to provide a basic to/from-file handle cache. +func NewCachingHandler(h nfs.Handler, limit int) nfs.Handler { + return NewCachingHandlerWithVerifierLimit(h, limit, limit) +} + +// NewCachingHandlerWithVerifierLimit provides a basic to/from-file handle cache that can be tuned with a smaller cache of active directory listings. +func NewCachingHandlerWithVerifierLimit(h nfs.Handler, limit int, verifierLimit int) nfs.Handler { + if limit < 2 || verifierLimit < 2 { + nfs.Log.Warnf("Caching handler created with insufficient cache to support directory listing", "size", limit, "verifiers", verifierLimit) + } + cache, _ := lru.New[uuid.UUID, entry](limit) + reverseCache := make(map[string][]uuid.UUID) + verifiers, _ := lru.New[uint64, verifier](verifierLimit) + return &CachingHandler{ + Handler: h, + activeHandles: cache, + reverseHandles: reverseCache, + activeVerifiers: verifiers, + cacheLimit: limit, + } +} + +// CachingHandler implements to/from handle via an LRU cache. +type CachingHandler struct { + nfs.Handler + activeHandles *lru.Cache[uuid.UUID, entry] + reverseHandles map[string][]uuid.UUID + activeVerifiers *lru.Cache[uint64, verifier] + cacheLimit int +} + +type entry struct { + f billy.Filesystem + p []string +} + +// ToHandle takes a file and represents it with an opaque handle to reference it. +// In stateless nfs (when it's serving a unix fs) this can be the device + inode +// but we can generalize with a stateful local cache of handed out IDs. +func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte { + joinedPath := f.Join(path...) + + if handle := c.searchReverseCache(f, joinedPath); handle != nil { + return handle + } + + id := uuid.New() + + newPath := make([]string, len(path)) + + copy(newPath, path) + evictedKey, evictedPath, ok := c.activeHandles.GetOldest() + if evicted := c.activeHandles.Add(id, entry{f, newPath}); evicted && ok { + rk := evictedPath.f.Join(evictedPath.p...) + c.evictReverseCache(rk, evictedKey) + } + + if _, ok := c.reverseHandles[joinedPath]; !ok { + c.reverseHandles[joinedPath] = []uuid.UUID{} + } + c.reverseHandles[joinedPath] = append(c.reverseHandles[joinedPath], id) + b, _ := id.MarshalBinary() + + return b +} + +// FromHandle converts from an opaque handle to the file it represents +func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, error) { + id, err := uuid.FromBytes(fh) + if err != nil { + return nil, []string{}, err + } + + if f, ok := c.activeHandles.Get(id); ok { + for _, k := range c.activeHandles.Keys() { + candidate, _ := c.activeHandles.Peek(k) + if hasPrefix(f.p, candidate.p) { + _, _ = c.activeHandles.Get(k) + } + } + + return f.f, slices.Clone(f.p), nil + } + return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale} +} + +func (c *CachingHandler) searchReverseCache(f billy.Filesystem, path string) []byte { + uuids, exists := c.reverseHandles[path] + + if !exists { + return nil + } + + for _, id := range uuids { + if candidate, ok := c.activeHandles.Get(id); ok { + if reflect.DeepEqual(candidate.f, f) { + return id[:] + } + } + } + + return nil +} + +func (c *CachingHandler) evictReverseCache(path string, handle uuid.UUID) { + uuids, exists := c.reverseHandles[path] + + if !exists { + return + } + for i, u := range uuids { + if u == handle { + uuids = append(uuids[:i], uuids[i+1:]...) + c.reverseHandles[path] = uuids + return + } + } +} + +func (c *CachingHandler) InvalidateHandle(fs billy.Filesystem, handle []byte) error { + //Remove from cache + id, _ := uuid.FromBytes(handle) + entry, ok := c.activeHandles.Get(id) + if ok { + rk := entry.f.Join(entry.p...) + c.evictReverseCache(rk, id) + } + c.activeHandles.Remove(id) + return nil +} + +// HandleLimit exports how many file handles can be safely stored by this cache. +func (c *CachingHandler) HandleLimit() int { + return c.cacheLimit +} + +func hasPrefix(path, prefix []string) bool { + if len(prefix) > len(path) { + return false + } + for i, e := range prefix { + if path[i] != e { + return false + } + } + return true +} + +type verifier struct { + path string + contents []fs.FileInfo +} + +func hashPathAndContents(path string, contents []fs.FileInfo) uint64 { + //calculate a cookie-verifier. + vHash := sha256.New() + + // Add the path to avoid collisions of directories with the same content + vHash.Write(binary.BigEndian.AppendUint64([]byte{}, uint64(len(path)))) + vHash.Write([]byte(path)) + + for _, c := range contents { + vHash.Write([]byte(c.Name())) // Never fails according to the docs + } + + verify := vHash.Sum(nil)[0:8] + return binary.BigEndian.Uint64(verify) +} + +func (c *CachingHandler) VerifierFor(path string, contents []fs.FileInfo) uint64 { + id := hashPathAndContents(path, contents) + c.activeVerifiers.Add(id, verifier{path, contents}) + return id +} + +func (c *CachingHandler) DataForVerifier(path string, id uint64) []fs.FileInfo { + if cache, ok := c.activeVerifiers.Get(id); ok { + return cache.contents + } + return nil +} diff --git a/src/export/nfs/handler.go b/src/export/nfs/handler.go index c722fb4..6baa241 100644 --- a/src/export/nfs/handler.go +++ b/src/export/nfs/handler.go @@ -17,7 +17,7 @@ func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) { bfs := &billyFsWrapper{fs: fs, log: nfslog} handler := nfshelper.NewNullAuthHandler(bfs) - cacheHelper := nfshelper.NewCachingHandler(handler, 1024*16) + cacheHelper := nfshelper.NewCachingHandler(handler, 1024) // cacheHelper := NewCachingHandler(handler) diff --git a/src/host/controller/torrent.go b/src/host/controller/torrent.go index 911a4d3..30aa6cc 100644 --- a/src/host/controller/torrent.go +++ b/src/host/controller/torrent.go @@ -1,6 +1,7 @@ package controller import ( + "context" "slices" "strings" @@ -11,10 +12,10 @@ import ( type Torrent struct { torrentFilePath string t *torrent.Torrent - rep *store.ExlcudedFiles + rep *store.FilesMappings } -func NewTorrent(t *torrent.Torrent, rep *store.ExlcudedFiles) *Torrent { +func NewTorrent(t *torrent.Torrent, rep *store.FilesMappings) *Torrent { return &Torrent{t: t, rep: rep} } @@ -26,13 +27,13 @@ func (s *Torrent) Torrent() *torrent.Torrent { return s.t } -func (s *Torrent) Name() string { - <-s.t.GotInfo() - if name := s.t.Name(); name != "" { +func (c *Torrent) Name() string { + <-c.t.GotInfo() + if name := c.t.Name(); name != "" { return name } - return s.InfoHash() + return c.InfoHash() } func (s *Torrent) InfoHash() string { @@ -50,8 +51,13 @@ func (s *Torrent) BytesMissing() int64 { return s.t.BytesMissing() } -func (s *Torrent) Files() ([]*torrent.File, error) { - excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash()) +func (s *Torrent) Length() int64 { + <-s.t.GotInfo() + return s.t.Length() +} + +func (s *Torrent) Files(ctx context.Context) ([]*torrent.File, error) { + fileMappings, err := s.rep.FileMappings(ctx, s.t.InfoHash()) if err != nil { return nil, err } @@ -60,25 +66,30 @@ func (s *Torrent) Files() ([]*torrent.File, error) { files := s.t.Files() files = slices.DeleteFunc(files, func(file *torrent.File) bool { p := file.Path() - if strings.Contains(p, "/.pad/") { - return false + return true } - - if !slices.Contains(excludedFiles, p) { - return false + if target, ok := fileMappings[p]; ok && target == "" { + return true } - - return true + return false }) - for _, tf := range files { - s.isFileComplete(tf.BeginPieceIndex(), tf.EndPieceIndex()) - } - return files, nil } +func Map[T, U any](ts []T, f func(T) U) []U { + us := make([]U, len(ts)) + for i := range ts { + us[i] = f(ts[i]) + } + return us +} + +func (s *Torrent) ExcludeFile(ctx context.Context, f *torrent.File) error { + return s.rep.ExcludeFile(ctx, f) +} + func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool { for i := startIndex; i < endIndex; i++ { if !s.t.Piece(i).State().Complete { @@ -88,35 +99,6 @@ func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool { return true } -func (s *Torrent) ExcludedFiles() ([]*torrent.File, error) { - excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash()) - if err != nil { - return nil, err - } - - <-s.t.GotInfo() - files := s.t.Files() - files = slices.DeleteFunc(files, func(file *torrent.File) bool { - p := file.Path() - - if strings.Contains(p, "/.pad/") { - return false - } - - if slices.Contains(excludedFiles, p) { - return false - } - - return true - }) - - return files, nil -} - -func (s *Torrent) ExcludeFile(f *torrent.File) error { - return s.rep.ExcludeFile(f) -} - func (s *Torrent) ValidateTorrent() error { <-s.t.GotInfo() s.t.VerifyData() diff --git a/src/host/datastorage/piece_storage.go b/src/host/datastorage/piece_storage.go new file mode 100644 index 0000000..26091f8 --- /dev/null +++ b/src/host/datastorage/piece_storage.go @@ -0,0 +1,172 @@ +package datastorage + +import ( + "context" + "fmt" + "io" + "os" + "path" + + "git.kmsign.ru/royalcat/tstor/src/host/controller" + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" + "github.com/anacrolix/torrent/types/infohash" + "github.com/hashicorp/go-multierror" + "github.com/royalcat/kv" +) + +type PieceStorage struct { + basePath string + completion storage.PieceCompletion + dirInfohash kv.Store[string, infohash.T] +} + +func NewPieceStorage(path string, completion storage.PieceCompletion) *PieceStorage { + return &PieceStorage{ + basePath: path, + completion: completion, + } +} + +var _ DataStorage = (*PieceStorage)(nil) + +// OpenTorrent implements FileStorageDeleter. +func (p *PieceStorage) OpenTorrent(info *metainfo.Info, infoHash infohash.T) (storage.TorrentImpl, error) { + torrentPath := path.Join(p.basePath, infoHash.HexString()) + descriptors := []*os.File{} + + return storage.TorrentImpl{ + Piece: func(piece metainfo.Piece) storage.PieceImpl { + hash := piece.Hash().HexString() + piecePrefixDir := path.Join(torrentPath, hash[:2]) + err := os.MkdirAll(piecePrefixDir, os.ModePerm|os.ModeDir) + if err != nil { + return &errPiece{err: err} + } + piecePath := path.Join(torrentPath, hash[:2], hash) + file, err := os.OpenFile(piecePath, os.O_CREATE|os.O_RDWR, os.ModePerm) + if err != nil { + return &errPiece{err: err} + } + pk := metainfo.PieceKey{ + InfoHash: infoHash, + Index: piece.Index(), + } + return newPieceFile(pk, file, p.completion) + + // file, err os.OpenFile(piecePath) + }, + Flush: func() error { + var res error + for _, f := range descriptors { + if err := f.Sync(); err != nil { + res = multierror.Append(res, err) + } + } + return res + }, + Close: func() error { + var res error + for _, f := range descriptors { + if err := f.Close(); err != nil { + res = multierror.Append(res, err) + } + } + return res + }, + }, nil +} + +// Close implements FileStorageDeleter. +func (p *PieceStorage) Close() error { + return nil +} + +// DeleteFile implements FileStorageDeleter. +func (p *PieceStorage) DeleteFile(file *torrent.File) error { + return fmt.Errorf("not implemented") +} + +// CleanupDirs implements DataStorage. +func (p *PieceStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) { + return 0, nil // TODO +} + +// CleanupFiles implements DataStorage. +func (p *PieceStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) { + return 0, nil // TODO +} + +func newPieceFile(pk metainfo.PieceKey, file *os.File, completion storage.PieceCompletionGetSetter) *piece { + return &piece{ + pk: pk, + File: file, + completion: completion, + } +} + +type piece struct { + *os.File + pk metainfo.PieceKey + completion storage.PieceCompletionGetSetter +} + +// Completion implements storage.PieceImpl. +func (p *piece) Completion() storage.Completion { + compl, err := p.completion.Get(p.pk) + if err != nil { + return storage.Completion{Complete: false, Ok: false, Err: err} + } + return compl +} + +// MarkComplete implements storage.PieceImpl. +func (p *piece) MarkComplete() error { + return p.completion.Set(p.pk, true) +} + +// MarkNotComplete implements storage.PieceImpl. +func (p *piece) MarkNotComplete() error { + return p.completion.Set(p.pk, false) +} + +var _ storage.PieceImpl = (*piece)(nil) +var _ io.WriterTo = (*piece)(nil) + +type errPiece struct { + err error +} + +// WriteTo implements io.WriterTo. +func (p *errPiece) WriteTo(io.Writer) (int64, error) { + return 0, p.err +} + +// ReadAt implements storage.PieceImpl. +func (p *errPiece) ReadAt([]byte, int64) (int, error) { + return 0, p.err +} + +// WriteAt implements storage.PieceImpl. +func (p *errPiece) WriteAt([]byte, int64) (int, error) { + return 0, p.err +} + +// Completion implements storage.PieceImpl. +func (p *errPiece) Completion() storage.Completion { + return storage.Completion{Complete: false, Ok: false, Err: p.err} +} + +// MarkComplete implements storage.PieceImpl. +func (p *errPiece) MarkComplete() error { + return p.err +} + +// MarkNotComplete implements storage.PieceImpl. +func (p *errPiece) MarkNotComplete() error { + return p.err +} + +var _ storage.PieceImpl = (*errPiece)(nil) +var _ io.WriterTo = (*errPiece)(nil) diff --git a/src/host/filestorage/setup.go b/src/host/datastorage/setup.go similarity index 93% rename from src/host/filestorage/setup.go rename to src/host/datastorage/setup.go index 17e5448..5209385 100644 --- a/src/host/filestorage/setup.go +++ b/src/host/datastorage/setup.go @@ -1,4 +1,4 @@ -package filestorage +package datastorage import ( "fmt" @@ -10,7 +10,7 @@ import ( "github.com/anacrolix/torrent/storage" ) -func Setup(cfg config.TorrentClient) (*FileStorage, storage.PieceCompletion, error) { +func Setup(cfg config.TorrentClient) (DataStorage, storage.PieceCompletion, error) { pcp := filepath.Join(cfg.MetadataFolder, "piece-completion") if err := os.MkdirAll(pcp, 0744); err != nil { return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err) diff --git a/src/host/filestorage/storage.go b/src/host/datastorage/storage.go similarity index 94% rename from src/host/filestorage/storage.go rename to src/host/datastorage/storage.go index cc933e2..c17768e 100644 --- a/src/host/filestorage/storage.go +++ b/src/host/datastorage/storage.go @@ -1,4 +1,4 @@ -package filestorage +package datastorage import ( "context" @@ -14,9 +14,11 @@ import ( "github.com/anacrolix/torrent/storage" ) -type FileStorageDeleter interface { +type DataStorage interface { storage.ClientImplCloser DeleteFile(file *torrent.File) error + CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) + CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) } // NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem. @@ -137,7 +139,7 @@ func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller. expectedEntries := []string{} { for _, e := range expected { - files, err := e.Files() + files, err := e.Files(ctx) if err != nil { return 0, err } diff --git a/src/host/service/queue.go b/src/host/service/queue.go new file mode 100644 index 0000000..a67f286 --- /dev/null +++ b/src/host/service/queue.go @@ -0,0 +1,130 @@ +package service + +import ( + "context" + "fmt" + + "git.kmsign.ru/royalcat/tstor/pkg/uuid" + "git.kmsign.ru/royalcat/tstor/src/host/controller" + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/types/infohash" +) + +type TorrentDownloadTask struct { + ID uuid.UUID + InfoHash infohash.T + File string +} + +func (s *Service) Download(ctx context.Context, task *TorrentDownloadTask) error { + t, ok := s.c.Torrent(task.InfoHash) + if !ok { + return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString()) + } + + if task.File != "" { + var file *torrent.File + for _, tf := range t.Files() { + if tf.Path() == task.File { + file = tf + break + } + } + + if file == nil { + return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString()) + } + + file.Download() + return nil + } + + t.DownloadAll() + return nil +} + +// func (s *Service) DownloadAndWait(ctx context.Context, task *TorrentDownloadTask) error { +// t, ok := s.c.Torrent(task.InfoHash) +// if !ok { +// return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString()) +// } + +// if task.File != "" { +// var file *torrent.File +// for _, tf := range t.Files() { +// if tf.Path() == task.File { +// file = tf +// break +// } +// } + +// if file == nil { +// return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString()) +// } + +// file.Download() +// return waitPieceRange(ctx, t, file.BeginPieceIndex(), file.EndPieceIndex()) + +// } + +// t.DownloadAll() +// select { +// case <-ctx.Done(): +// return ctx.Err() +// case <-t.Complete.On(): +// return nil +// } +// } + +// func waitPieceRange(ctx context.Context, t *torrent.Torrent, start, end int) error { +// for i := start; i < end; i++ { +// timer := time.NewTimer(time.Millisecond) +// for { +// select { +// case <-ctx.Done(): +// return ctx.Err() +// case <-timer.C: +// if t.PieceState(i).Complete { +// continue +// } +// } + +// } +// } +// return nil +// } + +type TorrentProgress struct { + Torrent *controller.Torrent + Current int64 + Total int64 +} + +func (s *Service) DownloadProgress(ctx context.Context) (<-chan TorrentProgress, error) { + torrents, err := s.ListTorrents(ctx) + if err != nil { + return nil, err + } + + out := make(chan TorrentProgress, 1) + go func() { + defer close(out) + for _, t := range torrents { + sub := t.Torrent().SubscribePieceStateChanges() + go func() { + for range sub.Values { + out <- TorrentProgress{ + Torrent: t, + Current: t.BytesCompleted(), + Total: t.Length(), + } + } + }() + defer sub.Close() + } + + <-ctx.Done() + }() + + return out, nil +} diff --git a/src/host/service/service.go b/src/host/service/service.go index 718e30e..4388058 100644 --- a/src/host/service/service.go +++ b/src/host/service/service.go @@ -11,9 +11,10 @@ import ( "time" "git.kmsign.ru/royalcat/tstor/src/host/controller" - "git.kmsign.ru/royalcat/tstor/src/host/filestorage" + "git.kmsign.ru/royalcat/tstor/src/host/datastorage" "git.kmsign.ru/royalcat/tstor/src/host/store" "git.kmsign.ru/royalcat/tstor/src/host/vfs" + "go.uber.org/multierr" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/bencode" @@ -24,21 +25,21 @@ import ( type Service struct { c *torrent.Client - excludedFiles *store.ExlcudedFiles + excludedFiles *store.FilesMappings infoBytes *store.InfoBytes torrentLoaded chan struct{} // stats *Stats DefaultPriority types.PiecePriority - Storage *filestorage.FileStorage + Storage datastorage.DataStorage SourceDir string log *slog.Logger addTimeout, readTimeout int } -func NewService(sourceDir string, c *torrent.Client, storage *filestorage.FileStorage, excludedFiles *store.ExlcudedFiles, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service { +func NewService(sourceDir string, c *torrent.Client, storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service { s := &Service{ log: slog.With("component", "torrent-service"), c: c, @@ -66,6 +67,12 @@ func NewService(sourceDir string, c *torrent.Client, storage *filestorage.FileSt var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs +func (s *Service) Close() error { + err := multierr.Combine(s.c.Close()...) + err = multierr.Append(err, s.Storage.Close()) + return err +} + func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) { defer f.Close() @@ -102,17 +109,17 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, if err != nil { infoBytes = nil } else { - for _, t := range s.c.Torrents() { - if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash { - <-t.GotInfo() - if !isTorrentCompatable(*t.Info(), info) { - return nil, fmt.Errorf( - "torrent with name '%s' not compatable existing infohash: %s, new: %s", - t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(), - ) - } - } - } + // for _, t := range s.c.Torrents() { + // if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash { + // <-t.GotInfo() + // if !isTorrentCompatable(*t.Info(), info) { + // return nil, fmt.Errorf( + // "torrent with name '%s' not compatable existing infohash: %s, new: %s", + // t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(), + // ) + // } + // } + // } } t, _ = s.c.AddTorrentOpt(torrent.AddTorrentOpts{ @@ -123,7 +130,6 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, }) t.AllowDataDownload() t.AllowDataUpload() - t.DownloadAll() select { case <-ctx.Done(): diff --git a/src/host/store/excluded-files.go b/src/host/store/excluded-files.go deleted file mode 100644 index cdcd585..0000000 --- a/src/host/store/excluded-files.go +++ /dev/null @@ -1,94 +0,0 @@ -package store - -import ( - "errors" - "path/filepath" - "sync" - - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "github.com/philippgille/gokv" - "github.com/philippgille/gokv/badgerdb" - "github.com/philippgille/gokv/encoding" -) - -func NewExcludedFiles(metaDir string, storage TorrentFileDeleter) (*ExlcudedFiles, error) { - excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{ - Dir: filepath.Join(metaDir, "excluded-files"), - Codec: encoding.JSON, - }) - - if err != nil { - return nil, err - } - - r := &ExlcudedFiles{ - excludedFiles: excludedFilesStore, - storage: storage, - } - - return r, nil -} - -type ExlcudedFiles struct { - m sync.RWMutex - excludedFiles gokv.Store - storage TorrentFileDeleter -} - -var ErrNotFound = errors.New("not found") - -type TorrentFileDeleter interface { - DeleteFile(file *torrent.File) error -} - -func (r *ExlcudedFiles) ExcludeFile(file *torrent.File) error { - r.m.Lock() - defer r.m.Unlock() - - hash := file.Torrent().InfoHash() - var excludedFiles []string - found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles) - if err != nil { - return err - } - if !found { - excludedFiles = []string{} - } - excludedFiles = unique(append(excludedFiles, file.Path())) - - err = r.storage.DeleteFile(file) - if err != nil { - return err - } - - return r.excludedFiles.Set(hash.AsString(), excludedFiles) -} - -func (r *ExlcudedFiles) ExcludedFiles(hash metainfo.Hash) ([]string, error) { - r.m.Lock() - defer r.m.Unlock() - - var excludedFiles []string - found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles) - if err != nil { - return nil, err - } - if !found { - return nil, nil - } - - return excludedFiles, nil -} - -func unique[C comparable](intSlice []C) []C { - keys := make(map[C]bool) - list := []C{} - for _, entry := range intSlice { - if _, value := keys[entry]; !value { - keys[entry] = true - list = append(list, entry) - } - } - return list -} diff --git a/src/host/store/file-mappings.go b/src/host/store/file-mappings.go new file mode 100644 index 0000000..2a1c5b2 --- /dev/null +++ b/src/host/store/file-mappings.go @@ -0,0 +1,57 @@ +package store + +import ( + "context" + "errors" + "path/filepath" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/types/infohash" + "github.com/royalcat/kv" +) + +func NewFileMappings(metaDir string, storage TorrentFileDeleter) (*FilesMappings, error) { + str, err := kv.NewBadgerKVBytes[string, string](filepath.Join(metaDir, "file-mappings")) + if err != nil { + return nil, err + } + + r := &FilesMappings{ + mappings: str, + storage: storage, + } + + return r, nil +} + +type FilesMappings struct { + mappings kv.Store[string, string] + storage TorrentFileDeleter +} + +var ErrNotFound = errors.New("not found") + +type TorrentFileDeleter interface { + DeleteFile(file *torrent.File) error +} + +func fileKey(file *torrent.File) string { + return file.Torrent().InfoHash().HexString() + "/" + file.Path() +} + +func (r *FilesMappings) MapFile(ctx context.Context, file *torrent.File, target string) error { + return r.mappings.Set(ctx, fileKey(file), target) +} + +func (r *FilesMappings) ExcludeFile(ctx context.Context, file *torrent.File) error { + return r.mappings.Set(ctx, fileKey(file), "") +} + +func (r *FilesMappings) FileMappings(ctx context.Context, ih infohash.T) (map[string]string, error) { + out := map[string]string{} + err := r.mappings.RangeWithPrefix(ctx, ih.HexString(), func(k, v string) bool { + out[k] = v + return true + }) + return out, err +} diff --git a/src/host/store/store.go b/src/host/store/fileitem.go similarity index 100% rename from src/host/store/store.go rename to src/host/store/fileitem.go diff --git a/src/host/vfs/fs_test.go b/src/host/vfs/fs_test.go index 414f556..3caef23 100644 --- a/src/host/vfs/fs_test.go +++ b/src/host/vfs/fs_test.go @@ -21,7 +21,7 @@ func TestFileinfo(t *testing.T) { require.Zero(fi.Type() & fs.ModeDir) require.Zero(fi.Mode() & fs.ModeDir) require.Equal(fs.FileMode(0555), fi.Mode()) - require.Equal(nil, fi.Sys()) + require.Nil(fi.Sys()) } func TestDirInfo(t *testing.T) { @@ -38,6 +38,6 @@ func TestDirInfo(t *testing.T) { require.NotZero(fi.Type() & fs.ModeDir) require.NotZero(fi.Mode() & fs.ModeDir) require.Equal(defaultMode|fs.ModeDir, fi.Mode()) - require.Equal(nil, fi.Sys()) + require.Nil(fi.Sys()) } diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go index 6d3069e..0bbe3b6 100644 --- a/src/host/vfs/torrent.go +++ b/src/host/vfs/torrent.go @@ -12,6 +12,7 @@ import ( "git.kmsign.ru/royalcat/tstor/src/host/controller" "git.kmsign.ru/royalcat/tstor/src/iio" + "github.com/RoaringBitmap/roaring" "github.com/anacrolix/missinggo/v2" "github.com/anacrolix/torrent" "golang.org/x/exp/maps" @@ -46,17 +47,14 @@ func (fs *TorrentFs) files() (map[string]File, error) { return fs.filesCache, nil } - files, err := fs.c.Files() + files, err := fs.c.Files(context.Background()) if err != nil { return nil, err } fs.filesCache = make(map[string]File) for _, file := range files { - if file.BytesCompleted() == 0 { - continue - } - + file.Download() p := AbsPath(file.Path()) fs.filesCache[p] = &torrentFile{ @@ -107,6 +105,24 @@ DEFAULT_DIR: return fs.filesCache, nil } +func anyPeerHasFiles(file *torrent.File) bool { + for _, conn := range file.Torrent().PeerConns() { + if bitmapHaveFile(conn.PeerPieces(), file) { + return true + } + } + return false +} + +func bitmapHaveFile(bitmap *roaring.Bitmap, file *torrent.File) bool { + for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ { + if !bitmap.ContainsInt(i) { + return false + } + } + return true +} + func listFilesRecursive(vfs Filesystem, start string) (map[string]File, error) { out := make(map[string]File, 0) entries, err := vfs.ReadDir(start) @@ -222,7 +238,7 @@ func (fs *TorrentFs) Unlink(name string) error { return ErrNotImplemented } - return fs.c.ExcludeFile(tfile.file) + return fs.c.ExcludeFile(context.Background(), tfile.file) } type reader interface { diff --git a/src/host/vfs/virtdir/vds.go b/src/host/vfs/virtdir/vds.go new file mode 100644 index 0000000..31ccd60 --- /dev/null +++ b/src/host/vfs/virtdir/vds.go @@ -0,0 +1,21 @@ +package virtdir + +type SourceType string + +const ( + VirtDirYtDlp SourceType = "yt-dlp" +) + +type VirtDirSource interface { + SourceType() SourceType +} + +var _ VirtDirSource = (*VirtDirSourceYtDlp)(nil) + +type VirtDirSourceYtDlp struct { + URL string `json:"url"` +} + +func (VirtDirSourceYtDlp) SourceType() SourceType { + return VirtDirYtDlp +} diff --git a/src/log/nfs.go b/src/log/nfs.go index cde0632..b48b46c 100644 --- a/src/log/nfs.go +++ b/src/log/nfs.go @@ -11,13 +11,14 @@ import ( var _ nfs.Logger = (*NFSLog)(nil) type NFSLog struct { + level nfs.LogLevel // r *slog.Logger l *slog.Logger } func NewNFSLog(r *slog.Logger) nfs.Logger { return &NFSLog{ - // r: r, + level: nfs.DebugLevel, // l: r.Level(zerolog.DebugLevel), l: r, } @@ -25,43 +26,75 @@ func NewNFSLog(r *slog.Logger) nfs.Logger { // Debug implements nfs.Logger. func (l *NFSLog) Debug(args ...interface{}) { + if l.level < nfs.DebugLevel { + return + } + l.l.Debug(fmt.Sprint(args...)) } // Debugf implements nfs.Logger. func (l *NFSLog) Debugf(format string, args ...interface{}) { + if l.level < nfs.DebugLevel { + return + } + l.l.Debug(fmt.Sprintf(format, args...)) } // Error implements nfs.Logger. func (l *NFSLog) Error(args ...interface{}) { + if l.level < nfs.ErrorLevel { + return + } + l.l.Error(fmt.Sprint(args...)) } // Errorf implements nfs.Logger. func (l *NFSLog) Errorf(format string, args ...interface{}) { + if l.level < nfs.ErrorLevel { + return + } + l.l.Error(fmt.Sprintf(format, args...)) } // Fatal implements nfs.Logger. func (l *NFSLog) Fatal(args ...interface{}) { + if l.level < nfs.FatalLevel { + return + } + l.l.Error(fmt.Sprint(args...)) log.Fatal(args...) } // Fatalf implements nfs.Logger. func (l *NFSLog) Fatalf(format string, args ...interface{}) { + if l.level < nfs.FatalLevel { + return + } + l.l.Error(fmt.Sprintf(format, args...)) log.Fatalf(format, args...) } // Info implements nfs.Logger. func (l *NFSLog) Info(args ...interface{}) { + if l.level < nfs.InfoLevel { + return + } + l.l.Info(fmt.Sprint(args...)) } // Infof implements nfs.Logger. func (l *NFSLog) Infof(format string, args ...interface{}) { + if l.level < nfs.InfoLevel { + return + } + l.l.Info(fmt.Sprintf(format, args...)) } @@ -79,102 +112,85 @@ func (l *NFSLog) Panicf(format string, args ...interface{}) { // Print implements nfs.Logger. func (l *NFSLog) Print(args ...interface{}) { + if l.level < nfs.InfoLevel { + return + } + l.l.Info(fmt.Sprint(args...)) } // Printf implements nfs.Logger. func (l *NFSLog) Printf(format string, args ...interface{}) { + if l.level < nfs.InfoLevel { + return + } + l.l.Info(fmt.Sprintf(format, args...)) } // Trace implements nfs.Logger. func (l *NFSLog) Trace(args ...interface{}) { + if l.level < nfs.TraceLevel { + return + } + l.l.Debug(fmt.Sprint(args...)) } // Tracef implements nfs.Logger. func (l *NFSLog) Tracef(format string, args ...interface{}) { + if l.level < nfs.TraceLevel { + return + } + l.l.Debug(fmt.Sprintf(format, args...)) } // Warn implements nfs.Logger. func (l *NFSLog) Warn(args ...interface{}) { + if l.level < nfs.WarnLevel { + return + } + l.l.Warn(fmt.Sprint(args...)) } // Warnf implements nfs.Logger. func (l *NFSLog) Warnf(format string, args ...interface{}) { + if l.level < nfs.WarnLevel { + return + } + l.l.Warn(fmt.Sprintf(format, args...)) } // GetLevel implements nfs.Logger. func (l *NFSLog) GetLevel() nfs.LogLevel { - // zl := l.l.Handler() - // switch zl { - // case zerolog.PanicLevel, zerolog.Disabled: - // return nfs.PanicLevel - // case zerolog.FatalLevel: - // return nfs.FatalLevel - // case zerolog.ErrorLevel: - // return nfs.ErrorLevel - // case zerolog.WarnLevel: - // return nfs.WarnLevel - // case zerolog.InfoLevel: - // return nfs.InfoLevel - // case zerolog.DebugLevel: - // return nfs.DebugLevel - // case zerolog.TraceLevel: - // return nfs.TraceLevel - // } - return nfs.TraceLevel + return l.level } // ParseLevel implements nfs.Logger. func (l *NFSLog) ParseLevel(level string) (nfs.LogLevel, error) { - // switch level { - // case "panic": - // return nfs.PanicLevel, nil - // case "fatal": - // return nfs.FatalLevel, nil - // case "error": - // return nfs.ErrorLevel, nil - // case "warn": - // return nfs.WarnLevel, nil - // case "info": - // return nfs.InfoLevel, nil - // case "debug": - // return nfs.DebugLevel, nil - // case "trace": - // return nfs.TraceLevel, nil - // } - // var ll nfs.LogLevel - // return ll, fmt.Errorf("invalid log level %q", level) - return nfs.TraceLevel, fmt.Errorf("level change not supported") + switch level { + case "panic": + return nfs.PanicLevel, nil + case "fatal": + return nfs.FatalLevel, nil + case "error": + return nfs.ErrorLevel, nil + case "warn": + return nfs.WarnLevel, nil + case "info": + return nfs.InfoLevel, nil + case "debug": + return nfs.DebugLevel, nil + case "trace": + return nfs.TraceLevel, nil + } + return 0, fmt.Errorf("invalid log level %q", level) } // SetLevel implements nfs.Logger. func (l *NFSLog) SetLevel(level nfs.LogLevel) { - // switch level { - // case nfs.PanicLevel: - // l.l = l.r.Level(zerolog.PanicLevel) - // return - // case nfs.FatalLevel: - // l.l = l.r.Level(zerolog.FatalLevel) - // return - // case nfs.ErrorLevel: - // l.l = l.r.Level(zerolog.ErrorLevel) - // return - // case nfs.WarnLevel: - // l.l = l.r.Level(zerolog.WarnLevel) - // return - // case nfs.InfoLevel: - // l.l = l.r.Level(zerolog.InfoLevel) - // return - // case nfs.DebugLevel: - // l.l = l.r.Level(zerolog.DebugLevel) - // return - // case nfs.TraceLevel: - // l.l = l.r.Level(zerolog.TraceLevel) - // return - // } + l.level = level }