mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
sfs v0.1: mountable synced file system for AI agents
- sfs mnt/umnt/sync/status/log/remote/whoami CLI (cobra) - per-device append-only journals + content-addressed blob store - deterministic lamport-ordered merge, LWW with conflict-copy preservation - cloud-agnostic backends: S3, GCS, file:// (S3-compatible via AWS_ENDPOINT_URL) - offline-first: real files on disk, journal locally, push on reconnect - background sync daemon per mount with change tracking (device/author/time) - macOS + Linux; Homebrew formula + goreleaser release pipeline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Package config manages sfs's global state under the sfs home directory
|
||||
// (default ~/.sfs, overridable with $SFS_HOME): the device identity and the
|
||||
// registry of mounted folders.
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Home returns the sfs home directory ($SFS_HOME or ~/.sfs).
|
||||
func Home() (string, error) {
|
||||
if h := os.Getenv("SFS_HOME"); h != "" {
|
||||
return h, nil
|
||||
}
|
||||
uh, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(uh, ".sfs"), nil
|
||||
}
|
||||
|
||||
// Device identifies this machine and its operator in journals.
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
// LoadDevice loads the device identity, creating one on first use.
|
||||
func LoadDevice() (Device, error) {
|
||||
home, err := Home()
|
||||
if err != nil {
|
||||
return Device{}, err
|
||||
}
|
||||
p := filepath.Join(home, "device.json")
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
var d Device
|
||||
if err := json.Unmarshal(data, &d); err == nil && d.ID != "" {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
d := Device{ID: randID(), Name: hostname(), Author: detectAuthor()}
|
||||
if err := os.MkdirAll(home, 0o755); err != nil {
|
||||
return Device{}, err
|
||||
}
|
||||
if err := writeJSON(p, d); err != nil {
|
||||
return Device{}, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func randID() string {
|
||||
b := make([]byte, 6)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "device000000"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func hostname() string {
|
||||
h, _ := os.Hostname()
|
||||
h = strings.TrimSuffix(h, ".local")
|
||||
if h == "" {
|
||||
h = "device"
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func detectAuthor() string {
|
||||
if out, err := exec.Command("git", "config", "--get", "user.email").Output(); err == nil {
|
||||
if s := strings.TrimSpace(string(out)); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
u := os.Getenv("USER")
|
||||
if u == "" {
|
||||
if cu, err := user.Current(); err == nil {
|
||||
u = cu.Username
|
||||
}
|
||||
}
|
||||
if u == "" {
|
||||
u = "unknown"
|
||||
}
|
||||
return u + "@" + hostname()
|
||||
}
|
||||
|
||||
// MountInfo describes one mounted folder.
|
||||
type MountInfo struct {
|
||||
Volume string `json:"volume"`
|
||||
Remote string `json:"remote,omitempty"`
|
||||
}
|
||||
|
||||
func mountsPath() (string, error) {
|
||||
home, err := Home()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "mounts.json"), nil
|
||||
}
|
||||
|
||||
// LoadMounts returns the abs-folder → mount registry.
|
||||
func LoadMounts() (map[string]MountInfo, error) {
|
||||
p, err := mountsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]MountInfo{}
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return out, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func SaveMounts(m map[string]MountInfo) error {
|
||||
p, err := mountsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSON(p, m)
|
||||
}
|
||||
|
||||
// VolumeDir returns (and creates parents for) the local store dir of a volume.
|
||||
func VolumeDir(volume string) (string, error) {
|
||||
home, err := Home()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "volumes", volume), nil
|
||||
}
|
||||
|
||||
func writeJSON(path string, v any) error {
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".sfs-tmp-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp.Name(), path)
|
||||
}
|
||||
Reference in New Issue
Block a user