mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 05:07:31 +00:00
feat: store assets in database (#3290)
* feat: use database as asset storage * being only uploading assets if allowed * tests * fixes * cleanup after merge * renaming * various fixes * fix: change to repository event types and removed unused code * feat: set default features * error handling * error handling and naming * fix tests * fix tests * fix merge * rename
This commit is contained in:
@@ -1,114 +1,30 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
"database/sql"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
"github.com/caos/zitadel/internal/static/database"
|
||||
"github.com/caos/zitadel/internal/static/s3"
|
||||
)
|
||||
|
||||
type AssetStorageConfig struct {
|
||||
Type string
|
||||
Config static.Config
|
||||
Config map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
var storage = map[string]func() static.Config{
|
||||
"s3": func() static.Config { return &s3.Config{} },
|
||||
"none": func() static.Config { return &NoStorage{} },
|
||||
"": func() static.Config { return &NoStorage{} },
|
||||
}
|
||||
|
||||
func (c *AssetStorageConfig) UnmarshalJSON(data []byte) error {
|
||||
var rc struct {
|
||||
Type string
|
||||
Config json.RawMessage
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &rc); err != nil {
|
||||
return errors.ThrowInternal(err, "STATIC-Bfn5r", "error parsing config")
|
||||
}
|
||||
|
||||
c.Type = rc.Type
|
||||
|
||||
var err error
|
||||
c.Config, err = newStorageConfig(c.Type, rc.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newStorageConfig(storageType string, configData []byte) (static.Config, error) {
|
||||
t, ok := storage[storageType]
|
||||
func (a *AssetStorageConfig) NewStorage(client *sql.DB) (static.Storage, error) {
|
||||
t, ok := storage[a.Type]
|
||||
if !ok {
|
||||
return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", storageType)
|
||||
return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", a.Type)
|
||||
}
|
||||
|
||||
staticConfig := t()
|
||||
if len(configData) == 0 {
|
||||
return staticConfig, nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configData, staticConfig); err != nil {
|
||||
return nil, errors.ThrowInternal(err, "STATIC-GB4nw", "Could not read config: %v")
|
||||
}
|
||||
|
||||
return staticConfig, nil
|
||||
return t(client, a.Config)
|
||||
}
|
||||
|
||||
var (
|
||||
errNoStorage = errors.ThrowInternal(nil, "STATIC-ashg4", "Errors.Assets.Store.NotConfigured")
|
||||
)
|
||||
|
||||
type NoStorage struct{}
|
||||
|
||||
func (_ *NoStorage) NewStorage() (static.Storage, error) {
|
||||
return &NoStorage{}, nil
|
||||
}
|
||||
|
||||
func (_ *NoStorage) CreateBucket(ctx context.Context, name, location string) error {
|
||||
return errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) RemoveBucket(ctx context.Context, name string) error {
|
||||
return errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
|
||||
return nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
|
||||
return nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
|
||||
return nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
|
||||
return nil, nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
|
||||
return nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
|
||||
return nil, errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error {
|
||||
return errNoStorage
|
||||
}
|
||||
|
||||
func (_ *NoStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
|
||||
return errNoStorage
|
||||
var storage = map[string]static.CreateStorage{
|
||||
"db": database.NewStorage,
|
||||
"": database.NewStorage,
|
||||
"s3": s3.NewStorage,
|
||||
}
|
||||
|
182
internal/static/database/crdb.go
Normal file
182
internal/static/database/crdb.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
errs "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
|
||||
caos_errors "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
|
||||
var _ static.Storage = (*crdbStorage)(nil)
|
||||
|
||||
const (
|
||||
assetsTable = "system.assets"
|
||||
AssetColInstanceID = "instance_id"
|
||||
AssetColType = "asset_type"
|
||||
AssetColLocation = "location"
|
||||
AssetColResourceOwner = "resource_owner"
|
||||
AssetColName = "name"
|
||||
AssetColData = "data"
|
||||
AssetColContentType = "content_type"
|
||||
AssetColHash = "hash"
|
||||
AssetColUpdatedAt = "updated_at"
|
||||
)
|
||||
|
||||
type crdbStorage struct {
|
||||
client *sql.DB
|
||||
}
|
||||
|
||||
func NewStorage(client *sql.DB, _ map[string]interface{}) (static.Storage, error) {
|
||||
return &crdbStorage{client: client}, nil
|
||||
}
|
||||
|
||||
func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
|
||||
data, err := io.ReadAll(object)
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInternal(err, "DATAB-Dfwvq", "Errors.Internal")
|
||||
}
|
||||
stmt, args, err := squirrel.Insert(assetsTable).
|
||||
Columns(AssetColInstanceID, AssetColResourceOwner, AssetColName, AssetColType, AssetColContentType, AssetColData, AssetColUpdatedAt).
|
||||
Values(instanceID, resourceOwner, name, objectType, contentType, data, "now()").
|
||||
Suffix(fmt.Sprintf(
|
||||
"ON CONFLICT (%s, %s, %s) DO UPDATE"+
|
||||
" SET %s = $5, %s = $6"+
|
||||
" RETURNING %s, %s", AssetColInstanceID, AssetColResourceOwner, AssetColName, AssetColContentType, AssetColData, AssetColHash, AssetColUpdatedAt)).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInternal(err, "DATAB-32DG1", "Errors.Internal")
|
||||
}
|
||||
var hash string
|
||||
var updatedAt time.Time
|
||||
err = c.client.QueryRowContext(ctx, stmt, args...).Scan(&hash, &updatedAt)
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInternal(err, "DATAB-D2g2q", "Errors.Internal")
|
||||
}
|
||||
return &static.Asset{
|
||||
InstanceID: instanceID,
|
||||
Name: name,
|
||||
Hash: hash,
|
||||
Size: objectSize,
|
||||
LastModified: updatedAt,
|
||||
Location: location,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
|
||||
query, args, err := squirrel.Select(AssetColData, AssetColContentType, AssetColHash, AssetColUpdatedAt).
|
||||
From(assetsTable).
|
||||
Where(squirrel.Eq{
|
||||
AssetColInstanceID: instanceID,
|
||||
AssetColResourceOwner: resourceOwner,
|
||||
AssetColName: name,
|
||||
}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, nil, caos_errors.ThrowInternal(err, "DATAB-GE3hz", "Errors.Internal")
|
||||
}
|
||||
var data []byte
|
||||
asset := &static.Asset{
|
||||
InstanceID: instanceID,
|
||||
ResourceOwner: resourceOwner,
|
||||
Name: name,
|
||||
}
|
||||
err = c.client.QueryRowContext(ctx, query, args...).
|
||||
Scan(
|
||||
&data,
|
||||
&asset.ContentType,
|
||||
&asset.Hash,
|
||||
&asset.LastModified,
|
||||
)
|
||||
if err != nil {
|
||||
if errs.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil, caos_errors.ThrowNotFound(err, "DATAB-pCP8P", "Errors.Assets.Object.NotFound")
|
||||
}
|
||||
return nil, nil, caos_errors.ThrowInternal(err, "DATAB-Sfgb3", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
asset.Size = int64(len(data))
|
||||
return data,
|
||||
func() (*static.Asset, error) {
|
||||
return asset, nil
|
||||
},
|
||||
nil
|
||||
}
|
||||
|
||||
func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) {
|
||||
query, args, err := squirrel.Select(AssetColContentType, AssetColLocation, "length("+AssetColData+")", AssetColHash, AssetColUpdatedAt).
|
||||
From(assetsTable).
|
||||
Where(squirrel.Eq{
|
||||
AssetColInstanceID: instanceID,
|
||||
AssetColResourceOwner: resourceOwner,
|
||||
AssetColName: name,
|
||||
}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInternal(err, "DATAB-rggt2", "Errors.Internal")
|
||||
}
|
||||
asset := &static.Asset{
|
||||
InstanceID: instanceID,
|
||||
ResourceOwner: resourceOwner,
|
||||
Name: name,
|
||||
}
|
||||
err = c.client.QueryRowContext(ctx, query, args...).
|
||||
Scan(
|
||||
&asset.ContentType,
|
||||
&asset.Location,
|
||||
&asset.Size,
|
||||
&asset.Hash,
|
||||
&asset.LastModified,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInternal(err, "DATAB-Dbh2s", "Errors.Internal")
|
||||
}
|
||||
return asset, nil
|
||||
}
|
||||
|
||||
func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
|
||||
stmt, args, err := squirrel.Delete(assetsTable).
|
||||
Where(squirrel.Eq{
|
||||
AssetColInstanceID: instanceID,
|
||||
AssetColResourceOwner: resourceOwner,
|
||||
AssetColName: name,
|
||||
}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return caos_errors.ThrowInternal(err, "DATAB-Sgvwq", "Errors.Internal")
|
||||
}
|
||||
_, err = c.client.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return caos_errors.ThrowInternal(err, "DATAB-RHNgf", "Errors.Assets.Object.RemoveFailed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
|
||||
stmt, args, err := squirrel.Delete(assetsTable).
|
||||
Where(squirrel.Eq{
|
||||
AssetColInstanceID: instanceID,
|
||||
AssetColResourceOwner: resourceOwner,
|
||||
AssetColType: objectType,
|
||||
}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return caos_errors.ThrowInternal(err, "DATAB-Sfgeq", "Errors.Internal")
|
||||
}
|
||||
_, err = c.client.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return caos_errors.ThrowInternal(err, "DATAB-Efgt2", "Errors.Assets.Object.RemoveFailed")
|
||||
}
|
||||
return nil
|
||||
}
|
203
internal/static/database/crdb_test.go
Normal file
203
internal/static/database/crdb_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"io"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
|
||||
var (
|
||||
testNow = time.Now()
|
||||
)
|
||||
|
||||
const (
|
||||
objectStmt = "INSERT INTO system.assets" +
|
||||
" (instance_id,resource_owner,name,asset_type,content_type,data,updated_at)" +
|
||||
" VALUES ($1,$2,$3,$4,$5,$6,$7)" +
|
||||
" ON CONFLICT (instance_id, resource_owner, name) DO UPDATE SET" +
|
||||
" content_type = $5, data = $6" +
|
||||
" RETURNING hash"
|
||||
)
|
||||
|
||||
func Test_crdbStorage_CreateObject(t *testing.T) {
|
||||
type fields struct {
|
||||
client db
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
instanceID string
|
||||
location string
|
||||
resourceOwner string
|
||||
name string
|
||||
contentType string
|
||||
objectType static.ObjectType
|
||||
data io.Reader
|
||||
objectSize int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *static.Asset
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"create ok",
|
||||
fields{
|
||||
client: prepareDB(t,
|
||||
expectQuery(
|
||||
objectStmt,
|
||||
[]string{
|
||||
"hash",
|
||||
"updated_at",
|
||||
},
|
||||
[][]driver.Value{
|
||||
{
|
||||
"md5Hash",
|
||||
testNow,
|
||||
},
|
||||
},
|
||||
"instanceID",
|
||||
"resourceOwner",
|
||||
"name",
|
||||
static.ObjectTypeUserAvatar,
|
||||
"contentType",
|
||||
[]byte("test"),
|
||||
"now()",
|
||||
)),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
instanceID: "instanceID",
|
||||
location: "location",
|
||||
resourceOwner: "resourceOwner",
|
||||
name: "name",
|
||||
contentType: "contentType",
|
||||
data: bytes.NewReader([]byte("test")),
|
||||
objectSize: 4,
|
||||
},
|
||||
&static.Asset{
|
||||
InstanceID: "instanceID",
|
||||
Name: "name",
|
||||
Hash: "md5Hash",
|
||||
Size: 4,
|
||||
LastModified: testNow,
|
||||
Location: "location",
|
||||
ContentType: "contentType",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &crdbStorage{
|
||||
client: tt.fields.client.db,
|
||||
}
|
||||
got, err := c.PutObject(tt.args.ctx, tt.args.instanceID, tt.args.location, tt.args.resourceOwner, tt.args.name, tt.args.contentType, tt.args.objectType, tt.args.data, tt.args.objectSize)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CreateObject() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CreateObject() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type db struct {
|
||||
mock sqlmock.Sqlmock
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func prepareDB(t *testing.T, expectations ...expectation) db {
|
||||
t.Helper()
|
||||
client, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create sql mock: %v", err)
|
||||
}
|
||||
for _, expectation := range expectations {
|
||||
expectation(mock)
|
||||
}
|
||||
return db{
|
||||
mock: mock,
|
||||
db: client,
|
||||
}
|
||||
}
|
||||
|
||||
type expectation func(m sqlmock.Sqlmock)
|
||||
|
||||
func expectExists(query string, value bool, args ...driver.Value) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(value))
|
||||
}
|
||||
}
|
||||
|
||||
func expectQueryErr(query string, err error, args ...driver.Value) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnError(err)
|
||||
}
|
||||
}
|
||||
func expectQuery(stmt string, cols []string, rows [][]driver.Value, args ...driver.Value) func(m sqlmock.Sqlmock) {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
q := m.ExpectQuery(regexp.QuoteMeta(stmt)).WithArgs(args...)
|
||||
result := sqlmock.NewRows(cols)
|
||||
count := uint64(len(rows))
|
||||
for _, row := range rows {
|
||||
if cols[len(cols)-1] == "count" {
|
||||
row = append(row, count)
|
||||
}
|
||||
result.AddRow(row...)
|
||||
}
|
||||
q.WillReturnRows(result)
|
||||
q.RowsWillBeClosed()
|
||||
}
|
||||
}
|
||||
|
||||
func expectExec(stmt string, err error, args ...driver.Value) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
query := m.ExpectExec(regexp.QuoteMeta(stmt)).WithArgs(args...)
|
||||
if err != nil {
|
||||
query.WillReturnError(err)
|
||||
return
|
||||
}
|
||||
query.WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
}
|
||||
}
|
||||
|
||||
func expectBegin(err error) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
query := m.ExpectBegin()
|
||||
if err != nil {
|
||||
query.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func expectCommit(err error) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
query := m.ExpectCommit()
|
||||
if err != nil {
|
||||
query.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func expectRollback(err error) expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
query := m.ExpectRollback()
|
||||
if err != nil {
|
||||
query.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,6 +22,7 @@ Errors:
|
||||
Object:
|
||||
PutFailed: Objekt konnte nicht erstellt werden
|
||||
GetFailed: Objekt konnte nicht gelesen werden
|
||||
NotFound: Objekt konnte nicht gefunden werden
|
||||
PresignedTokenFailed: Signiertes Token konnte nicht erstellt werden
|
||||
ListFailed: Objektliste konnte nicht gelesen werden
|
||||
RemoveFailed: Objekt konnte nicht gelöscht werden
|
||||
|
@@ -22,6 +22,7 @@ Errors:
|
||||
Object:
|
||||
PutFailed: Object not created
|
||||
GetFailed: Object could not be read
|
||||
NotFound: Object could not be found
|
||||
PresignedTokenFailed: Signed token could not be created
|
||||
ListFailed: Objectlist could not be read
|
||||
RemoveFailed: Object could not be removed
|
||||
|
@@ -22,6 +22,7 @@ Errors:
|
||||
Object:
|
||||
PutFailed: Oggetto non creato
|
||||
GetFailed: Oggetto non può essere letto
|
||||
NotFound: Oggetto non trovato
|
||||
PresignedTokenFailed: Il token non può essere creato
|
||||
ListFailed: La lista degli oggetti non può essere letta
|
||||
RemoveFailed: L'oggetto non può essere rimosso
|
||||
|
@@ -7,11 +7,8 @@ package mock
|
||||
import (
|
||||
context "context"
|
||||
io "io"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
domain "github.com/caos/zitadel/internal/domain"
|
||||
static "github.com/caos/zitadel/internal/static"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
@@ -39,187 +36,76 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CreateBucket mocks base method.
|
||||
func (m *MockStorage) CreateBucket(ctx context.Context, name, location string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateBucket", ctx, name, location)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateBucket indicates an expected call of CreateBucket.
|
||||
func (mr *MockStorageMockRecorder) CreateBucket(ctx, name, location interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockStorage)(nil).CreateBucket), ctx, name, location)
|
||||
}
|
||||
|
||||
// GetObject mocks base method.
|
||||
func (m *MockStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
|
||||
func (m *MockStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetObject", ctx, bucketName, objectName)
|
||||
ret0, _ := ret[0].(io.Reader)
|
||||
ret1, _ := ret[1].(func() (*domain.AssetInfo, error))
|
||||
ret := m.ctrl.Call(m, "GetObject", ctx, instanceID, resourceOwner, name)
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(func() (*static.Asset, error))
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetObject indicates an expected call of GetObject.
|
||||
func (mr *MockStorageMockRecorder) GetObject(ctx, bucketName, objectName interface{}) *gomock.Call {
|
||||
func (mr *MockStorageMockRecorder) GetObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, bucketName, objectName)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, instanceID, resourceOwner, name)
|
||||
}
|
||||
|
||||
// GetObjectInfo mocks base method.
|
||||
func (m *MockStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
|
||||
func (m *MockStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetObjectInfo", ctx, bucketName, objectName)
|
||||
ret0, _ := ret[0].(*domain.AssetInfo)
|
||||
ret := m.ctrl.Call(m, "GetObjectInfo", ctx, instanceID, resourceOwner, name)
|
||||
ret0, _ := ret[0].(*static.Asset)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetObjectInfo indicates an expected call of GetObjectInfo.
|
||||
func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, bucketName, objectName interface{}) *gomock.Call {
|
||||
func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, bucketName, objectName)
|
||||
}
|
||||
|
||||
// GetObjectPresignedURL mocks base method.
|
||||
func (m *MockStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetObjectPresignedURL", ctx, bucketName, objectName, expiration)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetObjectPresignedURL indicates an expected call of GetObjectPresignedURL.
|
||||
func (mr *MockStorageMockRecorder) GetObjectPresignedURL(ctx, bucketName, objectName, expiration interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectPresignedURL", reflect.TypeOf((*MockStorage)(nil).GetObjectPresignedURL), ctx, bucketName, objectName, expiration)
|
||||
}
|
||||
|
||||
// ListBuckets mocks base method.
|
||||
func (m *MockStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListBuckets", ctx)
|
||||
ret0, _ := ret[0].([]*domain.BucketInfo)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListBuckets indicates an expected call of ListBuckets.
|
||||
func (mr *MockStorageMockRecorder) ListBuckets(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockStorage)(nil).ListBuckets), ctx)
|
||||
}
|
||||
|
||||
// ListObjectInfos mocks base method.
|
||||
func (m *MockStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListObjectInfos", ctx, bucketName, prefix, recursive)
|
||||
ret0, _ := ret[0].([]*domain.AssetInfo)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListObjectInfos indicates an expected call of ListObjectInfos.
|
||||
func (mr *MockStorageMockRecorder) ListObjectInfos(ctx, bucketName, prefix, recursive interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectInfos", reflect.TypeOf((*MockStorage)(nil).ListObjectInfos), ctx, bucketName, prefix, recursive)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, instanceID, resourceOwner, name)
|
||||
}
|
||||
|
||||
// PutObject mocks base method.
|
||||
func (m *MockStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
|
||||
func (m *MockStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PutObject", ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting)
|
||||
ret0, _ := ret[0].(*domain.AssetInfo)
|
||||
ret := m.ctrl.Call(m, "PutObject", ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
|
||||
ret0, _ := ret[0].(*static.Asset)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PutObject indicates an expected call of PutObject.
|
||||
func (mr *MockStorageMockRecorder) PutObject(ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting interface{}) *gomock.Call {
|
||||
func (mr *MockStorageMockRecorder) PutObject(ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting)
|
||||
}
|
||||
|
||||
// RemoveBucket mocks base method.
|
||||
func (m *MockStorage) RemoveBucket(ctx context.Context, name string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveBucket", ctx, name)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveBucket indicates an expected call of RemoveBucket.
|
||||
func (mr *MockStorageMockRecorder) RemoveBucket(ctx, name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveBucket", reflect.TypeOf((*MockStorage)(nil).RemoveBucket), ctx, name)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
|
||||
}
|
||||
|
||||
// RemoveObject mocks base method.
|
||||
func (m *MockStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error {
|
||||
func (m *MockStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveObject", ctx, bucketName, objectName)
|
||||
ret := m.ctrl.Call(m, "RemoveObject", ctx, instanceID, resourceOwner, name)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveObject indicates an expected call of RemoveObject.
|
||||
func (mr *MockStorageMockRecorder) RemoveObject(ctx, bucketName, objectName interface{}) *gomock.Call {
|
||||
func (mr *MockStorageMockRecorder) RemoveObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, bucketName, objectName)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, instanceID, resourceOwner, name)
|
||||
}
|
||||
|
||||
// RemoveObjects mocks base method.
|
||||
func (m *MockStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
|
||||
func (m *MockStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveObjects", ctx, bucketName, path, recursive)
|
||||
ret := m.ctrl.Call(m, "RemoveObjects", ctx, instanceID, resourceOwner, objectType)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveObjects indicates an expected call of RemoveObjects.
|
||||
func (mr *MockStorageMockRecorder) RemoveObjects(ctx, bucketName, path, recursive interface{}) *gomock.Call {
|
||||
func (mr *MockStorageMockRecorder) RemoveObjects(ctx, instanceID, resourceOwner, objectType interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, bucketName, path, recursive)
|
||||
}
|
||||
|
||||
// MockConfig is a mock of Config interface.
|
||||
type MockConfig struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockConfigMockRecorder
|
||||
}
|
||||
|
||||
// MockConfigMockRecorder is the mock recorder for MockConfig.
|
||||
type MockConfigMockRecorder struct {
|
||||
mock *MockConfig
|
||||
}
|
||||
|
||||
// NewMockConfig creates a new mock instance.
|
||||
func NewMockConfig(ctrl *gomock.Controller) *MockConfig {
|
||||
mock := &MockConfig{ctrl: ctrl}
|
||||
mock.recorder = &MockConfigMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockConfig) EXPECT() *MockConfigMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// NewStorage mocks base method.
|
||||
func (m *MockConfig) NewStorage() (static.Storage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NewStorage")
|
||||
ret0, _ := ret[0].(static.Storage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NewStorage indicates an expected call of NewStorage.
|
||||
func (mr *MockConfigMockRecorder) NewStorage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStorage", reflect.TypeOf((*MockConfig)(nil).NewStorage))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, instanceID, resourceOwner, objectType)
|
||||
}
|
||||
|
@@ -1,34 +1,49 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
caos_errors "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
|
||||
func NewStorage(t *testing.T) *MockStorage {
|
||||
return NewMockStorage(gomock.NewController(t))
|
||||
}
|
||||
|
||||
func (m *MockStorage) ExpectAddObjectNoError() *MockStorage {
|
||||
func (m *MockStorage) ExpectPutObject() *MockStorage {
|
||||
m.EXPECT().
|
||||
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, nil)
|
||||
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
|
||||
hash, _ := io.ReadAll(object)
|
||||
return &static.Asset{
|
||||
InstanceID: instanceID,
|
||||
Name: name,
|
||||
Hash: string(hash),
|
||||
Size: objectSize,
|
||||
LastModified: time.Now(),
|
||||
Location: location,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MockStorage) ExpectAddObjectError() *MockStorage {
|
||||
func (m *MockStorage) ExpectPutObjectError() *MockStorage {
|
||||
m.EXPECT().
|
||||
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, caos_errors.ThrowInternal(nil, "", ""))
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MockStorage) ExpectRemoveObjectNoError() *MockStorage {
|
||||
m.EXPECT().
|
||||
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil)
|
||||
return m
|
||||
}
|
||||
@@ -42,7 +57,7 @@ func (m *MockStorage) ExpectRemoveObjectsNoError() *MockStorage {
|
||||
|
||||
func (m *MockStorage) ExpectRemoveObjectError() *MockStorage {
|
||||
m.EXPECT().
|
||||
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(caos_errors.ThrowInternal(nil, "", ""))
|
||||
return m
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
@@ -34,3 +38,15 @@ func (c *Config) NewStorage() (static.Storage, error) {
|
||||
MultiDelete: c.MultiDelete,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewStorage(_ *sql.DB, rawConfig map[string]interface{}) (static.Storage, error) {
|
||||
configData, err := json.Marshal(rawConfig)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "MINIO-Ef2f2", "could not map config")
|
||||
}
|
||||
c := new(Config)
|
||||
if err := json.Unmarshal(configData, c); err != nil {
|
||||
return nil, errors.ThrowInternal(err, "MINIO-GB4nw", "could not map config")
|
||||
}
|
||||
return c.NewStorage()
|
||||
}
|
||||
|
@@ -2,11 +2,10 @@ package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/minio/minio-go/v7"
|
||||
@@ -14,8 +13,11 @@ import (
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
|
||||
var _ static.Storage = (*Minio)(nil)
|
||||
|
||||
type Minio struct {
|
||||
Client *minio.Client
|
||||
Location string
|
||||
@@ -23,129 +25,66 @@ type Minio struct {
|
||||
MultiDelete bool
|
||||
}
|
||||
|
||||
func (m *Minio) CreateBucket(ctx context.Context, name, location string) error {
|
||||
if location == "" {
|
||||
location = m.Location
|
||||
func (m *Minio) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
|
||||
err := m.createBucket(ctx, instanceID, location)
|
||||
if err != nil && !caos_errs.IsErrorAlreadyExists(err) {
|
||||
return nil, err
|
||||
}
|
||||
name = m.prefixBucketName(name)
|
||||
exists, err := m.Client.BucketExists(ctx, name)
|
||||
if err != nil {
|
||||
logging.LogWithFields("MINIO-ADvf3", "bucketname", name).WithError(err).Error("cannot check if bucket exists")
|
||||
return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal")
|
||||
}
|
||||
if exists {
|
||||
return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists")
|
||||
}
|
||||
err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location})
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Minio) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
|
||||
infos, err := m.Client.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-390OP", "Errors.Assets.Bucket.ListFailed")
|
||||
}
|
||||
buckets := make([]*domain.BucketInfo, len(infos))
|
||||
for i, info := range infos {
|
||||
buckets[i] = &domain.BucketInfo{
|
||||
Name: info.Name,
|
||||
CreationDate: info.CreationDate,
|
||||
}
|
||||
}
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
func (m *Minio) RemoveBucket(ctx context.Context, name string) error {
|
||||
name = m.prefixBucketName(name)
|
||||
err := m.Client.RemoveBucket(ctx, name)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "MINIO-338Hs", "Errors.Assets.Bucket.RemoveFailed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Minio) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
|
||||
if createBucketIfNotExisting {
|
||||
err := m.CreateBucket(ctx, bucketName, "")
|
||||
if err != nil && !caos_errs.IsErrorAlreadyExists(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
bucketName := m.prefixBucketName(instanceID)
|
||||
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
|
||||
info, err := m.Client.PutObject(ctx, bucketName, objectName, object, objectSize, minio.PutObjectOptions{ContentType: contentType})
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-590sw", "Errors.Assets.Object.PutFailed")
|
||||
}
|
||||
return &domain.AssetInfo{
|
||||
Bucket: info.Bucket,
|
||||
Key: info.Key,
|
||||
ETag: info.ETag,
|
||||
Size: info.Size,
|
||||
LastModified: info.LastModified,
|
||||
Location: info.Location,
|
||||
VersionID: info.VersionID,
|
||||
return &static.Asset{
|
||||
InstanceID: info.Bucket,
|
||||
ResourceOwner: resourceOwner,
|
||||
Name: info.Key,
|
||||
Hash: info.ETag,
|
||||
Size: info.Size,
|
||||
LastModified: info.LastModified,
|
||||
Location: info.Location,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Minio) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
objectinfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
|
||||
func (m *Minio) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
|
||||
bucketName := m.prefixBucketName(instanceID)
|
||||
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
|
||||
object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
info := func() (*static.Asset, error) {
|
||||
info, err := object.Stat()
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
return m.objectToAssetInfo(instanceID, resourceOwner, info), nil
|
||||
}
|
||||
asset, err := io.ReadAll(object)
|
||||
if err != nil {
|
||||
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-SFef1", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
return asset, info, nil
|
||||
}
|
||||
|
||||
func (m *Minio) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) {
|
||||
bucketName := m.prefixBucketName(instanceID)
|
||||
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
|
||||
objectInfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound {
|
||||
return nil, caos_errs.ThrowNotFound(err, "MINIO-Gdfh4", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-1vySX", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
return m.objectToAssetInfo(bucketName, objectinfo), nil
|
||||
return m.objectToAssetInfo(instanceID, resourceOwner, objectInfo), nil
|
||||
}
|
||||
|
||||
func (m *Minio) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
info := func() (*domain.AssetInfo, error) {
|
||||
info, err := object.Stat()
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed")
|
||||
}
|
||||
return m.objectToAssetInfo(bucketName, info), nil
|
||||
}
|
||||
return object, info, nil
|
||||
}
|
||||
|
||||
func (m *Minio) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
reqParams := make(url.Values)
|
||||
presignedURL, err := m.Client.PresignedGetObject(ctx, bucketName, objectName, expiration, reqParams)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "MINIO-19Mp0", "Errors.Assets.Object.PresignedTokenFailed")
|
||||
}
|
||||
return presignedURL, nil
|
||||
}
|
||||
|
||||
func (m *Minio) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
assetInfos := make([]*domain.AssetInfo, 0)
|
||||
|
||||
objects, cancel := m.listObjects(ctx, bucketName, prefix, recursive)
|
||||
defer cancel()
|
||||
for object := range objects {
|
||||
if object.Err != nil {
|
||||
logging.LogWithFields("MINIO-wC8sd", "bucket-name", bucketName, "prefix", prefix).WithError(object.Err).Debug("unable to get object")
|
||||
return nil, caos_errs.ThrowInternal(object.Err, "MINIO-1m09S", "Errors.Assets.Object.ListFailed")
|
||||
}
|
||||
assetInfos = append(assetInfos, m.objectToAssetInfo(bucketName, object))
|
||||
}
|
||||
return assetInfos, nil
|
||||
}
|
||||
|
||||
func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string) error {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
func (m *Minio) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
|
||||
bucketName := m.prefixBucketName(instanceID)
|
||||
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
|
||||
err := m.Client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{})
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "MINIO-x85RT", "Errors.Assets.Object.RemoveFailed")
|
||||
@@ -153,19 +92,27 @@ func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
|
||||
bucketName = m.prefixBucketName(bucketName)
|
||||
func (m *Minio) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
|
||||
bucketName := m.prefixBucketName(instanceID)
|
||||
objectsCh := make(chan minio.ObjectInfo)
|
||||
g := new(errgroup.Group)
|
||||
|
||||
var path string
|
||||
switch objectType {
|
||||
case static.ObjectTypeStyling:
|
||||
path = domain.LabelPolicyPrefix + "/"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
defer close(objectsCh)
|
||||
objects, cancel := m.listObjects(ctx, bucketName, path, recursive)
|
||||
objects, cancel := m.listObjects(ctx, bucketName, resourceOwner, true)
|
||||
for object := range objects {
|
||||
if err := object.Err; err != nil {
|
||||
cancel()
|
||||
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound {
|
||||
logging.LogWithFields("MINIO-ss8va", "bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found")
|
||||
logging.WithFields("bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found")
|
||||
continue
|
||||
}
|
||||
return caos_errs.ThrowInternal(object.Err, "MINIO-WQF32", "Errors.Assets.Object.ListFailed")
|
||||
@@ -189,6 +136,26 @@ func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recu
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (m *Minio) createBucket(ctx context.Context, name, location string) error {
|
||||
if location == "" {
|
||||
location = m.Location
|
||||
}
|
||||
name = m.prefixBucketName(name)
|
||||
exists, err := m.Client.BucketExists(ctx, name)
|
||||
if err != nil {
|
||||
logging.WithFields("bucketname", name).WithError(err).Error("cannot check if bucket exists")
|
||||
return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal")
|
||||
}
|
||||
if exists {
|
||||
return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists")
|
||||
}
|
||||
err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location})
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recursive bool) (<-chan minio.ObjectInfo, context.CancelFunc) {
|
||||
ctxCancel, cancel := context.WithCancel(ctx)
|
||||
|
||||
@@ -198,17 +165,15 @@ func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recu
|
||||
}), cancel
|
||||
}
|
||||
|
||||
func (m *Minio) objectToAssetInfo(bucketName string, object minio.ObjectInfo) *domain.AssetInfo {
|
||||
return &domain.AssetInfo{
|
||||
Bucket: bucketName,
|
||||
Key: object.Key,
|
||||
ETag: object.ETag,
|
||||
Size: object.Size,
|
||||
LastModified: object.LastModified,
|
||||
VersionID: object.VersionID,
|
||||
Expiration: object.Expires,
|
||||
ContentType: object.ContentType,
|
||||
AutheticatedURL: m.Client.EndpointURL().String() + "/" + bucketName + "/" + object.Key,
|
||||
func (m *Minio) objectToAssetInfo(bucketName string, resourceOwner string, object minio.ObjectInfo) *static.Asset {
|
||||
return &static.Asset{
|
||||
InstanceID: bucketName,
|
||||
ResourceOwner: resourceOwner,
|
||||
Name: object.Key,
|
||||
Hash: object.ETag,
|
||||
Size: object.Size,
|
||||
LastModified: object.LastModified,
|
||||
ContentType: object.ContentType,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,25 +2,36 @@ package static
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type CreateStorage func(client *sql.DB, rawConfig map[string]interface{}) (Storage, error)
|
||||
|
||||
type Storage interface {
|
||||
CreateBucket(ctx context.Context, name, location string) error
|
||||
RemoveBucket(ctx context.Context, name string) error
|
||||
ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error)
|
||||
PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error)
|
||||
GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error)
|
||||
GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error)
|
||||
ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error)
|
||||
GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error)
|
||||
RemoveObject(ctx context.Context, bucketName, objectName string) error
|
||||
RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error
|
||||
PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType ObjectType, object io.Reader, objectSize int64) (*Asset, error)
|
||||
GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*Asset, error), error)
|
||||
GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*Asset, error)
|
||||
RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error
|
||||
RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType ObjectType) error
|
||||
//TODO: add functionality to move asset location
|
||||
}
|
||||
type Config interface {
|
||||
NewStorage() (Storage, error)
|
||||
|
||||
type ObjectType int32
|
||||
|
||||
const (
|
||||
ObjectTypeUserAvatar = iota
|
||||
ObjectTypeStyling
|
||||
)
|
||||
|
||||
type Asset struct {
|
||||
InstanceID string
|
||||
ResourceOwner string
|
||||
Name string
|
||||
Hash string
|
||||
Size int64
|
||||
LastModified time.Time
|
||||
Location string
|
||||
ContentType string
|
||||
}
|
||||
|
Reference in New Issue
Block a user