From b9ad969a3eace87f3cf04ffd65bbf971a91dfc32 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 12:05:20 +0900 Subject: [PATCH 1/9] close #4 Signed-off-by: mohemohe --- s3fs.go | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/s3fs.go b/s3fs.go index 8fc73bb..cb34a22 100644 --- a/s3fs.go +++ b/s3fs.go @@ -21,13 +21,15 @@ type ( config *Config } Config struct { - NameSpace string - Domain string - Region string - Bucket string - EnableIAMAuth bool - AccessKeyID string - AccessSecretKey string + NameSpace string + Domain string + Region string + Bucket string + EnableIAMAuth bool + AccessKeyID string + AccessSecretKey string + EnableMinioCompat bool + Endpoint string } FileInfo struct { Name string `json:"name"` @@ -48,25 +50,29 @@ const ( ) func New(config *Config) *S3FS { - var sess *session.Session if config.Region == "" { config.Region = endpoints.ApNortheast1RegionID } + + options := session.Options{ + Config: aws.Config{ + Region: aws.String(config.Region), + }, + } if config.EnableIAMAuth { - sess = session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Region: aws.String(config.Region), - Credentials: credentials.NewStaticCredentials(config.AccessKeyID, config.AccessSecretKey, ""), - }, - SharedConfigState: session.SharedConfigDisable, - })) - } else { - sess = session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Region: aws.String(config.Region), - }, - })) + options.Config.Credentials = credentials.NewStaticCredentials(config.AccessKeyID, config.AccessSecretKey, "") + options.SharedConfigState = session.SharedConfigDisable } + + if config.EnableMinioCompat { + options.Config.S3ForcePathStyle = aws.Bool(config.EnableMinioCompat) + } + + if config.Endpoint != "" { + options.Config.Endpoint = aws.String(config.Endpoint) + } + + sess := session.Must(session.NewSessionWithOptions(options)) serv := s3.New(sess) return &S3FS{ From 92ee6a00574a1046687bc96fe4f3bad8548a0142 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 13:09:29 +0900 Subject: [PATCH 2/9] #3 add basic test Signed-off-by: mohemohe --- s3fs_test.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/s3fs_test.go b/s3fs_test.go index 8fc1666..da797ee 100644 --- a/s3fs_test.go +++ b/s3fs_test.go @@ -1 +1,211 @@ package s3fs + +import ( + "bytes" + "io/ioutil" + "os" + "testing" +) + +var fs *S3FS + +func setup() { + fs = New(&Config{ + EnableMinioCompat: true, + Endpoint: "http://127.0.0.1:9000", + EnableIAMAuth: true, + AccessKeyID: "accesskey", + AccessSecretKey: "secretkey", + Bucket: "test", + }) +} + +func teardown() { + if fs != nil { + if err := fs.BulkDelete("/"); err != nil { + println("teardown error:", err.Error()) + } + } +} + +func TestMain(m *testing.M) { + setup() + exitCode := m.Run() + teardown() + + os.Exit(exitCode) +} + +func TestS3FS_List_1(t *testing.T) { + list := fs.List("/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 0 { + t.Fatal("invalid state") + } +} + +func TestS3FS_Put1(t *testing.T) { + body := []byte("this is test string") + readCloser := ioutil.NopCloser(bytes.NewReader(body)) + + if err := fs.Put("testfile", readCloser, "text/plain"); err != nil { + t.Fatal(err) + } +} + +func TestS3FS_List_2(t *testing.T) { + list := fs.List("/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "testfile" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testfile" { + t.Fatal("invalid file path:", file.Path) + } + if file.Size != int64(len([]byte("this is test string"))) { + t.Fatal("invalid file size:", file.Size) + } + if file.Type != File { + t.Fatal("invalid file type:", file.Type) + } +} + +func TestS3FS_Get(t *testing.T) { + t.Run("exists", func(st *testing.T){ + readCloser, err := fs.Get("/testfile") + if err != nil { + st.Fatal("get file error:", err) + } + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(*readCloser); err != nil { + st.Fatal("io error:", err) + } + body := buf.String() + if body != "this is test string" { + st.Fatal("invalid data") + } + }) + t.Run("non exists", func(st *testing.T){ + _, err := fs.Get("/foobar") + if err == nil { + st.Fatal("io error:", err) + } + }) +} + +func TestS3FS_MkDir(t *testing.T) { + if err := fs.MkDir("/testdir1"); err != nil { + t.Fatal(err) + } + + list := fs.List("/testdir1") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "testdir1" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testdir1/" { + t.Fatal("invalid file path:", file.Path) + } + if file.Type != Directory { + t.Fatal("invalid file type:", file.Type) + } +} + +func TestS3FS_MkDirP(t *testing.T) { + if err := fs.MkDir("/testdir2/child"); err != nil { + t.Fatal(err) + } + + list := fs.List("/testdir2/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "child" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testdir2/child/" { + t.Fatal("invalid file path:", file.Path) + } + if file.Type != Directory { + t.Fatal("invalid file type:", file.Type) + } +} + +func TestS3FS_Copy(t *testing.T) { + if err := fs.Copy("/testfile", "/testdir1/testfile", nil); err != nil { + t.Fatal("copy error:", err) + } + + readCloser, err := fs.Get("/testdir1/testfile") + if err != nil { + t.Fatal("get file error:", err) + } + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(*readCloser); err != nil { + t.Fatal("io error:", err) + } + body := buf.String() + if body != "this is test string" { + t.Fatal("invalid data") + } +} + +func TestS3FS_Move(t *testing.T) { + beforeList := fs.List("/testdir1") + if beforeList == nil { + t.Fatal("cannot connect s3") + } + + if err := fs.Move("/testdir1/", "/testdir2/"); err != nil { + t.Fatal("move error:", err) + } + + afterList := fs.List("/testdir2/testdir1") + if afterList == nil { + t.Fatal("cannot connect s3") + } + + if len(*beforeList) != len(*afterList) { + t.Fatal("invalid files:", *beforeList, *afterList) + } + for i := range *beforeList { + if (*beforeList)[i].Name != (*afterList)[i].Name { + t.Fatal("name error:", (*beforeList)[i].Name, (*afterList)[i].Name) + } + if (*beforeList)[i].Size != (*afterList)[i].Size { + t.Fatal("size error:", (*beforeList)[i].Size, (*afterList)[i].Size) + } + if (*beforeList)[i].Type != (*afterList)[i].Type { + t.Fatal("type error:", (*beforeList)[i].Type, (*afterList)[i].Type) + } + } +} + +func TestS3FS_Delete(t *testing.T) { + if err := fs.Delete("/testfile"); err != nil { + t.Fatal("copy error:", err) + } + + _, err := fs.Get("/testfile") + if err == nil { + t.Fatal("io error:", err) + } +} \ No newline at end of file From 635141542d2d6d8e13cdbf98226e2e3f5464364e Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 13:26:06 +0900 Subject: [PATCH 3/9] #3 some tests merge into sub tests Signed-off-by: mohemohe --- s3fs_test.go | 155 ++++++++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 76 deletions(-) diff --git a/s3fs_test.go b/s3fs_test.go index da797ee..820cde1 100644 --- a/s3fs_test.go +++ b/s3fs_test.go @@ -36,17 +36,7 @@ func TestMain(m *testing.M) { os.Exit(exitCode) } -func TestS3FS_List_1(t *testing.T) { - list := fs.List("/") - if list == nil { - t.Fatal("cannot connect s3") - } - if len(*list) != 0 { - t.Fatal("invalid state") - } -} - -func TestS3FS_Put1(t *testing.T) { +func TestS3FS_Put(t *testing.T) { body := []byte("this is test string") readCloser := ioutil.NopCloser(bytes.NewReader(body)) @@ -55,29 +45,6 @@ func TestS3FS_Put1(t *testing.T) { } } -func TestS3FS_List_2(t *testing.T) { - list := fs.List("/") - if list == nil { - t.Fatal("cannot connect s3") - } - if len(*list) != 1 { - t.Fatal("invalid state") - } - file := (*list)[0] - if file.Name != "testfile" { - t.Fatal("invalid file name:", file.Name) - } - if file.Path != "/testfile" { - t.Fatal("invalid file path:", file.Path) - } - if file.Size != int64(len([]byte("this is test string"))) { - t.Fatal("invalid file size:", file.Size) - } - if file.Type != File { - t.Fatal("invalid file type:", file.Type) - } -} - func TestS3FS_Get(t *testing.T) { t.Run("exists", func(st *testing.T){ readCloser, err := fs.Get("/testfile") @@ -101,52 +68,88 @@ func TestS3FS_Get(t *testing.T) { }) } -func TestS3FS_MkDir(t *testing.T) { - if err := fs.MkDir("/testdir1"); err != nil { - t.Fatal(err) - } +func TestS3FS_List(t *testing.T) { + t.Run("exists", func(st *testing.T){ + list := fs.List("/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "testfile" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testfile" { + t.Fatal("invalid file path:", file.Path) + } + if file.Size != int64(len([]byte("this is test string"))) { + t.Fatal("invalid file size:", file.Size) + } + if file.Type != File { + t.Fatal("invalid file type:", file.Type) + } + }) + t.Run("non exists", func(st *testing.T){ + list := fs.List("/dummydir/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 0 { + t.Fatal("invalid state") + } + }) - list := fs.List("/testdir1") - if list == nil { - t.Fatal("cannot connect s3") - } - if len(*list) != 1 { - t.Fatal("invalid state") - } - file := (*list)[0] - if file.Name != "testdir1" { - t.Fatal("invalid file name:", file.Name) - } - if file.Path != "/testdir1/" { - t.Fatal("invalid file path:", file.Path) - } - if file.Type != Directory { - t.Fatal("invalid file type:", file.Type) - } } -func TestS3FS_MkDirP(t *testing.T) { - if err := fs.MkDir("/testdir2/child"); err != nil { - t.Fatal(err) - } +func TestS3FS_MkDir(t *testing.T) { + t.Run("mkdir", func(st *testing.T){ + if err := fs.MkDir("/testdir1"); err != nil { + t.Fatal(err) + } - list := fs.List("/testdir2/") - if list == nil { - t.Fatal("cannot connect s3") - } - if len(*list) != 1 { - t.Fatal("invalid state") - } - file := (*list)[0] - if file.Name != "child" { - t.Fatal("invalid file name:", file.Name) - } - if file.Path != "/testdir2/child/" { - t.Fatal("invalid file path:", file.Path) - } - if file.Type != Directory { - t.Fatal("invalid file type:", file.Type) - } + list := fs.List("/testdir1") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "testdir1" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testdir1/" { + t.Fatal("invalid file path:", file.Path) + } + if file.Type != Directory { + t.Fatal("invalid file type:", file.Type) + } + }) + t.Run("mkdir -p", func(st *testing.T){ + if err := fs.MkDir("/testdir2/child"); err != nil { + t.Fatal(err) + } + + list := fs.List("/testdir2/") + if list == nil { + t.Fatal("cannot connect s3") + } + if len(*list) != 1 { + t.Fatal("invalid state") + } + file := (*list)[0] + if file.Name != "child" { + t.Fatal("invalid file name:", file.Name) + } + if file.Path != "/testdir2/child/" { + t.Fatal("invalid file path:", file.Path) + } + if file.Type != Directory { + t.Fatal("invalid file type:", file.Type) + } + }) } func TestS3FS_Copy(t *testing.T) { From 65df81be9b9f13dc6598bd86842dd76ebfbc9f23 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 13:58:38 +0900 Subject: [PATCH 4/9] #6 add .travis.yml Signed-off-by: mohemohe --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..48c1403 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go +sudo: false +go: + - 1.12 + - tip +before_install: + - go get github.com/golang/dep/... +install: + - $GOPATH/bin/dep ensure +script: + - go test -race -coverprofile=coverage.txt -covermode=atomic +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file From 71c8cf293de62cae1cfe18a2d16005e489cba6c8 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 14:03:31 +0900 Subject: [PATCH 5/9] #6 add minio to .travis.yml Signed-off-by: mohemohe --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 48c1403..9bcdbf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: go -sudo: false +sudo: required go: - 1.12 - tip +services: + - docker before_install: - go get github.com/golang/dep/... install: - $GOPATH/bin/dep ensure +before_script: + - docker run -d --name minio -e 'MINIO_ACCESS_KEY=accesskey' -e 'MINIO_SECRET_KEY=secretkey' -p 9000:9000 minio/minio:RELEASE.2018-12-27T18-33-08Z server /data script: - go test -race -coverprofile=coverage.txt -covermode=atomic after_success: From 707ecd9d7d45d3f347017d0386166c420fb3d102 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 14:11:53 +0900 Subject: [PATCH 6/9] close #7 Signed-off-by: mohemohe --- s3fs.go | 21 +++++++++++++++++++++ s3fs_test.go | 1 + 2 files changed, 22 insertions(+) diff --git a/s3fs.go b/s3fs.go index cb34a22..fc902b5 100644 --- a/s3fs.go +++ b/s3fs.go @@ -82,6 +82,27 @@ func New(config *Config) *S3FS { } } +func (this *S3FS) CreateBucket(name string) error { + _, err := this.s3.CreateBucket(&s3.CreateBucketInput{ + Bucket: aws.String(name), + }) + if err != nil { + return err + } + + err = this.s3.WaitUntilBucketExists(&s3.HeadBucketInput{ + Bucket: aws.String(name), + }) + return err +} + +func (this *S3FS) DeleteBucket(name string) error { + _, err := this.s3.DeleteBucket(&s3.DeleteBucketInput{ + Bucket: aws.String(name), + }) + return err +} + func (this *S3FS) List(key string) *[]FileInfo { fileList := make([]FileInfo, 0) var continuationToken *string diff --git a/s3fs_test.go b/s3fs_test.go index 820cde1..60a660d 100644 --- a/s3fs_test.go +++ b/s3fs_test.go @@ -18,6 +18,7 @@ func setup() { AccessSecretKey: "secretkey", Bucket: "test", }) + _ = fs.CreateBucket("test") } func teardown() { From d4c49ff1bf2a8c1470fc76f77ea96bd85e07d2e9 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 18:05:27 +0900 Subject: [PATCH 7/9] #3 add some tests Signed-off-by: mohemohe --- s3fs_test.go | 58 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/s3fs_test.go b/s3fs_test.go index 60a660d..b75f12c 100644 --- a/s3fs_test.go +++ b/s3fs_test.go @@ -18,15 +18,14 @@ func setup() { AccessSecretKey: "secretkey", Bucket: "test", }) - _ = fs.CreateBucket("test") } func teardown() { - if fs != nil { - if err := fs.BulkDelete("/"); err != nil { - println("teardown error:", err.Error()) - } - } + // if fs != nil { + // if err := fs.BulkDelete("/"); err != nil { + // println("teardown error:", err.Error()) + // } + // } } func TestMain(m *testing.M) { @@ -37,6 +36,12 @@ func TestMain(m *testing.M) { os.Exit(exitCode) } +func TestS3FS_CreateBucket(t *testing.T) { + if err := fs.CreateBucket("test"); err != nil { + t.Fatal("bucket create error:", err) + } +} + func TestS3FS_Put(t *testing.T) { body := []byte("this is test string") readCloser := ioutil.NopCloser(bytes.NewReader(body)) @@ -204,12 +209,39 @@ func TestS3FS_Move(t *testing.T) { } func TestS3FS_Delete(t *testing.T) { - if err := fs.Delete("/testfile"); err != nil { - t.Fatal("copy error:", err) - } + t.Run("rm", func(st *testing.T){ + if err := fs.Delete("/testfile"); err != nil { + t.Fatal("copy error:", err) + } - _, err := fs.Get("/testfile") - if err == nil { - t.Fatal("io error:", err) - } + _, err := fs.Get("/testfile") + if err == nil { + t.Fatal("io error:", err) + } + }) + t.Run("rm -r", func(st *testing.T){ + if err := fs.Delete("/"); err != nil { + t.Fatal("copy error:", err) + } + list := fs.List("/") + if list == nil { + t.Fatal("s3 error") + } + if len(*list) != 0 { + t.Fatal("io error") + } + }) +} + +func TestS3FS_DeleteBucket(t *testing.T) { + t.Run("exists", func(st *testing.T){ + if err := fs.DeleteBucket("test"); err != nil { + t.Fatal("bucket delete error:", err) + } + }) + t.Run("non exists", func(st *testing.T){ + if err := fs.DeleteBucket("dummybucket"); err == nil { + t.Fatal("io error:", err) + } + }) } \ No newline at end of file From 949a4e57c0a1dfff37ecac96e4bd26f51b624da6 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 21:21:30 +0900 Subject: [PATCH 8/9] close #2 Signed-off-by: mohemohe --- README.md | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..1616c60 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +go-s3fs +==== + +[![Build Status](https://travis-ci.org/mobilusoss/go-s3fs.svg?branch=develop)](https://travis-ci.org/mobilusoss/go-s3fs) +[![codecov](https://codecov.io/gh/mobilusoss/go-s3fs/branch/develop/graph/badge.svg)](https://codecov.io/gh/mobilusoss/go-s3fs) +[![Go Report Card](https://goreportcard.com/badge/github.com/mobilusoss/go-s3fs)](https://goreportcard.com/report/github.com/mobilusoss/go-s3fs) +[![codebeat badge](https://codebeat.co/badges/e03e3de0-9d71-43ce-a6ac-8e0c6445485a)](https://codebeat.co/projects/github-com-mobilusoss-go-s3fs-master) +![GitHub](https://img.shields.io/github/license/mobilusoss/go-s3fs.svg) + +Amazon S3 wrapper for human + + + +## Overview + +AWS offers a complex and bizarre SDK. We needed a library that would make it easier and faster to use S3. +go-s3fs solves this problem. It wraps the official SDK, making S3 extremely easy to use. + +## Installation + +```bash +go get -u github.com/mobilusoss/go-s3fs +``` + +## Usage + +### Connect to S3 using IAM role + +```go +package main + +import ( + "fmt" + "github.com/mobilusoss/go-s3fs" +) + +func main() { + fs := s3fs.New(&s3fs.Config{ + Bucket: "samplebucket", + }) + readCloser, err := fs.Get("/file.txt") + if err != nil { + panic("s3 error") + } + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(*readCloser); err != nil { + panic("io error") + } + text := buf.String() + fmt.Println(text) +} +``` + +### Connect to [MinIO](https://github.com/minio/minio) + +```go +package main + +import ( + "fmt" + "github.com/mobilusoss/go-s3fs" +) + +func main() { + fs := s3fs.New(&s3fs.Config{ + Bucket: "samplebucket", + EnableMinioCompat: true, + Endpoint: "http://127.0.0.1:9000", + EnableIAMAuth: true, + AccessKeyID: "accesskey", + AccessSecretKey: "secretkey", + }) + readCloser, err := fs.Get("/file.txt") + if err != nil { + panic("s3 error") + } + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(*readCloser); err != nil { + panic("io error") + } + text := buf.String() + fmt.Println(text) +} +``` + +### Can be logically divided tenant + +```go +package main + +import ( + "github.com/mobilusoss/go-s3fs" +) + +func main() { + _ = s3fs.New(&s3fs.Config{ + Bucket: "samplebucket", + Domain: "tenantone", + }) +} +``` + +### Can be logically divided tenant per application + +```go +package main + +import ( + "github.com/mobilusoss/go-s3fs" +) + +func main() { + _ = s3fs.New(&s3fs.Config{ + Bucket: "samplebucket", + Namespace: "appone", + Domain: "tenantone", + }) +} +``` + +## License + +MIT \ No newline at end of file From 9eaf2f57f246b862908dd68a9490466469cebc16 Mon Sep 17 00:00:00 2001 From: mohemohe Date: Sun, 5 May 2019 21:32:32 +0900 Subject: [PATCH 9/9] change badge to master Signed-off-by: mohemohe --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1616c60..46f86a1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ go-s3fs ==== -[![Build Status](https://travis-ci.org/mobilusoss/go-s3fs.svg?branch=develop)](https://travis-ci.org/mobilusoss/go-s3fs) -[![codecov](https://codecov.io/gh/mobilusoss/go-s3fs/branch/develop/graph/badge.svg)](https://codecov.io/gh/mobilusoss/go-s3fs) +[![Build Status](https://travis-ci.org/mobilusoss/go-s3fs.svg?branch=master)](https://travis-ci.org/mobilusoss/go-s3fs) +[![codecov](https://codecov.io/gh/mobilusoss/go-s3fs/branch/master/graph/badge.svg)](https://codecov.io/gh/mobilusoss/go-s3fs) [![Go Report Card](https://goreportcard.com/badge/github.com/mobilusoss/go-s3fs)](https://goreportcard.com/report/github.com/mobilusoss/go-s3fs) [![codebeat badge](https://codebeat.co/badges/e03e3de0-9d71-43ce-a6ac-8e0c6445485a)](https://codebeat.co/projects/github-com-mobilusoss-go-s3fs-master) ![GitHub](https://img.shields.io/github/license/mobilusoss/go-s3fs.svg)