mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Implement metadata handling for database and compose backups. Update backup schemas to include metadata fields for various database types. Enhance backup creation and update processes to accommodate new metadata requirements. Modify UI components to support metadata input for different database types during backup operations.
This commit is contained in:
@@ -58,7 +58,36 @@ import { z } from "zod";
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
const AddPostgresBackup1Schema = z.object({
|
||||
const getMetadataSchema = (
|
||||
backupType: "database" | "compose",
|
||||
databaseType: Props["databaseType"],
|
||||
) => {
|
||||
if (backupType !== "compose") return z.object({}).optional();
|
||||
|
||||
const schemas = {
|
||||
postgres: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
}),
|
||||
mariadb: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
databasePassword: z.string().min(1, "Database password is required"),
|
||||
}),
|
||||
mongo: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
databasePassword: z.string().min(1, "Database password is required"),
|
||||
}),
|
||||
mysql: z.object({
|
||||
databaseRootPassword: z.string().min(1, "Root password is required"),
|
||||
}),
|
||||
"web-server": z.object({}),
|
||||
};
|
||||
|
||||
return z.object({
|
||||
[databaseType]: schemas[databaseType],
|
||||
});
|
||||
};
|
||||
|
||||
const Schema = z.object({
|
||||
destinationId: z.string().min(1, "Destination required"),
|
||||
schedule: z.string().min(1, "Schedule (Cron) required"),
|
||||
prefix: z.string().min(1, "Prefix required"),
|
||||
@@ -68,7 +97,7 @@ const AddPostgresBackup1Schema = z.object({
|
||||
serviceName: z.string().nullable(),
|
||||
});
|
||||
|
||||
type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -89,7 +118,11 @@ export const AddBackup = ({
|
||||
const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
|
||||
api.backup.create.useMutation();
|
||||
|
||||
const form = useForm<AddPostgresBackup>({
|
||||
const schema = Schema.extend({
|
||||
metadata: getMetadataSchema(backupType, databaseType),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
database: "",
|
||||
destinationId: "",
|
||||
@@ -98,8 +131,9 @@ export const AddBackup = ({
|
||||
schedule: "",
|
||||
keepLatestCount: undefined,
|
||||
serviceName: null,
|
||||
metadata: {},
|
||||
},
|
||||
resolver: zodResolver(AddPostgresBackup1Schema),
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -128,10 +162,11 @@ export const AddBackup = ({
|
||||
schedule: "",
|
||||
keepLatestCount: undefined,
|
||||
serviceName: null,
|
||||
metadata: {},
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]);
|
||||
|
||||
const onSubmit = async (data: AddPostgresBackup) => {
|
||||
const onSubmit = async (data: Schema) => {
|
||||
if (backupType === "compose" && !data.serviceName) {
|
||||
form.setError("serviceName", {
|
||||
type: "manual",
|
||||
@@ -489,6 +524,115 @@ export const AddBackup = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{backupType === "compose" && (
|
||||
<>
|
||||
{databaseType === "postgres" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.postgres.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="postgres" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{databaseType === "mariadb" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mariadb.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mariadb" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mariadb.databasePassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{databaseType === "mongo" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mongo.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mongo" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mongo.databasePassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{databaseType === "mysql" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mysql.databaseRootPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Root Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -61,21 +61,24 @@ export const ShowBackups = ({
|
||||
? query()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
|
||||
console.log(postgres);
|
||||
const mutationMap =
|
||||
backupType === "database"
|
||||
? {
|
||||
postgres: api.backup.manualBackupPostgres.useMutation(),
|
||||
mysql: api.backup.manualBackupMySql.useMutation(),
|
||||
mariadb: api.backup.manualBackupMariadb.useMutation(),
|
||||
mongo: api.backup.manualBackupMongo.useMutation(),
|
||||
"web-server": api.backup.manualBackupWebServer.useMutation(),
|
||||
}
|
||||
: {
|
||||
compose: api.backup.manualBackupCompose.useMutation(),
|
||||
};
|
||||
|
||||
const mutationMap = {
|
||||
postgres: () => api.backup.manualBackupPostgres.useMutation(),
|
||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||
compose: () => api.backup.manualBackupCompose.useMutation(),
|
||||
};
|
||||
const key2 = backupType === "database" ? databaseType : "compose";
|
||||
const mutation = mutationMap[key2 as keyof typeof mutationMap];
|
||||
|
||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||
databaseType
|
||||
]
|
||||
? mutationMap[databaseType]()
|
||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutation
|
||||
? mutation
|
||||
: api.backup.manualBackupMongo.useMutation();
|
||||
|
||||
const { mutateAsync: deleteBackup, isLoading: isRemoving } =
|
||||
|
||||
@@ -63,7 +63,36 @@ import { z } from "zod";
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
const UpdateBackupSchema = z.object({
|
||||
const getMetadataSchema = (
|
||||
backupType: "database" | "compose",
|
||||
databaseType: string,
|
||||
) => {
|
||||
if (backupType !== "compose") return z.object({}).optional();
|
||||
|
||||
const schemas = {
|
||||
postgres: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
}),
|
||||
mariadb: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
databasePassword: z.string().min(1, "Database password is required"),
|
||||
}),
|
||||
mongo: z.object({
|
||||
databaseUser: z.string().min(1, "Database user is required"),
|
||||
databasePassword: z.string().min(1, "Database password is required"),
|
||||
}),
|
||||
mysql: z.object({
|
||||
databaseRootPassword: z.string().min(1, "Root password is required"),
|
||||
}),
|
||||
"web-server": z.object({}),
|
||||
};
|
||||
|
||||
return z.object({
|
||||
[databaseType]: schemas[databaseType as keyof typeof schemas],
|
||||
});
|
||||
};
|
||||
|
||||
const Schema = z.object({
|
||||
destinationId: z.string().min(1, "Destination required"),
|
||||
schedule: z.string().min(1, "Schedule (Cron) required"),
|
||||
prefix: z.string().min(1, "Prefix required"),
|
||||
@@ -71,10 +100,9 @@ const UpdateBackupSchema = z.object({
|
||||
database: z.string().min(1, "Database required"),
|
||||
keepLatestCount: z.coerce.number().optional(),
|
||||
serviceName: z.string().nullable(),
|
||||
metadata: z.object({}).optional(),
|
||||
});
|
||||
|
||||
type UpdateBackup = z.infer<typeof UpdateBackupSchema>;
|
||||
|
||||
interface Props {
|
||||
backupId: string;
|
||||
refetch: () => void;
|
||||
@@ -114,7 +142,13 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
const { mutateAsync, isLoading: isLoadingUpdate } =
|
||||
api.backup.update.useMutation();
|
||||
|
||||
const form = useForm<UpdateBackup>({
|
||||
const schema = backup
|
||||
? Schema.extend({
|
||||
metadata: getMetadataSchema(backup.backupType, backup.databaseType),
|
||||
})
|
||||
: Schema;
|
||||
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
database: "",
|
||||
destinationId: "",
|
||||
@@ -123,8 +157,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
schedule: "",
|
||||
keepLatestCount: undefined,
|
||||
serviceName: null,
|
||||
metadata: {},
|
||||
},
|
||||
resolver: zodResolver(UpdateBackupSchema),
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,11 +174,12 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
keepLatestCount: backup.keepLatestCount
|
||||
? Number(backup.keepLatestCount)
|
||||
: undefined,
|
||||
metadata: backup.metadata || {},
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, backup]);
|
||||
|
||||
const onSubmit = async (data: UpdateBackup) => {
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
if (backup?.backupType === "compose" && !data.serviceName) {
|
||||
form.setError("serviceName", {
|
||||
type: "manual",
|
||||
@@ -161,6 +197,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
database: data.database,
|
||||
serviceName: data.serviceName,
|
||||
keepLatestCount: data.keepLatestCount as number | null,
|
||||
metadata: data.metadata || {},
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Backup Updated");
|
||||
@@ -473,6 +510,115 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{backup?.backupType === "compose" && (
|
||||
<>
|
||||
{backup.databaseType === "postgres" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.postgres.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="postgres" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{backup.databaseType === "mariadb" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mariadb.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mariadb" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mariadb.databasePassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{backup.databaseType === "mongo" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mongo.databaseUser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database User</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mongo" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mongo.databasePassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{backup.databaseType === "mysql" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata.mysql.databaseRootPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Root Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
1
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
1
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;
|
||||
5454
apps/dokploy/drizzle/meta/0089_snapshot.json
Normal file
5454
apps/dokploy/drizzle/meta/0089_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -624,6 +624,13 @@
|
||||
"when": 1745801614194,
|
||||
"tag": "0088_same_ezekiel",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 89,
|
||||
"version": "7",
|
||||
"when": 1745812150155,
|
||||
"tag": "0089_dazzling_marrow",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IS_CLOUD,
|
||||
createBackup,
|
||||
findBackupById,
|
||||
findComposeByBackupId,
|
||||
findMariadbByBackupId,
|
||||
findMariadbById,
|
||||
findMongoByBackupId,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { runComposeBackup } from "@dokploy/server/utils/backups/compose";
|
||||
import {
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
@@ -240,9 +242,18 @@ export const backupRouter = createTRPCRouter({
|
||||
manualBackupCompose: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
// const backup = await findBackupById(input.backupId);
|
||||
// await runComposeBackup(backup);
|
||||
return true;
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const compose = await findComposeByBackupId(backup.backupId);
|
||||
await runComposeBackup(compose, backup);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error running manual Compose backup ",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupMongo: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
createMount,
|
||||
deleteMount,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findUserById,
|
||||
@@ -268,8 +267,7 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "You are not authorized to get this compose",
|
||||
});
|
||||
}
|
||||
const domains = await findDomainsByComposeId(input.composeId);
|
||||
const composeFile = await addDomainToCompose(compose, domains);
|
||||
const composeFile = await addDomainToCompose(compose);
|
||||
return dump(composeFile, {
|
||||
lineWidth: 1000,
|
||||
});
|
||||
@@ -723,18 +721,4 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackup: protectedProcedure
|
||||
.input(z.object({ composeId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to backup this compose",
|
||||
});
|
||||
}
|
||||
await createBackup({
|
||||
composeId: compose.composeId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user