Delete attachments

This commit is contained in:
binwiederhier
2026-01-18 16:04:42 -05:00
parent c23d201186
commit ac9cfbfaf4
4 changed files with 123 additions and 16 deletions

View File

@@ -72,11 +72,12 @@ const (
INSERT INTO messages (mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) INSERT INTO messages (mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
deleteScheduledBySequenceIDQuery = `DELETE FROM messages WHERE topic = ? AND sequence_id = ? AND published = 0` selectScheduledMessageIDsBySeqIDQuery = `SELECT mid FROM messages WHERE topic = ? AND sequence_id = ? AND published = 0`
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` deleteScheduledBySequenceIDQuery = `DELETE FROM messages WHERE topic = ? AND sequence_id = ? AND published = 0`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
selectMessagesByIDQuery = ` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = `
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages FROM messages
WHERE mid = ? WHERE mid = ?
@@ -608,12 +609,42 @@ func (c *messageCache) DeleteMessages(ids ...string) error {
return tx.Commit() return tx.Commit()
} }
// DeleteScheduledBySequenceID deletes unpublished (scheduled) messages with the given topic and sequence ID // DeleteScheduledBySequenceID deletes unpublished (scheduled) messages with the given topic and sequence ID.
func (c *messageCache) DeleteScheduledBySequenceID(topic, sequenceID string) error { // It returns the message IDs of the deleted messages, which can be used to clean up attachment files.
func (c *messageCache) DeleteScheduledBySequenceID(topic, sequenceID string) ([]string, error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
_, err := c.db.Exec(deleteScheduledBySequenceIDQuery, topic, sequenceID) tx, err := c.db.Begin()
return err if err != nil {
return nil, err
}
defer tx.Rollback()
// First, get the message IDs of scheduled messages to be deleted
rows, err := tx.Query(selectScheduledMessageIDsBySeqIDQuery, topic, sequenceID)
if err != nil {
return nil, err
}
defer rows.Close()
ids := make([]string, 0)
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
rows.Close() // Close rows before executing delete in same transaction
// Then delete the messages
if _, err := tx.Exec(deleteScheduledBySequenceIDQuery, topic, sequenceID); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return ids, nil
} }
func (c *messageCache) ExpireMessages(topics ...string) error { func (c *messageCache) ExpireMessages(topics ...string) error {

View File

@@ -742,9 +742,11 @@ func testDeleteScheduledBySequenceID(t *testing.T, c *messageCache) {
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(messages)) require.Equal(t, 1, len(messages))
// Delete scheduled message by sequence ID // Delete scheduled message by sequence ID and verify returned IDs
err = c.DeleteScheduledBySequenceID("mytopic", "seq123") deletedIDs, err := c.DeleteScheduledBySequenceID("mytopic", "seq123")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(deletedIDs))
require.Equal(t, "scheduled1", deletedIDs[0])
// Verify scheduled message is deleted // Verify scheduled message is deleted
messages, err = c.Messages("mytopic", sinceAllMessages, true) messages, err = c.Messages("mytopic", sinceAllMessages, true)
@@ -758,13 +760,15 @@ func testDeleteScheduledBySequenceID(t *testing.T, c *messageCache) {
require.Equal(t, 1, len(messages)) require.Equal(t, 1, len(messages))
require.Equal(t, "other scheduled", messages[0].Message) require.Equal(t, "other scheduled", messages[0].Message)
// Deleting non-existent sequence ID should not error // Deleting non-existent sequence ID should return empty list
err = c.DeleteScheduledBySequenceID("mytopic", "nonexistent") deletedIDs, err = c.DeleteScheduledBySequenceID("mytopic", "nonexistent")
require.Nil(t, err) require.Nil(t, err)
require.Empty(t, deletedIDs)
// Deleting published message should not affect it (only deletes unpublished) // Deleting published message should not affect it (only deletes unpublished)
err = c.DeleteScheduledBySequenceID("mytopic", "seq456") deletedIDs, err = c.DeleteScheduledBySequenceID("mytopic", "seq456")
require.Nil(t, err) require.Nil(t, err)
require.Empty(t, deletedIDs)
messages, err = c.Messages("mytopic", sinceAllMessages, true) messages, err = c.Messages("mytopic", sinceAllMessages, true)
require.Nil(t, err) require.Nil(t, err)

View File

@@ -864,9 +864,16 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
} }
if cache { if cache {
// Delete any existing scheduled message with the same sequence ID // Delete any existing scheduled message with the same sequence ID
if err := s.messageCache.DeleteScheduledBySequenceID(t.ID, m.SequenceID); err != nil { deletedIDs, err := s.messageCache.DeleteScheduledBySequenceID(t.ID, m.SequenceID)
if err != nil {
return nil, err return nil, err
} }
// Delete attachment files for deleted scheduled messages
if s.fileCache != nil && len(deletedIDs) > 0 {
if err := s.fileCache.Remove(deletedIDs...); err != nil {
logvrm(v, r, m).Tag(tagPublish).Err(err).Warn("Error removing attachments for deleted scheduled messages")
}
}
logvrm(v, r, m).Tag(tagPublish).Debug("Adding message to cache") logvrm(v, r, m).Tag(tagPublish).Debug("Adding message to cache")
if err := s.messageCache.AddMessage(m); err != nil { if err := s.messageCache.AddMessage(m); err != nil {
return nil, err return nil, err
@@ -963,9 +970,16 @@ func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *
go s.publishToWebPushEndpoints(v, m) go s.publishToWebPushEndpoints(v, m)
} }
// Delete any existing scheduled message with the same sequence ID // Delete any existing scheduled message with the same sequence ID
if err := s.messageCache.DeleteScheduledBySequenceID(t.ID, sequenceID); err != nil { deletedIDs, err := s.messageCache.DeleteScheduledBySequenceID(t.ID, sequenceID)
if err != nil {
return err return err
} }
// Delete attachment files for deleted scheduled messages
if s.fileCache != nil && len(deletedIDs) > 0 {
if err := s.fileCache.Remove(deletedIDs...); err != nil {
logvrm(v, r, m).Tag(tagPublish).Err(err).Warn("Error removing attachments for deleted scheduled messages")
}
}
// Add to message cache // Add to message cache
if err := s.messageCache.AddMessage(m); err != nil { if err := s.messageCache.AddMessage(m); err != nil {
return err return err

View File

@@ -3593,6 +3593,64 @@ func TestServer_UpdateScheduledMessage_TopicScoped(t *testing.T) {
require.Equal(t, "topic2 scheduled", messages[0].Message) require.Equal(t, "topic2 scheduled", messages[0].Message)
} }
func TestServer_UpdateScheduledMessage_WithAttachment(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
// Publish a scheduled message with an attachment
content := util.RandomString(5000) // > 4096 to trigger attachment
response := request(t, s, "PUT", "/mytopic/attach-seq?delay=1h", content, nil)
require.Equal(t, 200, response.Code)
msg1 := toMessage(t, response.Body.String())
require.Equal(t, "attach-seq", msg1.SequenceID)
require.NotNil(t, msg1.Attachment)
// Verify attachment file exists
attachmentFile1 := filepath.Join(s.config.AttachmentCacheDir, msg1.ID)
require.FileExists(t, attachmentFile1)
// Update the scheduled message with a new attachment
newContent := util.RandomString(5000)
response = request(t, s, "PUT", "/mytopic/attach-seq?delay=2h", newContent, nil)
require.Equal(t, 200, response.Code)
msg2 := toMessage(t, response.Body.String())
require.Equal(t, "attach-seq", msg2.SequenceID)
require.NotEqual(t, msg1.ID, msg2.ID)
// Verify old attachment file was deleted
require.NoFileExists(t, attachmentFile1)
// Verify new attachment file exists
attachmentFile2 := filepath.Join(s.config.AttachmentCacheDir, msg2.ID)
require.FileExists(t, attachmentFile2)
}
func TestServer_DeleteScheduledMessage_WithAttachment(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
// Publish a scheduled message with an attachment
content := util.RandomString(5000) // > 4096 to trigger attachment
response := request(t, s, "PUT", "/mytopic/delete-attach-seq?delay=1h", content, nil)
require.Equal(t, 200, response.Code)
msg := toMessage(t, response.Body.String())
require.Equal(t, "delete-attach-seq", msg.SequenceID)
require.NotNil(t, msg.Attachment)
// Verify attachment file exists
attachmentFile := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
require.FileExists(t, attachmentFile)
// Delete the scheduled message
response = request(t, s, "DELETE", "/mytopic/delete-attach-seq", "", nil)
require.Equal(t, 200, response.Code)
deleteMsg := toMessage(t, response.Body.String())
require.Equal(t, "message_delete", deleteMsg.Event)
// Verify attachment file was deleted
require.NoFileExists(t, attachmentFile)
}
func newTestConfig(t *testing.T) *Config { func newTestConfig(t *testing.T) *Config {
conf := NewConfig() conf := NewConfig()
conf.BaseURL = "http://127.0.0.1:12345" conf.BaseURL = "http://127.0.0.1:12345"