193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NoopBackend is a deterministic no-op archive/storage adapter.
|
|
type NoopBackend struct{}
|
|
|
|
// Archive returns the requested items as archived with placeholder metadata.
|
|
func (n *NoopBackend) Archive(ctx context.Context, req ArchiveRequest) (ArchiveResult, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ArchiveResult{}, err
|
|
}
|
|
return ArchiveResult{Archived: append([]ArchiveItem(nil), req.Items...), Metadata: map[string]any{"placeholder": true}}, nil
|
|
}
|
|
|
|
// FakeBackend captures archive requests and returns deterministic responses.
|
|
type FakeBackend struct {
|
|
Requests []ArchiveRequest
|
|
Err error
|
|
Result ArchiveResult
|
|
|
|
Objects map[string]FakeObject
|
|
|
|
ListErr error
|
|
DownloadErr error
|
|
UploadErr error
|
|
ExistsErr error
|
|
}
|
|
|
|
// Archive records request and returns configured response.
|
|
func (f *FakeBackend) Archive(ctx context.Context, req ArchiveRequest) (ArchiveResult, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ArchiveResult{}, err
|
|
}
|
|
f.Requests = append(f.Requests, req)
|
|
if f.Err != nil {
|
|
return ArchiveResult{}, f.Err
|
|
}
|
|
res := f.Result
|
|
if res.Archived == nil {
|
|
res.Archived = append([]ArchiveItem(nil), req.Items...)
|
|
}
|
|
if res.Metadata == nil {
|
|
res.Metadata = map[string]any{"fake": true}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// FakeObject is a deterministic fake object-store record.
|
|
type FakeObject struct {
|
|
Key string
|
|
Data []byte
|
|
Metadata map[string]string
|
|
ETag string
|
|
LastModified *time.Time
|
|
}
|
|
|
|
// SeedObject inserts or replaces an object in the fake object store.
|
|
func (f *FakeBackend) SeedObject(obj FakeObject) {
|
|
if f.Objects == nil {
|
|
f.Objects = map[string]FakeObject{}
|
|
}
|
|
key := normalizeObjectKey(obj.Key)
|
|
obj.Key = key
|
|
obj.Data = append([]byte(nil), obj.Data...)
|
|
obj.Metadata = copyMetadata(obj.Metadata)
|
|
f.Objects[key] = obj
|
|
}
|
|
|
|
// List returns deterministic prefix-filtered objects.
|
|
func (f *FakeBackend) List(ctx context.Context, prefix string) ([]ObjectInfo, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if f.ListErr != nil {
|
|
return nil, f.ListErr
|
|
}
|
|
|
|
normalizedPrefix := normalizeObjectKey(prefix)
|
|
keys := make([]string, 0, len(f.Objects))
|
|
for key := range f.Objects {
|
|
if strings.HasPrefix(key, normalizedPrefix) {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
out := make([]ObjectInfo, 0, len(keys))
|
|
for _, key := range keys {
|
|
obj := f.Objects[key]
|
|
out = append(out, ObjectInfo{
|
|
Key: obj.Key,
|
|
Size: int64(len(obj.Data)),
|
|
ETag: obj.ETag,
|
|
LastModified: obj.LastModified,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Download writes one object to a local path.
|
|
func (f *FakeBackend) Download(ctx context.Context, key, localPath string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if f.DownloadErr != nil {
|
|
return f.DownloadErr
|
|
}
|
|
if strings.TrimSpace(localPath) == "" {
|
|
return fmt.Errorf("download object: local path is required")
|
|
}
|
|
|
|
obj, ok := f.Objects[normalizeObjectKey(key)]
|
|
if !ok {
|
|
return fmt.Errorf("download object %q: %w", key, os.ErrNotExist)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
|
|
return fmt.Errorf("download object %q: create parent directory: %w", key, err)
|
|
}
|
|
if err := os.WriteFile(localPath, obj.Data, 0o644); err != nil {
|
|
return fmt.Errorf("download object %q: write local file: %w", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Upload reads a local file and stores it under key.
|
|
func (f *FakeBackend) Upload(ctx context.Context, localPath, key string, opts UploadOptions) (ObjectInfo, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ObjectInfo{}, err
|
|
}
|
|
if f.UploadErr != nil {
|
|
return ObjectInfo{}, f.UploadErr
|
|
}
|
|
if strings.TrimSpace(localPath) == "" {
|
|
return ObjectInfo{}, fmt.Errorf("upload object: local path is required")
|
|
}
|
|
if strings.TrimSpace(key) == "" {
|
|
return ObjectInfo{}, fmt.Errorf("upload object: key is required")
|
|
}
|
|
|
|
data, err := os.ReadFile(localPath)
|
|
if err != nil {
|
|
return ObjectInfo{}, fmt.Errorf("upload object %q from %q: %w", key, localPath, err)
|
|
}
|
|
|
|
normalizedKey := normalizeObjectKey(key)
|
|
now := time.Now().UTC()
|
|
obj := FakeObject{
|
|
Key: normalizedKey,
|
|
Data: data,
|
|
Metadata: copyMetadata(opts.Metadata),
|
|
LastModified: &now,
|
|
}
|
|
f.SeedObject(obj)
|
|
return ObjectInfo{
|
|
Key: normalizedKey,
|
|
Size: int64(len(data)),
|
|
LastModified: &now,
|
|
}, nil
|
|
}
|
|
|
|
// Exists checks object presence.
|
|
func (f *FakeBackend) Exists(ctx context.Context, key string) (bool, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return false, err
|
|
}
|
|
if f.ExistsErr != nil {
|
|
return false, f.ExistsErr
|
|
}
|
|
_, ok := f.Objects[normalizeObjectKey(key)]
|
|
return ok, nil
|
|
}
|
|
|
|
func copyMetadata(in map[string]string) map[string]string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|