feat(plugins): package database, cache, logger, and storage as infra plugins

This commit is contained in:
ryan
2026-08-27 23:51:10 +08:00
parent 4efc2c92b7
commit a25bf7a4f9
21 changed files with 1285 additions and 19 deletions
+145
View File
@@ -0,0 +1,145 @@
// Package storage provides the object storage and ingestion infrastructure plugin for Cordis.
package storage
import (
"context"
"fmt"
"io"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/internal/apps/upload/ingest"
"github.com/Rain-kl/Wavelet/internal/infra/objectstore"
"github.com/Rain-kl/Wavelet/internal/model"
)
// Option configures the storage plugin.
type Option func(*Plugin)
// WithBackend sets an explicit storage backend instance (useful for testing or custom engines).
func WithBackend(b objectstore.Backend) Option {
return func(p *Plugin) {
p.backend = b
}
}
// Plugin implements core.Plugin to provide contracts.StorageService.
type Plugin struct {
backend objectstore.Backend
}
// New creates a new storage infrastructure plugin.
func New(opts ...Option) *Plugin {
p := &Plugin{}
for _, opt := range opts {
if opt != nil {
opt(p)
}
}
return p
}
// Name returns the unique identifier of the storage plugin.
func (p *Plugin) Name() string {
return "storage"
}
// Apply mounts the storage service into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
svc := &storageServiceImpl{
backend: p.backend,
}
core.Provide[contracts.StorageService](ctx, svc)
return nil
}
type storageServiceImpl struct {
backend objectstore.Backend
}
func (s *storageServiceImpl) getBackend(ctx context.Context) (objectstore.Backend, error) {
if s.backend != nil {
return s.backend, nil
}
_, b, err := objectstore.Active(ctx)
if err != nil {
return nil, fmt.Errorf("storage: get active backend failed: %w", err)
}
return b, nil
}
func (s *storageServiceImpl) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (contracts.StoragePutResult, error) {
b, err := s.getBackend(ctx)
if err != nil {
return contracts.StoragePutResult{}, err
}
res, err := b.Put(ctx, key, body, size, contentType)
if err != nil {
return contracts.StoragePutResult{}, err
}
return contracts.StoragePutResult{
Key: res.Key,
Bucket: res.Bucket,
}, nil
}
func (s *storageServiceImpl) Get(ctx context.Context, key string) (*contracts.StorageObject, error) {
b, err := s.getBackend(ctx)
if err != nil {
return nil, err
}
obj, err := b.Get(ctx, key)
if err != nil {
return nil, err
}
return &contracts.StorageObject{
Key: key,
CachePath: obj.CachePath,
Body: obj.Body,
ContentLength: obj.ContentLength,
ContentType: obj.ContentType,
}, nil
}
func (s *storageServiceImpl) Delete(ctx context.Context, key string) error {
b, err := s.getBackend(ctx)
if err != nil {
return err
}
return b.Delete(ctx, key)
}
func (s *storageServiceImpl) Ingest(ctx context.Context, reader io.Reader, opts contracts.IngestOptions) (*contracts.IngestResult, error) {
meta := model.UploadMetadata{
Extra: opts.Metadata,
}
req := ingest.Request{
UserID: opts.UserID,
Type: opts.Type,
FileName: opts.FileName,
MimeType: opts.MimeType,
Extension: opts.Extension,
Size: opts.Size,
Reader: reader,
Policy: ingest.Policy(opts.Policy),
Metadata: meta,
}
res, err := ingest.Ingest(ctx, req)
if err != nil {
return nil, err
}
return &contracts.IngestResult{
ID: res.Upload.ID,
Key: res.Upload.FilePath,
Created: res.Created,
Stored: res.Stored,
Resolved: res.Resolved,
}, nil
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package storage_test
import (
"bytes"
"context"
"errors"
"io"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/internal/infra/objectstore"
"github.com/Rain-kl/Wavelet/plugins/infra/storage"
)
type mockStorageBackend struct {
mu sync.RWMutex
files map[string][]byte
}
func newMockStorageBackend() *mockStorageBackend {
return &mockStorageBackend{files: make(map[string][]byte)}
}
func (m *mockStorageBackend) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (objectstore.PutResult, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := io.ReadAll(body)
if err != nil {
return objectstore.PutResult{}, err
}
m.files[key] = data
return objectstore.PutResult{Key: key, Bucket: "mock-bucket"}, nil
}
func (m *mockStorageBackend) Get(ctx context.Context, key string) (*objectstore.Object, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.files[key]
if !ok {
return nil, errors.New("file not found")
}
return &objectstore.Object{
Body: io.NopCloser(bytes.NewReader(data)),
ContentLength: int64(len(data)),
ContentType: "application/octet-stream",
}, nil
}
func (m *mockStorageBackend) Delete(ctx context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.files, key)
return nil
}
func (m *mockStorageBackend) Test(ctx context.Context) error {
return nil
}
func TestStoragePluginOperations(t *testing.T) {
backend := newMockStorageBackend()
p := storage.New(storage.WithBackend(backend))
assert.Equal(t, "storage", p.Name())
ctx := core.NewContext(context.Background())
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.StorageService](ctx)
require.NoError(t, err)
require.NotNil(t, svc)
testCtx := context.Background()
// Put
content := []byte("wavelet storage content")
putRes, err := svc.Put(testCtx, "avatar/user1.png", bytes.NewReader(content), int64(len(content)), "image/png")
require.NoError(t, err)
assert.Equal(t, "avatar/user1.png", putRes.Key)
assert.Equal(t, "mock-bucket", putRes.Bucket)
// Get
obj, err := svc.Get(testCtx, "avatar/user1.png")
require.NoError(t, err)
data, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content, data)
// Delete
require.NoError(t, svc.Delete(testCtx, "avatar/user1.png"))
_, err = svc.Get(testCtx, "avatar/user1.png")
assert.Error(t, err)
}