mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-05 22:15:22 +02:00
feat(libsql): add support for libsql database backups and restores
- Updated backup and restore functionalities to include support for the 'libsql' database type. - Enhanced the backup process with new methods for running and restoring libsql backups. - Modified existing components and schemas to accommodate libsql, including updates to the database type enumerations and backup schemas. - Removed obsolete bottomless replication features from the libsql schema. - Updated related UI components to reflect changes in backup handling for libsql.
This commit is contained in:
@@ -65,7 +65,7 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server" | "libsql";
|
||||
|
||||
const Schema = z
|
||||
.object({
|
||||
@@ -77,7 +77,7 @@ const Schema = z
|
||||
keepLatestCount: z.coerce.number().optional(),
|
||||
serviceName: z.string().nullable(),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
metadata: z
|
||||
@@ -209,7 +209,7 @@ export const HandleBackup = ({
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
database: databaseType === "web-server" ? "dokploy" : "",
|
||||
database: databaseType === "web-server" ? "dokploy" : databaseType === "libsql" ? "iku.db" : "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
prefix: "/",
|
||||
@@ -246,7 +246,9 @@ export const HandleBackup = ({
|
||||
? backup?.database
|
||||
: databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: "",
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
destinationId: backup?.destinationId ?? "",
|
||||
enabled: backup?.enabled ?? true,
|
||||
prefix: backup?.prefix ?? "/",
|
||||
@@ -281,6 +283,10 @@ export const HandleBackup = ({
|
||||
? {
|
||||
mongoId: id,
|
||||
}
|
||||
: databaseType === "libsql"
|
||||
? {
|
||||
libsqlId: id,
|
||||
}
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
userId: id,
|
||||
|
||||
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
|
||||
message: "Database name is required",
|
||||
}),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]).default("database"),
|
||||
metadata: z
|
||||
|
||||
@@ -40,7 +40,7 @@ import { RestoreBackup } from "./restore-backup";
|
||||
interface Props {
|
||||
id: string;
|
||||
databaseType?:
|
||||
| Exclude<ServiceType, "application" | "redis" | "libsql">
|
||||
| Exclude<ServiceType, "application" | "redis">
|
||||
| "web-server";
|
||||
backupType?: "database" | "compose";
|
||||
}
|
||||
@@ -63,6 +63,8 @@ export const ShowBackups = ({
|
||||
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
libsql: () =>
|
||||
api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
||||
"web-server": () => api.user.getBackups.useQuery(),
|
||||
}
|
||||
: {
|
||||
@@ -83,6 +85,7 @@ export const ShowBackups = ({
|
||||
mongo: api.backup.manualBackupMongo.useMutation(),
|
||||
mysql: api.backup.manualBackupMySql.useMutation(),
|
||||
postgres: api.backup.manualBackupPostgres.useMutation(),
|
||||
libsql: api.backup.manualBackupLibsql.useMutation(),
|
||||
"web-server": api.backup.manualBackupWebServer.useMutation(),
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
libsqlId: string;
|
||||
enableBottomlessReplication: boolean;
|
||||
bottomlessReplicationDestinationId?: string | null;
|
||||
}
|
||||
|
||||
export const ShowBottomlessReplication = ({
|
||||
libsqlId,
|
||||
enableBottomlessReplication,
|
||||
bottomlessReplicationDestinationId,
|
||||
}: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const switchId = useId();
|
||||
const commandId = useId();
|
||||
const { mutateAsync, isLoading } = api.libsql.update.useMutation();
|
||||
const { data: destinations, isLoading: isLoadingDestinations } =
|
||||
api.destination.all.useQuery();
|
||||
const [isDestinationOpen, setIsDestinationOpen] = useState(false);
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
libsqlId,
|
||||
enableBottomlessReplication: checked,
|
||||
});
|
||||
toast.success("Bottomless replication updated successfully");
|
||||
utils.libsql.one.invalidate({ libsqlId });
|
||||
} catch (error) {
|
||||
toast.error("Error updating bottomless replication");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDestinationSelect = async (destinationId: string | null) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
libsqlId,
|
||||
enableBottomlessReplication:
|
||||
destinationId === null ? false : enableBottomlessReplication,
|
||||
bottomlessReplicationDestinationId: destinationId,
|
||||
});
|
||||
toast.success("Bottomless replication destination updated successfully");
|
||||
utils.libsql.one.invalidate({ libsqlId });
|
||||
setIsDestinationOpen(false);
|
||||
} catch (error) {
|
||||
toast.error("Error updating bottomless replication destination");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Bottomless Replication</CardTitle>
|
||||
<CardDescription>
|
||||
Bottomless replication allows automatically backing up your database
|
||||
to an S3-compatible storage.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<AlertBlock type="warning">
|
||||
The service needs to be restarted for bottomless replication changes
|
||||
to take effect. Please redeploy the service after enabling or
|
||||
disabling this feature.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={switchId} className="text-sm font-medium">
|
||||
Enable Bottomless Replication
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically replicate database changes to S3-compatible storage
|
||||
</p>
|
||||
{!bottomlessReplicationDestinationId && (
|
||||
<p className="text-sm text-orange-600">
|
||||
Select a destination above to enable bottomless replication
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
id={switchId}
|
||||
checked={enableBottomlessReplication}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading || !bottomlessReplicationDestinationId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={commandId} className="text-sm font-medium">
|
||||
Destination
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select the S3-compatible destination for bottomless replication
|
||||
</p>
|
||||
<Popover open={isDestinationOpen} onOpenChange={setIsDestinationOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!bottomlessReplicationDestinationId &&
|
||||
"text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingDestinations
|
||||
? "Loading...."
|
||||
: bottomlessReplicationDestinationId
|
||||
? destinations?.find(
|
||||
(destination) =>
|
||||
destination.destinationId ===
|
||||
bottomlessReplicationDestinationId,
|
||||
)?.name
|
||||
: "Select Destination"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command id={commandId}>
|
||||
<CommandInput
|
||||
placeholder="Search Destination..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingDestinations && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Destinations....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No destinations found.</CommandEmpty>
|
||||
<ScrollArea className="h-64">
|
||||
<CommandGroup>
|
||||
{destinations?.map((destination) => (
|
||||
<CommandItem
|
||||
value={destination.destinationId}
|
||||
key={destination.destinationId}
|
||||
onSelect={() =>
|
||||
handleDestinationSelect(destination.destinationId)
|
||||
}
|
||||
>
|
||||
{destination.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
destination.destinationId ===
|
||||
bottomlessReplicationDestinationId
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => handleDestinationSelect(null)}
|
||||
>
|
||||
None
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
!bottomlessReplicationDestinationId
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -56,7 +56,7 @@ import { api } from "@/utils/api";
|
||||
type DbType = z.infer<typeof mySchema>["type"];
|
||||
|
||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||
libsql: "ghcr.io/tursodatabase/libsql-server:latest",
|
||||
libsql: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||
mongo: "mongo:7",
|
||||
mariadb: "mariadb:11",
|
||||
mysql: "mysql:8",
|
||||
@@ -104,7 +104,7 @@ const mySchema = z
|
||||
type: z.literal("libsql"),
|
||||
dockerImage: z
|
||||
.string()
|
||||
.default("ghcr.io/tursodatabase/libsql-server:latest"),
|
||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||
databaseUser: z.string().default("libsql"),
|
||||
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
||||
sqldPrimaryUrl: z.string().optional(),
|
||||
|
||||
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-inset",
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
7
apps/dokploy/drizzle/0154_tan_living_mummy.sql
Normal file
7
apps/dokploy/drizzle/0154_tan_living_mummy.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
ALTER TYPE "public"."databaseType" ADD VALUE 'libsql';--> statement-breakpoint
|
||||
ALTER TABLE "libsql" DROP CONSTRAINT "libsql_bottomlessReplicationDestinationId_destination_destinationId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD COLUMN "libsqlId" text;--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "libsql" DROP COLUMN "enableBottomlessReplication";--> statement-breakpoint
|
||||
ALTER TABLE "libsql" DROP COLUMN "bottomlessReplicationDestinationId";
|
||||
8138
apps/dokploy/drizzle/meta/0154_snapshot.json
Normal file
8138
apps/dokploy/drizzle/meta/0154_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1079,6 +1079,13 @@
|
||||
"when": 1773940853496,
|
||||
"tag": "0153_even_morlocks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 154,
|
||||
"version": "7",
|
||||
"when": 1773942481550,
|
||||
"tag": "0154_tan_living_mummy",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBottomlessReplication } from "@/components/dashboard/libsql/general/show-bottomless-replication";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { ShowExternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-external-libsql-credentials";
|
||||
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
|
||||
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
|
||||
@@ -276,14 +276,10 @@ const Libsql = (
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBottomlessReplication
|
||||
libsqlId={libsqlId}
|
||||
enableBottomlessReplication={
|
||||
data?.enableBottomlessReplication || false
|
||||
}
|
||||
bottomlessReplicationDestinationId={
|
||||
data?.bottomlessReplicationDestinationId
|
||||
}
|
||||
<ShowBackups
|
||||
id={libsqlId}
|
||||
databaseType="libsql"
|
||||
backupType="database"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
findBackupById,
|
||||
findComposeByBackupId,
|
||||
findComposeById,
|
||||
findLibsqlByBackupId,
|
||||
findLibsqlById,
|
||||
findMariadbByBackupId,
|
||||
findMariadbById,
|
||||
findMongoByBackupId,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
keepLatestNBackups,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runLibsqlBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -36,6 +39,7 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
restoreComposeBackup,
|
||||
restoreLibsqlBackup,
|
||||
restoreMariadbBackup,
|
||||
restoreMongoBackup,
|
||||
restoreMySqlBackup,
|
||||
@@ -82,6 +86,7 @@ export const backupRouter = createTRPCRouter({
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.libsqlId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -103,6 +108,8 @@ export const backupRouter = createTRPCRouter({
|
||||
serverId = backup.mongo.serverId;
|
||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||
serverId = backup.mariadb.serverId;
|
||||
} else if (databaseType === "libsql" && backup.libsql?.serverId) {
|
||||
serverId = backup.libsql.serverId;
|
||||
} else if (
|
||||
backup.backupType === "compose" &&
|
||||
backup.compose?.serverId
|
||||
@@ -154,6 +161,7 @@ export const backupRouter = createTRPCRouter({
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.libsqlId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -173,6 +181,7 @@ export const backupRouter = createTRPCRouter({
|
||||
existing.mysqlId ||
|
||||
existing.mariadbId ||
|
||||
existing.mongoId ||
|
||||
existing.libsqlId ||
|
||||
existing.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -400,6 +409,33 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupLibsql: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.libsqlId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.libsqlId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const libsql = await findLibsqlByBackupId(backup.backupId);
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, libsql?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error running manual Libsql backup ",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -536,6 +572,12 @@ export const backupRouter = createTRPCRouter({
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "libsql") {
|
||||
const libsql = await findLibsqlById(input.databaseId);
|
||||
restoreLibsqlBackup(libsql, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "web-server") {
|
||||
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
||||
queue.push(log);
|
||||
|
||||
Reference in New Issue
Block a user