mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
fix(media): add local filesystem fallback and fix gallery lightbox matching
- Add localfs storage provider as fallback when containerfs is unreachable - Wrap media service with fallback provider in both entry points - Fix gallery lightbox src matching by comparing pathnames only
This commit is contained in:
@@ -58,7 +58,7 @@ function normalizeSrc(src: string): string {
|
|||||||
if (!src || src.startsWith('data:')) return src
|
if (!src || src.startsWith('data:')) return src
|
||||||
try {
|
try {
|
||||||
const u = new URL(src, window.location.origin)
|
const u = new URL(src, window.location.origin)
|
||||||
return u.pathname + u.search
|
return u.pathname
|
||||||
} catch {
|
} catch {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@ import (
|
|||||||
sessionpkg "github.com/memohai/memoh/internal/session"
|
sessionpkg "github.com/memohai/memoh/internal/session"
|
||||||
"github.com/memohai/memoh/internal/settings"
|
"github.com/memohai/memoh/internal/settings"
|
||||||
"github.com/memohai/memoh/internal/storage/providers/containerfs"
|
"github.com/memohai/memoh/internal/storage/providers/containerfs"
|
||||||
|
"github.com/memohai/memoh/internal/storage/providers/fallback"
|
||||||
|
"github.com/memohai/memoh/internal/storage/providers/localfs"
|
||||||
ttspkg "github.com/memohai/memoh/internal/tts"
|
ttspkg "github.com/memohai/memoh/internal/tts"
|
||||||
ttsedge "github.com/memohai/memoh/internal/tts/adapter/edge"
|
ttsedge "github.com/memohai/memoh/internal/tts/adapter/edge"
|
||||||
"github.com/memohai/memoh/internal/version"
|
"github.com/memohai/memoh/internal/version"
|
||||||
@@ -672,8 +675,14 @@ func provideSessionHandler(log *slog.Logger, sessionService *sessionpkg.Service,
|
|||||||
return handlers.NewSessionHandler(log, sessionService, botService, accountService)
|
return handlers.NewSessionHandler(log, sessionService, botService, accountService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func provideMediaService(log *slog.Logger, manager *workspace.Manager) *media.Service {
|
func provideMediaService(log *slog.Logger, manager *workspace.Manager, cfg config.Config) *media.Service {
|
||||||
provider := containerfs.New(manager)
|
primary := containerfs.New(manager)
|
||||||
|
dataRoot := cfg.Workspace.DataRoot
|
||||||
|
if dataRoot == "" {
|
||||||
|
dataRoot = config.DefaultDataRoot
|
||||||
|
}
|
||||||
|
secondary := localfs.New(filepath.Join(dataRoot, "media"))
|
||||||
|
provider := fallback.New(primary, secondary)
|
||||||
return media.NewService(log, provider)
|
return media.NewService(log, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -82,6 +83,8 @@ import (
|
|||||||
sessionpkg "github.com/memohai/memoh/internal/session"
|
sessionpkg "github.com/memohai/memoh/internal/session"
|
||||||
"github.com/memohai/memoh/internal/settings"
|
"github.com/memohai/memoh/internal/settings"
|
||||||
"github.com/memohai/memoh/internal/storage/providers/containerfs"
|
"github.com/memohai/memoh/internal/storage/providers/containerfs"
|
||||||
|
"github.com/memohai/memoh/internal/storage/providers/fallback"
|
||||||
|
"github.com/memohai/memoh/internal/storage/providers/localfs"
|
||||||
ttspkg "github.com/memohai/memoh/internal/tts"
|
ttspkg "github.com/memohai/memoh/internal/tts"
|
||||||
ttsedge "github.com/memohai/memoh/internal/tts/adapter/edge"
|
ttsedge "github.com/memohai/memoh/internal/tts/adapter/edge"
|
||||||
"github.com/memohai/memoh/internal/version"
|
"github.com/memohai/memoh/internal/version"
|
||||||
@@ -576,8 +579,14 @@ func (h *memohAuthHandler) Register(e *echo.Echo) {
|
|||||||
e.POST("/api/auth/refresh", h.inner.Refresh)
|
e.POST("/api/auth/refresh", h.inner.Refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
func provideMediaService(log *slog.Logger, manager *workspace.Manager) *media.Service {
|
func provideMediaService(log *slog.Logger, manager *workspace.Manager, cfg config.Config) *media.Service {
|
||||||
provider := containerfs.New(manager)
|
primary := containerfs.New(manager)
|
||||||
|
dataRoot := cfg.Workspace.DataRoot
|
||||||
|
if dataRoot == "" {
|
||||||
|
dataRoot = config.DefaultDataRoot
|
||||||
|
}
|
||||||
|
secondary := localfs.New(filepath.Join(dataRoot, "media"))
|
||||||
|
provider := fallback.New(primary, secondary)
|
||||||
return media.NewService(log, provider)
|
return media.NewService(log, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Package localfs implements storage.Provider backed by the host filesystem.
|
||||||
|
// Files are stored under {root}/{routingKey}.
|
||||||
|
package localfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider stores media assets on the host filesystem.
|
||||||
|
type Provider struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a local filesystem storage provider rooted at dir.
|
||||||
|
func New(root string) *Provider {
|
||||||
|
return &Provider{root: root}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Put(_ context.Context, key string, reader io.Reader) error {
|
||||||
|
dest := p.resolve(key)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dest), 0o750); err != nil {
|
||||||
|
return fmt.Errorf("mkdir: %w", err)
|
||||||
|
}
|
||||||
|
f, err := os.Create(dest) //nolint:gosec // path is constructed from trusted storage key
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
if _, err := io.Copy(f, reader); err != nil {
|
||||||
|
return fmt.Errorf("write: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Open(_ context.Context, key string) (io.ReadCloser, error) {
|
||||||
|
return os.Open(p.resolve(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Delete(_ context.Context, key string) error {
|
||||||
|
return os.Remove(p.resolve(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) AccessPath(key string) string {
|
||||||
|
return p.resolve(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPrefix returns all keys sharing a common prefix (directory listing).
|
||||||
|
func (p *Provider) ListPrefix(_ context.Context, prefix string) ([]string, error) {
|
||||||
|
dir := filepath.Dir(p.resolve(prefix))
|
||||||
|
base := filepath.Base(prefix)
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var keys []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(e.Name(), base) {
|
||||||
|
rel, _ := filepath.Rel(p.root, filepath.Join(dir, e.Name()))
|
||||||
|
if rel != "" {
|
||||||
|
keys = append(keys, filepath.ToSlash(rel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) resolve(key string) string {
|
||||||
|
return filepath.Join(p.root, filepath.FromSlash(key))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user