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 Uploads []FakeUploadCall ListErr error DownloadErr error UploadErr error ExistsErr error } // FakeUploadCall captures one upload invocation in call order. type FakeUploadCall struct { LocalPath string Key string Options UploadOptions } // 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) f.Uploads = append(f.Uploads, FakeUploadCall{ LocalPath: localPath, Key: normalizedKey, Options: UploadOptions{ Metadata: copyMetadata(opts.Metadata), ContentType: opts.ContentType, }, }) 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 }