Compare commits
3 Commits
2353a0bf53
...
60817c2249
| Author | SHA1 | Date | |
|---|---|---|---|
| 60817c2249 | |||
| bead881af2 | |||
| 80302b62f4 |
126
internal/s3/minio.go
Normal file
126
internal/s3/minio.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/s3browser/internal/types"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type minioS3 struct {
|
||||||
|
client *minio.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMinio(config types.AppConfig) (S3Service, error) {
|
||||||
|
client, err := minio.New(config.S3Endoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(config.S3AccessKey, config.S3SecretKey, ""),
|
||||||
|
Secure: config.S3SSL,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &minioS3{
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) ListBuckets(ctx context.Context) ([]Bucket, error) {
|
||||||
|
buckets, err := m.client.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var rtn []Bucket
|
||||||
|
|
||||||
|
for _, v := range buckets {
|
||||||
|
rtn = append(rtn, Bucket(v.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) ListObjects(ctx context.Context, id types.ID) ([]types.File, []types.Directory, error) {
|
||||||
|
var files []types.File
|
||||||
|
var dirs []types.Directory
|
||||||
|
|
||||||
|
for objInfo := range m.client.ListObjects(ctx, id.Bucket, minio.ListObjectsOptions{}) {
|
||||||
|
objId := types.ID{
|
||||||
|
Bucket: id.Bucket,
|
||||||
|
Key: objInfo.Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
objId.Normalize()
|
||||||
|
|
||||||
|
if objId.IsDirectory() {
|
||||||
|
dirs = append(dirs, *obkInfoToDir(objInfo, objId))
|
||||||
|
} else {
|
||||||
|
files = append(files, *objInfoToFile(objInfo, objId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, dirs, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) GetObject(ctx context.Context, id types.ID) (Object, error) {
|
||||||
|
object, err := m.client.GetObject(ctx, id.Bucket, id.Key, minio.GetObjectOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) PutObject(ctx context.Context, id types.ID, reader io.Reader, objectSize int64) error {
|
||||||
|
_, err := m.client.PutObject(ctx, id.Bucket, id.Key, reader, objectSize, minio.PutObjectOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) CopyObject(ctx context.Context, src types.ID, dest types.ID) error {
|
||||||
|
_, err := m.client.CopyObject(ctx, minio.CopyDestOptions{
|
||||||
|
Bucket: dest.Bucket,
|
||||||
|
Object: dest.Key,
|
||||||
|
}, minio.CopySrcOptions{
|
||||||
|
Bucket: src.Bucket,
|
||||||
|
Object: src.Key,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) StatObject(ctx context.Context, id types.ID) (*types.File, error) {
|
||||||
|
info, err := m.client.StatObject(ctx, id.Bucket, id.Key, minio.GetObjectOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objInfoToFile(info, id), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *minioS3) RemoveObject(ctx context.Context, id types.ID) error {
|
||||||
|
return m.client.RemoveObject(ctx, id.Bucket, id.Key, minio.RemoveObjectOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func objInfoToFile(objInfo minio.ObjectInfo, id types.ID) *types.File {
|
||||||
|
return &types.File{
|
||||||
|
ID: id,
|
||||||
|
Name: id.Name(),
|
||||||
|
Size: objInfo.Size,
|
||||||
|
ContentType: objInfo.ContentType,
|
||||||
|
ETag: objInfo.ETag,
|
||||||
|
LastModified: objInfo.LastModified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obkInfoToDir(objInfo minio.ObjectInfo, id types.ID) *types.Directory {
|
||||||
|
return &types.Directory{
|
||||||
|
ID: id,
|
||||||
|
Name: id.Name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
141
internal/s3/mock.go
Normal file
141
internal/s3/mock.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/s3browser/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockS3 struct {
|
||||||
|
buckets []string
|
||||||
|
objects map[types.ID]mockObject
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockObject struct {
|
||||||
|
content []byte
|
||||||
|
contentType string
|
||||||
|
lastMod time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockObjectReader struct {
|
||||||
|
*bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r mockObjectReader) Close() error {
|
||||||
|
// NOOP
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockS3(buckets []string) (S3Service, error) {
|
||||||
|
return &mockS3{
|
||||||
|
buckets: buckets,
|
||||||
|
objects: map[types.ID]mockObject{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) ListBuckets(ctx context.Context) ([]Bucket, error) {
|
||||||
|
var rtn []Bucket
|
||||||
|
for _, v := range m.buckets {
|
||||||
|
rtn = append(rtn, Bucket(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) ListObjects(ctx context.Context, id types.ID) ([]types.File, []types.Directory, error) {
|
||||||
|
var files []types.File
|
||||||
|
var dirs []types.Directory
|
||||||
|
|
||||||
|
for k, v := range m.objects {
|
||||||
|
if k.Bucket == id.Bucket {
|
||||||
|
if k.Parent().Key == id.Key {
|
||||||
|
if k.IsDirectory() {
|
||||||
|
dirs = append(dirs, *mockObjToDir(v, k))
|
||||||
|
} else {
|
||||||
|
files = append(files, *mockObjToFile(v, k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, dirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) GetObject(ctx context.Context, id types.ID) (Object, error) {
|
||||||
|
mockObj, exist := m.objects[id]
|
||||||
|
|
||||||
|
if !exist {
|
||||||
|
return nil, fmt.Errorf("Object not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(mockObj.content)
|
||||||
|
|
||||||
|
return mockObjectReader{reader}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) PutObject(ctx context.Context, id types.ID, reader io.Reader, objectSize int64) error {
|
||||||
|
content, err := ioutil.ReadAll(reader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.objects[id] = mockObject{
|
||||||
|
content: content,
|
||||||
|
lastMod: time.Now(),
|
||||||
|
contentType: "application/octet-stream", // TODO: detect MIME type or dont its just a mock after all
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) CopyObject(ctx context.Context, src types.ID, dest types.ID) error {
|
||||||
|
srcObj, exist := m.objects[src]
|
||||||
|
|
||||||
|
if !exist {
|
||||||
|
return fmt.Errorf("Object not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.objects[dest] = srcObj
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) StatObject(ctx context.Context, id types.ID) (*types.File, error) {
|
||||||
|
mockObj, exist := m.objects[id]
|
||||||
|
|
||||||
|
if !exist {
|
||||||
|
return nil, fmt.Errorf("Object not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockObjToFile(mockObj, id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) RemoveObject(ctx context.Context, id types.ID) error {
|
||||||
|
delete(m.objects, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockObjToFile(mockObj mockObject, id types.ID) *types.File {
|
||||||
|
return &types.File{
|
||||||
|
ID: id,
|
||||||
|
Name: id.Name(),
|
||||||
|
Size: int64(len(mockObj.content)),
|
||||||
|
ContentType: mockObj.contentType,
|
||||||
|
LastModified: mockObj.lastMod,
|
||||||
|
ETag: fmt.Sprintf("%x", md5.Sum(mockObj.content)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockObjToDir(mockObj mockObject, id types.ID) *types.Directory {
|
||||||
|
return &types.Directory{
|
||||||
|
ID: id,
|
||||||
|
Name: id.Name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
29
internal/s3/s3.go
Normal file
29
internal/s3/s3.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/s3browser/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bucket string
|
||||||
|
|
||||||
|
type Object interface {
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
io.ReaderAt
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3Service interface {
|
||||||
|
ListBuckets(ctx context.Context) ([]Bucket, error)
|
||||||
|
|
||||||
|
GetObject(ctx context.Context, id types.ID) (Object, error)
|
||||||
|
PutObject(ctx context.Context, id types.ID, reader io.Reader, objectSize int64) error
|
||||||
|
|
||||||
|
ListObjects(ctx context.Context, id types.ID) ([]types.File, []types.Directory, error)
|
||||||
|
CopyObject(ctx context.Context, src types.ID, dest types.ID) error
|
||||||
|
StatObject(ctx context.Context, id types.ID) (*types.File, error)
|
||||||
|
RemoveObject(ctx context.Context, id types.ID) error
|
||||||
|
}
|
||||||
@@ -80,6 +80,11 @@ func (i ID) Parent() *ID {
|
|||||||
return parent
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns filename or directory name
|
||||||
|
func (i ID) Name() string {
|
||||||
|
return filepath.Base(i.Key)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseID parses a string to an ID. Null if invalid
|
// ParseID parses a string to an ID. Null if invalid
|
||||||
func ParseID(id string) *ID {
|
func ParseID(id string) *ID {
|
||||||
match := idRegex.FindStringSubmatch(id)
|
match := idRegex.FindStringSubmatch(id)
|
||||||
|
|||||||
Reference in New Issue
Block a user