initial commit
This commit is contained in:
commit
48eea73833
7
README.md
Normal file
7
README.md
Normal file
@ -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)
|
13
cmd/s3Browser.go
Normal file
13
cmd/s3Browser.go
Normal file
@ -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",
|
||||||
|
})
|
||||||
|
}
|
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@ -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
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -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
|
||||||
|
)
|
84
go.sum
Normal file
84
go.sum
Normal file
@ -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=
|
200
internal/dataloader.go
Normal file
200
internal/dataloader.go
Normal file
@ -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
|
||||||
|
}
|
175
internal/graphqlTypes.go
Normal file
175
internal/graphqlTypes.go
Normal file
@ -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
|
||||||
|
}
|
102
internal/s3Broswer.go
Normal file
102
internal/s3Broswer.go
Normal file
@ -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)
|
||||||
|
}
|
80
internal/schema.go
Normal file
80
internal/schema.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user