mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
feat: add custom SQL cleanup for scheduled maintenance (#781)
- Add CustomSqlCleanup type to models - Add validateCustomSql and executeCustomSqlCleanup functions - Add SQL validation: DELETE only, single statement, max 1000 chars - Integrate custom SQL cleanup with scheduled job - Add frontend UI with tabs for basic/custom SQL cleanup - Support i18n for English and Chinese 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { CleaningServicesFilled } from '@vicons/material'
|
import { CleaningServicesFilled, AddFilled, DeleteFilled } from '@vicons/material'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const { loading } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const cleanupModel = ref({
|
const cleanupModel = ref({
|
||||||
enableMailsAutoCleanup: false,
|
enableMailsAutoCleanup: false,
|
||||||
@@ -21,6 +23,7 @@ const cleanupModel = ref({
|
|||||||
cleanUnboundAddressDays: 30,
|
cleanUnboundAddressDays: 30,
|
||||||
enableEmptyAddressAutoCleanup: false,
|
enableEmptyAddressAutoCleanup: false,
|
||||||
cleanEmptyAddressDays: 30,
|
cleanEmptyAddressDays: 30,
|
||||||
|
customSqlCleanupList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -37,8 +40,18 @@ const { t } = useI18n({
|
|||||||
cleanupNow: "Cleanup now",
|
cleanupNow: "Cleanup now",
|
||||||
autoCleanup: "Auto cleanup",
|
autoCleanup: "Auto cleanup",
|
||||||
cleanupSuccess: "Cleanup success",
|
cleanupSuccess: "Cleanup success",
|
||||||
|
saveSuccess: "Save success",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
||||||
|
basicCleanup: "Basic Cleanup",
|
||||||
|
customSqlCleanup: "Custom SQL Cleanup",
|
||||||
|
customSqlTip: "Add custom DELETE SQL statements for scheduled cleanup. Only single DELETE statement is allowed per entry.",
|
||||||
|
addCustomSql: "Add Custom SQL",
|
||||||
|
sqlName: "Name",
|
||||||
|
sqlStatement: "SQL Statement (DELETE only)",
|
||||||
|
sqlNamePlaceholder: "e.g. Clean old logs",
|
||||||
|
sqlPlaceholder: "e.g. DELETE FROM raw_mails WHERE source GLOB '*@example.com' AND created_at < datetime('now', '-3 day')",
|
||||||
|
deleteCustomSql: "Delete",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '请输入天数',
|
tip: '请输入天数',
|
||||||
@@ -51,9 +64,19 @@ const { t } = useI18n({
|
|||||||
emptyAddressLabel: "清理 n 天前空邮件的邮箱地址",
|
emptyAddressLabel: "清理 n 天前空邮件的邮箱地址",
|
||||||
autoCleanup: "自动清理",
|
autoCleanup: "自动清理",
|
||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
|
saveSuccess: "保存成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
save: "保存",
|
save: "保存",
|
||||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||||
|
basicCleanup: "基础清理",
|
||||||
|
customSqlCleanup: "自定义 SQL 清理",
|
||||||
|
customSqlTip: "添加自定义 DELETE SQL 语句进行定时清理。每条记录仅允许单条 DELETE 语句。",
|
||||||
|
addCustomSql: "添加自定义 SQL",
|
||||||
|
sqlName: "名称",
|
||||||
|
sqlStatement: "SQL 语句 (仅限 DELETE)",
|
||||||
|
sqlNamePlaceholder: "例如: 清理旧日志",
|
||||||
|
sqlPlaceholder: "例如: DELETE FROM raw_mails WHERE source GLOB '*@example.com' AND created_at < datetime('now', '-3 day')",
|
||||||
|
deleteCustomSql: "删除",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -70,6 +93,22 @@ const cleanup = async (cleanType, cleanDays) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addCustomSql = () => {
|
||||||
|
if (!cleanupModel.value.customSqlCleanupList) {
|
||||||
|
cleanupModel.value.customSqlCleanupList = [];
|
||||||
|
}
|
||||||
|
cleanupModel.value.customSqlCleanupList.push({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '',
|
||||||
|
sql: '',
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCustomSql = (index) => {
|
||||||
|
cleanupModel.value.customSqlCleanupList.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch('/admin/auto_cleanup');
|
const res = await api.fetch('/admin/auto_cleanup');
|
||||||
@@ -85,7 +124,7 @@ const save = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(cleanupModel.value)
|
body: JSON.stringify(cleanupModel.value)
|
||||||
});
|
});
|
||||||
message.success(t('cleanupSuccess'));
|
message.success(t('saveSuccess'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
@@ -108,92 +147,132 @@ onMounted(async () => {
|
|||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-form :model="cleanupModel">
|
<n-tabs type="segment" style="margin-top: 16px;">
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
<n-tab-pane name="basic" :tab="t('basicCleanup')">
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
<n-form :model="cleanupModel">
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('mailBoxLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('mailUnknowLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('mailUnknowLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('sendBoxLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('sendBoxLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('addressCreateLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('addressCreateLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanAddressDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('addressCreated', cleanupModel.cleanAddressDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanAddressDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('addressCreated', cleanupModel.cleanAddressDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('inactiveAddressLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableInactiveAddressAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('inactiveAddressLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableInactiveAddressAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanInactiveAddressDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('inactiveAddress', cleanupModel.cleanInactiveAddressDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanInactiveAddressDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('inactiveAddress', cleanupModel.cleanInactiveAddressDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('unboundAddressLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('unboundAddressLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
<n-form-item-row :label="t('emptyAddressLabel')">
|
</n-button>
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableEmptyAddressAutoCleanup">
|
</n-form-item-row>
|
||||||
{{ t('autoCleanup') }}
|
<n-form-item-row :label="t('emptyAddressLabel')">
|
||||||
</n-checkbox>
|
<n-checkbox v-model:checked="cleanupModel.enableEmptyAddressAutoCleanup">
|
||||||
<n-input-number v-model:value="cleanupModel.cleanEmptyAddressDays" :placeholder="t('tip')" />
|
{{ t('autoCleanup') }}
|
||||||
<n-button @click="cleanup('emptyAddress', cleanupModel.cleanEmptyAddressDays)">
|
</n-checkbox>
|
||||||
<template #icon>
|
<n-input-number v-model:value="cleanupModel.cleanEmptyAddressDays" :placeholder="t('tip')" />
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
<n-button @click="cleanup('emptyAddress', cleanupModel.cleanEmptyAddressDays)">
|
||||||
</template>
|
<template #icon>
|
||||||
{{ t('cleanupNow') }}
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
</n-button>
|
</template>
|
||||||
</n-form-item-row>
|
{{ t('cleanupNow') }}
|
||||||
</n-form>
|
</n-button>
|
||||||
|
</n-form-item-row>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="custom_sql" :tab="t('customSqlCleanup')">
|
||||||
|
<n-alert :show-icon="false" :bordered="false" type="info" style="margin-bottom: 16px;">
|
||||||
|
<span>{{ t('customSqlTip') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<n-space vertical>
|
||||||
|
<n-card v-for="(item, index) in cleanupModel.customSqlCleanupList" :key="item.id" size="small">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-checkbox v-model:checked="item.enabled">
|
||||||
|
{{ t('autoCleanup') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input v-model:value="item.name" :placeholder="t('sqlNamePlaceholder')" style="width: 200px;" />
|
||||||
|
<n-button @click="removeCustomSql(index)" type="error" quaternary>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="DeleteFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('deleteCustomSql') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<n-input
|
||||||
|
v-model:value="item.sql"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="t('sqlPlaceholder')"
|
||||||
|
:autosize="{ minRows: 2 }"
|
||||||
|
class="sql-input"
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
<n-button @click="addCustomSql">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('addCustomSql') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -214,8 +293,7 @@ onMounted(async () => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.sql-input {
|
||||||
display: flex;
|
text-align: left;
|
||||||
margin: 10px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,78 @@ import { Context } from 'hono';
|
|||||||
import { cleanup } from '../common';
|
import { cleanup } from '../common';
|
||||||
import { CONSTANTS } from '../constants';
|
import { CONSTANTS } from '../constants';
|
||||||
import { getJsonSetting, saveSetting } from '../utils';
|
import { getJsonSetting, saveSetting } from '../utils';
|
||||||
import { CleanupSettings } from '../models';
|
import { CleanupSettings, CustomSqlCleanup } from '../models';
|
||||||
|
|
||||||
|
// Normalize SQL: trim and remove trailing semicolon
|
||||||
|
const normalizeSql = (sql: string): string => {
|
||||||
|
let normalized = sql.trim();
|
||||||
|
if (normalized.endsWith(';')) {
|
||||||
|
normalized = normalized.slice(0, -1).trim();
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate custom SQL cleanup statement
|
||||||
|
export const validateCustomSql = (sql: string): { valid: boolean; error?: string } => {
|
||||||
|
if (!sql || !sql.trim()) {
|
||||||
|
return { valid: false, error: "SQL statement is empty" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedSql = normalizeSql(sql);
|
||||||
|
|
||||||
|
// Check SQL length (max 1000 characters)
|
||||||
|
if (trimmedSql.length > 1000) {
|
||||||
|
return { valid: false, error: "SQL statement is too long (max 1000 characters)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlUpper = trimmedSql.toUpperCase();
|
||||||
|
|
||||||
|
// Only allow DELETE statements
|
||||||
|
if (!sqlUpper.startsWith('DELETE ')) {
|
||||||
|
return { valid: false, error: "Only DELETE statements are allowed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow single statement (no semicolons after trimming)
|
||||||
|
if (trimmedSql.includes(';')) {
|
||||||
|
return { valid: false, error: "Only single SQL statement is allowed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forbid SQL comments
|
||||||
|
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
|
||||||
|
return { valid: false, error: "SQL comments are not allowed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute custom SQL cleanup
|
||||||
|
export const executeCustomSqlCleanup = async (
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
customSql: CustomSqlCleanup
|
||||||
|
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
|
||||||
|
if (!customSql || !customSql.sql) {
|
||||||
|
return { success: false, error: "Invalid custom SQL cleanup config" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = validateCustomSql(customSql.sql);
|
||||||
|
if (!validation.valid) {
|
||||||
|
return { success: false, error: validation.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = normalizeSql(customSql.sql);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Executing custom SQL cleanup [${customSql.name}]: ${sql}`);
|
||||||
|
const result = await c.env.DB.prepare(sql).run();
|
||||||
|
const rowsAffected = result.meta?.changes ?? 0;
|
||||||
|
console.log(`Custom SQL cleanup [${customSql.name}] completed, rows affected: ${rowsAffected}`);
|
||||||
|
return { success: true, rowsAffected };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message || "Unknown error";
|
||||||
|
console.error(`Custom SQL cleanup [${customSql.name}] failed:`, errorMessage);
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
cleanup: async (c: Context<HonoCustomType>) => {
|
cleanup: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -22,6 +93,19 @@ export default {
|
|||||||
},
|
},
|
||||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||||
const cleanupSetting = await c.req.json<CleanupSettings>();
|
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||||
|
|
||||||
|
// Validate custom SQL cleanup list
|
||||||
|
if (cleanupSetting.customSqlCleanupList && cleanupSetting.customSqlCleanupList.length > 0) {
|
||||||
|
for (const customSql of cleanupSetting.customSqlCleanupList) {
|
||||||
|
if (customSql.sql) {
|
||||||
|
const validation = validateCustomSql(customSql.sql);
|
||||||
|
if (!validation.valid) {
|
||||||
|
return c.text(`Invalid SQL [${customSql.name || 'unnamed'}]: ${validation.error}`, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ export type WebhookMail = {
|
|||||||
parsedHtml: string;
|
parsedHtml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CustomSqlCleanup = {
|
||||||
|
id: string; // Unique identifier
|
||||||
|
name: string; // Cleanup task name
|
||||||
|
sql: string; // Custom SQL statement (DELETE only)
|
||||||
|
enabled: boolean; // Whether to enable auto cleanup
|
||||||
|
}
|
||||||
|
|
||||||
export type CleanupSettings = {
|
export type CleanupSettings = {
|
||||||
|
|
||||||
enableMailsAutoCleanup: boolean | undefined;
|
enableMailsAutoCleanup: boolean | undefined;
|
||||||
@@ -50,6 +57,7 @@ export type CleanupSettings = {
|
|||||||
cleanUnboundAddressDays: number;
|
cleanUnboundAddressDays: number;
|
||||||
enableEmptyAddressAutoCleanup: boolean | undefined;
|
enableEmptyAddressAutoCleanup: boolean | undefined;
|
||||||
cleanEmptyAddressDays: number;
|
cleanEmptyAddressDays: number;
|
||||||
|
customSqlCleanupList: CustomSqlCleanup[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoData {
|
export class GeoData {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cleanup } from './common'
|
|||||||
import { CONSTANTS } from './constants'
|
import { CONSTANTS } from './constants'
|
||||||
import { getJsonSetting } from './utils';
|
import { getJsonSetting } from './utils';
|
||||||
import { CleanupSettings } from './models';
|
import { CleanupSettings } from './models';
|
||||||
|
import { executeCustomSqlCleanup } from './admin_api/cleanup_api';
|
||||||
|
|
||||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||||
console.log("Scheduled event: ", event);
|
console.log("Scheduled event: ", event);
|
||||||
@@ -64,4 +65,18 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
|
|||||||
autoCleanupSetting.cleanEmptyAddressDays
|
autoCleanupSetting.cleanEmptyAddressDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Execute custom SQL cleanup tasks
|
||||||
|
if (autoCleanupSetting.customSqlCleanupList && autoCleanupSetting.customSqlCleanupList.length > 0) {
|
||||||
|
for (const customSql of autoCleanupSetting.customSqlCleanupList) {
|
||||||
|
if (customSql.enabled && customSql.sql) {
|
||||||
|
const result = await executeCustomSqlCleanup(
|
||||||
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
|
customSql
|
||||||
|
);
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`Custom SQL cleanup [${customSql.name}] failed: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user