mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on retention policy and cron system.
This commit is contained in:
@@ -3,6 +3,8 @@ import {enforceRetentionCount} from "@/lib/tasks/database/retention-count";
|
||||
import {enforceRetentionDays} from "@/lib/tasks/database/retention-days";
|
||||
import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf";
|
||||
import {retentionPolicy} from "@/db/schema/07_database";
|
||||
import {eq, isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
|
||||
export const retentionCleanTask = async () => {
|
||||
@@ -10,13 +12,16 @@ export const retentionCleanTask = async () => {
|
||||
const databases = await db.query.database.findMany({
|
||||
with: {
|
||||
retentionPolicy: true,
|
||||
backups: true,
|
||||
backups: {
|
||||
where: isNull(drizzleDb.schemas.backup.deletedAt),
|
||||
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(databases);
|
||||
|
||||
for (const db of databases) {
|
||||
if (!db.retentionPolicy) continue; // no policy = skip
|
||||
|
||||
await enforceRetention(db.id, db.retentionPolicy);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -29,6 +34,8 @@ export async function enforceRetention(
|
||||
databaseId: string,
|
||||
policy: typeof retentionPolicy.$inferSelect
|
||||
) {
|
||||
console.log(`EnforceRetention: ${policy}`);
|
||||
|
||||
switch (policy.type) {
|
||||
case "count":
|
||||
await enforceRetentionCount(databaseId, policy.count ?? 7);
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {desc, eq} from "drizzle-orm";
|
||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||
|
||||
export async function enforceRetentionCount(databaseId: string, count: number) {
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||
with:{
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toDelete = backups.slice(count); // keep first `count`, delete rest
|
||||
|
||||
for (const b of toDelete) {
|
||||
await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
|
||||
// TODO: delete backup file from storage
|
||||
// await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
|
||||
|
||||
const deletion = await deleteBackupCronAction({
|
||||
backupId: b.id,
|
||||
databaseId: b.databaseId,
|
||||
file: b.file!,
|
||||
projectSlug: b.database.project?.slug!
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (deletion.data.success) {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionSuccess.message);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionError.message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,45 @@
|
||||
import {db} from "@/db";
|
||||
import {eq, lt} from "drizzle-orm";
|
||||
import {eq, lt, and, desc} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||
|
||||
export async function enforceRetentionDays(databaseId: string, days: number) {
|
||||
const cutoff = new Date(Date.now() - days * 86400000); // days → ms
|
||||
const cutoff = new Date(Date.now() - days * 86400000);
|
||||
|
||||
await db.delete(drizzleDb.schemas.backup).where(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId) &&
|
||||
lt(drizzleDb.schemas.backup.createdAt, cutoff)
|
||||
);
|
||||
const expiredBackups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
lt(drizzleDb.schemas.backup.createdAt, cutoff)
|
||||
),
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
for (const backup of expiredBackups) {
|
||||
|
||||
const deletion = await deleteBackupCronAction({
|
||||
backupId: backup.id,
|
||||
databaseId: backup.databaseId,
|
||||
file: backup.file!,
|
||||
projectSlug: backup.database.project?.slug!
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (deletion.data.success) {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionSuccess.message);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionError.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Note: files should also be deleted from storage
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {db} from "@/db";
|
||||
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
|
||||
import {eq, desc} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||
|
||||
export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
||||
daily: number;
|
||||
@@ -12,6 +13,13 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
@@ -52,8 +60,26 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
||||
// Delete backups not in `toKeep`
|
||||
for (const b of backups) {
|
||||
if (!toKeep.has(b.id)) {
|
||||
await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
|
||||
// TODO: delete backup file from storage
|
||||
// await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
|
||||
|
||||
|
||||
const deletion = await deleteBackupCronAction({
|
||||
backupId: b.id,
|
||||
databaseId: b.databaseId,
|
||||
file: b.file!,
|
||||
projectSlug: b.database.project?.slug!
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (deletion.data.success) {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionSuccess.message);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
console.log(deletion.data.actionError.message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"use server"
|
||||
import {action, userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Backup} from "@/db/schema/07_database";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {deleteFileS3Private, deleteLocalPrivate} from "@/features/upload/private/upload.action";
|
||||
import {env} from "@/env.mjs";
|
||||
|
||||
|
||||
export const deleteBackupCronAction = action
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
projectSlug: z.string(),
|
||||
file: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "No settings found.",
|
||||
status: 404,
|
||||
cause: "No settings found.",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.backup)
|
||||
.set({
|
||||
deletedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||
|
||||
|
||||
|
||||
let success: boolean, message: string;
|
||||
|
||||
const result =
|
||||
settings.storage === "local"
|
||||
? await deleteLocalPrivate(parsedInput.file)
|
||||
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
||||
|
||||
({success, message} = result);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: message,
|
||||
status: 404,
|
||||
cause: "Unable to delete backup from storage",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// await db
|
||||
// .delete(drizzleDb.schemas.backup)
|
||||
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||
// .execute();
|
||||
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
actionSuccess: {
|
||||
message: "Backup deleted successfully.",
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user