Files
LocalAI/core/services/storage/filesystem.go
Ettore Di Giacinto 59108fbe32 feat: add distributed mode (#9124)
* feat: add distributed mode (experimental)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix data races, mutexes, transactions

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix events and tool stream in agent chat

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* use ginkgo

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(cron): compute correctly time boundaries avoiding re-triggering

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* enhancements, refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* do not flood of healthy checks

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* do not list obvious backends as text backends

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* tests fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop redundant healthcheck

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* enhancements, refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-30 00:47:27 +02:00

147 lines
3.7 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// validateKey ensures the resolved path stays inside the store root.
func (fs *FilesystemStore) validateKey(key string) error {
p := filepath.Join(fs.root, filepath.FromSlash(key))
absRoot, err := filepath.Abs(fs.root)
if err != nil {
return fmt.Errorf("resolving store root: %w", err)
}
absPath, err := filepath.Abs(p)
if err != nil {
return fmt.Errorf("resolving key path: %w", err)
}
if !strings.HasPrefix(absPath, absRoot+string(filepath.Separator)) && absPath != absRoot {
return fmt.Errorf("key %q resolves outside storage root", key)
}
return nil
}
// FilesystemStore implements ObjectStore backed by a local directory.
type FilesystemStore struct {
root string
}
// NewFilesystemStore creates a new filesystem-backed ObjectStore rooted at the given directory.
func NewFilesystemStore(root string) (*FilesystemStore, error) {
if err := os.MkdirAll(root, 0750); err != nil {
return nil, fmt.Errorf("creating storage root %s: %w", root, err)
}
return &FilesystemStore{root: root}, nil
}
func (fs *FilesystemStore) path(key string) string {
return filepath.Join(fs.root, filepath.FromSlash(key))
}
func (fs *FilesystemStore) Put(_ context.Context, key string, r io.Reader) error {
if err := fs.validateKey(key); err != nil {
return err
}
p := fs.path(key)
if err := os.MkdirAll(filepath.Dir(p), 0750); err != nil {
return fmt.Errorf("creating directories for %s: %w", key, err)
}
f, err := os.Create(p)
if err != nil {
return fmt.Errorf("creating file %s: %w", key, err)
}
defer f.Close()
if _, err := io.Copy(f, r); err != nil {
return fmt.Errorf("writing %s: %w", key, err)
}
return nil
}
func (fs *FilesystemStore) Get(_ context.Context, key string) (io.ReadCloser, error) {
if err := fs.validateKey(key); err != nil {
return nil, err
}
f, err := os.Open(fs.path(key))
if err != nil {
return nil, fmt.Errorf("opening %s: %w", key, err)
}
return f, nil
}
func (fs *FilesystemStore) Head(_ context.Context, key string) (*ObjectMeta, error) {
if err := fs.validateKey(key); err != nil {
return nil, err
}
info, err := os.Stat(fs.path(key))
if err != nil {
return nil, fmt.Errorf("stat %s: %w", key, err)
}
return &ObjectMeta{
Key: key,
Size: info.Size(),
LastModified: info.ModTime(),
}, nil
}
func (fs *FilesystemStore) Exists(_ context.Context, key string) (bool, error) {
if err := fs.validateKey(key); err != nil {
return false, err
}
_, err := os.Stat(fs.path(key))
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func (fs *FilesystemStore) Delete(_ context.Context, key string) error {
if err := fs.validateKey(key); err != nil {
return err
}
err := os.Remove(fs.path(key))
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("deleting %s: %w", key, err)
}
return nil
}
// Close implements io.Closer. FilesystemStore holds no resources that need
// explicit cleanup, so this is a no-op.
func (fs *FilesystemStore) Close() error { return nil }
func (fs *FilesystemStore) List(_ context.Context, prefix string) ([]string, error) {
if err := fs.validateKey(prefix); err != nil {
return nil, err
}
var keys []string
base := fs.path(prefix)
// If the prefix path doesn't exist, return empty list
if _, err := os.Stat(base); os.IsNotExist(err) {
return keys, nil
}
err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
rel, err := filepath.Rel(fs.root, path)
if err != nil {
return err
}
keys = append(keys, strings.ReplaceAll(rel, string(filepath.Separator), "/"))
}
return nil
})
return keys, err
}