check / check (push) Canceled after 0s
The SQLite archive files the database target writes are now managed by internal/archive: file names, rotation, the writer of each target, pruning, renames, the gzipped JSON export, the idle sweep, and the rename lock, which was Handlers.renameMu. The database target stays in internal/delivery and writes through archive.Writers; the engine still closes the writers when it stops, so the stop order is unchanged. The handlers take the rename lock from archive.Archives.RenameLock, and rename and evict through archive.Archives. Exported names drop their Archive prefix in the new package. The tests moved with the code; the two that check a delivery's recorded attempt stay in delivery. Model: opus-5-5
171 lines
4.6 KiB
Go
171 lines
4.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// HandleSourceDelete handles webhook deletion.
|
|
func (h *Handlers) HandleSourceDelete() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := h.getUserID(r)
|
|
if !ok {
|
|
http.Redirect(
|
|
w, r, "/pages/login", http.StatusSeeOther,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
sourceID := chi.URLParam(r, "sourceID")
|
|
|
|
var webhook database.Webhook
|
|
|
|
err := h.db.DB().Where(
|
|
"id = ? AND user_id = ?", sourceID, userID,
|
|
).First(&webhook).Error
|
|
if err != nil {
|
|
h.renderError(w, r, http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
h.deleteWebhookResources(w, r, webhook, userID)
|
|
}
|
|
}
|
|
|
|
// The messages deleteWebhookResources logs when a file of the event
|
|
// database cannot be removed: the database file itself, or only a
|
|
// sidecar once the database file is gone.
|
|
const (
|
|
eventDBLeftMsg = "webhook deleted, but its event database file is " +
|
|
"still on disk; remove it by hand"
|
|
sidecarLeftMsg = "webhook deleted and its events are gone, but a " +
|
|
"-wal or -shm sidecar of its event database is " +
|
|
"still on disk; remove it by hand"
|
|
)
|
|
|
|
// deleteWebhookResources soft-deletes config and hard-deletes
|
|
// the per-webhook event database.
|
|
func (h *Handlers) deleteWebhookResources(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
webhook database.Webhook,
|
|
userID string,
|
|
) {
|
|
// The configuration delete commits before the event database
|
|
// is touched. No transaction spans the main database and the
|
|
// filesystem, so one side has to go first: committing the
|
|
// configuration first means a later failure leaves an unused
|
|
// event database file on disk, while removing the event
|
|
// database first would mean a failed commit destroys the
|
|
// history of a webhook that still exists. A leftover file can
|
|
// be removed by hand; deleted history cannot be recovered.
|
|
err := h.commitWebhookDeletion(&webhook)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to delete webhook", err)
|
|
|
|
return
|
|
}
|
|
|
|
h.log.Info(
|
|
"webhook deleted",
|
|
"webhook_id", webhook.ID,
|
|
"user_id", userID,
|
|
)
|
|
|
|
// Release the webhook's archive writers so a deleted
|
|
// webhook's archive writer (and any handle open
|
|
// within its debounce window) does not linger for the
|
|
// process lifetime. The archive file itself is deliberately
|
|
// left on disk; see evictArchiveWriter.
|
|
h.evictArchiveWriter(webhook.ID)
|
|
|
|
err = h.dbMgr.DeleteDB(webhook.ID)
|
|
if err != nil {
|
|
// The configuration is committed, so the webhook is gone,
|
|
// but a file of its event database is still on disk with
|
|
// nothing referencing it. Report the failure rather than
|
|
// redirecting as though everything succeeded: the file
|
|
// needs removing by hand, and the logged error names it.
|
|
// When only a sidecar is left, the events are already
|
|
// gone, and the message must not suggest they survive.
|
|
msg := eventDBLeftMsg
|
|
if errors.Is(err, database.ErrSidecarNotRemoved) {
|
|
msg = sidecarLeftMsg
|
|
}
|
|
|
|
h.serverError(w, r, msg, err)
|
|
|
|
return
|
|
}
|
|
|
|
http.Redirect(
|
|
w, r, withNotice("/hooks", webhookDeleted), http.StatusSeeOther,
|
|
)
|
|
}
|
|
|
|
// commitWebhookDeletion soft-deletes a webhook's entrypoints,
|
|
// targets and the webhook row in one transaction. Every
|
|
// statement is checked and any failure rolls the whole
|
|
// transaction back, so a caller that gets an error knows the
|
|
// configuration is untouched and the event database must be
|
|
// left alone.
|
|
func (h *Handlers) commitWebhookDeletion(
|
|
webhook *database.Webhook,
|
|
) error {
|
|
tx := h.db.DB().Begin()
|
|
if tx.Error != nil {
|
|
return tx.Error
|
|
}
|
|
|
|
err := tx.Where(
|
|
"webhook_id = ?", webhook.ID,
|
|
).Delete(&database.Entrypoint{}).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
|
|
err = tx.Where(
|
|
"webhook_id = ?", webhook.ID,
|
|
).Delete(&database.Target{}).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
|
|
err = tx.Delete(webhook).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
|
|
return tx.Commit().Error
|
|
}
|
|
|
|
// evictArchiveWriter asks the archive writers to drop the cached
|
|
// writers of a webhook's database targets, closing their archive
|
|
// file handles.
|
|
//
|
|
// The archive database files are NOT deleted. Unlike the event
|
|
// database — which is per-webhook working storage and is
|
|
// hard-deleted with the webhook — an archive is explicitly
|
|
// long-term storage that an operator may want to keep or move
|
|
// away for offline retention. Destroying it as a side effect of
|
|
// deleting a webhook would be a surprising and unrecoverable
|
|
// data loss, so the file is left for the operator to handle.
|
|
func (h *Handlers) evictArchiveWriter(webhookID string) {
|
|
if h.archives == nil {
|
|
return
|
|
}
|
|
|
|
h.archives.EvictWebhook(webhookID)
|
|
}
|