fix(upload): clean up S3 objects when attachments are deleted
- Add Delete/DeleteKeys/KeyFromURL methods to S3Storage - DeleteAttachment handler now removes the S3 object after DB delete - DeleteComment collects attachment URLs before CASCADE, then cleans S3 - DeleteIssue collects all attachment URLs (issue + comment level) before CASCADE, then cleans S3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c76a0b905
commit
acba0b8139
6 changed files with 131 additions and 0 deletions
|
|
@ -342,11 +342,16 @@ func (h *Handler) DeleteComment(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Collect attachment URLs before CASCADE delete removes them.
|
||||
attachmentURLs, _ := h.Queries.ListAttachmentURLsByCommentID(r.Context(), parseUUID(commentId))
|
||||
|
||||
if err := h.Queries.DeleteComment(r.Context(), parseUUID(commentId)); err != nil {
|
||||
slog.Warn("delete comment failed", append(logger.RequestAttrs(r), "error", err, "comment_id", commentId)...)
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
h.deleteS3Objects(r.Context(), attachmentURLs)
|
||||
slog.Info("comment deleted", append(logger.RequestAttrs(r), "comment_id", commentId, "issue_id", uuidToString(comment.IssueID))...)
|
||||
h.publish(protocol.EventCommentDeleted, workspaceID, actorType, actorID, map[string]any{
|
||||
"comment_id": commentId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
|
@ -292,5 +293,26 @@ func (h *Handler) DeleteAttachment(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
h.deleteS3Object(r.Context(), att.Url)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// deleteS3Object removes a single file from S3 by its CDN URL.
|
||||
func (h *Handler) deleteS3Object(ctx context.Context, url string) {
|
||||
if h.Storage == nil || url == "" {
|
||||
return
|
||||
}
|
||||
h.Storage.Delete(ctx, h.Storage.KeyFromURL(url))
|
||||
}
|
||||
|
||||
// deleteS3Objects removes multiple files from S3 by their CDN URLs.
|
||||
func (h *Handler) deleteS3Objects(ctx context.Context, urls []string) {
|
||||
if h.Storage == nil || len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
keys := make([]string, len(urls))
|
||||
for i, u := range urls {
|
||||
keys[i] = h.Storage.KeyFromURL(u)
|
||||
}
|
||||
h.Storage.DeleteKeys(ctx, keys)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -548,12 +548,16 @@ func (h *Handler) DeleteIssue(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
h.TaskService.CancelTasksForIssue(r.Context(), issue.ID)
|
||||
|
||||
// Collect all attachment URLs (issue-level + comment-level) before CASCADE delete.
|
||||
attachmentURLs, _ := h.Queries.ListAttachmentURLsByIssueOrComments(r.Context(), issue.ID)
|
||||
|
||||
err := h.Queries.DeleteIssue(r.Context(), parseUUID(id))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete issue")
|
||||
return
|
||||
}
|
||||
|
||||
h.deleteS3Objects(r.Context(), attachmentURLs)
|
||||
userID := requestUserID(r)
|
||||
actorType, actorID := h.resolveActor(r, userID, uuidToString(issue.WorkspaceID))
|
||||
h.publish(protocol.EventIssueDeleted, uuidToString(issue.WorkspaceID), actorType, actorID, map[string]any{"issue_id": id})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue