the big refactor

This commit is contained in:
2021-09-24 15:39:23 +02:00
parent a3e66cd351
commit ed932e3c92
13 changed files with 196 additions and 171 deletions

View File

@@ -0,0 +1,245 @@
package gql
import (
"fmt"
"path/filepath"
"time"
"github.com/graph-gophers/dataloader"
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
)
type LoginResult struct {
Token string `json:"token"`
Successful bool `json:"successful"`
}
var graphqlDirType *graphql.Object
var graphqlFileType *graphql.Object
var graphqlLoginResultType *graphql.Object
//GraphqlTypes create all graphql types and stores the in the global variables
func GraphqlTypes() {
var dateTimeType = graphql.NewScalar(graphql.ScalarConfig{
Name: "DateTime",
Description: "DateTime is a DateTime in ISO 8601 format",
Serialize: func(value interface{}) interface{} {
switch value := value.(type) {
case time.Time:
return value.Format(time.RFC3339)
}
return "INVALID"
},
ParseValue: func(value interface{}) interface{} {
switch tvalue := value.(type) {
case string:
if tval, err := time.Parse(time.RFC3339, tvalue); err != nil {
return nil
} else {
return tval
}
}
return nil
},
ParseLiteral: func(valueAST ast.Value) interface{} {
switch valueAST := valueAST.(type) {
case *ast.StringValue:
return valueAST.Value
}
return nil
},
})
graphqlDirType = graphql.NewObject(graphql.ObjectConfig{
Name: "Directory",
Description: "Represents a directory",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.Directory)
if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve")
}
return filepath.Base(source.ID), nil
},
},
},
})
graphqlFileType = graphql.NewObject(graphql.ObjectConfig{
Name: "File",
Description: "Represents a file, not a directory",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
Description: "The uniqe ID of the file. Represents the path and the s3 key.",
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.File)
if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve")
}
return filepath.Base(source.ID), nil
},
},
"size": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
file, err := loadFile(p)
if err != nil {
return nil, err
}
return file.Size, nil
},
},
"contentType": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
file, err := loadFile(p)
if err != nil {
return nil, err
}
return file.ContentType, nil
},
},
"etag": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
file, err := loadFile(p)
if err != nil {
return nil, err
}
return file.ETag, nil
},
},
"lastModified": &graphql.Field{
Type: dateTimeType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
file, err := loadFile(p)
if err != nil {
return nil, err
}
return file.LastModified, nil
},
},
"parent": &graphql.Field{
Type: graphqlDirType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.File)
if !ok {
return nil, fmt.Errorf("Failed to parse Source for parent resolve")
}
basename := helper.GetPathFromId(source.ID)
return types.Directory{
ID: basename,
}, nil
},
},
},
})
graphqlDirType.AddFieldConfig("files", &graphql.Field{
Type: graphql.NewList(graphqlFileType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.Directory)
if !ok {
return nil, fmt.Errorf("Failed to parse Source for files resolve")
}
loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getFiles"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
return thunk()
},
})
graphqlDirType.AddFieldConfig("directorys", &graphql.Field{
Type: graphql.NewList(graphqlDirType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.Directory)
if !ok {
return nil, fmt.Errorf("Failed to parse Source for directories resolve")
}
loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getDirs"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
return thunk()
},
})
graphqlDirType.AddFieldConfig("parent", &graphql.Field{
Type: graphqlDirType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(types.Directory)
if !ok {
return nil, fmt.Errorf("Failed to parse Source for directories resolve")
}
return types.Directory{
ID: helper.GetParentDir(source.ID),
}, nil
},
})
graphqlLoginResultType = graphql.NewObject(graphql.ObjectConfig{
Name: "LoginResut",
Description: "Result of a login",
Fields: graphql.Fields{
"token": &graphql.Field{
Type: graphql.String,
Description: "JWT token if login was successful",
},
"successful": &graphql.Field{
Type: graphql.NewNonNull(graphql.Boolean),
Description: "If the login was successful",
},
},
})
}
//loadFile helper func for using the dataloader to get a file
func loadFile(p graphql.ResolveParams) (*types.File, error) {
source, ok := p.Source.(types.File)
if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve")
}
loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getFile"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
result, err := thunk()
if err != nil {
return nil, err
}
file, ok := result.(*types.File)
if !ok {
return nil, fmt.Errorf("Failed to load file")
}
return file, err
}

