From 48eea738338b4c74c34ef8b7c65b82097260aad2 Mon Sep 17 00:00:00 2001 From: Djeeberjr Date: Mon, 26 Jul 2021 14:52:36 +0200 Subject: [PATCH] initial commit --- README.md | 7 ++ cmd/s3Browser.go | 13 +++ docker-compose.yml | 18 ++++ go.mod | 11 +++ go.sum | 84 ++++++++++++++++ internal/dataloader.go | 200 +++++++++++++++++++++++++++++++++++++++ internal/graphqlTypes.go | 175 ++++++++++++++++++++++++++++++++++ internal/s3Broswer.go | 102 ++++++++++++++++++++ internal/schema.go | 80 ++++++++++++++++ 9 files changed, 690 insertions(+) create mode 100644 README.md create mode 100644 cmd/s3Browser.go create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/dataloader.go create mode 100644 internal/graphqlTypes.go create mode 100644 internal/s3Broswer.go create mode 100644 internal/schema.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..75b6476 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Usefull links + +[Graphql GH](https://github.com/graphql-go/graphql) + +[Graphql Handler GH](https://github.com/graphql-go/handler) + +[Dataloader GH](https://github.com/graph-gophers/dataloader) diff --git a/cmd/s3Browser.go b/cmd/s3Browser.go new file mode 100644 index 0000000..6605ea2 --- /dev/null +++ b/cmd/s3Browser.go @@ -0,0 +1,13 @@ +package main + +import s3browser "git.kapelle.org/niklas/s3browser/internal" + +func main() { + s3browser.Start(s3browser.AppConfig{ + S3Endoint: "localhost:9000", + S3SSL: false, + S3AccessKey: "testo", + S3SecretKey: "testotesto", + S3Buket: "dev", + }) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4881f38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '2' + +services: + minio: + container_name: minio_dev + image: minio/minio + environment: + - MINIO_ROOT_USER=admin + - MINIO_ROOT_PASSWORD=hunter22 + ports: + - 9000:9000 + - 9001:9001 + command: server /data --console-address ":9001" + volumes: + - minio_dev:/data +volumes: + minio_dev: + name: minio_dev \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..acd5336 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.kapelle.org/niklas/s3browser + +go 1.16 + +require ( + github.com/graph-gophers/dataloader v5.0.0+incompatible + github.com/graphql-go/graphql v0.7.9 + github.com/graphql-go/handler v0.2.3 + github.com/minio/minio-go/v7 v7.0.12 + github.com/opentracing/opentracing-go v1.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..67f720e --- /dev/null +++ b/go.sum @@ -0,0 +1,84 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug= +github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4= +github.com/graphql-go/graphql v0.7.9 h1:5Va/Rt4l5g3YjwDnid3vFfn43faaQBq7rMcIZ0VnV34= +github.com/graphql-go/graphql v0.7.9/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= +github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= +github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go/v7 v7.0.12 h1:/4pxUdwn9w0QEryNkrrWaodIESPRX+NxpO0Q6hVdaAA= +github.com/minio/minio-go/v7 v7.0.12/go.mod h1:S23iSP5/gbMwtxeY5FM71R+TkAYyzEdoNEDDwpt8yWs= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/dataloader.go b/internal/dataloader.go new file mode 100644 index 0000000..ae6cc61 --- /dev/null +++ b/internal/dataloader.go @@ -0,0 +1,200 @@ +package s3browser + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/graph-gophers/dataloader" + "github.com/minio/minio-go/v7" +) + +// listObjectsBatch batch func for calling s3.ListObjects() +func listObjectsBatch(c context.Context, k dataloader.Keys) []*dataloader.Result { + var results []*dataloader.Result + + s3Client, ok := c.Value("s3Client").(*minio.Client) + + if !ok { + return handleLoaderError(k, fmt.Errorf("Failed to get s3Client from context")) + } + + for _, v := range k { + results = append(results, &dataloader.Result{ + Data: listObjects(s3Client, bucketName, v.String(), false), + }) + } + + return results +} + +// listObjects helper func for listObjectsBatch +func listObjects(s3Client *minio.Client, bukitName, path string, recursive bool) []minio.ObjectInfo { + objectCh := s3Client.ListObjects(context.Background(), bukitName, minio.ListObjectsOptions{ + Prefix: path, + Recursive: false, + }) + + result := make([]minio.ObjectInfo, 0) + + for obj := range objectCh { + result = append(result, obj) + } + + return result +} + +// getFilesBatch batch func for getting all files in path. Uses "listObjects" dataloader +func getFilesBatch(c context.Context, k dataloader.Keys) []*dataloader.Result { + var results []*dataloader.Result + + loader, ok := c.Value("loader").(map[string]*dataloader.Loader) + if !ok { + return handleLoaderError(k, fmt.Errorf("Failed to get loader from context")) + } + + for _, v := range k { + + path := v.String() + files := make([]File, 0) + + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + + thunk := loader["listObjects"].Load(c, dataloader.StringKey(path)) + + objects, _ := thunk() + + // TODO: handle thunk error + + for _, obj := range objects.([]minio.ObjectInfo) { + if obj.Err != nil { + // TODO: how to handle? + } else { + if !strings.HasSuffix(obj.Key, "/") { + files = append(files, File{ + ID: obj.Key, + Name: filepath.Base(obj.Key), + Size: obj.Size, + ContentType: obj.ContentType, + ETag: obj.ETag, + }) + } + } + } + + results = append(results, &dataloader.Result{ + Data: files, + Error: nil, + }) + } + + return results +} + +// getFileBatch batch func for getting object info +func getFileBatch(c context.Context, k dataloader.Keys) []*dataloader.Result { + var results []*dataloader.Result + + s3Client, ok := c.Value("s3Client").(*minio.Client) + + if !ok { + return handleLoaderError(k, fmt.Errorf("Failed to get s3Client from context")) + } + + for _, v := range k { + obj, err := s3Client.StatObject(context.Background(), bucketName, v.String(), minio.StatObjectOptions{}) + + if err != nil { + results = append(results, &dataloader.Result{ + Data: nil, + Error: err, + }) + } else { + results = append(results, &dataloader.Result{ + Data: &File{ + ID: obj.Key, + Size: obj.Size, + ContentType: obj.ContentType, + ETag: obj.ETag, + }, + Error: nil, + }) + } + } + + return results +} + +// getDirsBatch batch func for getting dirs in a path +func getDirsBatch(c context.Context, k dataloader.Keys) []*dataloader.Result { + var results []*dataloader.Result + + loader, ok := c.Value("loader").(map[string]*dataloader.Loader) + if !ok { + return handleLoaderError(k, fmt.Errorf("Failed to get loader from context")) + } + + for _, v := range k { + + path := v.String() + dirs := make([]Directory, 0) + + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + + thunk := loader["listObjects"].Load(c, dataloader.StringKey(path)) + + objects, _ := thunk() + + // TODO: handle thunk error + + for _, obj := range objects.([]minio.ObjectInfo) { + if obj.Err != nil { + // TODO: how to handle? + } else { + if strings.HasSuffix(obj.Key, "/") { + dirs = append(dirs, Directory{ + ID: obj.Key, + Name: filepath.Base(obj.Key), + }) + } + } + } + + results = append(results, &dataloader.Result{ + Data: dirs, + Error: nil, + }) + } + + return results +} + +// handleLoaderError helper func when the whole batch failed +func handleLoaderError(k dataloader.Keys, err error) []*dataloader.Result { + var results []*dataloader.Result + for range k { + results = append(results, &dataloader.Result{ + Data: nil, + Error: err, + }) + } + + return results +} + +// createDataloader create all dataloaders and return a map of them +func createDataloader() map[string]*dataloader.Loader { + loaderMap := make(map[string]*dataloader.Loader, 0) + + loaderMap["getFiles"] = dataloader.NewBatchedLoader(getFilesBatch) + loaderMap["getFile"] = dataloader.NewBatchedLoader(getFileBatch) + loaderMap["listObjects"] = dataloader.NewBatchedLoader(listObjectsBatch) + loaderMap["getDirs"] = dataloader.NewBatchedLoader(getDirsBatch) + + return loaderMap +} diff --git a/internal/graphqlTypes.go b/internal/graphqlTypes.go new file mode 100644 index 0000000..f089134 --- /dev/null +++ b/internal/graphqlTypes.go @@ -0,0 +1,175 @@ +package s3browser + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/graph-gophers/dataloader" + "github.com/graphql-go/graphql" +) + +var graphqlDirType *graphql.Object +var graphqlFileType *graphql.Object + +// graphqlTypes create all graphql types and stores the in the global variables +func graphqlTypes() { + 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.(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.(File) + if !ok { + return nil, fmt.Errorf("Failed to parse source for resolve") + } + + return filepath.Base(source.ID), nil + }, + }, + "size": &graphql.Field{ + Type: 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 + }, + }, + "parent": &graphql.Field{ + Type: graphqlDirType, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + source, ok := p.Source.(File) + if !ok { + return nil, fmt.Errorf("Failed to parse Source for parent resolve") + } + + basename := filepath.Dir(source.ID) + + if basename == "." { + basename = "/" + } + + return Directory{ + ID: basename, + }, nil + }, + }, + }, + }) + + graphqlDirType.AddFieldConfig("files", &graphql.Field{ + Type: graphql.NewList(graphqlFileType), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + source, ok := p.Source.(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(source.ID)) + return thunk() + }, + }) + + graphqlDirType.AddFieldConfig("directorys", &graphql.Field{ + Type: graphql.NewList(graphqlDirType), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + source, ok := p.Source.(Directory) + if !ok { + return nil, fmt.Errorf("Failed to parse Source for directorys resolve") + } + + loader := p.Context.Value("loader").(map[string]*dataloader.Loader) + thunk := loader["getDirs"].Load(p.Context, dataloader.StringKey(source.ID)) + + return thunk() + }, + }) + + graphqlDirType.AddFieldConfig("parent", &graphql.Field{ + Type: graphqlDirType, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + source, ok := p.Source.(Directory) + if !ok { + return nil, fmt.Errorf("Failed to parse Source for directorys resolve") + } + + dirs := strings.Split(source.ID, "/") + + return Directory{ + ID: strings.Join(dirs[:len(dirs)-2], "/") + "/", + }, nil + }, + }) +} + +// graphqlTypes helper func for using the dataloader to get a file +func loadFile(p graphql.ResolveParams) (*File, error) { + source, ok := p.Source.(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(source.ID)) + result, err := thunk() + + file, ok := result.(*File) + + if !ok { + return nil, fmt.Errorf("Failed to load file") + } + + return file, err +} diff --git a/internal/s3Broswer.go b/internal/s3Broswer.go new file mode 100644 index 0000000..76343ed --- /dev/null +++ b/internal/s3Broswer.go @@ -0,0 +1,102 @@ +package s3browser + +import ( + "context" + "log" + "net/http" + + "github.com/graph-gophers/dataloader" + "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// AppConfig general config +type AppConfig struct { + S3Endoint string + S3AccessKey string + S3SecretKey string + S3SSL bool + S3Buket string +} + +// File represents a file with its metadata +type File struct { + ID string `json:"id"` + Name string `json:"name"` + Size int64 `json:"size"` + ContentType string `json:"contentType"` + ETag string `json:"etag"` +} + +// Directory represents a directory with its metadata +type Directory struct { + ID string `json:"id"` + Name string `json:"name"` + Files []File `json:"files"` + Directorys []Directory `json:"directorys"` +} + +var bucketName = "dev" + +// initHttp setup and start the http server. Blocking +func initHttp(schema graphql.Schema, s3Client *minio.Client, loaderMap map[string]*dataloader.Loader) { + h := handler.New(&handler.Config{ + Schema: &schema, + Pretty: true, + GraphiQL: false, + Playground: true, + }) + + resolveContext := context.WithValue(context.Background(), "s3Client", s3Client) + resolveContext = context.WithValue(resolveContext, "loader", loaderMap) + + http.HandleFunc("/graphql", func(rw http.ResponseWriter, r *http.Request) { + h.ContextHandler(resolveContext, rw, r) + }) + + http.ListenAndServe(":8080", nil) +} + +// setupS3Client connect the s3Client +func setupS3Client(config AppConfig) *minio.Client { + minioClient, err := minio.New(config.S3Endoint, &minio.Options{ + Creds: credentials.NewStaticV4(config.S3AccessKey, config.S3SecretKey, ""), + Secure: config.S3SSL, + }) + + if err != nil { + log.Fatalln(err) + } + + exists, err := minioClient.BucketExists(context.Background(), config.S3Buket) + + if err != nil { + log.Fatalln(err) + } + + if !exists { + log.Fatalf("Bucket '%s' does not exist", config.S3Buket) + } else { + log.Print("S3 client connected") + } + + return minioClient +} + +// Start starts the app +func Start(config AppConfig) { + s3Client := setupS3Client(config) + + loader := createDataloader() + + graphqlTypes() + schema, err := graphqlSchema() + + if err != nil { + log.Panic(err) + } + + initHttp(schema, s3Client, loader) +} diff --git a/internal/schema.go b/internal/schema.go new file mode 100644 index 0000000..83dc539 --- /dev/null +++ b/internal/schema.go @@ -0,0 +1,80 @@ +package s3browser + +import ( + "fmt" + + "github.com/graph-gophers/dataloader" + "github.com/graphql-go/graphql" +) + +// graphqlSchema generate the schema with its root query and mutation +func graphqlSchema() (graphql.Schema, error) { + + fields := graphql.Fields{ + "files": &graphql.Field{ + Type: graphql.NewNonNull(graphql.NewList(graphqlFileType)), + Args: graphql.FieldConfigArgument{ + "path": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + path, ok := p.Args["path"].(string) + + if !ok { + return nil, nil + } + 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(graphqlDirType)), + Args: graphql.FieldConfigArgument{ + "path": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + path, ok := p.Args["path"].(string) + + if !ok { + return nil, nil + } + 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) { + id, ok := p.Args["id"].(string) + if !ok { + return nil, fmt.Errorf("Failed to parse args") + } + + return File{ + ID: id, + }, nil + }, + }, + } + + rootQuery := graphql.ObjectConfig{ + Name: "RootQuery", + Fields: fields, + } + + schemaConfig := graphql.SchemaConfig{ + Query: graphql.NewObject(rootQuery), + } + + return graphql.NewSchema(schemaConfig) +}