2026-06-11 23:40:26 -07:00
|
|
|
package remote
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2026-08-03 16:20:51 +09:00
|
|
|
|
|
|
|
|
"github.com/runbear-io/beardrive/internal/store"
|
2026-06-11 23:40:26 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// localBackend stores objects in a plain directory. Useful for tests and for
|
|
|
|
|
// syncing through any mounted network drive.
|
|
|
|
|
type localBackend struct {
|
|
|
|
|
root string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newLocal(root string) (*localBackend, error) {
|
|
|
|
|
if root == "" {
|
|
|
|
|
return nil, fmt.Errorf("file:// remote needs an absolute path")
|
|
|
|
|
}
|
|
|
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &localBackend{root: root}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 16:20:51 +09:00
|
|
|
// path resolves a key under the root and refuses one that climbs out of it.
|
|
|
|
|
// Keys come from journals and API callers, so "the key is well-formed" is a
|
|
|
|
|
// caller's promise, not a fact — this is the last place to check it before a
|
|
|
|
|
// traversal becomes a read or a write anywhere on the hub host.
|
|
|
|
|
//
|
|
|
|
|
// Two checks, because they answer different questions: the lexical one rejects
|
|
|
|
|
// ".." in the key, and store.UnderRoot rejects a key whose path resolves out
|
|
|
|
|
// through a symlink planted inside the storage root — os.Open and os.Rename
|
|
|
|
|
// follow those, so the string staying under the root says nothing about where
|
|
|
|
|
// the bytes come from or land.
|
|
|
|
|
func (b *localBackend) path(key string) (string, error) {
|
|
|
|
|
p := filepath.Join(b.root, filepath.FromSlash(key))
|
|
|
|
|
if p != b.root && !strings.HasPrefix(p, b.root+string(filepath.Separator)) {
|
|
|
|
|
return "", fmt.Errorf("invalid key %q", key)
|
|
|
|
|
}
|
|
|
|
|
if !store.UnderRoot(b.root, p) {
|
|
|
|
|
return "", fmt.Errorf("invalid key %q", key)
|
|
|
|
|
}
|
|
|
|
|
return p, nil
|
2026-06-11 23:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *localBackend) Put(_ context.Context, key string, r io.Reader, _ int64) error {
|
2026-08-03 16:20:51 +09:00
|
|
|
dst, err := b.path(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-06-11 23:40:26 -07:00
|
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-08 07:13:00 -07:00
|
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), ".bdrive-tmp-*")
|
2026-06-11 23:40:26 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tmp.Name())
|
|
|
|
|
if _, err := io.Copy(tmp, r); err != nil {
|
|
|
|
|
tmp.Close()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := tmp.Close(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return os.Rename(tmp.Name(), dst)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *localBackend) Get(_ context.Context, key string) (io.ReadCloser, error) {
|
2026-08-03 16:20:51 +09:00
|
|
|
p, err := b.path(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return os.Open(p)
|
2026-06-11 23:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *localBackend) List(_ context.Context, prefix string) ([]Object, error) {
|
|
|
|
|
var out []Object
|
|
|
|
|
err := filepath.WalkDir(b.root, func(p string, d fs.DirEntry, err error) error {
|
|
|
|
|
if err != nil || d.IsDir() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-08 07:13:00 -07:00
|
|
|
if strings.HasPrefix(d.Name(), ".bdrive-tmp-") {
|
2026-06-11 23:40:26 -07:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
rel, err := filepath.Rel(b.root, p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
key := filepath.ToSlash(rel)
|
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
info, err := d.Info()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-03 16:20:51 +09:00
|
|
|
out = append(out, Object{Key: key, Size: info.Size(), Modified: info.ModTime()})
|
2026-06-11 23:40:26 -07:00
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *localBackend) Exists(_ context.Context, key string) (bool, error) {
|
2026-08-03 16:20:51 +09:00
|
|
|
p, perr := b.path(key)
|
|
|
|
|
if perr != nil {
|
|
|
|
|
return false, perr
|
|
|
|
|
}
|
|
|
|
|
_, err := os.Stat(p)
|
2026-06-11 23:40:26 -07:00
|
|
|
if err == nil {
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *localBackend) Close() error { return nil }
|