220
internal/gql/mutations.go Normal file
View File

@@ -0,0 +1,220 @@
package gql
import (
"context"
"fmt"
"strings"
"github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
)
func deleteMutation(ctx context.Context, id string) error {
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok {
return fmt.Errorf("Failed to get s3Client from context")
}
// TODO: it is posible to remove multiple objects with a single call.
// Is it better to batch this?
err := s3Client.RemoveObject(ctx, "dev", id, minio.RemoveObjectOptions{})
if err != nil {
return err
}
// Invalidate cache
return helper.InvalidateCache(ctx, helper.NomalizeID(id))
}
func copyMutation(ctx context.Context, src, dest string) (*types.File, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok {
return nil, fmt.Errorf("Failed to get s3Client from context")
}
// Check if dest is a file or a dir
if strings.HasSuffix(dest, "/") {
// create new dest id
// TODO: What if a file with this id already exists?
dest += helper.GetFilenameFromID(src)
}
info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{
Bucket: "dev",
Object: dest,
}, minio.CopySrcOptions{
Bucket: "dev",
Object: src,
})
if err != nil {
return nil, err
}
// Invalidate cache
// TODO: check error
helper.InvalidateCache(ctx, helper.NomalizeID(info.Key))
return &types.File{
ID: info.Key,
}, nil
}
func moveMutation(ctx context.Context, src, dest string) (*types.File, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok {
return nil, fmt.Errorf("Failed to get s3Client from context")
}
// Check if dest is a file or a dir
if strings.HasSuffix(dest, "/") {
// create new dest id
// TODO: What if a file with this id already exists?
dest += helper.GetFilenameFromID(src)
}
// There is no (spoon) move. Only copy and delete
info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{
Bucket: "dev",
Object: dest,
}, minio.CopySrcOptions{
Bucket: "dev",
Object: src,
})
if err != nil {
return nil, err
}
err = deleteMutation(ctx, src)
if err != nil {
return nil, err
}
helper.InvalidateCache(ctx, helper.NomalizeID(info.Key))
return &types.File{
ID: info.Key,
}, nil
}
func createDirectory(ctx context.Context, path string) (*types.Directory, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok {
return nil, fmt.Errorf("Failed to get s3Client from context")
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
info, err := s3Client.PutObject(ctx, "dev", path, strings.NewReader(""), 0, minio.PutObjectOptions{
ContentType: "application/x-directory",
})
if err != nil {
return nil, err
}
// Invalidate cache
// TODO: check error
helper.InvalidateCacheForDir(ctx, helper.NomalizeID(info.Key))
return &types.Directory{
ID: info.Key,
}, nil
}
func deleteDirectory(ctx context.Context, path string) error {
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok {
return fmt.Errorf("Failed to get s3Client from context")
}
loader, ok := ctx.Value("loader").(map[string]*dataloader.Loader)
if !ok {
return fmt.Errorf("Failed to get dataloader from context")
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
// Get all files inside the directory
thunk := loader["listObjectsRecursive"].Load(ctx, dataloader.StringKey(helper.NomalizeID(path)))
result, err := thunk()
if err != nil {
return err
}
files, ok := result.([]minio.ObjectInfo)
if !ok {
return fmt.Errorf("Failed to get parse result from listObjects")
}
// Delete all child files
err = helper.DeleteMultiple(ctx, *s3Client, files)
if err != nil {
return err
}
// If the dir had no children it exists as an object (object with "/" at the end).
// If it exists as an object and had children it will get delete once the last child has been deleted
// If it had no children we have to delete it manualy
// This is at least the behavior when working with minio as s3 backend
// TODO: check if this is normal behavior when working with s3
if len(files) == 0 {
err := s3Client.RemoveObject(ctx, "dev", path, minio.RemoveObjectOptions{})
if err != nil {
return err
}
}
//Invalidate cache
helper.InvalidateCacheForDir(ctx, helper.NomalizeID(path))
return nil
}
//login Checks for valid username password combination. Returns singed jwt string
func login(ctx context.Context, username, password string) (LoginResult, error) {
// TODO: replace with propper user management
if username != "admin" && password != "hunter2" {
return LoginResult{
Successful: false,
}, nil
}
token := helper.CreateJWT(helper.CreateClaims(username))
tokenString, err := token.SignedString([]byte("TODO"))
if err != nil {
return LoginResult{
Successful: false,
}, err
}
return LoginResult{
Token: tokenString,
Successful: true,
}, nil
}

274
internal/gql/schema.go Normal file
View File

@@ -0,0 +1,274 @@
package gql
import (
"fmt"
"github.com/graph-gophers/dataloader"
"github.com/graphql-go/graphql"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
log "github.com/sirupsen/logrus"
)
//GraphqlSchema generate the schema with its root query and mutation
func GraphqlSchema() (graphql.Schema, error) {
queryFields := graphql.Fields{
"files": &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(graphqlFileType))),
Args: graphql.FieldConfigArgument{
"path": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
path, ok := p.Args["path"].(string)
if !ok {
return nil, nil
}
log.Debug("querry 'files': ", path)
loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getFiles"].Load(p.Context, dataloader.StringKey(path))
return thunk()
},
},
"directorys": &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(graphqlDirType))),
Args: graphql.FieldConfigArgument{
"path": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
path, ok := p.Args["path"].(string)
if !ok {
return nil, nil
}
log.Debug("querry 'directorys': ", path)
loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getDirs"].Load(p.Context, dataloader.StringKey(path))
return thunk()
},
},
"file": &graphql.Field{
Type: graphqlFileType,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
id, ok := p.Args["id"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("querry 'file': ", id)
return types.File{
ID: id,
}, nil
},
},
"authorized": &graphql.Field{
Name: "authorized",
Type: graphql.NewNonNull(graphql.Boolean),
Description: "True if the user is authorized",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
auth, _ := helper.IsAuth(p.Context)
return auth, nil
},
},
}
mutationFields := graphql.Fields{
"delete": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
id, ok := p.Args["id"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("mutation 'delete': ", id)
return id, deleteMutation(p.Context, id)
},
},
"copy": &graphql.Field{
Type: graphqlFileType,
Args: graphql.FieldConfigArgument{
"src": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
"dest": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
src, ok := p.Args["src"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
dest, ok := p.Args["dest"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("mutation 'copy': ", src, "-->", dest)
return copyMutation(p.Context, src, dest)
},
},
"move": &graphql.Field{
Type: graphqlFileType,
Args: graphql.FieldConfigArgument{
"src": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
"dest": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
src, ok := p.Args["src"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
dest, ok := p.Args["dest"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("mutation 'move': ", src, "-->", dest)
return moveMutation(p.Context, src, dest)
},
},
"createDir": &graphql.Field{
Type: graphql.NewNonNull(graphqlDirType),
Args: graphql.FieldConfigArgument{
"path": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
path, ok := p.Args["path"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("mutation 'createDir': ", path)
return createDirectory(p.Context, path)
},
},
"deleteDir": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Args: graphql.FieldConfigArgument{
"path": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := helper.IsAuth(p.Context); !is {
return nil, err
}
path, ok := p.Args["path"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
log.Debug("mutation 'deleteDir': ", path)
return path, deleteDirectory(p.Context, path)
},
},
"login": &graphql.Field{
Type: graphql.NewNonNull(graphqlLoginResultType),
Args: graphql.FieldConfigArgument{
"username": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"password": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
username, ok := p.Args["username"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
password, ok := p.Args["password"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
return login(p.Context, username, password)
},
},
}
rootQuery := graphql.ObjectConfig{
Name: "RootQuery",
Fields: queryFields,
}
rootMutation := graphql.ObjectConfig{
Name: "RootMutation",
Fields: mutationFields,
}
schemaConfig := graphql.SchemaConfig{
Query: graphql.NewObject(rootQuery),
Mutation: graphql.NewObject(rootMutation),
}
return graphql.NewSchema(schemaConfig)
}