check / check (push) Waiting to run
`internal/handlers/source_management.go` had grown to about 2,370 lines holding the webhook, event log, entrypoint and target handlers, so unrelated units had to wait on each other to touch it. It is split, as pure code movement, into files named for what they hold: `webhook_list.go`, `webhook_create.go`, `webhook_detail.go`, `webhook_edit.go`, `webhook_delete.go`, `event_log.go`, `entrypoint.go`, `target_create.go`, `target_delete.go`, `target_toggle.go` and `shared.go`. No function body, signature, comment or behaviour changed. The README's file tree and log-line caveat, and one middleware comment, name the new files. 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 delivery engine's per-webhook archiving state
|
|
// 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 delivery engine to drop the cached
|
|
// archive 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)
|
|
}
|