mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: block if already ongoing or waiting
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
import {and, inArray, isNull} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
@@ -14,17 +15,35 @@ const bulkSchema = z.object({
|
|||||||
|
|
||||||
export const bulkBackupAction = userAction
|
export const bulkBackupAction = userAction
|
||||||
.schema(bulkSchema)
|
.schema(bulkSchema)
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<{queued: number}>> => {
|
.action(async ({parsedInput}): Promise<ServerActionResult<{queued: number; skipped: {databaseId: string; reason: string}[]}>> => {
|
||||||
try {
|
try {
|
||||||
const {projectId, databaseIds} = parsedInput;
|
const {projectId, databaseIds} = parsedInput;
|
||||||
await assertDatabasesInOrgProject(projectId, databaseIds);
|
await assertDatabasesInOrgProject(projectId, databaseIds);
|
||||||
await db
|
|
||||||
.insert(drizzleDb.schemas.backup)
|
const activeBackups = await db.query.backup.findMany({
|
||||||
.values(databaseIds.map((databaseId) => ({databaseId, status: "waiting" as const})));
|
where: and(
|
||||||
|
inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
||||||
|
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"]),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||||
|
),
|
||||||
|
columns: {databaseId: true},
|
||||||
|
});
|
||||||
|
const busy = new Set(activeBackups.map((b) => b.databaseId));
|
||||||
|
const toQueue = databaseIds.filter((id) => !busy.has(id));
|
||||||
|
const skipped = [...busy].map((databaseId) => ({databaseId, reason: "backup already in progress"}));
|
||||||
|
|
||||||
|
if (toQueue.length > 0) {
|
||||||
|
await db
|
||||||
|
.insert(drizzleDb.schemas.backup)
|
||||||
|
.values(toQueue.map((databaseId) => ({databaseId, status: "waiting" as const})));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
value: {queued: databaseIds.length},
|
value: {queued: toQueue.length, skipped},
|
||||||
actionSuccess: {message: `Queued ${databaseIds.length} backup(s).`},
|
actionSuccess: {
|
||||||
|
message: `Queued ${toQueue.length} backup(s)${skipped.length ? `, ${skipped.length} skipped` : ""}.`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {and, desc, eq, isNull} from "drizzle-orm";
|
import {and, desc, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
@@ -34,6 +34,7 @@ export type RestorePreviewRow = {
|
|||||||
backupStorageId?: string;
|
backupStorageId?: string;
|
||||||
backupDate?: string;
|
backupDate?: string;
|
||||||
restorable: boolean;
|
restorable: boolean;
|
||||||
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
|
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
|
||||||
@@ -45,6 +46,22 @@ async function resolveLatestRestorable(projectId: string, databaseIds: string[])
|
|||||||
|
|
||||||
const rows: RestorePreviewRow[] = [];
|
const rows: RestorePreviewRow[] = [];
|
||||||
for (const databaseId of databaseIds) {
|
for (const databaseId of databaseIds) {
|
||||||
|
const activeRestore = await db.query.restoration.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
||||||
|
inArray(drizzleDb.schemas.restoration.status, ["waiting", "ongoing"]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (activeRestore) {
|
||||||
|
rows.push({
|
||||||
|
databaseId,
|
||||||
|
name: nameById.get(databaseId) ?? databaseId,
|
||||||
|
restorable: false,
|
||||||
|
reason: "restore already in progress",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||||
@@ -76,7 +93,7 @@ async function resolveLatestRestorable(projectId: string, databaseIds: string[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push(picked ?? {databaseId, name: nameById.get(databaseId) ?? databaseId, restorable: false});
|
rows.push(picked ?? {databaseId, name: nameById.get(databaseId) ?? databaseId, restorable: false, reason: "no successful backup"});
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -117,7 +134,7 @@ export const bulkRestoreLatestAction = userAction
|
|||||||
const restorable = rows.filter((r) => r.restorable);
|
const restorable = rows.filter((r) => r.restorable);
|
||||||
const skipped = rows
|
const skipped = rows
|
||||||
.filter((r) => !r.restorable)
|
.filter((r) => !r.restorable)
|
||||||
.map((r) => ({databaseId: r.databaseId, reason: "no successful backup"}));
|
.map((r) => ({databaseId: r.databaseId, reason: r.reason ?? "no successful backup"}));
|
||||||
|
|
||||||
if (restorable.length > 0) {
|
if (restorable.length > 0) {
|
||||||
await db.insert(drizzleDb.schemas.restoration).values(
|
await db.insert(drizzleDb.schemas.restoration).values(
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const BulkRestoreModal = (props: BulkRestoreModalProps) => {
|
|||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{r.restorable
|
{r.restorable
|
||||||
? <span className="text-green-600">{r.backupDate ? new Date(r.backupDate).toLocaleString() : "latest backup"}</span>
|
? <span className="text-green-600">{r.backupDate ? new Date(r.backupDate).toLocaleString() : "latest backup"}</span>
|
||||||
: <span className="text-amber-500">no successful backup — skipped</span>}
|
: <span className="text-amber-500">{r.reason ?? "no successful backup"} — skipped</span>}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user