diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go index de5c6a9..71d5179 100644 --- a/cmd/tstor/main.go +++ b/cmd/tstor/main.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io/fs" "log/slog" "net" @@ -119,15 +118,15 @@ func run(configPath string) error { return err } - go func() { - log := log.WithComponent("background-scanner") - err := vfs.Walk(ctx, sfs, "/", func(path string, info fs.FileInfo, err error) error { - return nil - }) - if err != nil { - log.Error(ctx, "error walking filesystem", rlog.Error(err)) - } - }() + // go func() { + // log := log.WithComponent("background-scanner") + // err := vfs.Walk(ctx, sfs, "/", func(path string, info fs.FileInfo, err error) error { + // return nil + // }) + // if err != nil { + // log.Error(ctx, "error walking filesystem", rlog.Error(err)) + // } + // }() if conf.Mounts.Fuse.Enabled { mh := fuse.NewHandler(conf.Mounts.Fuse.AllowOther, conf.Mounts.Fuse.Path) diff --git a/daemons/qbittorrent/cleanup.go b/daemons/qbittorrent/cleanup.go index c8b08c9..9855fcc 100644 --- a/daemons/qbittorrent/cleanup.go +++ b/daemons/qbittorrent/cleanup.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path" + "slices" "git.kmsign.ru/royalcat/tstor/pkg/qbittorrent" "git.kmsign.ru/royalcat/tstor/pkg/rlog" @@ -15,6 +16,59 @@ import ( func (d *Daemon) Cleanup(ctx context.Context, run bool) ([]string, error) { d.log.Info(ctx, "cleanup started") + torrentInfos, err := d.client.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{}) + if err != nil { + d.log.Error(ctx, "failed to get torrents", rlog.Error(err)) + return nil, fmt.Errorf("failed to get torrents: %w", err) + } + daemonsHashes := []string{} + for _, info := range torrentInfos { + daemonsHashes = append(daemonsHashes, info.Hash) + } + + dataDirs, err := os.ReadDir(d.dataDir) + if err != nil { + d.log.Error(ctx, "failed to read data directory", slog.String("path", d.dataDir), rlog.Error(err)) + return nil, fmt.Errorf("failed to read data directory: %w", err) + } + dataHashes := []string{} + for _, entry := range dataDirs { + dataHashes = append(dataHashes, entry.Name()) + } + + hashToDelete := make([]string, 0, 5) + + for _, v := range dataHashes { + if !slices.Contains(daemonsHashes, v) { + hashToDelete = append(hashToDelete, v) + } + } + + d.log.Info(ctx, "marked torrents to delete", + slog.Int("count", len(hashToDelete)), + slog.Any("infohashes", hashToDelete), + ) + + if !run { + d.log.Info(ctx, "dry run, skipping deletion") + return hashToDelete, nil + } + + for _, hash := range hashToDelete { + d.log.Info(ctx, "deleting stale torrent data", slog.String("infohash", hash)) + err := os.RemoveAll(path.Join(d.dataDir, hash)) + if err != nil { + d.log.Error(ctx, "failed to delete torrent data", slog.String("infohash", hash), rlog.Error(err)) + return nil, fmt.Errorf("failed to delete torrent data: %w", err) + } + } + + return hashToDelete, nil +} + +func (d *Daemon) CleanupUnregistred(ctx context.Context, run bool) ([]string, error) { + d.log.Info(ctx, "cleanup started") + torrentInfos, err := d.client.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{}) if err != nil { d.log.Error(ctx, "failed to get torrents", rlog.Error(err)) diff --git a/graphql/sources/qbittorrent_mutation.graphql b/graphql/sources/qbittorrent_mutation.graphql index aba4408..cd4323b 100644 --- a/graphql/sources/qbittorrent_mutation.graphql +++ b/graphql/sources/qbittorrent_mutation.graphql @@ -1,8 +1,14 @@ type QBitTorrentDaemonMutation { cleanup(run: Boolean!): QBitCleanupResponse! @resolver + cleanupUnregistred(run: Boolean!): QBitCleanupUnregistredResponse! @resolver } type QBitCleanupResponse { count: Int! hashes: [String!]! } + +type QBitCleanupUnregistredResponse { + count: Int! + hashes: [String!]! +} diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go index fc48e02..de5e82e 100644 --- a/src/delivery/graphql/generated.go +++ b/src/delivery/graphql/generated.go @@ -91,8 +91,14 @@ type ComplexityRoot struct { Hashes func(childComplexity int) int } + QBitCleanupUnregistredResponse struct { + Count func(childComplexity int) int + Hashes func(childComplexity int) int + } + QBitTorrentDaemonMutation struct { - Cleanup func(childComplexity int, run bool) int + Cleanup func(childComplexity int, run bool) int + CleanupUnregistred func(childComplexity int, run bool) int } QBitTorrentDaemonQuery struct { @@ -234,6 +240,7 @@ type MutationResolver interface { } type QBitTorrentDaemonMutationResolver interface { Cleanup(ctx context.Context, obj *model.QBitTorrentDaemonMutation, run bool) (*model.QBitCleanupResponse, error) + CleanupUnregistred(ctx context.Context, obj *model.QBitTorrentDaemonMutation, run bool) (*model.QBitCleanupUnregistredResponse, error) } type QBitTorrentDaemonQueryResolver interface { Torrents(ctx context.Context, obj *model.QBitTorrentDaemonQuery) ([]*model.QTorrent, error) @@ -388,6 +395,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.QBitCleanupResponse.Hashes(childComplexity), true + case "QBitCleanupUnregistredResponse.count": + if e.complexity.QBitCleanupUnregistredResponse.Count == nil { + break + } + + return e.complexity.QBitCleanupUnregistredResponse.Count(childComplexity), true + + case "QBitCleanupUnregistredResponse.hashes": + if e.complexity.QBitCleanupUnregistredResponse.Hashes == nil { + break + } + + return e.complexity.QBitCleanupUnregistredResponse.Hashes(childComplexity), true + case "QBitTorrentDaemonMutation.cleanup": if e.complexity.QBitTorrentDaemonMutation.Cleanup == nil { break @@ -400,6 +421,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.QBitTorrentDaemonMutation.Cleanup(childComplexity, args["run"].(bool)), true + case "QBitTorrentDaemonMutation.cleanupUnregistred": + if e.complexity.QBitTorrentDaemonMutation.CleanupUnregistred == nil { + break + } + + args, err := ec.field_QBitTorrentDaemonMutation_cleanupUnregistred_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.QBitTorrentDaemonMutation.CleanupUnregistred(childComplexity, args["run"].(bool)), true + case "QBitTorrentDaemonQuery.torrents": if e.complexity.QBitTorrentDaemonQuery.Torrents == nil { break @@ -1106,12 +1139,18 @@ interface Progress { }`, BuiltIn: false}, {Name: "../../../graphql/sources/qbittorrent_mutation.graphql", Input: `type QBitTorrentDaemonMutation { cleanup(run: Boolean!): QBitCleanupResponse! @resolver + cleanupUnregistred(run: Boolean!): QBitCleanupUnregistredResponse! @resolver } type QBitCleanupResponse { count: Int! hashes: [String!]! } + +type QBitCleanupUnregistredResponse { + count: Int! + hashes: [String!]! +} `, BuiltIn: false}, {Name: "../../../graphql/sources/qbittorrent_query.graphql", Input: `type QBitTorrentDaemonQuery { torrents: [QTorrent!]! @resolver @@ -1381,6 +1420,38 @@ func (ec *executionContext) field_Mutation_uploadFile_argsFile( return zeroVal, nil } +func (ec *executionContext) field_QBitTorrentDaemonMutation_cleanupUnregistred_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + arg0, err := ec.field_QBitTorrentDaemonMutation_cleanupUnregistred_argsRun(ctx, rawArgs) + if err != nil { + return nil, err + } + args["run"] = arg0 + return args, nil +} +func (ec *executionContext) field_QBitTorrentDaemonMutation_cleanupUnregistred_argsRun( + ctx context.Context, + rawArgs map[string]interface{}, +) (bool, error) { + // We won't call the directive if the argument is null. + // Set call_argument_directives_with_null to true to call directives + // even if the argument is null. + _, ok := rawArgs["run"] + if !ok { + var zeroVal bool + return zeroVal, nil + } + + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("run")) + if tmp, ok := rawArgs["run"]; ok { + return ec.unmarshalNBoolean2bool(ctx, tmp) + } + + var zeroVal bool + return zeroVal, nil +} + func (ec *executionContext) field_QBitTorrentDaemonMutation_cleanup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2267,6 +2338,8 @@ func (ec *executionContext) fieldContext_Mutation_qbitTorrentDaemon(_ context.Co switch field.Name { case "cleanup": return ec.fieldContext_QBitTorrentDaemonMutation_cleanup(ctx, field) + case "cleanupUnregistred": + return ec.fieldContext_QBitTorrentDaemonMutation_cleanupUnregistred(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type QBitTorrentDaemonMutation", field.Name) }, @@ -2461,6 +2534,94 @@ func (ec *executionContext) fieldContext_QBitCleanupResponse_hashes(_ context.Co return fc, nil } +func (ec *executionContext) _QBitCleanupUnregistredResponse_count(ctx context.Context, field graphql.CollectedField, obj *model.QBitCleanupUnregistredResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_QBitCleanupUnregistredResponse_count(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.Count, 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_QBitCleanupUnregistredResponse_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "QBitCleanupUnregistredResponse", + 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) _QBitCleanupUnregistredResponse_hashes(ctx context.Context, field graphql.CollectedField, obj *model.QBitCleanupUnregistredResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_QBitCleanupUnregistredResponse_hashes(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.Hashes, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + fc.Result = res + return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_QBitCleanupUnregistredResponse_hashes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "QBitCleanupUnregistredResponse", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _QBitTorrentDaemonMutation_cleanup(ctx context.Context, field graphql.CollectedField, obj *model.QBitTorrentDaemonMutation) (ret graphql.Marshaler) { fc, err := ec.fieldContext_QBitTorrentDaemonMutation_cleanup(ctx, field) if err != nil { @@ -2544,6 +2705,89 @@ func (ec *executionContext) fieldContext_QBitTorrentDaemonMutation_cleanup(ctx c return fc, nil } +func (ec *executionContext) _QBitTorrentDaemonMutation_cleanupUnregistred(ctx context.Context, field graphql.CollectedField, obj *model.QBitTorrentDaemonMutation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_QBitTorrentDaemonMutation_cleanupUnregistred(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.QBitTorrentDaemonMutation().CleanupUnregistred(rctx, obj, fc.Args["run"].(bool)) + } + + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.Resolver == nil { + var zeroVal *model.QBitCleanupUnregistredResponse + return zeroVal, 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.QBitCleanupUnregistredResponse); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.QBitCleanupUnregistredResponse`, 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.QBitCleanupUnregistredResponse) + fc.Result = res + return ec.marshalNQBitCleanupUnregistredResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQBitCleanupUnregistredResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_QBitTorrentDaemonMutation_cleanupUnregistred(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "QBitTorrentDaemonMutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "count": + return ec.fieldContext_QBitCleanupUnregistredResponse_count(ctx, field) + case "hashes": + return ec.fieldContext_QBitCleanupUnregistredResponse_hashes(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type QBitCleanupUnregistredResponse", 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_QBitTorrentDaemonMutation_cleanupUnregistred_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _QBitTorrentDaemonQuery_torrents(ctx context.Context, field graphql.CollectedField, obj *model.QBitTorrentDaemonQuery) (ret graphql.Marshaler) { fc, err := ec.fieldContext_QBitTorrentDaemonQuery_torrents(ctx, field) if err != nil { @@ -9387,6 +9631,50 @@ func (ec *executionContext) _QBitCleanupResponse(ctx context.Context, sel ast.Se return out } +var qBitCleanupUnregistredResponseImplementors = []string{"QBitCleanupUnregistredResponse"} + +func (ec *executionContext) _QBitCleanupUnregistredResponse(ctx context.Context, sel ast.SelectionSet, obj *model.QBitCleanupUnregistredResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, qBitCleanupUnregistredResponseImplementors) + + 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("QBitCleanupUnregistredResponse") + case "count": + out.Values[i] = ec._QBitCleanupUnregistredResponse_count(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "hashes": + out.Values[i] = ec._QBitCleanupUnregistredResponse_hashes(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 qBitTorrentDaemonMutationImplementors = []string{"QBitTorrentDaemonMutation"} func (ec *executionContext) _QBitTorrentDaemonMutation(ctx context.Context, sel ast.SelectionSet, obj *model.QBitTorrentDaemonMutation) graphql.Marshaler { @@ -9433,6 +9721,42 @@ func (ec *executionContext) _QBitTorrentDaemonMutation(ctx context.Context, sel continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "cleanupUnregistred": + 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._QBitTorrentDaemonMutation_cleanupUnregistred(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)) @@ -11454,6 +11778,16 @@ func (ec *executionContext) marshalNQBitCleanupResponse2ᚖgitᚗkmsignᚗruᚋr return ec._QBitCleanupResponse(ctx, sel, v) } +func (ec *executionContext) marshalNQBitCleanupUnregistredResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQBitCleanupUnregistredResponse(ctx context.Context, sel ast.SelectionSet, v *model.QBitCleanupUnregistredResponse) 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._QBitCleanupUnregistredResponse(ctx, sel, v) +} + func (ec *executionContext) marshalNQTorrent2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQTorrentᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.QTorrent) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go index 2a5396a..00e2c04 100644 --- a/src/delivery/graphql/model/models_gen.go +++ b/src/delivery/graphql/model/models_gen.go @@ -101,8 +101,14 @@ type QBitCleanupResponse struct { Hashes []string `json:"hashes"` } +type QBitCleanupUnregistredResponse struct { + Count int64 `json:"count"` + Hashes []string `json:"hashes"` +} + type QBitTorrentDaemonMutation struct { - Cleanup *QBitCleanupResponse `json:"cleanup"` + Cleanup *QBitCleanupResponse `json:"cleanup"` + CleanupUnregistred *QBitCleanupUnregistredResponse `json:"cleanupUnregistred"` } type QBitTorrentDaemonQuery struct { diff --git a/src/delivery/graphql/resolver/qbittorrent_mutation.resolvers.go b/src/delivery/graphql/resolver/qbittorrent_mutation.resolvers.go index bd5efb2..fb5e8ad 100644 --- a/src/delivery/graphql/resolver/qbittorrent_mutation.resolvers.go +++ b/src/delivery/graphql/resolver/qbittorrent_mutation.resolvers.go @@ -23,6 +23,18 @@ func (r *qBitTorrentDaemonMutationResolver) Cleanup(ctx context.Context, obj *mo }, nil } +// CleanupUnregistred is the resolver for the cleanupUnregistred field. +func (r *qBitTorrentDaemonMutationResolver) CleanupUnregistred(ctx context.Context, obj *model.QBitTorrentDaemonMutation, run bool) (*model.QBitCleanupUnregistredResponse, error) { + hahses, err := r.QBitTorrentDaemon.CleanupUnregistred(ctx, run) + if err != nil { + return nil, err + } + return &model.QBitCleanupUnregistredResponse{ + Count: int64(len(hahses)), + Hashes: hahses, + }, nil +} + // QBitTorrentDaemonMutation returns graph.QBitTorrentDaemonMutationResolver implementation. func (r *Resolver) QBitTorrentDaemonMutation() graph.QBitTorrentDaemonMutationResolver { return &qBitTorrentDaemonMutationResolver{r} diff --git a/src/source/source.go b/src/source/source.go new file mode 100644 index 0000000..e6a53be --- /dev/null +++ b/src/source/source.go @@ -0,0 +1,31 @@ +package source + +type Source interface { + ID() string + MappedPath() string +} + +const TorrentPrefix string = "torrent" + +type TorrentSource struct { + Hash string `json:"hash"` + Mapped string `json:"mapped_path"` +} + +func (t *TorrentSource) ID() string { + return TorrentPrefix + "/" + t.Hash +} + +func (t *TorrentSource) MappedPath() string { + return t.Mapped +} + +const FolderPrefix string = "folder" + +type FolderSource struct { + Name string `json:"name"` +} + +func (f *FolderSource) ID() string { + return FolderPrefix + "/" + f.Name +} diff --git a/ui/lib/api/schema.graphql b/ui/lib/api/schema.graphql index 0070d3c..f1673c9 100644 --- a/ui/lib/api/schema.graphql +++ b/ui/lib/api/schema.graphql @@ -61,8 +61,13 @@ type QBitCleanupResponse { count: Int! hashes: [String!]! } +type QBitCleanupUnregistredResponse { + count: Int! + hashes: [String!]! +} type QBitTorrentDaemonMutation { cleanup(run: Boolean!): QBitCleanupResponse! @resolver + cleanupUnregistred(run: Boolean!): QBitCleanupUnregistredResponse! @resolver } type QBitTorrentDaemonQuery { torrents: [QTorrent!]! @resolver