mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
3fa311c6cb
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.
99 lines
2.7 KiB
Go
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
|
|
}
|