diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-14 10:17:55 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-14 10:17:55 +0300 |
| commit | f4ed08916ef727b5caafd207ba4ef9f406a7d86e (patch) | |
| tree | 7d21132a3215e83946308a365f3e8015e34b569a | |
| parent | 7374be02a89cad4d0df4460a98b42764d191ad92 (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.md | 56 | ||||
| -rw-r--r-- | internal/daemon/daemon_test.go | 38 |
2 files changed, 94 insertions, 0 deletions
@@ -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) + } + }) + } +} |
