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
67 lines
1.8 KiB
Go
67 lines
1.8 KiB
Go
package webapp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// DirSource serves a plain local folder straight from disk — no bdrive remote
|
|
// or volume needed. Meant for debugging the webapp (and as a quick local
|
|
// markdown browser): the tree reflects the folder live, provenance is just
|
|
// file mtimes, and content streams from the filesystem.
|
|
type DirSource struct {
|
|
Root string
|
|
}
|
|
|
|
var skipNames = map[string]bool{".DS_Store": true, ".bdrive": true}
|
|
var skipDirs = map[string]bool{".git": true, ".bdrive": true}
|
|
|
|
func (d *DirSource) Files(_ context.Context) (map[string]FileInfo, error) {
|
|
files := make(map[string]FileInfo)
|
|
err := filepath.WalkDir(d.Root, func(p string, e fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return nil // skip unreadable entries
|
|
}
|
|
rel, err := filepath.Rel(d.Root, p)
|
|
if err != nil || rel == "." {
|
|
return nil
|
|
}
|
|
if e.IsDir() {
|
|
if skipDirs[e.Name()] {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if !e.Type().IsRegular() || skipNames[e.Name()] || strings.HasPrefix(e.Name(), ".bdrive-tmp-") {
|
|
return nil
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
files[filepath.ToSlash(rel)] = FileInfo{
|
|
// Synthetic content identity for the ETag; changes when the
|
|
// file does, which is all revalidation needs.
|
|
Blob: fmt.Sprintf("dir-%d-%d", info.ModTime().UnixNano(), info.Size()),
|
|
Size: info.Size(),
|
|
Time: info.ModTime().UTC(),
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
// Open streams a file from disk. Paths are only ever snapshot map keys
|
|
// (produced by Files above), so they cannot escape Root.
|
|
func (d *DirSource) Open(_ context.Context, path string, _ FileInfo) (io.ReadCloser, error) {
|
|
return os.Open(filepath.Join(d.Root, filepath.FromSlash(path)))
|
|
}
|