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)
242 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|