Enhance backup functionality: add support for 'web-server' database type in backup components, update related schemas, and implement new backup procedures. Introduce userId reference in backups schema and adjust UI components to accommodate the new type.

This commit is contained in:
Mauricio Siu 2025-03-29 17:53:57 -06:00
parent 930a03de60
commit 50c8503cf9
14 changed files with 10851 additions and 58 deletions

View File

@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
interface Props {
databaseId: string;
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
refetch: () => void;
}
@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
useEffect(() => {
form.reset({
database: "",
database: databaseType === "web-server" ? "dokploy" : "",
destinationId: "",
enabled: true,
prefix: "/",
@ -112,6 +112,10 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
? {
mongoId: databaseId,
}
: databaseType === "web-server"
? {
userId: databaseId,
}
: undefined;
await createBackup({
@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder={"dokploy"} {...field} />
<Input
disabled={databaseType === "web-server"}
placeholder={"dokploy"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@ -47,8 +47,8 @@ import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props {
databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis">;
serverId: string | null;
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
serverId?: string | null;
}
const RestoreBackupSchema = z.object({

View File

@ -25,7 +25,7 @@ import { UpdateBackup } from "./update-backup";
interface Props {
id: string;
type: Exclude<ServiceType, "application" | "redis">;
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
}
export const ShowBackups = ({ id, type }: Props) => {
const [activeManualBackup, setActiveManualBackup] = useState<
@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
"web-server": () => api.user.getBackups.useQuery(),
};
const { data } = api.destination.all.useQuery();
const { data: postgres, refetch } = queryMap[type]
@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mysql: () => api.backup.manualBackupMySql.useMutation(),
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
mongo: () => api.backup.manualBackupMongo.useMutation(),
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
};
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
@ -73,11 +75,17 @@ export const ShowBackups = ({ id, type }: Props) => {
{postgres && postgres?.backups?.length > 0 && (
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
{type !== "web-server" && (
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
)}
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
serverId={type === "web-server" ? undefined : postgres?.serverId}
/>
</div>
)}
@ -115,7 +123,9 @@ export const ShowBackups = ({ id, type }: Props) => {
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
serverId={
type === "web-server" ? undefined : postgres?.serverId
}
/>
</div>
</div>

View File

@ -0,0 +1,2 @@
ALTER TABLE "backup" ADD COLUMN "userId" text;--> statement-breakpoint
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;

View File

@ -0,0 +1 @@
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -575,6 +575,20 @@
"when": 1743281254393,
"tag": "0081_lovely_mentallo",
"breakpoints": true
},
{
"idx": 82,
"version": "7",
"when": 1743287689974,
"tag": "0082_clean_mandarin",
"breakpoints": true
},
{
"idx": 83,
"version": "7",
"when": 1743288371413,
"tag": "0083_parallel_stranger",
"breakpoints": true
}
]
}

View File

@ -8,56 +8,16 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
const Page = () => {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<WebDomain />
<WebServer />
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
Paid Features
</CardTitle>
<CardDescription>
Enable or disable paid features like monitoring
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-row gap-2 items-center">
<span className="text-sm font-medium text-muted-foreground">
Enable Paid Features:
</span>
<Switch
checked={data?.enablePaidFeatures}
onCheckedChange={() => {
update({
enablePaidFeatures: !data?.enablePaidFeatures,
})
.then(() => {
toast.success(
`Paid features ${
data?.enablePaidFeatures ? "disabled" : "enabled"
} successfully`,
);
refetch();
})
.catch(() => {
toast.error("Error updating paid features");
});
}}
/>
</div>
</CardContent>
{data?.enablePaidFeatures && <SetupMonitoring />}
</div>
</Card> */}
{/* */}
<ShowBackups id={user?.userId ?? ""} type="web-server" />
</div>
</div>
);

View File

@ -19,6 +19,7 @@ import {
findPostgresByBackupId,
findPostgresById,
findServerById,
paths,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@ -85,9 +86,13 @@ export const backupRouter = createTRPCRouter({
}
}
} catch (error) {
console.error(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the Backup",
message:
error instanceof Error
? error.message
: "Error creating the Backup",
cause: error,
});
}
@ -227,6 +232,35 @@ export const backupRouter = createTRPCRouter({
});
}
}),
manualBackupWebServer: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const destination = await findDestinationById(backup.destinationId);
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const { BASE_PATH } = paths();
const tempDir = `${BASE_PATH}`;
try {
const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/${timestamp}.sql`;
console.log("Executing backup command:", postgresCommand);
await execAsync(postgresCommand);
return true;
} finally {
// Keep files for inspection
}
} catch (error) {
console.error("Backup error:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error running manual Web Server backup",
cause: error,
});
}
}),
listBackupFiles: protectedProcedure
.input(
z.object({
@ -301,7 +335,13 @@ export const backupRouter = createTRPCRouter({
.input(
z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
databaseType: z.enum([
"postgres",
"mysql",
"mariadb",
"mongo",
"web-server",
]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),
destinationId: z.string().min(1),

View File

@ -91,6 +91,28 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: {
with: {
backups: {
with: {
destination: true,
},
},
apiKeys: true,
},
},
},
});
return memberResult?.user;
}),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(

View File

@ -15,12 +15,13 @@ import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { users_temp } from "./user";
export const databaseType = pgEnum("databaseType", [
"postgres",
"mariadb",
"mysql",
"mongo",
"web-server",
]);
export const backups = pgTable("backup", {
@ -58,6 +59,7 @@ export const backups = pgTable("backup", {
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => users_temp.id),
});
export const backupsRelations = relations(backups, ({ one }) => ({
@ -81,6 +83,10 @@ export const backupsRelations = relations(backups, ({ one }) => ({
fields: [backups.mongoId],
references: [mongo.mongoId],
}),
user: one(users_temp, {
fields: [backups.userId],
references: [users_temp.id],
}),
}));
const createSchema = createInsertSchema(backups, {
@ -91,11 +97,12 @@ const createSchema = createInsertSchema(backups, {
database: z.string().min(1),
schedule: z.string(),
keepLatestCount: z.number().optional(),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
postgresId: z.string().optional(),
mariadbId: z.string().optional(),
mysqlId: z.string().optional(),
mongoId: z.string().optional(),
userId: z.string().optional(),
});
export const apiCreateBackup = createSchema.pick({
@ -110,6 +117,7 @@ export const apiCreateBackup = createSchema.pick({
postgresId: true,
mongoId: true,
databaseType: true,
userId: true,
});
export const apiFindOneBackup = createSchema

View File

@ -13,6 +13,7 @@ import { z } from "zod";
import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@ -124,6 +125,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
organizations: many(organization),
projects: many(projects),
apiKeys: many(apikey),
backups: many(backups),
}));
const createSchema = createInsertSchema(users_temp, {

View File

@ -23,6 +23,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb.serverId);
} else if (databaseType === "web-server") {
// await runWebServerBackup(user, backup);
await keepLatestNBackups(backup);
}
});
};