diff --git a/graphql/mutation.graphql b/graphql/mutation.graphql index 0f45036..83a3a86 100644 --- a/graphql/mutation.graphql +++ b/graphql/mutation.graphql @@ -2,6 +2,7 @@ type Mutation { validateTorrents(filter: TorrentFilter!): Boolean! cleanupTorrents(files: Boolean, dryRun: Boolean!): CleanupResponse! downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse + uploadFile(dir: String!, file: Upload!): Boolean! dedupeStorage: Int! } diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 9342a90..21b5cdf 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -2,6 +2,7 @@ directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION directive @stream on FIELD_DEFINITION scalar DateTime +scalar Upload type Schema { query: Query diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go index 67b98c0..8b8390f 100644 --- a/src/delivery/graphql/generated.go +++ b/src/delivery/graphql/generated.go @@ -75,6 +75,7 @@ type ComplexityRoot struct { CleanupTorrents func(childComplexity int, files *bool, dryRun bool) int DedupeStorage func(childComplexity int) int DownloadTorrent func(childComplexity int, infohash string, file *string) int + UploadFile func(childComplexity int, dir string, file graphql.Upload) int ValidateTorrents func(childComplexity int, filter model.TorrentFilter) int } @@ -164,6 +165,7 @@ type MutationResolver interface { ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error) CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (*model.CleanupResponse, error) DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error) + UploadFile(ctx context.Context, dir string, file graphql.Upload) (bool, error) DedupeStorage(ctx context.Context) (int64, error) } type QueryResolver interface { @@ -283,6 +285,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DownloadTorrent(childComplexity, args["infohash"].(string), args["file"].(*string)), true + case "Mutation.uploadFile": + if e.complexity.Mutation.UploadFile == nil { + break + } + + args, err := ec.field_Mutation_uploadFile_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UploadFile(childComplexity, args["dir"].(string), args["file"].(graphql.Upload)), true + case "Mutation.validateTorrents": if e.complexity.Mutation.ValidateTorrents == nil { break @@ -716,6 +730,7 @@ var sources = []*ast.Source{ validateTorrents(filter: TorrentFilter!): Boolean! cleanupTorrents(files: Boolean, dryRun: Boolean!): CleanupResponse! downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse + uploadFile(dir: String!, file: Upload!): Boolean! dedupeStorage: Int! } @@ -789,6 +804,7 @@ input BooleanFilter @oneOf { directive @stream on FIELD_DEFINITION scalar DateTime +scalar Upload type Schema { query: Query @@ -942,6 +958,30 @@ func (ec *executionContext) field_Mutation_downloadTorrent_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_uploadFile_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["dir"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dir")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["dir"] = arg0 + var arg1 graphql.Upload + if tmp, ok := rawArgs["file"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file")) + arg1, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, tmp) + if err != nil { + return nil, err + } + } + args["file"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_validateTorrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1492,6 +1532,61 @@ func (ec *executionContext) fieldContext_Mutation_downloadTorrent(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_uploadFile(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_uploadFile(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UploadFile(rctx, fc.Args["dir"].(string), fc.Args["file"].(graphql.Upload)) + }) + 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.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_uploadFile(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_uploadFile_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_dedupeStorage(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_dedupeStorage(ctx, field) if err != nil { @@ -1951,6 +2046,8 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi return ec.fieldContext_Mutation_cleanupTorrents(ctx, field) case "downloadTorrent": return ec.fieldContext_Mutation_downloadTorrent(ctx, field) + case "uploadFile": + return ec.fieldContext_Mutation_uploadFile(ctx, field) case "dedupeStorage": return ec.fieldContext_Mutation_dedupeStorage(ctx, field) } @@ -6226,6 +6323,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_downloadTorrent(ctx, field) }) + case "uploadFile": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_uploadFile(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "dedupeStorage": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_dedupeStorage(ctx, field) @@ -7797,6 +7901,21 @@ func (ec *executionContext) marshalNTorrentPeer2ᚖgitᚗkmsignᚗruᚋroyalcat return ec._TorrentPeer(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) +} + +func (ec *executionContext) marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, sel ast.SelectionSet, v graphql.Upload) graphql.Marshaler { + res := graphql.MarshalUpload(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) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } diff --git a/src/delivery/graphql/resolver/mutation.resolvers.go b/src/delivery/graphql/resolver/mutation.resolvers.go index 0ec91aa..e1cb709 100644 --- a/src/delivery/graphql/resolver/mutation.resolvers.go +++ b/src/delivery/graphql/resolver/mutation.resolvers.go @@ -6,11 +6,16 @@ package resolver import ( "context" + "fmt" + "io" + "os" + pathlib "path" "git.kmsign.ru/royalcat/tstor/pkg/uuid" graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" "git.kmsign.ru/royalcat/tstor/src/host/service" + "github.com/99designs/gqlgen/graphql" aih "github.com/anacrolix/torrent/types/infohash" ) @@ -86,6 +91,34 @@ func (r *mutationResolver) DownloadTorrent(ctx context.Context, infohash string, return &model.DownloadTorrentResponse{}, nil } +// UploadFile is the resolver for the uploadFile field. +func (r *mutationResolver) UploadFile(ctx context.Context, dir string, file graphql.Upload) (bool, error) { + dir = pathlib.Join(r.Service.SourceDir, dir) + + dirInfo, err := os.Stat(dir) + if err != nil { + return false, err + } + + if !dirInfo.IsDir() { + return false, fmt.Errorf("path %s is not a directory", dir) + } + + filename := pathlib.Join(dir, file.Filename) + target, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm) + defer target.Close() + if err != nil { + return false, err + } + + _, err = io.CopyN(target, file.File, file.Size) + if err != nil { + return false, err + } + + return true, nil +} + // DedupeStorage is the resolver for the dedupeStorage field. func (r *mutationResolver) DedupeStorage(ctx context.Context) (int64, error) { deduped, err := r.Service.Storage.Dedupe(ctx) diff --git a/ui/lib/api/schema.graphql b/ui/lib/api/schema.graphql index 294aec5..2098c19 100644 --- a/ui/lib/api/schema.graphql +++ b/ui/lib/api/schema.graphql @@ -46,7 +46,7 @@ type Mutation { validateTorrents(filter: TorrentFilter!): Boolean! cleanupTorrents(files: Boolean, dryRun: Boolean!): CleanupResponse! downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse - uploadFile(file: Upload!): Boolean! + uploadFile(dir: String!, file: Upload!): Boolean! dedupeStorage: Int! } input Pagination {