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
70 lines
1.8 KiB
Go
70 lines
1.8 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Prefixed namespaces a backend under prefix: every key is stored at
|
|
// <prefix>/<key>. Used by the web server to host many projects in one
|
|
// storage root. Closing the returned backend does not close the underlying
|
|
// one (it is shared across prefixes).
|
|
func Prefixed(be Backend, prefix string) Backend {
|
|
prefix = strings.Trim(prefix, "/")
|
|
p := &prefixed{be: be, prefix: prefix}
|
|
if signer, ok := be.(PutSigner); ok {
|
|
return &prefixedSigner{prefixed: p, signer: signer}
|
|
}
|
|
return p
|
|
}
|
|
|
|
type prefixed struct {
|
|
be Backend
|
|
prefix string
|
|
}
|
|
|
|
func (p *prefixed) key(key string) string { return p.prefix + "/" + key }
|
|
|
|
func (p *prefixed) Put(ctx context.Context, key string, r io.Reader, size int64) error {
|
|
return p.be.Put(ctx, p.key(key), r, size)
|
|
}
|
|
|
|
func (p *prefixed) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
return p.be.Get(ctx, p.key(key))
|
|
}
|
|
|
|
func (p *prefixed) Exists(ctx context.Context, key string) (bool, error) {
|
|
return p.be.Exists(ctx, p.key(key))
|
|
}
|
|
|
|
func (p *prefixed) List(ctx context.Context, prefix string) ([]Object, error) {
|
|
objs, err := p.be.List(ctx, p.key(prefix))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
strip := p.prefix + "/"
|
|
out := make([]Object, 0, len(objs))
|
|
for _, o := range objs {
|
|
if !strings.HasPrefix(o.Key, strip) {
|
|
continue
|
|
}
|
|
out = append(out, Object{Key: strings.TrimPrefix(o.Key, strip), Size: o.Size})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (p *prefixed) Close() error { return nil }
|
|
|
|
// prefixedSigner adds presigning when the underlying backend supports it, so
|
|
// direct uploads keep working through the namespace.
|
|
type prefixedSigner struct {
|
|
*prefixed
|
|
signer PutSigner
|
|
}
|
|
|
|
func (p *prefixedSigner) SignPut(ctx context.Context, key string, size int64, ttl time.Duration) (*SignedPut, error) {
|
|
return p.signer.SignPut(ctx, p.key(key), size, ttl)
|
|
}
|