From 8c2e75df51d1a74d8bca1d3d1a423f777159ba82 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 6 May 2024 20:32:35 +0300 Subject: refactor project file structure --- Taskfile.yml | 19 ++++++ cmd/gosd/main.go | 57 +++++++++++++++++ entry.go | 32 ---------- handlers.go | 76 ---------------------- health.go | 104 ------------------------------- health_test.go | 24 ------- internal/io.go | 16 +++++ internal/server/handle/handle.go | 80 ++++++++++++++++++++++++ internal/server/health/health.go | 104 +++++++++++++++++++++++++++++++ internal/server/health/health_test.go | 24 +++++++ internal/server/repository/repository.go | 36 +++++++++++ internal/types/entry.go | 34 ++++++++++ io.go | 16 ----- main.go | 54 ---------------- 14 files changed, 370 insertions(+), 306 deletions(-) create mode 100644 Taskfile.yml create mode 100644 cmd/gosd/main.go delete mode 100644 entry.go delete mode 100644 handlers.go delete mode 100644 health.go delete mode 100644 health_test.go create mode 100644 internal/io.go create mode 100644 internal/server/handle/handle.go create mode 100644 internal/server/health/health.go create mode 100644 internal/server/health/health_test.go create mode 100644 internal/server/repository/repository.go create mode 100644 internal/types/entry.go delete mode 100644 io.go delete mode 100644 main.go diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..4363202 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,19 @@ +version: '3' + +tasks: + build: + cmds: + - go build -o gosd cmd/gosd/main.go + dev: + deps: ["vet", "lint"] + cmds: + - go build -race -o gosd cmd/gosd/main.go + vet: + cmds: + - go vet **/*.go + lint: + cmds: + - golangci-lint run + lint-install: + cmds: + - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/cmd/gosd/main.go b/cmd/gosd/main.go new file mode 100644 index 0000000..8f43d3f --- /dev/null +++ b/cmd/gosd/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + + "codeberg.org/snonux/gos/internal/server/handle" + "codeberg.org/snonux/gos/internal/server/health" +) + +func main() { + listenAddr := flag.String("listenAddr", "localhost:8080", "The listen address") + dataDir := flag.String("dataDir", "data", "The data directory") + hs := health.NewStatus() + + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + log.Println("Someone requested /health") + fmt.Fprint(w, hs.String()) + }) + + http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { + log.Println("Someone requested /submit") + if err := handle.Submit(w, r, *dataDir); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + hs.Set(health.Critical, "submitHandler", err.Error()) + return + } + hs.Clear("submitHandler") + }) + + http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) { + log.Println("Someone requested /list") + if err := handle.List(w, r, *dataDir); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + hs.Set(health.Critical, "listHandler", err.Error()) + return + } + hs.Clear("listHandler") + }) + + http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { + log.Println("Someone requested /get") + if err := handle.Get(w, r, *dataDir); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + hs.Set(health.Critical, "getHandler", err.Error()) + return + } + hs.Clear("getHandler") + }) + + log.Println("Server is starting on ", *listenAddr) + if err := http.ListenAndServe(*listenAddr, nil); err != err { + log.Fatal("Error starting server: ", err) + } +} diff --git a/entry.go b/entry.go deleted file mode 100644 index a91e0b7..0000000 --- a/entry.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/json" - "fmt" -) - -type shared struct { - Name string `json:"id"` - Is bool `json:"is,omitempty"` -} - -type entry struct { - Body string `json:"body"` - Shared []shared `json:"shared,omitempty"` - Epoch int `json:"epoch,omitempty"` - id string -} - -func newEntry(bytes []byte) (entry, error) { - var entry entry - if err := json.Unmarshal(bytes, &entry); err != nil { - return entry, fmt.Errorf("unable to deserialise payload: %w", err) - } - entry.id = fmt.Sprintf("%x", sha256.Sum256(bytes)) - return entry, nil -} - -func (e entry) serialize() ([]byte, error) { - return json.Marshal(e) -} diff --git a/handlers.go b/handlers.go deleted file mode 100644 index f293e9c..0000000 --- a/handlers.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "regexp" - "time" -) - -var getIDRe = regexp.MustCompile(`^/[0-9]{4}/[a-z0-9]{64}\.json$`) - -func handleSubmit(w http.ResponseWriter, r *http.Request, dataDir string) error { - if r.Method != "POST" { - return fmt.Errorf("expexted POST request") - } - - bytes, err := io.ReadAll(r.Body) - if err != nil { - return err - } - - entry, err := newEntry(bytes) - if err != nil { - return err - } - filePath := fmt.Sprintf("%s/%s/%s.json", dataDir, time.Now().Format("2006"), entry.id) - - jsonStr, err := entry.serialize() - if err != nil { - return err - } - - if err := saveFile(filePath, jsonStr); err != nil { - return err - } - - return nil -} - -func handleList(w http.ResponseWriter, r *http.Request, dataDir string) error { - if r.Method != "GET" { - return fmt.Errorf("expexted GET request") - } - - repository := newRepository(dataDir) - ids, err := repository.list() - if err != nil { - return err - } - - jsonData, err := json.Marshal(ids) - if err != nil { - return err - } - - fmt.Fprint(w, string(jsonData)) - return nil -} - -func handleGet(w http.ResponseWriter, r *http.Request, dataDir string) error { - path := r.URL.Query().Get("path") - if !getIDRe.MatchString(path) { - return fmt.Errorf("invalid path %s", path) - } - - data, err := os.ReadFile(fmt.Sprintf("%s/%s", dataDir, path)) - if err != err { - return err - } - - fmt.Fprint(w, string(data)) - return nil -} diff --git a/health.go b/health.go deleted file mode 100644 index 0ab9ba1..0000000 --- a/health.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "fmt" - "log" - "strings" - "sync" -) - -type alertSeverity int - -const ( - ok alertSeverity = iota - warning - critical - unknown -) - -func (s alertSeverity) String() string { - switch s { - case ok: - return "OK" - case warning: - return "WARNING" - case critical: - return "CRITICAL" - case unknown: - return "UNKNOWN" - default: - panic("encountered an unknown alertSeverity") - } -} - -type alert struct { - text string - severity alertSeverity -} - -func (a alert) String() string { - return fmt.Sprintf("%s: %s", a.severity, a.text) -} - -type healthStatus struct { - alerts map[string]alert - mu sync.Mutex -} - -func newHealthStatus() healthStatus { - return healthStatus{ - alerts: make(map[string]alert), - } -} - -func (hs healthStatus) set(s alertSeverity, what, text string) { - log.Println("alerting", what, "to", text, "with severity", s) - - hs.mu.Lock() - defer hs.mu.Unlock() - - hs.alerts[what] = alert{ - text: text, - severity: s, - } -} - -func (hs healthStatus) clear(what string) { - hs.mu.Lock() - defer hs.mu.Unlock() - - if _, ok := hs.alerts[what]; ok { - log.Println("clearing alert for", what) - delete(hs.alerts, what) - } -} - -func (hs healthStatus) String() string { - var ( - alerts [4][]string // Alerts by severity - sb strings.Builder - ) - - hs.mu.Lock() - defer hs.mu.Unlock() - - for _, alert := range hs.alerts { - alerts[alert.severity] = append(alerts[alert.severity], alert.String()) - } - - possible := [4]alertSeverity{unknown, critical, warning, ok} - for _, severity := range possible { - if len(alerts[severity]) == 0 { - continue - } - for _, alert := range alerts[severity] { - sb.WriteString(alert) - sb.WriteString("\n") - } - } - - if result := sb.String(); result != "" { - return result - } - return "OK: all is fine :-)\n" -} diff --git a/health_test.go b/health_test.go deleted file mode 100644 index 8dc35bc..0000000 --- a/health_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "testing" - -func TestHealthStatus(t *testing.T) { - t.Parallel() - - h := newHealthStatus() - h.set(warning, "fooService", "this is not good") - h.set(critical, "barService", "this is not good either") - h.set(warning, "bazService", "urgh!") - h.set(unknown, "bazService", "don't know what happened here!") - h.clear("fooService") - - result := h.String() - expected := `UNKNOWN: don't know what happened here! -CRITICAL: this is not good either -` - - if result != expected { - t.Error("expected", expected, "but got", result) - } - t.Log("got as expexted", result) -} diff --git a/internal/io.go b/internal/io.go new file mode 100644 index 0000000..c0f28bf --- /dev/null +++ b/internal/io.go @@ -0,0 +1,16 @@ +package internal + +import ( + "os" + "path/filepath" +) + +func SaveFile(filePath string, bytes []byte) error { + dir := filepath.Dir(filePath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + return os.WriteFile(filePath, bytes, 0644) +} diff --git a/internal/server/handle/handle.go b/internal/server/handle/handle.go new file mode 100644 index 0000000..d1f7748 --- /dev/null +++ b/internal/server/handle/handle.go @@ -0,0 +1,80 @@ +package handle + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "time" + + "codeberg.org/snonux/gos/internal" + "codeberg.org/snonux/gos/internal/server/repository" + "codeberg.org/snonux/gos/internal/types" +) + +var getIDRe = regexp.MustCompile(`^/[0-9]{4}/[a-z0-9]{64}\.json$`) + +func Submit(w http.ResponseWriter, r *http.Request, dataDir string) error { + if r.Method != "POST" { + return fmt.Errorf("expexted POST request") + } + + bytes, err := io.ReadAll(r.Body) + if err != nil { + return err + } + + entry, err := types.NewEntry(bytes) + if err != nil { + return err + } + filePath := fmt.Sprintf("%s/%s/%s.json", dataDir, time.Now().Format("2006"), entry.ID) + + jsonStr, err := entry.Serialize() + if err != nil { + return err + } + + if err := internal.SaveFile(filePath, jsonStr); err != nil { + return err + } + + return nil +} + +func List(w http.ResponseWriter, r *http.Request, dataDir string) error { + if r.Method != "GET" { + return fmt.Errorf("expexted GET request") + } + + repository := repository.New(dataDir) + ids, err := repository.List() + if err != nil { + return err + } + + jsonData, err := json.Marshal(ids) + if err != nil { + return err + } + + fmt.Fprint(w, string(jsonData)) + return nil +} + +func Get(w http.ResponseWriter, r *http.Request, dataDir string) error { + path := r.URL.Query().Get("path") + if !getIDRe.MatchString(path) { + return fmt.Errorf("invalid path %s", path) + } + + data, err := os.ReadFile(fmt.Sprintf("%s/%s", dataDir, path)) + if err != err { + return err + } + + fmt.Fprint(w, string(data)) + return nil +} diff --git a/internal/server/health/health.go b/internal/server/health/health.go new file mode 100644 index 0000000..d87bd3d --- /dev/null +++ b/internal/server/health/health.go @@ -0,0 +1,104 @@ +package health + +import ( + "fmt" + "log" + "strings" + "sync" +) + +type Severity int + +const ( + OK Severity = iota + Warning + Critical + Unknown +) + +func (s Severity) String() string { + switch s { + case OK: + return "OK" + case Warning: + return "WARNING" + case Critical: + return "CRITICAL" + case Unknown: + fallthrough + default: + return "UNKNOWN" + } +} + +type alert struct { + text string + severity Severity +} + +func (a alert) String() string { + return fmt.Sprintf("%s: %s", a.severity, a.text) +} + +type Status struct { + alerts map[string]alert + mu sync.Mutex +} + +func NewStatus() Status { + return Status{ + alerts: make(map[string]alert), + } +} + +func (hs Status) Set(s Severity, what, text string) { + log.Println("alerting", what, "to", text, "with severity", s) + + hs.mu.Lock() + defer hs.mu.Unlock() + + hs.alerts[what] = alert{ + text: text, + severity: s, + } +} + +func (hs Status) Clear(what string) { + hs.mu.Lock() + defer hs.mu.Unlock() + + if _, ok := hs.alerts[what]; ok { + log.Println("clearing alert for", what) + delete(hs.alerts, what) + } +} + +func (hs Status) String() string { + var ( + alerts [4][]string // Alerts by severity + sb strings.Builder + ) + + hs.mu.Lock() + defer hs.mu.Unlock() + + for _, alert := range hs.alerts { + alerts[alert.severity] = append(alerts[alert.severity], alert.String()) + } + + possible := [4]Severity{Unknown, Critical, Warning, OK} + for _, severity := range possible { + if len(alerts[severity]) == 0 { + continue + } + for _, alert := range alerts[severity] { + sb.WriteString(alert) + sb.WriteString("\n") + } + } + + if result := sb.String(); result != "" { + return result + } + return "OK: all is fine :-)\n" +} diff --git a/internal/server/health/health_test.go b/internal/server/health/health_test.go new file mode 100644 index 0000000..1723d42 --- /dev/null +++ b/internal/server/health/health_test.go @@ -0,0 +1,24 @@ +package health + +import "testing" + +func TestHealthStatus(t *testing.T) { + t.Parallel() + + h := NewStatus() + h.Set(warning, "fooService", "this is not good") + h.Set(critical, "barService", "this is not good either") + h.Set(warning, "bazService", "urgh!") + h.Set(unknown, "bazService", "don't know what happened here!") + h.Clear("fooService") + + result := h.String() + expected := `UNKNOWN: don't know what happened here! +CRITICAL: this is not good either +` + + if result != expected { + t.Error("expected", expected, "but got", result) + } + t.Log("got as expexted", result) +} diff --git a/internal/server/repository/repository.go b/internal/server/repository/repository.go new file mode 100644 index 0000000..5fe79fd --- /dev/null +++ b/internal/server/repository/repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "log" + "os" + "path/filepath" + "strings" +) + +type Repository struct { + dataDir string +} + +func New(dataDir string) Repository { + return Repository{dataDir} +} + +func (r Repository) List() ([]string, error) { + var ids []string + + visit := func(files *[]string) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Println(err) + return nil + } + if !info.IsDir() && strings.HasSuffix(path, ".json") { + *files = append(*files, strings.TrimPrefix(path, r.dataDir)) + } + return nil + } + } + + err := filepath.Walk(r.dataDir, visit(&ids)) + return ids, err +} diff --git a/internal/types/entry.go b/internal/types/entry.go new file mode 100644 index 0000000..a274e2c --- /dev/null +++ b/internal/types/entry.go @@ -0,0 +1,34 @@ +package types + +import ( + "crypto/sha256" + "encoding/json" + "fmt" +) + +type Shared struct { + Name string `json:"id"` + Is bool `json:"is,omitempty"` +} + +type Entry struct { + Body string `json:"body"` + Shared []Shared `json:"shared,omitempty"` + Epoch int `json:"epoch,omitempty"` + ID string `json:"id,omitempty"` +} + +func NewEntry(bytes []byte) (Entry, error) { + var entry Entry + if err := json.Unmarshal(bytes, &entry); err != nil { + return entry, fmt.Errorf("unable to deserialise payload: %w", err) + } + if entry.ID == "" { + entry.ID = fmt.Sprintf("%x", sha256.Sum256(bytes)) + } + return entry, nil +} + +func (e Entry) Serialize() ([]byte, error) { + return json.Marshal(e) +} diff --git a/io.go b/io.go deleted file mode 100644 index 4bf795f..0000000 --- a/io.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "os" - "path/filepath" -) - -func saveFile(filePath string, bytes []byte) error { - dir := filepath.Dir(filePath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - } - return os.WriteFile(filePath, bytes, 0644) -} diff --git a/main.go b/main.go deleted file mode 100644 index f4a3e5c..0000000 --- a/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "net/http" -) - -func main() { - listenAddr := flag.String("listenAddr", "localhost:8080", "The listen address") - dataDir := flag.String("dataDir", "data", "The data directory") - health := newHealthStatus() - - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - log.Println("Someone requested /health") - fmt.Fprint(w, health.String()) - }) - - http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { - log.Println("Someone requested /submit") - if err := handleSubmit(w, r, *dataDir); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - health.set(critical, "submitHandler", err.Error()) - return - } - health.clear("submitHandler") - }) - - http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) { - log.Println("Someone requested /list") - if err := handleList(w, r, *dataDir); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - health.set(critical, "listHandler", err.Error()) - return - } - health.clear("listHandler") - }) - - http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { - log.Println("Someone requested /get") - if err := handleGet(w, r, *dataDir); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - health.set(critical, "getHandler", err.Error()) - return - } - health.clear("getHandler") - }) - - log.Println("Server is starting on ", *listenAddr) - if err := http.ListenAndServe(*listenAddr, nil); err != err { - log.Fatal("Error starting server: ", err) - } -} -- cgit v1.2.3