mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-05 16:02:48 +00:00
feat: delete the storage objects of a step's attachments when the step is deleted, since the attachment rows cascade away with the sequence and left their bytes counted against the organization's storage quota with no row left to reach them
This commit is contained in:
@@ -1332,6 +1332,11 @@ func main() {
|
||||
if aware, ok := campaignService.(campaign.AttachmentAware); ok {
|
||||
aware.WireAttachments(attachmentRepoForHandler, s3ForHandler)
|
||||
}
|
||||
// Deleting a step cascades its attachment rows away, so the sequence
|
||||
// service needs the same store to drop the objects behind them.
|
||||
if aware, ok := sequenceService.(sequence.AttachmentAware); ok {
|
||||
aware.WireAttachments(attachmentRepoForHandler, s3ForHandler)
|
||||
}
|
||||
// Attaching a lead to a running campaign has to wake that campaign's
|
||||
// parked send chain, or the lead sits queued until the chain's next
|
||||
// tick. Wired here because contactService is built before the scheduler
|
||||
|
||||
@@ -2,6 +2,10 @@ package sequence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/warmbly/warmbly/internal/errx"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
@@ -29,6 +33,51 @@ func (s *sequenceService) UpdateLayout(ctx context.Context, userID, campaignID s
|
||||
return s.sequenceRepository.UpdateLayout(ctx, userID, campaignID, positions)
|
||||
}
|
||||
|
||||
// Delete removes a step. Its attachment rows go with it through the cascade,
|
||||
// so the objects behind them are listed first and dropped once the delete has
|
||||
// committed — otherwise the bytes stay in storage against the org's quota with
|
||||
// no row left to reach them.
|
||||
func (s *sequenceService) Delete(ctx context.Context, userID, campaignID, sequenceID string) *errx.Error {
|
||||
return s.sequenceRepository.Delete(ctx, userID, campaignID, sequenceID)
|
||||
keys := s.stepObjectKeys(ctx, campaignID, sequenceID)
|
||||
|
||||
if xerr := s.sequenceRepository.Delete(ctx, userID, campaignID, sequenceID); xerr != nil {
|
||||
return xerr
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if err := s.storage.Delete(ctx, key); err != nil {
|
||||
sentry.CaptureException(fmt.Errorf("sequence %s delete: object %s: %w", sequenceID, key, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stepObjectKeys lists the storage keys of the files scoped to one step. Best
|
||||
// effort: a step still deletes when they cannot be read, it just leaves its
|
||||
// objects behind rather than refusing the edit.
|
||||
func (s *sequenceService) stepObjectKeys(ctx context.Context, campaignID, sequenceID string) []string {
|
||||
if s.attachmentRepo == nil || s.storage == nil {
|
||||
return nil
|
||||
}
|
||||
cID, err := uuid.Parse(campaignID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
sID, err := uuid.Parse(sequenceID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
atts, err := s.attachmentRepo.ListForStep(ctx, cID, sID)
|
||||
if err != nil {
|
||||
sentry.CaptureException(fmt.Errorf("sequence %s delete: list attachments: %w", sequenceID, err))
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(atts))
|
||||
for _, a := range atts {
|
||||
// ListForStep also returns the campaign-wide files, which outlive the step.
|
||||
if a.SequenceID != nil && *a.SequenceID == sID {
|
||||
keys = append(keys, a.S3Key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/warmbly/warmbly/internal/errx"
|
||||
"github.com/warmbly/warmbly/internal/infrastructure/storage"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
"github.com/warmbly/warmbly/internal/repository"
|
||||
)
|
||||
@@ -18,6 +19,8 @@ type SequenceService interface {
|
||||
|
||||
type sequenceService struct {
|
||||
sequenceRepository repository.SequenceRepository
|
||||
attachmentRepo repository.AttachmentRepository
|
||||
storage storage.Store
|
||||
}
|
||||
|
||||
func NewService(sequenceRepository repository.SequenceRepository) SequenceService {
|
||||
@@ -25,3 +28,15 @@ func NewService(sequenceRepository repository.SequenceRepository) SequenceServic
|
||||
sequenceRepository: sequenceRepository,
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentAware is implemented by the sequence service so main can hand it
|
||||
// the attachment repository and object store. Deleting a step cascades its
|
||||
// attachment rows away, so without these the files scoped to that step leave
|
||||
// their bytes in storage forever, still counted against the org's quota.
|
||||
type AttachmentAware interface {
|
||||
WireAttachments(repo repository.AttachmentRepository, store storage.Store)
|
||||
}
|
||||
|
||||
func (s *sequenceService) WireAttachments(repo repository.AttachmentRepository, store storage.Store) {
|
||||
s.attachmentRepo, s.storage = repo, store
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user