diff --git a/graphql/sources/torrent_query.graphql b/graphql/sources/torrent_query.graphql index 0bf5335..f9096f5 100644 --- a/graphql/sources/torrent_query.graphql +++ b/graphql/sources/torrent_query.graphql @@ -1,5 +1,6 @@ type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver + stats: TorrentStats! @resolver } input TorrentsFilter { diff --git a/graphql/sources/torrent_types.graphql b/graphql/sources/torrent_types.graphql index 54b4548..1b64219 100644 --- a/graphql/sources/torrent_types.graphql +++ b/graphql/sources/torrent_types.graphql @@ -32,3 +32,22 @@ enum TorrentPriority { READAHEAD NOW } + +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! +} diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go index 9723c03..72d0235 100644 --- a/src/delivery/graphql/generated.go +++ b/src/delivery/graphql/generated.go @@ -135,6 +135,7 @@ type ComplexityRoot struct { } TorrentDaemonQuery struct { + Stats func(childComplexity int) int Torrents func(childComplexity int, filter *model.TorrentsFilter) int } @@ -170,6 +171,22 @@ type ComplexityRoot struct { Torrent func(childComplexity int) int Total func(childComplexity int) int } + + 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 + } } type ArchiveFSResolver interface { @@ -208,6 +225,7 @@ 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) } type TorrentFSResolver interface { Entries(ctx context.Context, obj *model.TorrentFs) ([]model.FsEntry, error) @@ -503,6 +521,13 @@ 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 { + break + } + + return e.complexity.TorrentDaemonQuery.Stats(childComplexity), true + case "TorrentDaemonQuery.torrents": if e.complexity.TorrentDaemonQuery.Torrents == nil { break @@ -641,6 +666,97 @@ 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 { + break + } + + return e.complexity.TorrentStats.BytesRead(childComplexity), true + + case "TorrentStats.bytesReadData": + if e.complexity.TorrentStats.BytesReadData == nil { + break + } + + return e.complexity.TorrentStats.BytesReadData(childComplexity), true + + case "TorrentStats.bytesReadUsefulData": + if e.complexity.TorrentStats.BytesReadUsefulData == nil { + break + } + + return e.complexity.TorrentStats.BytesReadUsefulData(childComplexity), true + + case "TorrentStats.bytesReadUsefulIntendedData": + if e.complexity.TorrentStats.BytesReadUsefulIntendedData == nil { + break + } + + return e.complexity.TorrentStats.BytesReadUsefulIntendedData(childComplexity), true + + case "TorrentStats.bytesWritten": + if e.complexity.TorrentStats.BytesWritten == nil { + break + } + + return e.complexity.TorrentStats.BytesWritten(childComplexity), true + + case "TorrentStats.bytesWrittenData": + if e.complexity.TorrentStats.BytesWrittenData == 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 0, false } @@ -838,6 +954,7 @@ type DownloadTorrentResponse { `, BuiltIn: false}, {Name: "../../../graphql/sources/torrent_query.graphql", Input: `type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver + stats: TorrentStats! @resolver } input TorrentsFilter { @@ -898,6 +1015,25 @@ enum TorrentPriority { READAHEAD NOW } + +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! +} `, BuiltIn: false}, {Name: "../../../graphql/types/filters.graphql", Input: `input Pagination { offset: Int! @@ -1691,6 +1827,8 @@ 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) } return nil, fmt.Errorf("no field named %q was found under type TorrentDaemonQuery", field.Name) }, @@ -3268,6 +3406,98 @@ 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) + 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().Stats(rctx, obj) + } + 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_stats(_ 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 "bytesWritten": + return ec.fieldContext_TorrentStats_bytesWritten(ctx, field) + case "bytesWrittenData": + return ec.fieldContext_TorrentStats_bytesWrittenData(ctx, field) + case "bytesRead": + return ec.fieldContext_TorrentStats_bytesRead(ctx, field) + case "bytesReadData": + return ec.fieldContext_TorrentStats_bytesReadData(ctx, field) + case "bytesReadUsefulData": + return ec.fieldContext_TorrentStats_bytesReadUsefulData(ctx, field) + case "bytesReadUsefulIntendedData": + return ec.fieldContext_TorrentStats_bytesReadUsefulIntendedData(ctx, field) + case "chunksWritten": + return ec.fieldContext_TorrentStats_chunksWritten(ctx, field) + case "chunksRead": + return ec.fieldContext_TorrentStats_chunksRead(ctx, field) + case "chunksReadUseful": + return ec.fieldContext_TorrentStats_chunksReadUseful(ctx, field) + case "chunksReadWasted": + return ec.fieldContext_TorrentStats_chunksReadWasted(ctx, field) + case "metadataChunksRead": + return ec.fieldContext_TorrentStats_metadataChunksRead(ctx, field) + case "piecesDirtiedGood": + return ec.fieldContext_TorrentStats_piecesDirtiedGood(ctx, field) + case "piecesDirtiedBad": + return ec.fieldContext_TorrentStats_piecesDirtiedBad(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TorrentStats", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _TorrentFS_name(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFs) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TorrentFS_name(ctx, field) if err != nil { @@ -4160,6 +4390,578 @@ 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) + 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_TorrentStats_bytesWritten(_ 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_bytesWrittenData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_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_TorrentStats_bytesWrittenData(_ 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_bytesRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_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_TorrentStats_bytesRead(_ 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_bytesReadData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_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_TorrentStats_bytesReadData(_ 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_bytesReadUsefulData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_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_TorrentStats_bytesReadUsefulData(_ 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_bytesReadUsefulIntendedData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TorrentStats_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_TorrentStats_bytesReadUsefulIntendedData(_ 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 fc, nil +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_name(ctx, field) if err != nil { @@ -7804,6 +8606,42 @@ func (ec *executionContext) _TorrentDaemonQuery(ctx context.Context, sel ast.Sel continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "stats": + 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_stats(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) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -8150,6 +8988,105 @@ func (ec *executionContext) _TorrentProgress(ctx context.Context, sel ast.Select return out } +var torrentStatsImplementors = []string{"TorrentStats"} + +func (ec *executionContext) _TorrentStats(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentStats) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, torrentStatsImplementors) + + 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("TorrentStats") + case "bytesWritten": + out.Values[i] = ec._TorrentStats_bytesWritten(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesWrittenData": + out.Values[i] = ec._TorrentStats_bytesWrittenData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesRead": + out.Values[i] = ec._TorrentStats_bytesRead(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesReadData": + out.Values[i] = ec._TorrentStats_bytesReadData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "bytesReadUsefulData": + out.Values[i] = ec._TorrentStats_bytesReadUsefulData(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) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -8847,6 +9784,16 @@ 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 { + 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._TorrentStats(ctx, sel, v) +} + 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/models_gen.go b/src/delivery/graphql/model/models_gen.go index cb5a4e6..d1fa34f 100644 --- a/src/delivery/graphql/model/models_gen.go +++ b/src/delivery/graphql/model/models_gen.go @@ -191,7 +191,8 @@ type TorrentDaemonMutation struct { } type TorrentDaemonQuery struct { - Torrents []*Torrent `json:"torrents"` + Torrents []*Torrent `json:"torrents"` + Stats *TorrentStats `json:"stats"` } type TorrentFs struct { @@ -269,6 +270,22 @@ func (TorrentProgress) IsProgress() {} 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"` +} + type TorrentsFilter struct { Infohash *StringFilter `json:"infohash,omitempty"` Name *StringFilter `json:"name,omitempty"` diff --git a/src/delivery/graphql/resolver/torrent_query.resolvers.go b/src/delivery/graphql/resolver/torrent_query.resolvers.go index 3661a7f..2c77a32 100644 --- a/src/delivery/graphql/resolver/torrent_query.resolvers.go +++ b/src/delivery/graphql/resolver/torrent_query.resolvers.go @@ -86,6 +86,26 @@ 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) { + stats := r.Service.Stats() + return &model.TorrentStats{ + BytesWritten: stats.BytesWritten.Int64(), + BytesRead: stats.BytesRead.Int64(), + BytesWrittenData: stats.BytesWrittenData.Int64(), + BytesReadData: stats.BytesReadData.Int64(), + BytesReadUsefulData: stats.BytesReadUsefulData.Int64(), + BytesReadUsefulIntendedData: stats.BytesReadUsefulIntendedData.Int64(), + ChunksWritten: stats.ChunksWritten.Int64(), + ChunksRead: stats.ChunksRead.Int64(), + ChunksReadUseful: stats.ChunksReadUseful.Int64(), + ChunksReadWasted: stats.ChunksReadWasted.Int64(), + MetadataChunksRead: stats.MetadataChunksRead.Int64(), + PiecesDirtiedGood: stats.PiecesDirtiedGood.Int64(), + PiecesDirtiedBad: stats.PiecesDirtiedBad.Int64(), + }, nil +} + // TorrentDaemonQuery returns graph.TorrentDaemonQueryResolver implementation. func (r *Resolver) TorrentDaemonQuery() graph.TorrentDaemonQueryResolver { return &torrentDaemonQueryResolver{r} diff --git a/src/delivery/http.go b/src/delivery/http.go index cc94cf8..c8f4f76 100644 --- a/src/delivery/http.go +++ b/src/delivery/http.go @@ -23,7 +23,7 @@ func New(fc *filecache.Cache, s *torrent.Daemon, vfs vfs.Filesystem, logPath str // middleware.Recover(), middleware.Gzip(), middleware.Decompress(), - Logger(), + // Logger(), ) echopprof.Register(r) diff --git a/src/sources/torrent/client.go b/src/sources/torrent/client.go index 355ad88..0eb0c41 100644 --- a/src/sources/torrent/client.go +++ b/src/sources/torrent/client.go @@ -22,15 +22,16 @@ func newClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient torrentCfg.PeerID = string(id[:]) torrentCfg.DefaultStorage = st torrentCfg.AlwaysWantConns = true + torrentCfg.DropMutuallyCompletePeers = true + torrentCfg.TorrentPeersLowWater = 100 + torrentCfg.TorrentPeersHighWater = 1000 torrentCfg.AcceptPeerConnections = true - torrentCfg.DisableAggressiveUpload = false - torrentCfg.Seed = true - // torrentCfg.DownloadRateLimiter = rate.NewLimiter(rate.Inf, 0) - // torrentCfg + torrentCfg.DisableAggressiveUpload = false tl := tlog.NewLogger() tl.SetHandlers(&dlog.Torrent{L: l}) + torrentCfg.Logger = tl torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.NewPeer, func(p *torrent.Peer) { l := l.With("ip", p.RemoteAddr.String()) @@ -55,6 +56,7 @@ func newClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient // l.Debug("peer closed", "ip", c.RemoteAddr.String()) // }) + torrentCfg.PeriodicallyAnnounceTorrentsToDht = true torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) { cfg.Store = fis cfg.Exp = 2 * time.Hour diff --git a/src/sources/torrent/controller.go b/src/sources/torrent/controller.go index d2d45fa..d641ed2 100644 --- a/src/sources/torrent/controller.go +++ b/src/sources/torrent/controller.go @@ -29,12 +29,12 @@ type Controller struct { log *rlog.Logger } -func newController(t *torrent.Torrent, torrentFileProperties kv.Store[string, FileProperties], storage TorrentFileDeleter) *Controller { +func newController(t *torrent.Torrent, torrentFileProperties kv.Store[string, FileProperties], storage TorrentFileDeleter, log *rlog.Logger) *Controller { return &Controller{ t: t, storage: storage, fileProperties: torrentFileProperties, - log: rlog.Component("torrent-client", "controller").With(slog.String("infohash", t.InfoHash().HexString())), + log: log.WithComponent("controller").With(slog.String("infohash", t.InfoHash().HexString())), } } @@ -101,7 +101,7 @@ func (s *Controller) Files(ctx context.Context) ([]*FileController, error) { } props := kvsingle.New(s.fileProperties, v.Path()) - ctl := NewFileController(v, props) + ctl := NewFileController(v, props, s.log) files = append(files, ctl) } @@ -179,15 +179,25 @@ func (s *Controller) ValidateTorrent(ctx context.Context) error { } func (c *Controller) SetPriority(ctx context.Context, priority types.PiecePriority) error { - // log := c.log.With(slog.Int("priority", int(priority))) - - for _, f := range c.t.Files() { - err := c.setFilePriority(ctx, f, priority) + log := c.log.With(slog.Int("priority", int(priority))) + files, err := c.Files(ctx) + if err != nil { + return err + } + for _, f := range files { + excluded, err := f.Excluded(ctx) if err != nil { - return err + log.Error(ctx, "failed to get file exclusion status", rlog.Error(err)) + } + if excluded { + continue + } + + err = f.SetPriority(ctx, priority) + if err != nil { + log.Error(ctx, "failed to set file priority", rlog.Error(err)) } } - return nil } @@ -200,37 +210,35 @@ func (c *Controller) Priority(ctx context.Context) (types.PiecePriority, error) return 0, err } for _, v := range files { - props, err := v.Properties(ctx) - if err != nil { - return 0, err - } - if props.Priority > prio { - prio = props.Priority + filePriority := v.Priority() + if filePriority > prio { + prio = filePriority } } return prio, nil } -func (c *Controller) setFilePriority(ctx context.Context, file *torrent.File, priority types.PiecePriority) error { - err := c.fileProperties.Edit(ctx, file.Path(), func(ctx context.Context, v FileProperties) (FileProperties, error) { - v.Priority = priority - return v, nil - }) - if err == kv.ErrKeyNotFound { - seterr := c.fileProperties.Set(ctx, file.Path(), FileProperties{Priority: priority}) - if seterr != nil { - return seterr - } - err = nil - } +// func (c *Controller) setFilePriority(ctx context.Context, file *torrent.File, priority types.PiecePriority) error { +// err := c.fileProperties.Edit(ctx, file.Path(), func(ctx context.Context, v FileProperties) (FileProperties, error) { +// v.Priority = priority +// return v, nil +// }) - if err != nil { - return err - } - file.SetPriority(priority) - return nil -} +// if err == kv.ErrKeyNotFound { +// seterr := c.fileProperties.Set(ctx, file.Path(), FileProperties{Priority: priority}) +// if seterr != nil { +// return seterr +// } +// err = nil +// } + +// if err != nil { +// return err +// } +// file.SetPriority(priority) +// return nil +// } func (c *Controller) initializeTorrentPriories(ctx context.Context) error { ctx, span := tracer.Start(ctx, "initializeTorrentPriories") @@ -248,12 +256,10 @@ func (c *Controller) initializeTorrentPriories(ctx context.Context) error { log.Error(ctx, "failed to get file properties", rlog.Error(err)) continue } - log = log.With(slog.Int("priority", int(props.Priority))) - file.file.SetPriority(props.Priority) } - log.Info(ctx, "torrent initialization complete", slog.String("infohash", c.InfoHash()), slog.String("torrent_name", c.Name())) + log.Debug(ctx, "torrent initialization complete", slog.String("infohash", c.InfoHash()), slog.String("torrent_name", c.Name())) return nil } diff --git a/src/sources/torrent/daemon.go b/src/sources/torrent/daemon.go index 7594b3c..b33df21 100644 --- a/src/sources/torrent/daemon.go +++ b/src/sources/torrent/daemon.go @@ -45,7 +45,7 @@ type Daemon struct { client *torrent.Client infoBytes *infoBytesStore Storage *fileStorage - fis *fileItemStore + fis *dhtFileItemStore dirsAquire kv.Store[string, DirAquire] fileProperties kv.Store[string, FileProperties] @@ -70,7 +70,7 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, return nil, fmt.Errorf("error creating metadata folder: %w", err) } - s.fis, err = newFileItemStore(filepath.Join(conf.MetadataFolder, "items"), 2*time.Hour) + s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), 3*time.Hour) if err != nil { return nil, fmt.Errorf("error starting item store: %w", err) } @@ -149,9 +149,11 @@ func (s *Daemon) loadTorrent(ctx context.Context, f vfs.File) (*Controller, erro return nil, fmt.Errorf("loading torrent metadata from file %s, error: %w", stat.Name(), err) } + var ctl *Controller t, ok := s.client.Torrent(mi.HashInfoBytes()) - if !ok { - + if ok { + ctl = s.newController(t) + } else { span.AddEvent("torrent not found, loading from file") log.Info(ctx, "torrent not found, loading from file") @@ -175,11 +177,13 @@ func (s *Daemon) loadTorrent(ctx context.Context, f vfs.File) (*Controller, erro } t, _ = s.client.AddTorrentOpt(torrent.AddTorrentOpts{ - InfoHash: spec.InfoHash, - Storage: s.Storage, - InfoBytes: infoBytes, - ChunkSize: spec.ChunkSize, + InfoHash: spec.InfoHash, + InfoHashV2: spec.InfoHashV2, + Storage: s.Storage, + InfoBytes: infoBytes, + ChunkSize: spec.ChunkSize, }) + t.AllowDataDownload() t.AllowDataUpload() @@ -199,6 +203,13 @@ func (s *Daemon) loadTorrent(ctx context.Context, f vfs.File) (*Controller, erro } span.AddEvent("got info") + ctl = s.newController(t) + + err = ctl.initializeTorrentPriories(ctx) + if err != nil { + return nil, fmt.Errorf("initialize torrent priorities: %w", err) + } + // info := t.Info() // if info == nil { // return nil, fmt.Errorf("info is nil") @@ -216,13 +227,6 @@ func (s *Daemon) loadTorrent(ctx context.Context, f vfs.File) (*Controller, erro // } } - ctl := s.newController(t) - - err = ctl.initializeTorrentPriories(ctx) - if err != nil { - log.Error(ctx, "error initializing torrent priorities", rlog.Error(err)) - } - return ctl, nil } @@ -232,11 +236,7 @@ func isValidInfoHashBytes(d []byte) bool { return err == nil } -func (s *Daemon) Stats() (*Stats, error) { - return &Stats{}, nil -} - -func (s *Daemon) GetStats() torrent.ConnStats { +func (s *Daemon) Stats() torrent.ConnStats { return s.client.ConnStats() } @@ -316,6 +316,7 @@ func (s *Daemon) newController(t *torrent.Torrent) *Controller { return newController(t, storeByTorrent(s.fileProperties, t.InfoHash()), s.Storage, + s.log, ) } diff --git a/src/sources/torrent/fileitem.go b/src/sources/torrent/dht_fileitem_store.go similarity index 73% rename from src/sources/torrent/fileitem.go rename to src/sources/torrent/dht_fileitem_store.go index 816388b..d8cfde5 100644 --- a/src/sources/torrent/fileitem.go +++ b/src/sources/torrent/dht_fileitem_store.go @@ -10,16 +10,16 @@ import ( "github.com/dgraph-io/badger/v4" ) -var _ bep44.Store = &fileItemStore{} +var _ bep44.Store = &dhtFileItemStore{} -type fileItemStore struct { +type dhtFileItemStore struct { ttl time.Duration db *badger.DB } -func newFileItemStore(path string, itemsTTL time.Duration) (*fileItemStore, error) { +func newDHTStore(path string, itemsTTL time.Duration) (*dhtFileItemStore, error) { opts := badger.DefaultOptions(path). - WithLogger(dlog.BadgerLogger("torrent-client", "item-store")). + WithLogger(dlog.BadgerLogger("torrent-client", "dht-item-store")). WithValueLogFileSize(1<<26 - 1) db, err := badger.Open(opts) @@ -32,13 +32,13 @@ func newFileItemStore(path string, itemsTTL time.Duration) (*fileItemStore, erro return nil, err } - return &fileItemStore{ + return &dhtFileItemStore{ db: db, ttl: itemsTTL, }, nil } -func (fis *fileItemStore) Put(i *bep44.Item) error { +func (fis *dhtFileItemStore) Put(i *bep44.Item) error { tx := fis.db.NewTransaction(true) defer tx.Discard() @@ -58,7 +58,7 @@ func (fis *fileItemStore) Put(i *bep44.Item) error { return tx.Commit() } -func (fis *fileItemStore) Get(t bep44.Target) (*bep44.Item, error) { +func (fis *dhtFileItemStore) Get(t bep44.Target) (*bep44.Item, error) { tx := fis.db.NewTransaction(false) defer tx.Discard() @@ -84,11 +84,11 @@ func (fis *fileItemStore) Get(t bep44.Target) (*bep44.Item, error) { return i, nil } -func (fis *fileItemStore) Del(t bep44.Target) error { +func (fis *dhtFileItemStore) Del(t bep44.Target) error { // ignore this return nil } -func (fis *fileItemStore) Close() error { +func (fis *dhtFileItemStore) Close() error { return fis.db.Close() } diff --git a/src/sources/torrent/file_controller.go b/src/sources/torrent/file_controller.go index 1a54702..67d8f4b 100644 --- a/src/sources/torrent/file_controller.go +++ b/src/sources/torrent/file_controller.go @@ -2,8 +2,10 @@ package torrent import ( "context" + "log/slog" "git.kmsign.ru/royalcat/tstor/pkg/kvsingle" + "git.kmsign.ru/royalcat/tstor/pkg/rlog" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/types" @@ -13,12 +15,14 @@ import ( type FileController struct { file *torrent.File properties *kvsingle.Value[string, FileProperties] + log *rlog.Logger } -func NewFileController(f *torrent.File, properties *kvsingle.Value[string, FileProperties]) *FileController { +func NewFileController(f *torrent.File, properties *kvsingle.Value[string, FileProperties], log *rlog.Logger) *FileController { return &FileController{ file: f, properties: properties, + log: log.WithComponent("file-controller").With(slog.String("file", f.Path())), } } @@ -38,6 +42,8 @@ func (s *FileController) Properties(ctx context.Context) (FileProperties, error) } func (s *FileController) SetPriority(ctx context.Context, priority types.PiecePriority) error { + log := s.log.With(slog.Int("priority", int(priority))) + err := s.properties.Edit(ctx, func(ctx context.Context, v FileProperties) (FileProperties, error) { v.Priority = priority return v, nil @@ -55,6 +61,7 @@ func (s *FileController) SetPriority(ctx context.Context, priority types.PiecePr return err } + log.Debug(ctx, "file priority set") s.file.SetPriority(priority) return nil @@ -83,6 +90,10 @@ func (s *FileController) Size() int64 { return s.file.Length() } +func (s *FileController) Priority() types.PiecePriority { + return s.file.Priority() +} + func (s *FileController) BytesCompleted() int64 { return s.file.BytesCompleted() } diff --git a/src/vfs/log.go b/src/vfs/log.go index 011c49b..d0a05c7 100644 --- a/src/vfs/log.go +++ b/src/vfs/log.go @@ -124,7 +124,7 @@ func (fs *LogFS) Open(ctx context.Context, filename string) (file File, err erro if isLoggableError(err) { fs.log.Error(ctx, "Failed to open file") } - file = wrapLogFile(file, filename, fs.log, fs.readTimeout, fs.tel) + file = WrapLogFile(file, filename, fs.log, fs.readTimeout, fs.tel) if file != nil { fs.tel.openedFiles.Add(ctx, 1) @@ -221,7 +221,7 @@ func (f *LogFile) Type() fs.FileMode { var _ File = (*LogFile)(nil) -func wrapLogFile(f File, filename string, log *rlog.Logger, timeout time.Duration, tel *fsTelemetry) *LogFile { +func WrapLogFile(f File, filename string, log *rlog.Logger, timeout time.Duration, tel *fsTelemetry) *LogFile { return &LogFile{ filename: filename, f: f, diff --git a/ui/lib/api/schema.graphql b/ui/lib/api/schema.graphql index 4c384cf..612f7ec 100644 --- a/ui/lib/api/schema.graphql +++ b/ui/lib/api/schema.graphql @@ -106,6 +106,7 @@ type TorrentDaemonMutation { } type TorrentDaemonQuery { torrents(filter: TorrentsFilter): [Torrent!]! @resolver + stats: TorrentStats! @resolver } type TorrentFS implements Dir & FsEntry { name: String! @@ -154,6 +155,21 @@ type TorrentProgress implements Progress { current: Int! 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! +} input TorrentsFilter { infohash: StringFilter name: StringFilter