From 34a6e73fa959f02e12299f8c030b53b88aa5dba6 Mon Sep 17 00:00:00 2001 From: zaunist Date: Mon, 10 Mar 2025 19:28:14 +0800 Subject: [PATCH] feat: clean orphaned attachments --- worker/src/database.ts | 192 +++++++++++++++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 24 deletions(-) diff --git a/worker/src/database.ts b/worker/src/database.ts index 5dea6ba..439e7f8 100644 --- a/worker/src/database.ts +++ b/worker/src/database.ts @@ -132,6 +132,47 @@ export async function deleteMailbox(db: D1Database, address: string): Promise { + try { + // 获取所有孤立附件的ID列表 + const orphanedAttachments = await db.prepare(`SELECT id, is_large FROM attachments a WHERE NOT EXISTS (SELECT 1 FROM emails e WHERE e.id = a.email_id)`).all(); + + if (!orphanedAttachments.results || orphanedAttachments.results.length === 0) { + return 0; + } + + console.log(`找到 ${orphanedAttachments.results.length} 个孤立附件,准备清理...`); + + // 清理每个孤立附件 + for (const attachment of orphanedAttachments.results) { + const attachmentId = attachment.id as string; + const isLarge = !!attachment.is_large; + + // 如果是大型附件,先删除所有分块 + if (isLarge) { + await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id = ?`).bind(attachmentId).run(); + console.log(`已清理孤立附件 ${attachmentId} 的所有分块`); + } + } + + // 删除所有孤立附件记录 + const result = await db.prepare(`DELETE FROM attachments WHERE NOT EXISTS (SELECT 1 FROM emails e WHERE e.id = attachments.email_id)`).run(); + + const deletedCount = result.meta?.changes || 0; + console.log(`已清理 ${deletedCount} 个孤立附件记录`); + + return deletedCount; + } catch (error) { + console.error('清理孤立附件时出错:', error); + return 0; + } +} + /** * 清理过期邮箱 * @param db 数据库实例 @@ -139,11 +180,134 @@ export async function deleteMailbox(db: D1Database, address: string): Promise { const now = getCurrentTimestamp(); + + // 获取过期邮箱的ID列表 + const expiredMailboxes = await db.prepare(`SELECT id FROM mailboxes WHERE expires_at <= ?`).bind(now).all(); + + if (expiredMailboxes.results && expiredMailboxes.results.length > 0) { + const mailboxIds = expiredMailboxes.results.map(row => row.id as string); + + // 记录日志 + console.log(`找到 ${mailboxIds.length} 个过期邮箱,准备清理...`); + + // 由于设置了外键级联删除,删除邮箱时会自动删除相关的邮件和附件 + // 但为了确保附件被正确清理,我们先获取这些邮箱中的所有邮件ID + for (const mailboxId of mailboxIds) { + const emails = await db.prepare(`SELECT id FROM emails WHERE mailbox_id = ?`).bind(mailboxId).all(); + + if (emails.results && emails.results.length > 0) { + const emailIds = emails.results.map(row => row.id as string); + console.log(`邮箱 ${mailboxId} 中有 ${emailIds.length} 封邮件需要清理`); + + // 清理每封邮件的附件 + for (const emailId of emailIds) { + await cleanupAttachments(db, emailId); + } + } + } + } + + // 删除过期邮箱(会级联删除邮件和附件) const result = await db.prepare(`DELETE FROM mailboxes WHERE expires_at <= ?`).bind(now).run(); + // 清理可能存在的孤立附件 + await cleanupOrphanedAttachments(db); + return result.meta?.changes || 0; } +/** + * 清理过期邮件 + * @param db 数据库实例 + * @returns 删除的邮件数量 + */ +export async function cleanupExpiredMails(db: D1Database): Promise { + const now = getCurrentTimestamp(); + const oneDayAgo = now - 24 * 60 * 60; // 24小时前的时间戳(秒) + + // 获取过期邮件的ID列表 + const expiredEmails = await db.prepare(`SELECT id FROM emails WHERE received_at <= ?`).bind(oneDayAgo).all(); + + if (expiredEmails.results && expiredEmails.results.length > 0) { + const emailIds = expiredEmails.results.map(row => row.id as string); + console.log(`找到 ${emailIds.length} 封过期邮件,准备清理...`); + + // 清理每封邮件的附件 + for (const emailId of emailIds) { + await cleanupAttachments(db, emailId); + } + } + + // 删除过期邮件(会级联删除附件) + const result = await db.prepare(`DELETE FROM emails WHERE received_at <= ?`).bind(oneDayAgo).run(); + + // 清理可能存在的孤立附件 + await cleanupOrphanedAttachments(db); + + return result.meta?.changes || 0; +} + +/** + * 清理已被阅读的邮件 + * @param db 数据库实例 + * @returns 删除的邮件数量 + */ +export async function cleanupReadMails(db: D1Database): Promise { + // 获取已读邮件的ID列表 + const readEmails = await db.prepare(`SELECT id FROM emails WHERE is_read = 1`).all(); + + if (readEmails.results && readEmails.results.length > 0) { + const emailIds = readEmails.results.map(row => row.id as string); + console.log(`找到 ${emailIds.length} 封已读邮件,准备清理...`); + + // 清理每封邮件的附件 + for (const emailId of emailIds) { + await cleanupAttachments(db, emailId); + } + } + + // 删除已读邮件(会级联删除附件) + const result = await db.prepare(`DELETE FROM emails WHERE is_read = 1`).run(); + + // 清理可能存在的孤立附件 + await cleanupOrphanedAttachments(db); + + return result.meta?.changes || 0; +} + +/** + * 清理指定邮件的所有附件 + * @param db 数据库实例 + * @param emailId 邮件ID + */ +async function cleanupAttachments(db: D1Database, emailId: string): Promise { + try { + // 获取邮件的所有附件 + const attachments = await db.prepare(`SELECT id, is_large FROM attachments WHERE email_id = ?`).bind(emailId).all(); + + if (attachments.results && attachments.results.length > 0) { + console.log(`邮件 ${emailId} 有 ${attachments.results.length} 个附件需要清理`); + + for (const attachment of attachments.results) { + const attachmentId = attachment.id as string; + const isLarge = !!attachment.is_large; + + // 如果是大型附件,先删除所有分块 + if (isLarge) { + await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id = ?`).bind(attachmentId).run(); + console.log(`已清理附件 ${attachmentId} 的所有分块`); + } + } + + // 删除所有附件记录 + await db.prepare(`DELETE FROM attachments WHERE email_id = ?`).bind(emailId).run(); + console.log(`已清理邮件 ${emailId} 的所有附件`); + } + } catch (error) { + console.error(`清理邮件 ${emailId} 的附件时出错:`, error); + } +} + /** * 保存邮件 * @param db 数据库实例 @@ -395,29 +559,9 @@ async function getAttachmentContent(db: D1Database, attachmentId: string, chunks * @param id 邮件ID */ export async function deleteEmail(db: D1Database, id: string): Promise { + // 先清理邮件的所有附件 + await cleanupAttachments(db, id); + + // 然后删除邮件 await db.prepare(`DELETE FROM emails WHERE id = ?`).bind(id).run(); -} - -/** - * 清理过期邮件 - * @param db 数据库实例 - * @returns 删除的邮件数量 - */ -export async function cleanupExpiredMails(db: D1Database): Promise { - const now = getCurrentTimestamp(); - const oneDayAgo = now - 24 * 60 * 60; // 24小时前的时间戳(秒) - const result = await db.prepare(`DELETE FROM emails WHERE received_at <= ?`).bind(oneDayAgo).run(); - - return result.meta?.changes || 0; -} - -/** - * 清理已被阅读的邮件 - * @param db 数据库实例 - * @returns 删除的邮件数量 - */ -export async function cleanupReadMails(db: D1Database): Promise { - const result = await db.prepare(`DELETE FROM emails WHERE is_read = 1`).run(); - - return result.meta?.changes || 0; } \ No newline at end of file