// cmd/shard/main.go package main import ( "crypto/sha256" "encoding/hex" "io" "log" "os" "path/filepath" "strings" "syscall" "time" "greencoast/internal/api" "greencoast/internal/index" "gopkg.in/yaml.v3" ) type cfgPrivacy struct { AllowAnonPlaintext bool `yaml:"allow_anon_plaintext"` } type shardConfig struct { Privacy cfgPrivacy `yaml:"privacy"` } func boolEnv(keys ...string) bool { for _, k := range keys { v := strings.ToLower(strings.TrimSpace(os.Getenv(k))) if v == "1" || v == "true" || v == "yes" || v == "on" { return true } } return false } func loadYAMLAllow(path string) bool { f, err := os.Open(path) if err != nil { return false } defer f.Close() var sc shardConfig if err := yaml.NewDecoder(f).Decode(&sc); err != nil { return false } return sc.Privacy.AllowAnonPlaintext } /* ------------------------- Minimal FS blob store (implements api.BlobStore) Layout: /var/lib/greencoast/objects/ content /var/lib/greencoast/objects/.priv empty sidecar => private --------------------------*/ type fsStore struct { root string } func newFSStore(root string) *fsStore { return &fsStore{root: root} } func (s *fsStore) ensureRoot() error { // create both parent and leaf to be safe on fresh volumes if err := os.MkdirAll(filepath.Dir(s.root), 0o755); err != nil { return err } return os.MkdirAll(s.root, 0o755) } func (s *fsStore) pathFor(hash string) string { return filepath.Join(s.root, hash) } func (s *fsStore) privPathFor(hash string) string { return filepath.Join(s.root, hash+".priv") } func (s *fsStore) Get(hash string) (io.ReadCloser, int64, error) { if err := s.ensureRoot(); err != nil { return nil, 0, err } f, err := os.Open(s.pathFor(hash)) if err != nil { return nil, 0, err } st, err := f.Stat() if err != nil { _ = f.Close() return nil, 0, err } return f, st.Size(), nil } func (s *fsStore) Put(r io.Reader, private bool) (string, int64, time.Time, error) { if err := s.ensureRoot(); err != nil { return "", 0, time.Time{}, err } tmp, err := os.CreateTemp(s.root, "put-*") if err != nil { return "", 0, time.Time{}, err } tmpName := tmp.Name() defer func() { // best-effort cleanup of temp path (original name) _ = os.Remove(tmpName) }() h := sha256.New() w := io.MultiWriter(tmp, h) n, err := io.Copy(w, r) if err != nil { _ = tmp.Close() return "", 0, time.Time{}, err } // IMPORTANT on Windows bind mounts: flush & close before rename if err := tmp.Sync(); err != nil { _ = tmp.Close() return "", 0, time.Time{}, err } if err := tmp.Close(); err != nil { return "", 0, time.Time{}, err } hash := hex.EncodeToString(h.Sum(nil)) final := s.pathFor(hash) // If a previous file with this hash exists, remove it first (idempotent writes) _ = os.Remove(final) if err := os.Rename(tmpName, final); err != nil { return "", 0, time.Time{}, err } // Optional: fsync directory to harden the rename on some filesystems if df, err := os.Open(s.root); err == nil { _ = syscall.Fsync(int(df.Fd())) _ = df.Close() } st, err := os.Stat(final) if err != nil { return "", 0, time.Time{}, err } // create sidecar only after main content is durable if private { if err := os.WriteFile(s.privPathFor(hash), nil, 0o600); err != nil { _ = os.Remove(final) return "", 0, time.Time{}, err } } return hash, n, st.ModTime().UTC(), nil } func (s *fsStore) Delete(hash string) error { if err := s.ensureRoot(); err != nil { return err } _ = os.Remove(s.privPathFor(hash)) return os.Remove(s.pathFor(hash)) } func (s *fsStore) Walk(fn func(hash string, bytes int64, private bool, storedAt time.Time) error) (int, error) { if err := s.ensureRoot(); err != nil { return 0, err } ents, err := os.ReadDir(s.root) if err != nil { return 0, err } count := 0 for _, e := range ents { if e.IsDir() { continue } name := e.Name() // skip sidecars and non-64-hex filenames if strings.HasSuffix(name, ".priv") || len(name) != 64 || !isHex(name) { continue } full := s.pathFor(name) st, err := os.Stat(full) if err != nil { continue } private := false if _, err := os.Stat(s.privPathFor(name)); err == nil { private = true } if err := fn(name, st.Size(), private, st.ModTime().UTC()); err != nil { return count, err } count++ } return count, nil } func isHex(s string) bool { for i := 0; i < len(s); i++ { c := s[i] if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true } /* ------------------------- main --------------------------*/ func main() { // Store & index store := newFSStore("/var/lib/greencoast/objects") idx := index.New() // Flags: env wins, else YAML (/app/shard.yaml), else false allowAnon := boolEnv("GC_ALLOW_ANON_PLAINTEXT") if !allowAnon { if st, err := os.Stat("/app/shard.yaml"); err == nil && !st.IsDir() { allowAnon = loadYAMLAllow("/app/shard.yaml") } } devMode := boolEnv("GC_DEV_ALLOW_UNAUTH") log.Printf("boot: privacy.allow_anon_plaintext=%v dev=%v at=%s", allowAnon, devMode, time.Now().UTC().Format(time.RFC3339)) var providers api.AuthProviders srv := api.New(store, idx, true, devMode, providers, allowAnon) // Frontend (static) go func() { if err := srv.ListenFrontend("0.0.0.0:9082"); err != nil { log.Printf("frontend server exited: %v", err) } }() // API if err := srv.ListenHTTP("0.0.0.0:9080"); err != nil { log.Fatal(err) } }