summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-14 10:17:55 +0300
committerPaul Buetow <paul@buetow.org>2026-04-14 10:17:55 +0300
commitf4ed08916ef727b5caafd207ba4ef9f406a7d86e (patch)
tree7d21132a3215e83946308a365f3e8015e34b569a
parent7374be02a89cad4d0df4460a98b42764d191ad92 (diff)
docs,test: HTTP upload API and curl auth example (ask 23)
Document daemon plain-HTTP mode, PUT /upload paths for five uptimed files, Bearer token header, and env flags. Add table-driven test asserting each kind maps to the expected filename in stats-dir. Made-with: Cursor
-rw-r--r--README.md56
-rw-r--r--internal/daemon/daemon_test.go38
2 files changed, 94 insertions, 0 deletions
diff --git a/README.md b/README.md
index 4841a6b..40b92d0 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,62 @@ go build -o goprecords ./cmd/goprecords
./goprecords -stats-dir=~/git/uprecords/stats -all -stats-order="Host:Uptime,Host:Boots"
```
+### Daemon mode (plain HTTP)
+
+Run an in-process HTTP server (no TLS here; terminate TLS in a reverse proxy or load balancer if clients need HTTPS):
+
+```bash
+goprecords --daemon -stats-dir="$HOME/git/uprecords/stats" -listen=":8080"
+```
+
+- **`-stats-dir`** (required, or set **`GOPRECORDS_STATS_DIR`**): uptimed stats directory (same layout as the CLI: per-host files like `HOST.txt`, `HOST.records`, …).
+- **`-listen`** (default `:8080`, or **`GOPRECORDS_LISTEN`**): TCP address for the server.
+- **`-auth-db`**: SQLite file for upload API keys (default: `<stats-dir>/goprecords-auth.db`).
+
+Endpoints include **`GET /health`**, **`GET /report?...`** (same query parameters as **`query`**), and uploads under **`PUT /upload/{HOSTNAME}/{kind}`**. The path segment **`kind`** selects which file is written in the stats directory:
+
+| `kind` in URL | File created |
+|-----------------|---------------------|
+| `txt` | `HOSTNAME.txt` |
+| `cur.txt` | `HOSTNAME.cur.txt` |
+| `records` | `HOSTNAME.records` |
+| `os.txt` | `HOSTNAME.os.txt` |
+| `cpuinfo.txt` | `HOSTNAME.cpuinfo.txt` |
+
+When the auth database contains at least one client key, each upload must include a Bearer token that matches the hostname. Create a key (prints the secret once):
+
+```bash
+goprecords --create-client-key myhost -stats-dir="$HOME/git/uprecords/stats"
+```
+
+Example **`curl`** uploads with the token in the **`Authorization`** header:
+
+```bash
+TOKEN="…" # output of --create-client-key
+
+curl -fsS -X PUT --data-binary @./myhost.records \
+ -H "Authorization: Bearer $TOKEN" \
+ "http://127.0.0.1:8080/upload/myhost/records"
+
+curl -fsS -X PUT --data-binary @./myhost.txt \
+ -H "Authorization: Bearer $TOKEN" \
+ "http://127.0.0.1:8080/upload/myhost/txt"
+
+curl -fsS -X PUT --data-binary @./myhost.cur.txt \
+ -H "Authorization: Bearer $TOKEN" \
+ "http://127.0.0.1:8080/upload/myhost/cur.txt"
+
+curl -fsS -X PUT --data-binary @./myhost.os.txt \
+ -H "Authorization: Bearer $TOKEN" \
+ "http://127.0.0.1:8080/upload/myhost/os.txt"
+
+curl -fsS -X PUT --data-binary @./myhost.cpuinfo.txt \
+ -H "Authorization: Bearer $TOKEN" \
+ "http://127.0.0.1:8080/upload/myhost/cpuinfo.txt"
+```
+
+If there are **no** keys in the auth database, uploads are accepted without **`Authorization`** (useful for local testing only).
+
## Test
Run the test subcommand (fixture comparison and import/query parity):
diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go
index d4133f2..404ff45 100644
--- a/internal/daemon/daemon_test.go
+++ b/internal/daemon/daemon_test.go
@@ -317,3 +317,41 @@ func TestUploadBadKind(t *testing.T) {
t.Fatalf("status %d", res.StatusCode)
}
}
+
+func TestUploadAllKindsWriteExpectedFiles(t *testing.T) {
+ statsDir := t.TempDir()
+ srv := httptest.NewServer(Handler(statsDir))
+ defer srv.Close()
+ cases := []struct {
+ kind string
+ wantName string
+ body string
+ }{
+ {"txt", "myhost.txt", "a"},
+ {"cur.txt", "myhost.cur.txt", "b"},
+ {"records", "myhost.records", "c"},
+ {"os.txt", "myhost.os.txt", "d"},
+ {"cpuinfo.txt", "myhost.cpuinfo.txt", "e"},
+ }
+ for _, tc := range cases {
+ t.Run(tc.kind, func(t *testing.T) {
+ url := srv.URL + "/upload/myhost/" + tc.kind
+ req, _ := http.NewRequest(http.MethodPut, url, strings.NewReader(tc.body))
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ if res.StatusCode != http.StatusNoContent {
+ t.Fatalf("status %d", res.StatusCode)
+ }
+ b, err := os.ReadFile(filepath.Join(statsDir, tc.wantName))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(b) != tc.body {
+ t.Fatalf("file %s: got %q want %q", tc.wantName, b, tc.body)
+ }
+ })
+ }
+}