diff --git a/main.go b/main.go
index 5450e6d..06c2019 100644
--- a/main.go
+++ b/main.go
@@ -145,6 +145,7 @@ var commands = []struct {
{"updates addsigned", cmdUpdatesAddSigned},
{"updates genkey", cmdUpdatesGenkey},
{"updates pubkey", cmdUpdatesPubkey},
+ {"updates serve", cmdUpdatesServe},
{"updates verify", cmdUpdatesVerify},
}
diff --git a/updates.go b/updates.go
index 3c3b39e..9c223e1 100644
--- a/updates.go
+++ b/updates.go
@@ -7,10 +7,14 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ htmltemplate "html/template"
"io"
"log"
+ "net/http"
"os"
+ "strings"
+ "github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/updates"
)
@@ -132,3 +136,142 @@ func cmdUpdatesPubkey(c *cmd) {
err = enc.Close()
xcheckf(err, "writing public key")
}
+
+var updatesTemplate = htmltemplate.Must(htmltemplate.New("changelog").Parse(`
+
+
+
+
+ mox changelog
+
+
+
+ Changes{{ if .FromVersion }} since {{ .FromVersion }}{{ end }}
+ {{ if not .Changes }}
+ No changes
+ {{ end }}
+ {{ range .Changes }}
+ {{ .Text }}
+
+ {{ end }}
+
+
+`))
+
+func cmdUpdatesServe(c *cmd) {
+ c.unlisted = true
+ c.help = "Serve changelog.json with updates."
+ var address, changelog string
+ c.flag.StringVar(&address, "address", "127.0.0.1:8596", "address to serve /changelog on")
+ c.flag.StringVar(&changelog, "changelog", "changelog.json", "changelog file to serve")
+ args := c.Parse()
+ if len(args) != 0 {
+ c.Usage()
+ }
+
+ parseFile := func() (*updates.Changelog, error) {
+ f, err := os.Open(changelog)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ var cl updates.Changelog
+ if err := json.NewDecoder(f).Decode(&cl); err != nil {
+ return nil, err
+ }
+ return &cl, nil
+ }
+
+ _, err := parseFile()
+ if err != nil {
+ log.Fatalf("parsing %s: %v", changelog, err)
+ }
+
+ srv := http.NewServeMux()
+ srv.HandleFunc("/changelog", func(w http.ResponseWriter, r *http.Request) {
+ cl, err := parseFile()
+ if err != nil {
+ log.Printf("parsing %s: %v", changelog, err)
+ http.Error(w, "500 - internal server error", http.StatusInternalServerError)
+ return
+ }
+ from := r.URL.Query().Get("from")
+ var fromVersion *updates.Version
+ if from != "" {
+ v, err := updates.ParseVersion(from)
+ if err == nil {
+ fromVersion = &v
+ }
+ }
+ if fromVersion != nil {
+ nextchange:
+ for i, c := range cl.Changes {
+ for _, line := range strings.Split(strings.Split(c.Text, "\n\n")[0], "\n") {
+ if strings.HasPrefix(line, "version:") {
+ v, err := updates.ParseVersion(strings.TrimSpace(strings.TrimPrefix(line, "version:")))
+ if err == nil && !v.After(*fromVersion) {
+ cl.Changes = cl.Changes[:i]
+ break nextchange
+ }
+ }
+ }
+ }
+ }
+
+ // Check if client accepts html. If so, we'll provide a human-readable version.
+ accept := r.Header.Get("Accept")
+ var html bool
+ accept:
+ for _, ac := range strings.Split(accept, ",") {
+ var ok bool
+ for i, kv := range strings.Split(strings.TrimSpace(ac), ";") {
+ if i == 0 {
+ ct := strings.TrimSpace(kv)
+ if strings.EqualFold(ct, "text/html") || strings.EqualFold(ct, "text/*") {
+ ok = true
+ continue
+ }
+ continue accept
+ }
+ t := strings.SplitN(strings.TrimSpace(kv), "=", 2)
+ if !strings.EqualFold(t[0], "q") || len(t) != 2 {
+ continue
+ }
+ switch t[1] {
+ case "0", "0.", "0.0", "0.00", "0.000":
+ ok = false
+ continue accept
+ }
+ break
+ }
+ if ok {
+ html = true
+ break
+ }
+ }
+
+ if html {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ err := updatesTemplate.Execute(w, map[string]any{
+ "FromVersion": fromVersion,
+ "Changes": cl.Changes,
+ })
+ if err != nil && !moxio.IsClosed(err) {
+ log.Printf("writing changelog html: %v", err)
+ }
+ } else {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ if err := json.NewEncoder(w).Encode(cl); err != nil && !moxio.IsClosed(err) {
+ log.Printf("writing changelog json: %v", err)
+ }
+ }
+ })
+ log.Printf("listening on %s", address)
+ log.Fatalln(http.ListenAndServe(address, srv))
+}