mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
The web server (bdrive web) becomes a full sync hub, and client devices
get one-command onboarding — without ever seeing storage info or holding
cloud credentials:
- bdrive web -c config.json: server configurable from a JSON file
(remote/addr/upload/upload_ttl/projects_db); explicit flags win.
- Hub mode: pointing bdrive web at a storage root hosts many projects,
each under <root>/<project-id>/ (remote.Prefixed). Projects live in a
file-backed registry (projects.json — loaded at open, rewritten
atomically per change) with create-or-join-by-name semantics.
- Per-project APIs: /api/projects (list/create/get) and
/api/p/<id>/{tree,file,render,download,upload/*,store/*}. The web UI
grows a project list with per-project browsing and hash deep links.
- Browser uploads and a store proxy for syncing devices: presigned
direct-to-storage PUTs when the backend can sign (S3 presign, GCS V4
signed URLs; expiring, credential-free), relayed through the server
otherwise. Journals are never presigned — only immutable blobs.
Blobs-before-journal and one-writer-per-journal invariants hold.
- https:// remote backend: a device syncs one hub project through
/api/p/<id>/store/* — mnt/sync/daemon/log all work unchanged.
- bdrive login <url>: verify a hub and remember it as the device default
(settings.json). bdrive init: create-or-join a project named after the
folder (--name/--project override), write .bdrive, seed a starter
.bdriveignore, mount, and start the daemon — one command per project.
- Hard-break rename: .beardrive->.bdrive, .beardriveignore->.bdriveignore,
~/.beardrive->~/.bdrive, BEARDRIVE_HOME->BDRIVE_HOME, temp/conflict
prefixes; old names are no longer read.
- Tests: presigning, project registry persistence, store API validation
and gating, project isolation over live HTTP, browser upload flows, and
two-device convergence through a hub (incl. read-only pull-only mode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
96 lines
2.0 KiB
Go
96 lines
2.0 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
func (b *localBackend) path(key string) string {
|
|
return filepath.Join(b.root, filepath.FromSlash(key))
|
|
}
|
|
|
|
func (b *localBackend) Put(_ context.Context, key string, r io.Reader, _ int64) error {
|
|
dst := b.path(key)
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), ".bdrive-tmp-*")
|
|
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) {
|
|
return os.Open(b.path(key))
|
|
}
|
|
|
|
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
|
|
}
|
|
if strings.HasPrefix(d.Name(), ".bdrive-tmp-") {
|
|
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
|
|
}
|
|
out = append(out, Object{Key: key, Size: info.Size()})
|
|
return nil
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
func (b *localBackend) Exists(_ context.Context, key string) (bool, error) {
|
|
_, err := os.Stat(b.path(key))
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (b *localBackend) Close() error { return nil }
|