check / check (push) Waiting to run
Each database target on the webhook page links to /hook/ID/targets/TARGETID/download, which streams the target's archive as archive-WEBHOOK-TARGET-YYYYMMDDTHHMMSSZ.json.gz: the webhook, the target, exported_at, and archived_events, one object per row keyed by column, a body that is not valid UTF-8 in base64 with body_encoding. The export reads one row at a time on its own connection inside a read-only transaction: one snapshot, and no write lock. The handler holds the rename lock only while it reads the names and opens the file. A missing archive exports empty and is not created. Model: opus-5-5
83 lines
2.3 KiB
Go
83 lines
2.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// HandleTargetDownload serves a database target's archive as one
|
|
// gzipped JSON file, named for the webhook, the target and the time;
|
|
// see delivery.ArchiveExport.WriteGzipJSON for what it holds. Other
|
|
// target types have no archive and are a 404.
|
|
func (h *Handlers) HandleTargetDownload() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
webhook, target, export, ok := h.openTargetArchive(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
defer func() { _ = export.Close() }()
|
|
|
|
now := time.Now()
|
|
|
|
w.Header().Set("Content-Type", "application/gzip")
|
|
w.Header().Set(
|
|
"Content-Disposition",
|
|
`attachment; filename="`+delivery.ArchiveExportFileName(
|
|
webhook.Name, target.Name, now,
|
|
)+`"`,
|
|
)
|
|
|
|
err := export.WriteGzipJSON(r.Context(), w, &webhook, target, now)
|
|
if err != nil {
|
|
// The 200 has gone out. The client is left with a file
|
|
// that does not decompress; the log is the record.
|
|
h.log.Error(
|
|
"failed to export archive",
|
|
"target_id", target.ID,
|
|
"error", err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// openTargetArchive opens the archive of the request's database target
|
|
// for export. It reports false once it has written the response.
|
|
//
|
|
// It holds renameMu, which every archive rename runs under, while it
|
|
// reads the stored names and opens the file, so the file it opens is
|
|
// the one those names give. It lets go before the export is streamed:
|
|
// once the file is open, a rename does not affect the export.
|
|
func (h *Handlers) openTargetArchive(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) (database.Webhook, *database.Target, *delivery.ArchiveExport, bool) {
|
|
h.renameMu.Lock()
|
|
defer h.renameMu.Unlock()
|
|
|
|
webhook, target, ok := h.ownedTarget(w, r)
|
|
if !ok {
|
|
return database.Webhook{}, nil, nil, false
|
|
}
|
|
|
|
if target.Type != database.TargetTypeDatabase {
|
|
h.renderError(w, r, http.StatusNotFound)
|
|
|
|
return database.Webhook{}, nil, nil, false
|
|
}
|
|
|
|
export, err := delivery.OpenArchiveExport(
|
|
r.Context(), delivery.ArchivePath(h.dbMgr, &webhook, target),
|
|
)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to open archive for export", err)
|
|
|
|
return database.Webhook{}, nil, nil, false
|
|
}
|
|
|
|
return webhook, target, export, true
|
|
}
|