Files
beardrive/cmd/bdrive/sync_run.go
T
Snow LeeandClaude Fable 5 2d7f2f8bfa feat: auth, move-proof projects, interactive init/login, web history
Authentication (previous phase, now landed together with its follow-ups):
- Email+password+name accounts behind an AuthProvider interface; the OSS
  server ships BuiltinAuth only (file-backed auth.json: bcrypt password
  hashes + SHA-256 token digests, plaintext never stored; server-owned
  /auth/* pages; managed deployments can swap in another provider).
- bdrive login: loopback-callback browser flow (sign-up on the page, the
  terminal finishes itself) with a device-code fallback for headless
  machines; long-lived revocable device tokens in settings.json.
- Password reset via plain SMTP (stdlib) with a log-link fallback when no
  SMTP is configured.

Move-proof projects:
- .bdrive is now a directory; config.json carries a stable mount id.
  The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed
  by that id — never the folder path — so renames/moves are free.
- The daemon re-reads the project config each tick and exits cleanly
  (propagating nothing) when its folder vanishes; the registry self-heals
  and the next bdrive command at the new location resumes with zero
  spurious changes.

bdrive init is the front door (mnt/umnt removed; bdrive stop pauses):
- Interactive on a TTY (create new / connect existing project from the
  server's list; whole folder / shared subfolder via the include list),
  full flag bypass (--name/--project/--shared/--yes), never prompts
  without a TTY. Runs the login flow first when there is no session.
  Default server: beardrive.ai (config.DefaultServer).

Web history (revert-ready):
- Hubs now always require auth; journal ops carry the signed-in account
  (user/user_name) alongside the git/OS fallback author.
- File-backed device registry: per-device name, OS, account, and the
  public IP the server observed, joined into history at read time.
- GET /api/p/<id>/history?path=|prefix= (newest first) and
  GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained
  forever, so the next phase's revert is re-putting an old blob.
- UI: History button (file versions or project feed), per-folder history
  shortcut, view/download of any past version.

Tests: auth flows (callback, device-code, reset single-use, persistence,
gating), history API + device registry, folder-move survival, registry
self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 13:12:49 -07:00

106 lines
2.9 KiB
Go

package main
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/runbear-io/beardrive/internal/config"
"github.com/runbear-io/beardrive/internal/daemon"
"github.com/runbear-io/beardrive/internal/store"
)
// startSync brings a project folder live: register the mount, open the
// volume store, run the initial cycle, start the background daemon. Called
// by `bdrive init` (and by anything that needs to resume a stopped project).
func startSync(ctx context.Context, folder string, proj config.Project, foreground bool, scanInterval, remoteInterval time.Duration) error {
if _, _, err := config.ResolveMount(folder); err != nil { // registers/updates the registry entry
return err
}
vdir, err := config.VolumeDir(proj.ID)
if err != nil {
return err
}
if _, err := store.Open(vdir); err != nil {
return err
}
// Initial cycle: import existing files, pull remote state.
sess, _, err := openSession(ctx, folder, true)
if err != nil {
return err
}
res, err := sess.Cycle(ctx)
closeSession(sess)
if err != nil {
return err
}
printCycle(res)
if foreground {
return daemon.Run(folder, scanInterval, remoteInterval)
}
pid, err := daemon.Start(folder, vdir, scanInterval, remoteInterval)
if err != nil {
return fmt.Errorf("start sync daemon: %w", err)
}
fmt.Printf(" daemon: running (pid %d, scan %s, remote sync %s)\n", pid, scanInterval, remoteInterval)
return nil
}
// stopCmd pauses syncing for a project folder (files stay on disk; run
// `bdrive init` again to resume).
func stopCmd() *cobra.Command {
var forget bool
c := &cobra.Command{
Use: "stop [folder]",
Aliases: []string{"pause"},
Short: "Stop syncing a project folder",
Long: `Stop the sync daemon for a project folder. Files stay on disk and the
project's local history is kept; run "bdrive init" in the folder to resume.
With --forget the mount is also removed from this device's registry (the
folder's .bdrive settings and the local volume data are kept).`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(args)
if err != nil {
return err
}
proj, err := mustProject(folder)
if err != nil {
return err
}
vdir, err := config.VolumeDir(proj.ID)
if err != nil {
return err
}
stopped, err := daemon.Stop(vdir)
if err != nil {
return err
}
if stopped {
fmt.Printf("stopped syncing %s\n", folder)
} else {
fmt.Printf("no sync daemon running for %s\n", folder)
}
if forget {
mounts, err := config.LoadMounts()
if err != nil {
return err
}
delete(mounts, proj.ID)
if err := config.SaveMounts(mounts); err != nil {
return err
}
fmt.Printf("forgot mount %s (local volume data kept under ~/.bdrive/volumes/%s)\n", proj.ID, proj.ID)
}
return nil
},
}
c.Flags().BoolVar(&forget, "forget", false, "also remove the mount from this device's registry")
return c
}