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 --- 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 ++++++++++ 6 files changed, 294 insertions(+) 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 (limited to 'internal') 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) +} -- cgit v1.2.3