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) }