This repository has been archived on 2025-08-23. You can view files and clone it, but cannot push or open issues or pull requests.
Files
GreenCoast/cmd/shard/main.go
Dani d87e9322b5 Added example/dropin replacements for .env.example
Fixed the issue with PlainText (Complete Anon) posting
Need to fix device sign on issues.
Need to make it so that the non-signed in devices can only see their equalivant level of posts. (i.e. plaintext, public-encrypted, private-encrypted)
2025-08-22 22:59:05 -04:00

242 lines
5.4 KiB
Go

// 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/<hash> content
/var/lib/greencoast/objects/<hash>.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)
}
}