diff --git a/.gqlgen.yml b/.gqlgen.yml index 1f2b337..80a8256 100644 --- a/.gqlgen.yml +++ b/.gqlgen.yml @@ -21,6 +21,9 @@ models: model: github.com/99designs/gqlgen/graphql.Time Int: model: github.com/99designs/gqlgen/graphql.Int64 + UInt: + model: + - github.com/99designs/gqlgen/graphql.Uint Torrent: extraFields: T: diff --git a/graphql/schema.graphql b/graphql/schema.graphql index e18fd22..fbc3282 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -5,6 +5,7 @@ directive @stream on FIELD_DEFINITION scalar DateTime scalar Upload +scalar UInt type Schema { query: Query diff --git a/graphql/sources/torrent_query.graphql b/graphql/sources/torrent_query.graphql index f9096f5..6ecd144 100644 --- a/graphql/sources/torrent_query.graphql +++ b/graphql/sources/torrent_query.graphql @@ -1,6 +1,7 @@ type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver - stats: TorrentStats! @resolver + clientStats: TorrentClientStats! @resolver + statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver } input TorrentsFilter { diff --git a/graphql/sources/torrent_types.graphql b/graphql/sources/torrent_types.graphql index 1b64219..d007e3a 100644 --- a/graphql/sources/torrent_types.graphql +++ b/graphql/sources/torrent_types.graphql @@ -33,7 +33,7 @@ enum TorrentPriority { NOW } -type TorrentStats { +type TorrentClientStats { bytesWritten: Int! bytesWrittenData: Int! bytesRead: Int! @@ -51,3 +51,12 @@ type TorrentStats { piecesDirtiedGood: Int! piecesDirtiedBad: Int! } + +type TorrentStats { + timestamp: DateTime! + downloadedBytes: UInt! + uploadedBytes: UInt! + totalPeers: UInt! + activePeers: UInt! + connectedSeeders: UInt! +} diff --git a/src/delivery/api.go b/src/delivery/api.go deleted file mode 100644 index feab526..0000000 --- a/src/delivery/api.go +++ /dev/null @@ -1,126 +0,0 @@ -package delivery - -import ( - "bytes" - "io" - "math" - "net/http" - "os" - - "git.kmsign.ru/royalcat/tstor/src/sources/torrent" - "github.com/anacrolix/missinggo/v2/filecache" - "github.com/gin-gonic/gin" -) - -var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.Stats) gin.HandlerFunc { - return func(ctx *gin.Context) { - stat := gin.H{ - "torrentStats": ss.GlobalStats(), - } - - if fc != nil { - stat["cacheItems"] = fc.Info().NumItems - stat["cacheFilled"] = fc.Info().Filled / 1024 / 1024 - stat["cacheCapacity"] = fc.Info().Capacity / 1024 / 1024 - } - - // TODO move to a struct - ctx.JSON(http.StatusOK, stat) - } -} - -// var apiServersHandler = func(ss []*service.Server) gin.HandlerFunc { -// return func(ctx *gin.Context) { -// var infos []*torrent.ServerInfo -// for _, s := range ss { -// infos = append(infos, s.Info()) -// } -// ctx.JSON(http.StatusOK, infos) -// } -// } - -// var apiRoutesHandler = func(ss *service.Stats) gin.HandlerFunc { -// return func(ctx *gin.Context) { -// s := ss.RoutesStats() -// sort.Sort(torrent.ByName(s)) -// ctx.JSON(http.StatusOK, s) -// } -// } - -// var apiAddTorrentHandler = func(s *service.Service) gin.HandlerFunc { -// return func(ctx *gin.Context) { -// route := ctx.Param("route") - -// var json RouteAdd -// if err := ctx.ShouldBindJSON(&json); err != nil { -// ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) -// return -// } - -// if err := s.AddMagnet(route, json.Magnet); err != nil { -// ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) -// return -// } - -// ctx.JSON(http.StatusOK, nil) -// } -// } - -// var apiDelTorrentHandler = func(s *service.Service) gin.HandlerFunc { -// return func(ctx *gin.Context) { -// route := ctx.Param("route") -// hash := ctx.Param("torrent_hash") - -// if err := s.RemoveFromHash(route, hash); err != nil { -// ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) -// return -// } - -// ctx.JSON(http.StatusOK, nil) -// } -// } - -var apiLogHandler = func(path string) gin.HandlerFunc { - return func(ctx *gin.Context) { - f, err := os.Open(path) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - fi, err := f.Stat() - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - max := math.Max(float64(-fi.Size()), -1024*8*8) - _, err = f.Seek(int64(max), io.SeekEnd) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - var b bytes.Buffer - ctx.Stream(func(w io.Writer) bool { - _, err := b.ReadFrom(f) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return false - } - - _, err = b.WriteTo(w) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return false - } - - return true - }) - - if err := f.Close(); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - } -} diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go index 72d0235..412d25b 100644 --- a/src/delivery/graphql/generated.go +++ b/src/delivery/graphql/generated.go @@ -128,6 +128,22 @@ type ComplexityRoot struct { TorrentFilePath func(childComplexity int) int } + TorrentClientStats struct { + BytesRead func(childComplexity int) int + BytesReadData func(childComplexity int) int + BytesReadUsefulData func(childComplexity int) int + BytesReadUsefulIntendedData func(childComplexity int) int + BytesWritten func(childComplexity int) int + BytesWrittenData func(childComplexity int) int + ChunksRead func(childComplexity int) int + ChunksReadUseful func(childComplexity int) int + ChunksReadWasted func(childComplexity int) int + ChunksWritten func(childComplexity int) int + MetadataChunksRead func(childComplexity int) int + PiecesDirtiedBad func(childComplexity int) int + PiecesDirtiedGood func(childComplexity int) int + } + TorrentDaemonMutation struct { Cleanup func(childComplexity int, files *bool, dryRun bool) int SetTorrentPriority func(childComplexity int, infohash string, file *string, priority types.PiecePriority) int @@ -135,8 +151,9 @@ type ComplexityRoot struct { } TorrentDaemonQuery struct { - Stats func(childComplexity int) int - Torrents func(childComplexity int, filter *model.TorrentsFilter) int + ClientStats func(childComplexity int) int + StatsHistory func(childComplexity int, since time.Time, infohash *string) int + Torrents func(childComplexity int, filter *model.TorrentsFilter) int } TorrentFS struct { @@ -173,19 +190,12 @@ type ComplexityRoot struct { } TorrentStats struct { - BytesRead func(childComplexity int) int - BytesReadData func(childComplexity int) int - BytesReadUsefulData func(childComplexity int) int - BytesReadUsefulIntendedData func(childComplexity int) int - BytesWritten func(childComplexity int) int - BytesWrittenData func(childComplexity int) int - ChunksRead func(childComplexity int) int - ChunksReadUseful func(childComplexity int) int - ChunksReadWasted func(childComplexity int) int - ChunksWritten func(childComplexity int) int - MetadataChunksRead func(childComplexity int) int - PiecesDirtiedBad func(childComplexity int) int - PiecesDirtiedGood func(childComplexity int) int + ActivePeers func(childComplexity int) int + ConnectedSeeders func(childComplexity int) int + DownloadedBytes func(childComplexity int) int + Timestamp func(childComplexity int) int + TotalPeers func(childComplexity int) int + UploadedBytes func(childComplexity int) int } } @@ -225,7 +235,8 @@ type TorrentDaemonMutationResolver interface { } type TorrentDaemonQueryResolver interface { Torrents(ctx context.Context, obj *model.TorrentDaemonQuery, filter *model.TorrentsFilter) ([]*model.Torrent, error) - Stats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentStats, error) + ClientStats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentClientStats, error) + StatsHistory(ctx context.Context, obj *model.TorrentDaemonQuery, since time.Time, infohash *string) ([]*model.TorrentStats, error) } type TorrentFSResolver interface { Entries(ctx context.Context, obj *model.TorrentFs) ([]model.FsEntry, error) @@ -485,6 +496,97 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Torrent.TorrentFilePath(childComplexity), true + case "TorrentClientStats.bytesRead": + if e.complexity.TorrentClientStats.BytesRead == nil { + break + } + + return e.complexity.TorrentClientStats.BytesRead(childComplexity), true + + case "TorrentClientStats.bytesReadData": + if e.complexity.TorrentClientStats.BytesReadData == nil { + break + } + + return e.complexity.TorrentClientStats.BytesReadData(childComplexity), true + + case "TorrentClientStats.bytesReadUsefulData": + if e.complexity.TorrentClientStats.BytesReadUsefulData == nil { + break + } + + return e.complexity.TorrentClientStats.BytesReadUsefulData(childComplexity), true + + case "TorrentClientStats.bytesReadUsefulIntendedData": + if e.complexity.TorrentClientStats.BytesReadUsefulIntendedData == nil { + break + } + + return e.complexity.TorrentClientStats.BytesReadUsefulIntendedData(childComplexity), true + + case "TorrentClientStats.bytesWritten": + if e.complexity.TorrentClientStats.BytesWritten == nil { + break + } + + return e.complexity.TorrentClientStats.BytesWritten(childComplexity), true + + case "TorrentClientStats.bytesWrittenData": + if e.complexity.TorrentClientStats.BytesWrittenData == nil { + break + } + + return e.complexity.TorrentClientStats.BytesWrittenData(childComplexity), true + + case "TorrentClientStats.chunksRead": + if e.complexity.TorrentClientStats.ChunksRead == nil { + break + } + + return e.complexity.TorrentClientStats.ChunksRead(childComplexity), true + + case "TorrentClientStats.chunksReadUseful": + if e.complexity.TorrentClientStats.ChunksReadUseful == nil { + break + } + + return e.complexity.TorrentClientStats.ChunksReadUseful(childComplexity), true + + case "TorrentClientStats.chunksReadWasted": + if e.complexity.TorrentClientStats.ChunksReadWasted == nil { + break + } + + return e.complexity.TorrentClientStats.ChunksReadWasted(childComplexity), true + + case "TorrentClientStats.chunksWritten": + if e.complexity.TorrentClientStats.ChunksWritten == nil { + break + } + + return e.complexity.TorrentClientStats.ChunksWritten(childComplexity), true + + case "TorrentClientStats.metadataChunksRead": + if e.complexity.TorrentClientStats.MetadataChunksRead == nil { + break + } + + return e.complexity.TorrentClientStats.MetadataChunksRead(childComplexity), true + + case "TorrentClientStats.piecesDirtiedBad": + if e.complexity.TorrentClientStats.PiecesDirtiedBad == nil { + break + } + + return e.complexity.TorrentClientStats.PiecesDirtiedBad(childComplexity), true + + case "TorrentClientStats.piecesDirtiedGood": + if e.complexity.TorrentClientStats.PiecesDirtiedGood == nil { + break + } + + return e.complexity.TorrentClientStats.PiecesDirtiedGood(childComplexity), true + case "TorrentDaemonMutation.cleanup": if e.complexity.TorrentDaemonMutation.Cleanup == nil { break @@ -521,12 +623,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TorrentDaemonMutation.ValidateTorrent(childComplexity, args["filter"].(model.TorrentFilter)), true - case "TorrentDaemonQuery.stats": - if e.complexity.TorrentDaemonQuery.Stats == nil { + case "TorrentDaemonQuery.clientStats": + if e.complexity.TorrentDaemonQuery.ClientStats == nil { break } - return e.complexity.TorrentDaemonQuery.Stats(childComplexity), true + return e.complexity.TorrentDaemonQuery.ClientStats(childComplexity), true + + case "TorrentDaemonQuery.statsHistory": + if e.complexity.TorrentDaemonQuery.StatsHistory == nil { + break + } + + args, err := ec.field_TorrentDaemonQuery_statsHistory_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.TorrentDaemonQuery.StatsHistory(childComplexity, args["since"].(time.Time), args["infohash"].(*string)), true case "TorrentDaemonQuery.torrents": if e.complexity.TorrentDaemonQuery.Torrents == nil { @@ -666,96 +780,47 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TorrentProgress.Total(childComplexity), true - case "TorrentStats.bytesRead": - if e.complexity.TorrentStats.BytesRead == nil { + case "TorrentStats.activePeers": + if e.complexity.TorrentStats.ActivePeers == nil { break } - return e.complexity.TorrentStats.BytesRead(childComplexity), true + return e.complexity.TorrentStats.ActivePeers(childComplexity), true - case "TorrentStats.bytesReadData": - if e.complexity.TorrentStats.BytesReadData == nil { + case "TorrentStats.connectedSeeders": + if e.complexity.TorrentStats.ConnectedSeeders == nil { break } - return e.complexity.TorrentStats.BytesReadData(childComplexity), true + return e.complexity.TorrentStats.ConnectedSeeders(childComplexity), true - case "TorrentStats.bytesReadUsefulData": - if e.complexity.TorrentStats.BytesReadUsefulData == nil { + case "TorrentStats.downloadedBytes": + if e.complexity.TorrentStats.DownloadedBytes == nil { break } - return e.complexity.TorrentStats.BytesReadUsefulData(childComplexity), true + return e.complexity.TorrentStats.DownloadedBytes(childComplexity), true - case "TorrentStats.bytesReadUsefulIntendedData": - if e.complexity.TorrentStats.BytesReadUsefulIntendedData == nil { + case "TorrentStats.timestamp": + if e.complexity.TorrentStats.Timestamp == nil { break } - return e.complexity.TorrentStats.BytesReadUsefulIntendedData(childComplexity), true + return e.complexity.TorrentStats.Timestamp(childComplexity), true - case "TorrentStats.bytesWritten": - if e.complexity.TorrentStats.BytesWritten == nil { + case "TorrentStats.totalPeers": + if e.complexity.TorrentStats.TotalPeers == nil { break } - return e.complexity.TorrentStats.BytesWritten(childComplexity), true + return e.complexity.TorrentStats.TotalPeers(childComplexity), true - case "TorrentStats.bytesWrittenData": - if e.complexity.TorrentStats.BytesWrittenData == nil { + case "TorrentStats.uploadedBytes": + if e.complexity.TorrentStats.UploadedBytes == nil { break } - return e.complexity.TorrentStats.BytesWrittenData(childComplexity), true - - case "TorrentStats.chunksRead": - if e.complexity.TorrentStats.ChunksRead == nil { - break - } - - return e.complexity.TorrentStats.ChunksRead(childComplexity), true - - case "TorrentStats.chunksReadUseful": - if e.complexity.TorrentStats.ChunksReadUseful == nil { - break - } - - return e.complexity.TorrentStats.ChunksReadUseful(childComplexity), true - - case "TorrentStats.chunksReadWasted": - if e.complexity.TorrentStats.ChunksReadWasted == nil { - break - } - - return e.complexity.TorrentStats.ChunksReadWasted(childComplexity), true - - case "TorrentStats.chunksWritten": - if e.complexity.TorrentStats.ChunksWritten == nil { - break - } - - return e.complexity.TorrentStats.ChunksWritten(childComplexity), true - - case "TorrentStats.metadataChunksRead": - if e.complexity.TorrentStats.MetadataChunksRead == nil { - break - } - - return e.complexity.TorrentStats.MetadataChunksRead(childComplexity), true - - case "TorrentStats.piecesDirtiedBad": - if e.complexity.TorrentStats.PiecesDirtiedBad == nil { - break - } - - return e.complexity.TorrentStats.PiecesDirtiedBad(childComplexity), true - - case "TorrentStats.piecesDirtiedGood": - if e.complexity.TorrentStats.PiecesDirtiedGood == nil { - break - } - - return e.complexity.TorrentStats.PiecesDirtiedGood(childComplexity), true + return e.complexity.TorrentStats.UploadedBytes(childComplexity), true } return 0, false @@ -911,6 +976,7 @@ directive @stream on FIELD_DEFINITION scalar DateTime scalar Upload +scalar UInt type Schema { query: Query @@ -954,7 +1020,8 @@ type DownloadTorrentResponse { `, BuiltIn: false}, {Name: "../../../graphql/sources/torrent_query.graphql", Input: `type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver - stats: TorrentStats! @resolver + clientStats: TorrentClientStats! @resolver + statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver } input TorrentsFilter { @@ -1016,7 +1083,7 @@ enum TorrentPriority { NOW } -type TorrentStats { +type TorrentClientStats { bytesWritten: Int! bytesWrittenData: Int! bytesRead: Int! @@ -1034,6 +1101,15 @@ type TorrentStats { piecesDirtiedGood: Int! piecesDirtiedBad: Int! } + +type TorrentStats { + timestamp: DateTime! + downloadedBytes: UInt! + uploadedBytes: UInt! + totalPeers: UInt! + activePeers: UInt! + connectedSeeders: UInt! +} `, BuiltIn: false}, {Name: "../../../graphql/types/filters.graphql", Input: `input Pagination { offset: Int! @@ -1263,6 +1339,30 @@ func (ec *executionContext) field_TorrentDaemonMutation_validateTorrent_args(ctx return args, nil } +func (ec *executionContext) field_TorrentDaemonQuery_statsHistory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 time.Time + if tmp, ok := rawArgs["since"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("since")) + arg0, err = ec.unmarshalNDateTime2timeᚐTime(ctx, tmp) + if err != nil { + return nil, err + } + } + args["since"] = arg0 + var arg1 *string + if tmp, ok := rawArgs["infohash"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("infohash")) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["infohash"] = arg1 + return args, nil +} + func (ec *executionContext) field_TorrentDaemonQuery_torrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1827,8 +1927,10 @@ func (ec *executionContext) fieldContext_Query_torrentDaemon(_ context.Context, switch field.Name { case "torrents": return ec.fieldContext_TorrentDaemonQuery_torrents(ctx, field) - case "stats": - return ec.fieldContext_TorrentDaemonQuery_stats(ctx, field) + case "clientStats": + return ec.fieldContext_TorrentDaemonQuery_clientStats(ctx, field) + case "statsHistory": + return ec.fieldContext_TorrentDaemonQuery_statsHistory(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TorrentDaemonQuery", field.Name) }, @@ -3080,6 +3182,578 @@ func (ec *executionContext) fieldContext_Torrent_peers(_ context.Context, field return fc, nil } +func (ec *executionContext) _TorrentClientStats_bytesWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesWritten(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.BytesWritten, 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_TorrentClientStats_bytesWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_bytesWrittenData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesWrittenData(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.BytesWrittenData, 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_TorrentClientStats_bytesWrittenData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_bytesRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesRead(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.BytesRead, 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_TorrentClientStats_bytesRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_bytesReadData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesReadData(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.BytesReadData, 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_TorrentClientStats_bytesReadData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_bytesReadUsefulData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesReadUsefulData(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.BytesReadUsefulData, 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_TorrentClientStats_bytesReadUsefulData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_bytesReadUsefulIntendedData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_bytesReadUsefulIntendedData(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.BytesReadUsefulIntendedData, 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_TorrentClientStats_bytesReadUsefulIntendedData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_chunksWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_chunksWritten(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.ChunksWritten, 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_TorrentClientStats_chunksWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_chunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_chunksRead(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.ChunksRead, 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_TorrentClientStats_chunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_chunksReadUseful(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_chunksReadUseful(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.ChunksReadUseful, 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_TorrentClientStats_chunksReadUseful(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_chunksReadWasted(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_chunksReadWasted(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.ChunksReadWasted, 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_TorrentClientStats_chunksReadWasted(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_metadataChunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_metadataChunksRead(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.MetadataChunksRead, 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_TorrentClientStats_metadataChunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_piecesDirtiedGood(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_piecesDirtiedGood(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.PiecesDirtiedGood, 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_TorrentClientStats_piecesDirtiedGood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentClientStats_piecesDirtiedBad(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentClientStats_piecesDirtiedBad(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.PiecesDirtiedBad, 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_TorrentClientStats_piecesDirtiedBad(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentClientStats", + 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) _TorrentDaemonMutation_validateTorrent(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonMutation) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TorrentDaemonMutation_validateTorrent(ctx, field) if err != nil { @@ -3406,8 +4080,8 @@ func (ec *executionContext) fieldContext_TorrentDaemonQuery_torrents(ctx context return fc, nil } -func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentDaemonQuery_stats(ctx, field) +func (ec *executionContext) _TorrentDaemonQuery_clientStats(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentDaemonQuery_clientStats(ctx, field) if err != nil { return graphql.Null } @@ -3421,7 +4095,7 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.TorrentDaemonQuery().Stats(rctx, obj) + return ec.resolvers.TorrentDaemonQuery().ClientStats(rctx, obj) } directive1 := func(ctx context.Context) (interface{}, error) { if ec.directives.Resolver == nil { @@ -3437,10 +4111,10 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field if tmp == nil { return nil, nil } - if data, ok := tmp.(*model.TorrentStats); ok { + if data, ok := tmp.(*model.TorrentClientStats); ok { return data, nil } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentStats`, tmp) + return nil, fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentClientStats`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -3452,12 +4126,12 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field } return graphql.Null } - res := resTmp.(*model.TorrentStats) + res := resTmp.(*model.TorrentClientStats) fc.Result = res - return ec.marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx, field.Selections, res) + return ec.marshalNTorrentClientStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentClientStats(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentDaemonQuery_stats(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentDaemonQuery_clientStats(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentDaemonQuery", Field: field, @@ -3466,35 +4140,124 @@ func (ec *executionContext) fieldContext_TorrentDaemonQuery_stats(_ context.Cont Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "bytesWritten": - return ec.fieldContext_TorrentStats_bytesWritten(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesWritten(ctx, field) case "bytesWrittenData": - return ec.fieldContext_TorrentStats_bytesWrittenData(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesWrittenData(ctx, field) case "bytesRead": - return ec.fieldContext_TorrentStats_bytesRead(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesRead(ctx, field) case "bytesReadData": - return ec.fieldContext_TorrentStats_bytesReadData(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesReadData(ctx, field) case "bytesReadUsefulData": - return ec.fieldContext_TorrentStats_bytesReadUsefulData(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesReadUsefulData(ctx, field) case "bytesReadUsefulIntendedData": - return ec.fieldContext_TorrentStats_bytesReadUsefulIntendedData(ctx, field) + return ec.fieldContext_TorrentClientStats_bytesReadUsefulIntendedData(ctx, field) case "chunksWritten": - return ec.fieldContext_TorrentStats_chunksWritten(ctx, field) + return ec.fieldContext_TorrentClientStats_chunksWritten(ctx, field) case "chunksRead": - return ec.fieldContext_TorrentStats_chunksRead(ctx, field) + return ec.fieldContext_TorrentClientStats_chunksRead(ctx, field) case "chunksReadUseful": - return ec.fieldContext_TorrentStats_chunksReadUseful(ctx, field) + return ec.fieldContext_TorrentClientStats_chunksReadUseful(ctx, field) case "chunksReadWasted": - return ec.fieldContext_TorrentStats_chunksReadWasted(ctx, field) + return ec.fieldContext_TorrentClientStats_chunksReadWasted(ctx, field) case "metadataChunksRead": - return ec.fieldContext_TorrentStats_metadataChunksRead(ctx, field) + return ec.fieldContext_TorrentClientStats_metadataChunksRead(ctx, field) case "piecesDirtiedGood": - return ec.fieldContext_TorrentStats_piecesDirtiedGood(ctx, field) + return ec.fieldContext_TorrentClientStats_piecesDirtiedGood(ctx, field) case "piecesDirtiedBad": - return ec.fieldContext_TorrentStats_piecesDirtiedBad(ctx, field) + return ec.fieldContext_TorrentClientStats_piecesDirtiedBad(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TorrentClientStats", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _TorrentDaemonQuery_statsHistory(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentDaemonQuery_statsHistory(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) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.TorrentDaemonQuery().StatsHistory(rctx, obj, fc.Args["since"].(time.Time), fc.Args["infohash"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.Resolver == nil { + return nil, errors.New("directive resolver is not implemented") + } + return ec.directives.Resolver(ctx, obj, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*model.TorrentStats); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentStats`, tmp) + }) + 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.TorrentStats) + fc.Result = res + return ec.marshalNTorrentStats2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStatsᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TorrentDaemonQuery_statsHistory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TorrentDaemonQuery", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "timestamp": + return ec.fieldContext_TorrentStats_timestamp(ctx, field) + case "downloadedBytes": + return ec.fieldContext_TorrentStats_downloadedBytes(ctx, field) + case "uploadedBytes": + return ec.fieldContext_TorrentStats_uploadedBytes(ctx, field) + case "totalPeers": + return ec.fieldContext_TorrentStats_totalPeers(ctx, field) + case "activePeers": + return ec.fieldContext_TorrentStats_activePeers(ctx, field) + case "connectedSeeders": + return ec.fieldContext_TorrentStats_connectedSeeders(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TorrentStats", 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_TorrentDaemonQuery_statsHistory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } return fc, nil } @@ -4390,8 +5153,8 @@ func (ec *executionContext) fieldContext_TorrentProgress_total(_ context.Context return fc, nil } -func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesWritten(ctx, field) +func (ec *executionContext) _TorrentStats_timestamp(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_timestamp(ctx, field) if err != nil { return graphql.Null } @@ -4404,7 +5167,7 @@ func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, fiel }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesWritten, nil + return obj.Timestamp, nil }) if err != nil { ec.Error(ctx, err) @@ -4416,26 +5179,26 @@ func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNDateTime2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_timestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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 nil, errors.New("field of type DateTime does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesWrittenData(ctx, field) +func (ec *executionContext) _TorrentStats_downloadedBytes(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_downloadedBytes(ctx, field) if err != nil { return graphql.Null } @@ -4448,7 +5211,7 @@ func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesWrittenData, nil + return obj.DownloadedBytes, nil }) if err != nil { ec.Error(ctx, err) @@ -4460,26 +5223,26 @@ func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context, } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(uint) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNUInt2uint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesWrittenData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_downloadedBytes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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 nil, errors.New("field of type UInt does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesRead(ctx, field) +func (ec *executionContext) _TorrentStats_uploadedBytes(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_uploadedBytes(ctx, field) if err != nil { return graphql.Null } @@ -4492,7 +5255,7 @@ func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesRead, nil + return obj.UploadedBytes, nil }) if err != nil { ec.Error(ctx, err) @@ -4504,26 +5267,26 @@ func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field g } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(uint) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNUInt2uint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_uploadedBytes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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 nil, errors.New("field of type UInt does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesReadData(ctx, field) +func (ec *executionContext) _TorrentStats_totalPeers(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_totalPeers(ctx, field) if err != nil { return graphql.Null } @@ -4536,7 +5299,7 @@ func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesReadData, nil + return obj.TotalPeers, nil }) if err != nil { ec.Error(ctx, err) @@ -4548,26 +5311,26 @@ func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, fie } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(uint) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNUInt2uint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesReadData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_totalPeers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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 nil, errors.New("field of type UInt does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesReadUsefulData(ctx, field) +func (ec *executionContext) _TorrentStats_activePeers(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_activePeers(ctx, field) if err != nil { return graphql.Null } @@ -4580,7 +5343,7 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Contex }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesReadUsefulData, nil + return obj.ActivePeers, nil }) if err != nil { ec.Error(ctx, err) @@ -4592,26 +5355,26 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Contex } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(uint) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNUInt2uint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesReadUsefulData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_activePeers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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 nil, errors.New("field of type UInt does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_bytesReadUsefulIntendedData(ctx, field) +func (ec *executionContext) _TorrentStats_connectedSeeders(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_connectedSeeders(ctx, field) if err != nil { return graphql.Null } @@ -4624,7 +5387,7 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx contex }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.BytesReadUsefulIntendedData, nil + return obj.ConnectedSeeders, nil }) if err != nil { ec.Error(ctx, err) @@ -4636,327 +5399,19 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx contex } return graphql.Null } - res := resTmp.(int64) + res := resTmp.(uint) fc.Result = res - return ec.marshalNInt2int64(ctx, field.Selections, res) + return ec.marshalNUInt2uint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TorrentStats_bytesReadUsefulIntendedData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TorrentStats_connectedSeeders(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TorrentStats", 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) _TorrentStats_chunksWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_chunksWritten(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.ChunksWritten, 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_TorrentStats_chunksWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_chunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_chunksRead(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.ChunksRead, 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_TorrentStats_chunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_chunksReadUseful(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_chunksReadUseful(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.ChunksReadUseful, 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_TorrentStats_chunksReadUseful(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_chunksReadWasted(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_chunksReadWasted(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.ChunksReadWasted, 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_TorrentStats_chunksReadWasted(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_metadataChunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_metadataChunksRead(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.MetadataChunksRead, 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_TorrentStats_metadataChunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_piecesDirtiedGood(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_piecesDirtiedGood(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.PiecesDirtiedGood, 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_TorrentStats_piecesDirtiedGood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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) _TorrentStats_piecesDirtiedBad(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TorrentStats_piecesDirtiedBad(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.PiecesDirtiedBad, 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_TorrentStats_piecesDirtiedBad(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TorrentStats", - 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 nil, errors.New("field of type UInt does not have child fields") }, } return fc, nil @@ -8418,6 +8873,105 @@ func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet, return out } +var torrentClientStatsImplementors = []string{"TorrentClientStats"} + +func (ec *executionContext) _TorrentClientStats(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentClientStats) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, torrentClientStatsImplementors) + + 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("TorrentClientStats") + case "bytesWritten": + out.Values[i] = ec._TorrentClientStats_bytesWritten(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesWrittenData": + out.Values[i] = ec._TorrentClientStats_bytesWrittenData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesRead": + out.Values[i] = ec._TorrentClientStats_bytesRead(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesReadData": + out.Values[i] = ec._TorrentClientStats_bytesReadData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesReadUsefulData": + out.Values[i] = ec._TorrentClientStats_bytesReadUsefulData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesReadUsefulIntendedData": + out.Values[i] = ec._TorrentClientStats_bytesReadUsefulIntendedData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "chunksWritten": + out.Values[i] = ec._TorrentClientStats_chunksWritten(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "chunksRead": + out.Values[i] = ec._TorrentClientStats_chunksRead(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "chunksReadUseful": + out.Values[i] = ec._TorrentClientStats_chunksReadUseful(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "chunksReadWasted": + out.Values[i] = ec._TorrentClientStats_chunksReadWasted(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "metadataChunksRead": + out.Values[i] = ec._TorrentClientStats_metadataChunksRead(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "piecesDirtiedGood": + out.Values[i] = ec._TorrentClientStats_piecesDirtiedGood(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "piecesDirtiedBad": + out.Values[i] = ec._TorrentClientStats_piecesDirtiedBad(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 torrentDaemonMutationImplementors = []string{"TorrentDaemonMutation"} func (ec *executionContext) _TorrentDaemonMutation(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentDaemonMutation) graphql.Marshaler { @@ -8607,7 +9161,7 @@ func (ec *executionContext) _TorrentDaemonQuery(ctx context.Context, sel ast.Sel } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "stats": + case "clientStats": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -8616,7 +9170,43 @@ func (ec *executionContext) _TorrentDaemonQuery(ctx context.Context, sel ast.Sel ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._TorrentDaemonQuery_stats(ctx, field, obj) + res = ec._TorrentDaemonQuery_clientStats(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "statsHistory": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._TorrentDaemonQuery_statsHistory(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -8999,68 +9589,33 @@ func (ec *executionContext) _TorrentStats(ctx context.Context, sel ast.Selection switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("TorrentStats") - case "bytesWritten": - out.Values[i] = ec._TorrentStats_bytesWritten(ctx, field, obj) + case "timestamp": + out.Values[i] = ec._TorrentStats_timestamp(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "bytesWrittenData": - out.Values[i] = ec._TorrentStats_bytesWrittenData(ctx, field, obj) + case "downloadedBytes": + out.Values[i] = ec._TorrentStats_downloadedBytes(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "bytesRead": - out.Values[i] = ec._TorrentStats_bytesRead(ctx, field, obj) + case "uploadedBytes": + out.Values[i] = ec._TorrentStats_uploadedBytes(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "bytesReadData": - out.Values[i] = ec._TorrentStats_bytesReadData(ctx, field, obj) + case "totalPeers": + out.Values[i] = ec._TorrentStats_totalPeers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "bytesReadUsefulData": - out.Values[i] = ec._TorrentStats_bytesReadUsefulData(ctx, field, obj) + case "activePeers": + out.Values[i] = ec._TorrentStats_activePeers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "bytesReadUsefulIntendedData": - out.Values[i] = ec._TorrentStats_bytesReadUsefulIntendedData(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "chunksWritten": - out.Values[i] = ec._TorrentStats_chunksWritten(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "chunksRead": - out.Values[i] = ec._TorrentStats_chunksRead(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "chunksReadUseful": - out.Values[i] = ec._TorrentStats_chunksReadUseful(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "chunksReadWasted": - out.Values[i] = ec._TorrentStats_chunksReadWasted(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "metadataChunksRead": - out.Values[i] = ec._TorrentStats_metadataChunksRead(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "piecesDirtiedGood": - out.Values[i] = ec._TorrentStats_piecesDirtiedGood(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "piecesDirtiedBad": - out.Values[i] = ec._TorrentStats_piecesDirtiedBad(ctx, field, obj) + case "connectedSeeders": + out.Values[i] = ec._TorrentStats_connectedSeeders(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9438,6 +9993,21 @@ func (ec *executionContext) marshalNCleanupResponse2ᚖgitᚗkmsignᚗruᚋroyal return ec._CleanupResponse(ctx, sel, v) } +func (ec *executionContext) unmarshalNDateTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) { + res, err := graphql.UnmarshalTime(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDateTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler { + res := graphql.MarshalTime(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) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) { res, err := graphql.UnmarshalFloatContext(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -9638,6 +10208,16 @@ func (ec *executionContext) marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋts return ec._Torrent(ctx, sel, v) } +func (ec *executionContext) marshalNTorrentClientStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentClientStats(ctx context.Context, sel ast.SelectionSet, v *model.TorrentClientStats) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._TorrentClientStats(ctx, sel, v) +} + func (ec *executionContext) marshalNTorrentFile2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFileᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentFile) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -9784,6 +10364,50 @@ var ( } ) +func (ec *executionContext) marshalNTorrentStats2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStatsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentStats) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx context.Context, sel ast.SelectionSet, v *model.TorrentStats) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -9794,6 +10418,21 @@ func (ec *executionContext) marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcat return ec._TorrentStats(ctx, sel, v) } +func (ec *executionContext) unmarshalNUInt2uint(ctx context.Context, v interface{}) (uint, error) { + res, err := graphql.UnmarshalUint(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNUInt2uint(ctx context.Context, sel ast.SelectionSet, v uint) graphql.Marshaler { + res := graphql.MarshalUint(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) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) (graphql.Upload, error) { res, err := graphql.UnmarshalUpload(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/src/delivery/graphql/model/mappers.go b/src/delivery/graphql/model/mappers.go index 217e171..b3ea223 100644 --- a/src/delivery/graphql/model/mappers.go +++ b/src/delivery/graphql/model/mappers.go @@ -7,6 +7,14 @@ import ( atorrent "github.com/anacrolix/torrent" ) +func Apply[I any, O any](in []I, f func(I) O) []O { + out := make([]O, len(in)) + for i, v := range in { + out[i] = f(v) + } + return out +} + func MapPeerSource(source atorrent.PeerSource) string { switch source { case atorrent.PeerSourceDirect: @@ -43,3 +51,14 @@ func MapTorrent(ctx context.Context, t *torrent.Controller) (*Torrent, error) { T: t, }, nil } + +func MapTorrentStats(s torrent.TorrentStats) *TorrentStats { + return &TorrentStats{ + Timestamp: s.Timestamp, + DownloadedBytes: uint(s.DownloadedBytes), + UploadedBytes: uint(s.UploadedBytes), + TotalPeers: uint(s.TotalPeers), + ActivePeers: uint(s.ActivePeers), + ConnectedSeeders: uint(s.ConnectedSeeders), + } +} diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go index d1fa34f..7a6e02f 100644 --- a/src/delivery/graphql/model/models_gen.go +++ b/src/delivery/graphql/model/models_gen.go @@ -184,6 +184,22 @@ type Torrent struct { T *torrent.Controller `json:"-"` } +type TorrentClientStats struct { + BytesWritten int64 `json:"bytesWritten"` + BytesWrittenData int64 `json:"bytesWrittenData"` + BytesRead int64 `json:"bytesRead"` + BytesReadData int64 `json:"bytesReadData"` + BytesReadUsefulData int64 `json:"bytesReadUsefulData"` + BytesReadUsefulIntendedData int64 `json:"bytesReadUsefulIntendedData"` + ChunksWritten int64 `json:"chunksWritten"` + ChunksRead int64 `json:"chunksRead"` + ChunksReadUseful int64 `json:"chunksReadUseful"` + ChunksReadWasted int64 `json:"chunksReadWasted"` + MetadataChunksRead int64 `json:"metadataChunksRead"` + PiecesDirtiedGood int64 `json:"piecesDirtiedGood"` + PiecesDirtiedBad int64 `json:"piecesDirtiedBad"` +} + type TorrentDaemonMutation struct { ValidateTorrent bool `json:"validateTorrent"` SetTorrentPriority bool `json:"setTorrentPriority"` @@ -191,8 +207,9 @@ type TorrentDaemonMutation struct { } type TorrentDaemonQuery struct { - Torrents []*Torrent `json:"torrents"` - Stats *TorrentStats `json:"stats"` + Torrents []*Torrent `json:"torrents"` + ClientStats *TorrentClientStats `json:"clientStats"` + StatsHistory []*TorrentStats `json:"statsHistory"` } type TorrentFs struct { @@ -271,19 +288,12 @@ func (this TorrentProgress) GetCurrent() int64 { return this.Current } func (this TorrentProgress) GetTotal() int64 { return this.Total } type TorrentStats struct { - BytesWritten int64 `json:"bytesWritten"` - BytesWrittenData int64 `json:"bytesWrittenData"` - BytesRead int64 `json:"bytesRead"` - BytesReadData int64 `json:"bytesReadData"` - BytesReadUsefulData int64 `json:"bytesReadUsefulData"` - BytesReadUsefulIntendedData int64 `json:"bytesReadUsefulIntendedData"` - ChunksWritten int64 `json:"chunksWritten"` - ChunksRead int64 `json:"chunksRead"` - ChunksReadUseful int64 `json:"chunksReadUseful"` - ChunksReadWasted int64 `json:"chunksReadWasted"` - MetadataChunksRead int64 `json:"metadataChunksRead"` - PiecesDirtiedGood int64 `json:"piecesDirtiedGood"` - PiecesDirtiedBad int64 `json:"piecesDirtiedBad"` + Timestamp time.Time `json:"timestamp"` + DownloadedBytes uint `json:"downloadedBytes"` + UploadedBytes uint `json:"uploadedBytes"` + TotalPeers uint `json:"totalPeers"` + ActivePeers uint `json:"activePeers"` + ConnectedSeeders uint `json:"connectedSeeders"` } type TorrentsFilter struct { diff --git a/src/delivery/graphql/resolver/torrent_query.resolvers.go b/src/delivery/graphql/resolver/torrent_query.resolvers.go index 2c77a32..081b45b 100644 --- a/src/delivery/graphql/resolver/torrent_query.resolvers.go +++ b/src/delivery/graphql/resolver/torrent_query.resolvers.go @@ -8,10 +8,13 @@ import ( "context" "slices" "strings" + "time" graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" "git.kmsign.ru/royalcat/tstor/src/sources/torrent" + + tinfohash "github.com/anacrolix/torrent/types/infohash" ) // Torrents is the resolver for the torrents field. @@ -86,10 +89,10 @@ func (r *torrentDaemonQueryResolver) Torrents(ctx context.Context, obj *model.To return tr, nil } -// Stats is the resolver for the stats field. -func (r *torrentDaemonQueryResolver) Stats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentStats, error) { +// ClientStats is the resolver for the clientStats field. +func (r *torrentDaemonQueryResolver) ClientStats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentClientStats, error) { stats := r.Service.Stats() - return &model.TorrentStats{ + return &model.TorrentClientStats{ BytesWritten: stats.BytesWritten.Int64(), BytesRead: stats.BytesRead.Int64(), BytesWrittenData: stats.BytesWrittenData.Int64(), @@ -106,6 +109,33 @@ func (r *torrentDaemonQueryResolver) Stats(ctx context.Context, obj *model.Torre }, nil } +// StatsHistory is the resolver for the statsHistory field. +func (r *torrentDaemonQueryResolver) StatsHistory(ctx context.Context, obj *model.TorrentDaemonQuery, since time.Time, infohash *string) ([]*model.TorrentStats, error) { + var stats []torrent.TorrentStats + if infohash == nil { + stats, err := r.Service.StatsHistory(ctx, since) + if err != nil { + return nil, err + } + return model.Apply(stats, model.MapTorrentStats), nil + } else if *infohash == "total" { + var err error + stats, err = r.Service.TotalStatsHistory(ctx, since) + if err != nil { + return nil, err + } + } else { + ih := tinfohash.FromHexString(*infohash) + var err error + stats, err = r.Service.TorrentStatsHistory(ctx, since, ih) + if err != nil { + return nil, err + } + } + + return model.Apply(stats, model.MapTorrentStats), nil +} + // TorrentDaemonQuery returns graph.TorrentDaemonQueryResolver implementation. func (r *Resolver) TorrentDaemonQuery() graph.TorrentDaemonQueryResolver { return &torrentDaemonQueryResolver{r} diff --git a/src/sources/torrent/client.go b/src/sources/torrent/client.go index 0eb0c41..9152a83 100644 --- a/src/sources/torrent/client.go +++ b/src/sources/torrent/client.go @@ -2,7 +2,6 @@ package torrent import ( "log/slog" - "time" "github.com/anacrolix/dht/v2" "github.com/anacrolix/dht/v2/bep44" @@ -59,9 +58,14 @@ func newClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient torrentCfg.PeriodicallyAnnounceTorrentsToDht = true torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) { cfg.Store = fis - cfg.Exp = 2 * time.Hour + cfg.Exp = dhtTTL cfg.NoSecurity = false } - return torrent.NewClient(torrentCfg) + t, err := torrent.NewClient(torrentCfg) + if err != nil { + return nil, err + } + + return t, nil } diff --git a/src/sources/torrent/daemon.go b/src/sources/torrent/daemon.go index b33df21..4074dcd 100644 --- a/src/sources/torrent/daemon.go +++ b/src/sources/torrent/daemon.go @@ -48,6 +48,7 @@ type Daemon struct { fis *dhtFileItemStore dirsAquire kv.Store[string, DirAquire] fileProperties kv.Store[string, FileProperties] + statsStore *statsStore loadMutex sync.Mutex torrentLoaded chan struct{} @@ -57,6 +58,8 @@ type Daemon struct { log *rlog.Logger } +const dhtTTL = 24 * time.Hour + func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, error) { s := &Daemon{ log: rlog.Component("torrent-service"), @@ -70,7 +73,7 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, return nil, fmt.Errorf("error creating metadata folder: %w", err) } - s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), 3*time.Hour) + s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), dhtTTL) if err != nil { return nil, fmt.Errorf("error starting item store: %w", err) } @@ -86,7 +89,6 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, } s.infoBytes, err = newInfoBytesStore(conf.MetadataFolder) - if err != nil { return nil, err } @@ -96,6 +98,11 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, return nil, fmt.Errorf("error creating node ID: %w", err) } + s.statsStore, err = newStatsStore(conf.MetadataFolder, time.Hour*24*30) + if err != nil { + return nil, err + } + s.client, err = newClient(s.Storage, s.fis, &conf, id) if err != nil { return nil, fmt.Errorf("error starting torrent client: %w", err) @@ -116,9 +123,77 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, close(s.torrentLoaded) }() + go func() { + const period = time.Second * 10 + + <-s.torrentLoaded + + timer := time.NewTicker(period) + for { + select { + case <-s.client.Closed(): + return + case <-timer.C: + s.updateStats(context.Background()) + } + } + }() + return s, nil } +func (s *Daemon) updateStats(ctx context.Context) { + log := s.log + + totalPeers := 0 + activePeers := 0 + connectedSeeders := 0 + + for _, v := range s.client.Torrents() { + stats := v.Stats() + err := s.statsStore.AddTorrentStats(v.InfoHash(), TorrentStats{ + Timestamp: time.Now(), + DownloadedBytes: uint64(stats.BytesRead.Int64()), + UploadedBytes: uint64(stats.BytesWritten.Int64()), + TotalPeers: uint16(stats.TotalPeers), + ActivePeers: uint16(stats.ActivePeers), + ConnectedSeeders: uint16(stats.ConnectedSeeders), + }) + if err != nil { + log.Error(ctx, "error saving torrent stats", rlog.Error(err)) + } + + totalPeers += stats.TotalPeers + activePeers += stats.ActivePeers + connectedSeeders += stats.ConnectedSeeders + } + + totalStats := s.client.Stats() + err := s.statsStore.AddTotalStats(TorrentStats{ + Timestamp: time.Now(), + DownloadedBytes: uint64(totalStats.BytesRead.Int64()), + UploadedBytes: uint64(totalStats.BytesWritten.Int64()), + TotalPeers: uint16(totalPeers), + ActivePeers: uint16(activePeers), + ConnectedSeeders: uint16(connectedSeeders), + }) + if err != nil { + log.Error(ctx, "error saving total stats", rlog.Error(err)) + } +} + +func (s *Daemon) TotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { + return s.statsStore.ReadTotalStatsHistory(ctx, since) +} + +func (s *Daemon) TorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) { + return s.statsStore.ReadTorrentStatsHistory(ctx, since, ih) +} + +func (s *Daemon) StatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { + return s.statsStore.ReadStatsHistory(ctx, since) +} + var _ vfs.FsFactory = (*Daemon)(nil).NewTorrentFs func (s *Daemon) Close(ctx context.Context) error { @@ -237,7 +312,7 @@ func isValidInfoHashBytes(d []byte) bool { } func (s *Daemon) Stats() torrent.ConnStats { - return s.client.ConnStats() + return s.client.Stats().ConnStats } const loadWorkers = 5 diff --git a/src/sources/torrent/dht_fileitem_store.go b/src/sources/torrent/dht_fileitem_store.go index d8cfde5..ae7ea1e 100644 --- a/src/sources/torrent/dht_fileitem_store.go +++ b/src/sources/torrent/dht_fileitem_store.go @@ -85,7 +85,25 @@ func (fis *dhtFileItemStore) Get(t bep44.Target) (*bep44.Item, error) { } func (fis *dhtFileItemStore) Del(t bep44.Target) error { - // ignore this + tx := fis.db.NewTransaction(true) + defer tx.Discard() + + err := tx.Delete(t[:]) + if err == badger.ErrKeyNotFound { + return nil + } + if err != nil { + return err + } + + err = tx.Commit() + if err == badger.ErrKeyNotFound { + return nil + } + if err != nil { + return err + } + return nil } diff --git a/src/sources/torrent/fs.go b/src/sources/torrent/fs.go index bbdb74d..fd06cbf 100644 --- a/src/sources/torrent/fs.go +++ b/src/sources/torrent/fs.go @@ -155,7 +155,6 @@ func (fs *TorrentFS) files(ctx context.Context) (map[string]vfs.File, error) { return fs.filesCache, nil } - } DEFAULT_DIR: diff --git a/src/sources/torrent/stats.go b/src/sources/torrent/stats.go index d03e961..5444a9a 100644 --- a/src/sources/torrent/stats.go +++ b/src/sources/torrent/stats.go @@ -1,218 +1,207 @@ package torrent import ( - "errors" - "sync" + "context" + "encoding/json" + "path" + "slices" "time" - "github.com/anacrolix/torrent" + "git.kmsign.ru/royalcat/tstor/src/log" + "github.com/anacrolix/torrent/types/infohash" + "github.com/dgraph-io/badger/v4" ) -var ErrTorrentNotFound = errors.New("torrent not found") +func newStatsStore(metaDir string, lifetime time.Duration) (*statsStore, error) { + db, err := badger.OpenManaged( + badger. + DefaultOptions(path.Join(metaDir, "stats")). + WithNumVersionsToKeep(int(^uint(0) >> 1)). + WithLogger(log.BadgerLogger("stats")), // Infinity + ) + if err != nil { + return nil, err + } -type PieceStatus string + go func() { + for n := range time.NewTimer(lifetime / 2).C { + db.SetDiscardTs(uint64(n.Add(-lifetime).Unix())) + } + }() + return &statsStore{ + db: db, + }, nil +} -const ( - Checking PieceStatus = "H" - Partial PieceStatus = "P" - Complete PieceStatus = "C" - Waiting PieceStatus = "W" - Error PieceStatus = "?" -) - -type PieceChunk struct { - Status PieceStatus `json:"status"` - NumPieces int `json:"numPieces"` +type statsStore struct { + db *badger.DB } type TorrentStats struct { - Name string `json:"name"` - Hash string `json:"hash"` - DownloadedBytes int64 `json:"downloadedBytes"` - UploadedBytes int64 `json:"uploadedBytes"` - Peers int `json:"peers"` - Seeders int `json:"seeders"` - TimePassed float64 `json:"timePassed"` - PieceChunks []*PieceChunk `json:"pieceChunks"` - TotalPieces int `json:"totalPieces"` - PieceSize int64 `json:"pieceSize"` + Timestamp time.Time + DownloadedBytes uint64 + UploadedBytes uint64 + TotalPeers uint16 + ActivePeers uint16 + ConnectedSeeders uint16 } -type byName []*TorrentStats - -func (a byName) Len() int { return len(a) } -func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } - -type TotalTorrentStats struct { - DownloadedBytes int64 `json:"downloadedBytes"` - UploadedBytes int64 `json:"uploadedBytes"` - TimePassed float64 `json:"timePassed"` +func (s TorrentStats) Same(o TorrentStats) bool { + return s.DownloadedBytes == o.DownloadedBytes && + s.UploadedBytes == o.UploadedBytes && + s.TotalPeers == o.TotalPeers && + s.ActivePeers == o.ActivePeers && + s.ConnectedSeeders == o.ConnectedSeeders } -type RouteStats struct { - Name string `json:"name"` - TorrentStats []*TorrentStats `json:"torrentStats"` -} +func (r *statsStore) addStats(key []byte, stat TorrentStats) error { + ts := uint64(stat.Timestamp.Unix()) -type ByName []*RouteStats + txn := r.db.NewTransactionAt(ts, true) + defer txn.Discard() -func (a ByName) Len() int { return len(a) } -func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } - -type stat struct { - totalDownloadBytes int64 - downloadBytes int64 - totalUploadBytes int64 - uploadBytes int64 - peers int - seeders int - time time.Time -} - -type Stats struct { - mut sync.Mutex - torrents map[string]*torrent.Torrent - previousStats map[string]*stat - - gTime time.Time -} - -func NewStats() *Stats { - return &Stats{ - gTime: time.Now(), - torrents: make(map[string]*torrent.Torrent), - // torrentsByRoute: make(map[string]map[string]*torrent.Torrent), - // previousStats: make(map[string]*stat), - } -} - -func (s *Stats) Add(path string, t *torrent.Torrent) { - s.mut.Lock() - defer s.mut.Unlock() - - s.torrents[path] = t - s.previousStats[path] = &stat{} -} - -func (s *Stats) Del(path string) { - s.mut.Lock() - defer s.mut.Unlock() - delete(s.torrents, path) - delete(s.previousStats, path) -} - -func (s *Stats) Stats(path string) (*TorrentStats, error) { - s.mut.Lock() - defer s.mut.Unlock() - - t, ok := s.torrents[path] - if !(ok) { - return nil, ErrTorrentNotFound + item, err := txn.Get(key) + if err != nil && err != badger.ErrKeyNotFound { + return err } - now := time.Now() - - return s.stats(now, t, true), nil -} - -func (s *Stats) GlobalStats() *TotalTorrentStats { - s.mut.Lock() - defer s.mut.Unlock() - - now := time.Now() - - var totalDownload int64 - var totalUpload int64 - for _, torrent := range s.torrents { - tStats := s.stats(now, torrent, false) - totalDownload += tStats.DownloadedBytes - totalUpload += tStats.UploadedBytes - } - - timePassed := now.Sub(s.gTime) - s.gTime = now - - return &TotalTorrentStats{ - DownloadedBytes: totalDownload, - UploadedBytes: totalUpload, - TimePassed: timePassed.Seconds(), - } -} - -func (s *Stats) stats(now time.Time, t *torrent.Torrent, chunks bool) *TorrentStats { - ts := &TorrentStats{} - prev, ok := s.previousStats[t.InfoHash().String()] - if !ok { - return &TorrentStats{} - } - if s.returnPreviousMeasurements(now) { - ts.DownloadedBytes = prev.downloadBytes - ts.UploadedBytes = prev.uploadBytes - } else { - st := t.Stats() - rd := st.BytesReadData.Int64() - wd := st.BytesWrittenData.Int64() - ist := &stat{ - downloadBytes: rd - prev.totalDownloadBytes, - uploadBytes: wd - prev.totalUploadBytes, - totalDownloadBytes: rd, - totalUploadBytes: wd, - time: now, - peers: st.TotalPeers, - seeders: st.ConnectedSeeders, + if err != badger.ErrKeyNotFound { + var prevStats TorrentStats + err = item.Value(func(val []byte) error { + return json.Unmarshal(val, &prevStats) + }) + if err != nil { + return err } - ts.DownloadedBytes = ist.downloadBytes - ts.UploadedBytes = ist.uploadBytes - ts.Peers = ist.peers - ts.Seeders = ist.seeders - - s.previousStats[t.InfoHash().String()] = ist + if prevStats.Same(stat) { + return nil + } } - ts.TimePassed = now.Sub(prev.time).Seconds() - var totalPieces int - if chunks { - var pch []*PieceChunk - for _, psr := range t.PieceStateRuns() { - var s PieceStatus - switch { - case psr.Checking: - s = Checking - case psr.Partial: - s = Partial - case psr.Complete: - s = Complete - case !psr.Ok: - s = Error - default: - s = Waiting + data, err := json.Marshal(stat) + if err != nil { + return err + } + err = txn.Set(key, data) + if err != nil { + return err + } + + return txn.CommitAt(ts, nil) +} + +func (r *statsStore) AddTorrentStats(ih infohash.T, stat TorrentStats) error { + return r.addStats(ih.Bytes(), stat) +} + +const totalKey = "total" + +func (r *statsStore) AddTotalStats(stat TorrentStats) error { + return r.addStats([]byte(totalKey), stat) +} + +func (r *statsStore) ReadTotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { + stats := []TorrentStats{} + + err := r.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.AllVersions = true + opts.SinceTs = uint64(since.Unix()) + + it := txn.NewKeyIterator([]byte(totalKey), opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + var stat TorrentStats + err := item.Value(func(v []byte) error { + return json.Unmarshal(v, &stat) + }) + if err != nil { + return err } - pch = append(pch, &PieceChunk{ - Status: s, - NumPieces: psr.Length, - }) - totalPieces += psr.Length + stats = append(stats, stat) } - ts.PieceChunks = pch + return nil + }) + if err != nil { + return nil, err } - ts.Hash = t.InfoHash().String() - ts.Name = t.Name() - ts.TotalPieces = totalPieces + slices.SortFunc(stats, func(a, b TorrentStats) int { + return a.Timestamp.Compare(b.Timestamp) + }) + stats = slices.Compact(stats) + return stats, nil +} - if t.Info() != nil { - ts.PieceSize = t.Info().PieceLength +func (r *statsStore) ReadTorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) { + stats := []TorrentStats{} + + err := r.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.AllVersions = true + opts.SinceTs = uint64(since.Unix()) + + it := txn.NewKeyIterator(ih.Bytes(), opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + var stat TorrentStats + err := item.Value(func(v []byte) error { + return json.Unmarshal(v, &stat) + }) + if err != nil { + return err + } + + stats = append(stats, stat) + } + return nil + }) + if err != nil { + return nil, err } - return ts + slices.SortFunc(stats, func(a, b TorrentStats) int { + return a.Timestamp.Compare(b.Timestamp) + }) + stats = slices.Compact(stats) + return stats, nil } -const gap time.Duration = 2 * time.Second +func (r *statsStore) ReadStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { + stats := []TorrentStats{} -func (s *Stats) returnPreviousMeasurements(now time.Time) bool { - return now.Sub(s.gTime) < gap + err := r.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.AllVersions = true + opts.SinceTs = uint64(since.Unix()) + + it := txn.NewIterator(opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + var stat TorrentStats + err := item.Value(func(v []byte) error { + return json.Unmarshal(v, &stat) + }) + if err != nil { + return err + } + + stats = append(stats, stat) + } + return nil + }) + if err != nil { + return nil, err + } + + slices.SortFunc(stats, func(a, b TorrentStats) int { + return a.Timestamp.Compare(b.Timestamp) + }) + stats = slices.Compact(stats) + return stats, nil } diff --git a/src/sources/torrent/stats_store.go b/src/sources/torrent/stats_store.go deleted file mode 100644 index b3f4b3e..0000000 --- a/src/sources/torrent/stats_store.go +++ /dev/null @@ -1,127 +0,0 @@ -package torrent - -import ( - "context" - "encoding/json" - "path" - "time" - - "git.kmsign.ru/royalcat/tstor/src/log" - "github.com/anacrolix/torrent/types/infohash" - "github.com/dgraph-io/badger/v4" - "golang.org/x/exp/maps" -) - -func newStatsStore(metaDir string, lifetime time.Duration) (*statsStore, error) { - db, err := badger.OpenManaged( - badger. - DefaultOptions(path.Join(metaDir, "stats-history")). - WithNumVersionsToKeep(int(^uint(0) >> 1)). - WithLogger(log.BadgerLogger("stats")), // Infinity - ) - if err != nil { - return nil, err - } - - go func() { - for n := range time.NewTimer(lifetime / 2).C { - db.SetDiscardTs(uint64(n.Add(-lifetime).Unix())) - } - }() - return &statsStore{ - db: db, - }, nil -} - -type statsStore struct { - db *badger.DB -} - -func (r *statsStore) AddStat(ih infohash.T, t time.Time, stat TorrentStats) error { - data, err := json.Marshal(stat) - if err != nil { - return err - } - ts := uint64(t.Unix()) - - txn := r.db.NewTransactionAt(ts, false) - defer txn.Discard() - - err = txn.Set(ih.Bytes(), data) - if err != nil { - return err - } - - return txn.CommitAt(ts, nil) -} - -func (r *statsStore) ReadTotalStatsHistory(ctx context.Context, since time.Time) ([]TotalTorrentStats, error) { - stats := map[time.Time]TotalTorrentStats{} - - err := r.db.View(func(txn *badger.Txn) error { - opts := badger.DefaultIteratorOptions - opts.AllVersions = true - opts.SinceTs = uint64(since.Unix()) - - it := txn.NewIterator(opts) - defer it.Close() - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - // k := item.Key() - var tstat TorrentStats - err := item.Value(func(v []byte) error { - return json.Unmarshal(v, &tstat) - }) - if err != nil { - return err - } - - t := time.Unix(int64(item.Version()), 0) - - if stat, ok := stats[t]; !ok { - stats[t] = TotalTorrentStats{ - DownloadedBytes: tstat.DownloadedBytes, - UploadedBytes: stat.DownloadedBytes, - } - } else { - stat.DownloadedBytes += tstat.DownloadedBytes - stat.UploadedBytes += tstat.UploadedBytes - stats[t] = stat - } - - } - return nil - }) - - return maps.Values(stats), err -} - -func (r *statsStore) ReadStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) { - var stats map[time.Time]TorrentStats - - err := r.db.View(func(txn *badger.Txn) error { - opts := badger.DefaultIteratorOptions - opts.AllVersions = true - opts.SinceTs = uint64(since.Unix()) - - it := txn.NewKeyIterator(ih.Bytes(), opts) - defer it.Close() - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - var tstat TorrentStats - err := item.Value(func(v []byte) error { - return json.Unmarshal(v, &tstat) - }) - if err != nil { - return err - } - - t := time.Unix(int64(item.Version()), 0) - - stats[t] = tstat - } - return nil - }) - - return maps.Values(stats), err -} diff --git a/ui/lib/api/schema.graphql b/ui/lib/api/schema.graphql index 612f7ec..7992756 100644 --- a/ui/lib/api/schema.graphql +++ b/ui/lib/api/schema.graphql @@ -99,6 +99,21 @@ type Torrent { excludedFiles: [TorrentFile!]! @resolver peers: [TorrentPeer!]! @resolver } +type TorrentClientStats { + bytesWritten: Int! + bytesWrittenData: Int! + bytesRead: Int! + bytesReadData: Int! + bytesReadUsefulData: Int! + bytesReadUsefulIntendedData: Int! + chunksWritten: Int! + chunksRead: Int! + chunksReadUseful: Int! + chunksReadWasted: Int! + metadataChunksRead: Int! + piecesDirtiedGood: Int! + piecesDirtiedBad: Int! +} type TorrentDaemonMutation { validateTorrent(filter: TorrentFilter!): Boolean! @resolver setTorrentPriority(infohash: String!, file: String, priority: TorrentPriority!): Boolean! @resolver @@ -106,7 +121,8 @@ type TorrentDaemonMutation { } type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver - stats: TorrentStats! @resolver + clientStats: TorrentClientStats! @resolver + statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver } type TorrentFS implements Dir & FsEntry { name: String! @@ -156,19 +172,12 @@ type TorrentProgress implements Progress { total: Int! } type TorrentStats { - bytesWritten: Int! - bytesWrittenData: Int! - bytesRead: Int! - bytesReadData: Int! - bytesReadUsefulData: Int! - bytesReadUsefulIntendedData: Int! - chunksWritten: Int! - chunksRead: Int! - chunksReadUseful: Int! - chunksReadWasted: Int! - metadataChunksRead: Int! - piecesDirtiedGood: Int! - piecesDirtiedBad: Int! + timestamp: DateTime! + downloadedBytes: UInt! + uploadedBytes: UInt! + totalPeers: UInt! + activePeers: UInt! + connectedSeeders: UInt! } input TorrentsFilter { infohash: StringFilter @@ -178,4 +187,5 @@ input TorrentsFilter { peersCount: IntFilter priority: TorrentPriorityFilter } +scalar UInt scalar Upload