Files
Memoh/internal/storage/providers/fallback/provider.go
T
Acbox 3fa311c6cb fix(media): proxy ContainerFileOpener through fallback storage provider
The fallback provider introduced in 5aeb2fd3 wrapped containerfs but did
not implement storage.ContainerFileOpener, causing IngestContainerFile to
fail with "provider does not support container file reading". This broke
outbound file attachments on all IM channels (Telegram, Discord, etc.)
because container paths like /data/xxx.xlsx were passed as-is to the
platform API instead of being ingested into the media store first.
2026-04-03 01:14:20 +08:00

99 lines
2.7 KiB
Go

// Package fallback implements storage.Provider that tries a primary provider
// first (e.g. containerfs) and falls back to a secondary (e.g. localfs) on
// failure. Reads check both providers so assets stored by either are reachable.
package fallback
import (
"context"
"io"
"github.com/memohai/memoh/internal/storage"
)
var _ storage.ContainerFileOpener = (*Provider)(nil)
// Provider delegates to primary and falls back to secondary on write errors.
type Provider struct {
primary storage.Provider
secondary storage.Provider
}
// New creates a fallback provider.
func New(primary, secondary storage.Provider) *Provider {
return &Provider{primary: primary, secondary: secondary}
}
func (p *Provider) Put(ctx context.Context, key string, reader io.Reader) error {
err := p.primary.Put(ctx, key, reader)
if err == nil {
return nil
}
if seeker, ok := reader.(io.Seeker); ok {
if _, seekErr := seeker.Seek(0, io.SeekStart); seekErr != nil {
return err
}
}
return p.secondary.Put(ctx, key, reader)
}
func (p *Provider) Open(ctx context.Context, key string) (io.ReadCloser, error) {
rc, err := p.primary.Open(ctx, key)
if err == nil {
return rc, nil
}
return p.secondary.Open(ctx, key)
}
func (p *Provider) Delete(ctx context.Context, key string) error {
err := p.primary.Delete(ctx, key)
if err == nil {
return nil
}
return p.secondary.Delete(ctx, key)
}
func (p *Provider) AccessPath(key string) string {
return p.primary.AccessPath(key)
}
// ListPrefix delegates to both providers and deduplicates.
func (p *Provider) ListPrefix(ctx context.Context, prefix string) ([]string, error) {
keys, _ := tryListPrefix(ctx, p.primary, prefix)
secondaryKeys, _ := tryListPrefix(ctx, p.secondary, prefix)
seen := make(map[string]struct{}, len(keys))
for _, k := range keys {
seen[k] = struct{}{}
}
for _, k := range secondaryKeys {
if _, ok := seen[k]; !ok {
keys = append(keys, k)
}
}
if len(keys) == 0 {
return nil, nil
}
return keys, nil
}
func tryListPrefix(ctx context.Context, p storage.Provider, prefix string) ([]string, error) {
if lister, ok := p.(storage.PrefixLister); ok {
return lister.ListPrefix(ctx, prefix)
}
return nil, nil
}
// OpenContainerFile delegates to whichever inner provider implements
// storage.ContainerFileOpener, trying the primary first.
func (p *Provider) OpenContainerFile(ctx context.Context, botID, containerPath string) (io.ReadCloser, error) {
if opener, ok := p.primary.(storage.ContainerFileOpener); ok {
rc, err := opener.OpenContainerFile(ctx, botID, containerPath)
if err == nil {
return rc, nil
}
}
if opener, ok := p.secondary.(storage.ContainerFileOpener); ok {
return opener.OpenContainerFile(ctx, botID, containerPath)
}
return nil, storage.ErrContainerFileNotSupported
}