Improve config parsing and intialization
This commit is contained in:
@@ -11,12 +11,13 @@ A single zooid instance can run any number of "virtual" relays. The `config` dir
|
||||
Zooid supports a few environment variables, which configure shared resources like the web server or sqlite database.
|
||||
|
||||
- `PORT` - the port the server will listen on for all requests. Defaults to `3334`.
|
||||
- `DATA` - the location of the directory for storing database files and media. Defaults to `./data`.
|
||||
- `CONF` - the location of the directory for storing relay configuration files. Defaults to `./conf`.
|
||||
- `CONFIG` - where to store relay configuration files. Defaults to `./config`.
|
||||
- `MEDIA` - where to store blossom media files. Defaults to `./media`.
|
||||
- `DATA` - where to store databse files. Defaults to `./data`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration files are written using [toml](https://toml.io) files placed in the `CONF` directory. Top level configuration options are required:
|
||||
Configuration files are written using [toml](https://toml.io). Top level configuration options are required:
|
||||
|
||||
- `host` - a hostname to serve this relay on.
|
||||
- `schema` - a string that identifies this relay. This cannot be changed, and must be usable as a sqlite identifier.
|
||||
|
||||
+12
-3
@@ -20,8 +20,17 @@ func main() {
|
||||
|
||||
port := zooid.Env("PORT")
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", port),
|
||||
Handler: http.HandlerFunc(zooid.ServeHTTP),
|
||||
Addr: fmt.Sprintf(":%s", port),
|
||||
Handler: http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
instance, exists := zooid.Dispatch(r.Host)
|
||||
if exists {
|
||||
instance.Relay.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -31,7 +40,7 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
go zooid.MonitorInstances()
|
||||
go zooid.Start()
|
||||
|
||||
<-shutdown
|
||||
|
||||
|
||||
+1
-12
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
@@ -18,19 +17,9 @@ type BlossomStore struct {
|
||||
Events eventstore.Store
|
||||
}
|
||||
|
||||
func (bl *BlossomStore) Init() error {
|
||||
dir := Env("DATA") + "/media"
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bl *BlossomStore) Enable(instance *Instance) {
|
||||
dir := Env("MEDIA")
|
||||
fs := afero.NewOsFs()
|
||||
dir := Env("DATA") + "/media"
|
||||
backend := blossom.New(instance.Relay, "https://"+bl.Config.Host)
|
||||
|
||||
backend.Store = blossom.EventStoreBlobIndexWrapper{
|
||||
|
||||
+12
-4
@@ -51,12 +51,20 @@ type Config struct {
|
||||
secret nostr.SecretKey
|
||||
}
|
||||
|
||||
func LoadConfig(hostname string) (*Config, error) {
|
||||
path := filepath.Join("config", hostname)
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
path := filepath.Join(Env("CONFIG"), filename)
|
||||
|
||||
var config Config
|
||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
|
||||
return nil, fmt.Errorf("Failed to parse config file %s: %w", path, err)
|
||||
}
|
||||
|
||||
if config.Host == "" {
|
||||
return nil, fmt.Errorf("host is required")
|
||||
}
|
||||
|
||||
if config.Schema == "" {
|
||||
return nil, fmt.Errorf("schema is required")
|
||||
}
|
||||
|
||||
secret, err := nostr.SecretKeyFromHex(config.Secret)
|
||||
@@ -84,7 +92,7 @@ func (config *Config) Sign(event *nostr.Event) error {
|
||||
}
|
||||
|
||||
func (config *Config) IsOwner(pubkey nostr.PubKey) bool {
|
||||
return pubkey == nostr.MustPubKeyFromHex(config.Info.Pubkey)
|
||||
return pubkey.Hex() == config.Info.Pubkey
|
||||
}
|
||||
|
||||
func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role {
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ func Env(k string, fallback ...string) (v string) {
|
||||
|
||||
env["PORT"] = "3334"
|
||||
env["DATA"] = "./data"
|
||||
env["CONF"] = "./conf"
|
||||
env["MEDIA"] = "./media"
|
||||
env["CONFIG"] = "./config"
|
||||
|
||||
for _, item := range os.Environ() {
|
||||
parts := strings.SplitN(item, "=", 2)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package zooid
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
instance, err := GetInstance(r.Host)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load config for hostname %s: %v", r.Host, err)
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
instance.Relay.ServeHTTP(w, r)
|
||||
}
|
||||
+13
-106
@@ -5,102 +5,16 @@ import (
|
||||
"iter"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
"fiatjaf.com/nostr/khatru"
|
||||
"fiatjaf.com/nostr/nip29"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gosimple/slug"
|
||||
)
|
||||
|
||||
// Top level instance creation/destruction
|
||||
|
||||
var (
|
||||
instances map[string]*Instance
|
||||
instancesOnce sync.Once
|
||||
instancesMux sync.RWMutex
|
||||
)
|
||||
|
||||
func GetInstance(hostname string) (*Instance, error) {
|
||||
instancesMux.RLock()
|
||||
defer instancesMux.RUnlock()
|
||||
|
||||
instancesOnce.Do(func() {
|
||||
instances = make(map[string]*Instance)
|
||||
})
|
||||
|
||||
instance, exists := instances[hostname]
|
||||
|
||||
if !exists {
|
||||
newInstance, err := MakeInstance(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances[hostname] = newInstance
|
||||
instance = newInstance
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func MonitorInstances() {
|
||||
dir := Env("CONF")
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Fatal("Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create file watcher: %v", err)
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
if err := watcher.Add(dir); err != nil {
|
||||
log.Printf("Failed to watch config directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Watching config directory for changes")
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
hostname := filepath.Base(event.Name)
|
||||
|
||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) {
|
||||
log.Printf("Config file changed/deleted: %s", event.Name)
|
||||
|
||||
instancesMux.Lock()
|
||||
if instance, exists := instances[hostname]; exists {
|
||||
instance.Cleanup()
|
||||
}
|
||||
instancesMux.Unlock()
|
||||
}
|
||||
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Printf("File watcher error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance struct
|
||||
|
||||
type Instance struct {
|
||||
Config *Config
|
||||
Events eventstore.Store
|
||||
@@ -109,13 +23,8 @@ type Instance struct {
|
||||
Relay *khatru.Relay
|
||||
}
|
||||
|
||||
func MakeInstance(hostname string) (*Instance, error) {
|
||||
config, err := LoadConfig(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubkey, err := nostr.PubKeyFromHex(config.Info.Pubkey)
|
||||
func MakeInstance(filename string) (*Instance, error) {
|
||||
config, err := LoadConfig(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -148,12 +57,20 @@ func MakeInstance(hostname string) (*Instance, error) {
|
||||
instance.Relay.Negentropy = true
|
||||
instance.Relay.Info.Name = config.Info.Name
|
||||
instance.Relay.Info.Icon = config.Info.Icon
|
||||
instance.Relay.Info.PubKey = &pubkey
|
||||
instance.Relay.Info.Description = config.Info.Description
|
||||
// instance.Relay.Info.Self = nostr.GetPublicKey(secret)
|
||||
instance.Relay.Info.Software = "https://github.com/coracle-social/zooid"
|
||||
instance.Relay.Info.Version = "v0.1.0"
|
||||
|
||||
if config.Info.Pubkey != "" {
|
||||
pubkey, err := nostr.PubKeyFromHex(config.Info.Pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instance.Relay.Info.PubKey = &pubkey
|
||||
}
|
||||
|
||||
instance.Relay.UseEventstore(instance.Events, 400)
|
||||
|
||||
instance.Relay.OnConnect = instance.OnConnect
|
||||
@@ -171,11 +88,7 @@ func MakeInstance(hostname string) (*Instance, error) {
|
||||
// Initialize stuff
|
||||
|
||||
if err := instance.Events.Init(); err != nil {
|
||||
log.Fatal("Failed to initialize event store:", err)
|
||||
}
|
||||
|
||||
if err := instance.Blossom.Init(); err != nil {
|
||||
log.Fatal("Failed to initialize blossom store:", err)
|
||||
log.Fatal("Failed to initialize event store: ", err)
|
||||
}
|
||||
|
||||
if config.Blossom.Enabled {
|
||||
@@ -189,14 +102,8 @@ func MakeInstance(hostname string) (*Instance, error) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (instance *Instance) Cleanup() bool {
|
||||
// Close the event store
|
||||
func (instance *Instance) Cleanup() {
|
||||
instance.Events.Close()
|
||||
|
||||
// Remove from instances map
|
||||
delete(instances, instance.Config.Host)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package zooid
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var (
|
||||
instancesByHost map[string]*Instance
|
||||
instancesByName map[string]*Instance
|
||||
instancesOnce sync.Once
|
||||
instancesMux sync.RWMutex
|
||||
)
|
||||
|
||||
func Dispatch(hostname string) (*Instance, bool) {
|
||||
instancesMux.RLock()
|
||||
defer instancesMux.RUnlock()
|
||||
|
||||
instance, exists := instancesByHost[hostname]
|
||||
|
||||
return instance, exists
|
||||
}
|
||||
|
||||
func Start() {
|
||||
dataDir := Env("DATA")
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
log.Fatal("Failed to create data directory: %v", err)
|
||||
}
|
||||
|
||||
mediaDir := Env("MEDIA")
|
||||
if err := os.MkdirAll(mediaDir, 0755); err != nil {
|
||||
log.Fatal("Failed to create media directory: %v", err)
|
||||
}
|
||||
|
||||
configDir := Env("CONFIG")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
log.Fatal("Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
instancesOnce.Do(func() {
|
||||
instancesByHost = make(map[string]*Instance)
|
||||
instancesByName = make(map[string]*Instance)
|
||||
})
|
||||
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to scan config directory: %v", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
instance, err := MakeInstance(entry.Name())
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to make instance for %s: %v", entry.Name(), err)
|
||||
} else {
|
||||
instancesByHost[instance.Config.Host] = instance
|
||||
instancesByName[entry.Name()] = instance
|
||||
log.Printf("Loaded %s", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create file watcher: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer watcher.Close()
|
||||
|
||||
if err := watcher.Add(configDir); err != nil {
|
||||
log.Printf("Failed to watch config directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
filename := filepath.Base(event.Name)
|
||||
|
||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) {
|
||||
instancesMux.Lock()
|
||||
|
||||
if instance, exists := instancesByName[filename]; exists {
|
||||
instance.Cleanup()
|
||||
|
||||
delete(instancesByHost, instance.Config.Host)
|
||||
delete(instancesByName, filename)
|
||||
}
|
||||
|
||||
instance, err := MakeInstance(filename)
|
||||
if err != nil {
|
||||
log.Printf("Failed to reload %s: %v", filename, err)
|
||||
} else {
|
||||
instancesByHost[instance.Config.Host] = instance
|
||||
instancesByName[filename] = instance
|
||||
log.Printf("Reloaded %v", filename)
|
||||
}
|
||||
|
||||
instancesMux.Unlock()
|
||||
}
|
||||
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("File watcher error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user