mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-12 06:55:19 +00: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 CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server" | "libsql";
|
||||||
|
|
||||||
const Schema = z
|
const Schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -77,7 +77,7 @@ const Schema = z
|
|||||||
keepLatestCount: z.coerce.number().optional(),
|
keepLatestCount: z.coerce.number().optional(),
|
||||||
serviceName: z.string().nullable(),
|
serviceName: z.string().nullable(),
|
||||||
databaseType: z
|
databaseType: z
|
||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]),
|
backupType: z.enum(["database", "compose"]),
|
||||||
metadata: z
|
metadata: z
|
||||||
@@ -209,7 +209,7 @@ export const HandleBackup = ({
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
database: databaseType === "web-server" ? "dokploy" : "",
|
database: databaseType === "web-server" ? "dokploy" : databaseType === "libsql" ? "iku.db" : "",
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
@@ -246,7 +246,9 @@ export const HandleBackup = ({
|
|||||||
? backup?.database
|
? backup?.database
|
||||||
: databaseType === "web-server"
|
: databaseType === "web-server"
|
||||||
? "dokploy"
|
? "dokploy"
|
||||||
: "",
|
: databaseType === "libsql"
|
||||||
|
? "iku.db"
|
||||||
|
: "",
|
||||||
destinationId: backup?.destinationId ?? "",
|
destinationId: backup?.destinationId ?? "",
|
||||||
enabled: backup?.enabled ?? true,
|
enabled: backup?.enabled ?? true,
|
||||||
prefix: backup?.prefix ?? "/",
|
prefix: backup?.prefix ?? "/",
|
||||||
@@ -281,6 +283,10 @@ export const HandleBackup = ({
|
|||||||
? {
|
? {
|
||||||
mongoId: id,
|
mongoId: id,
|
||||||
}
|
}
|
||||||
|
: databaseType === "libsql"
|
||||||
|
? {
|
||||||
|
libsqlId: id,
|
||||||
|
}
|
||||||
: databaseType === "web-server"
|
: databaseType === "web-server"
|
||||||
? {
|
? {
|
||||||
userId: id,
|
userId: id,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
|
|||||||
message: "Database name is required",
|
message: "Database name is required",
|
||||||
}),
|
}),
|
||||||
databaseType: z
|
databaseType: z
|
||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]).default("database"),
|
backupType: z.enum(["database", "compose"]).default("database"),
|
||||||
metadata: z
|
metadata: z
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import { RestoreBackup } from "./restore-backup";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
databaseType?:
|
databaseType?:
|
||||||
| Exclude<ServiceType, "application" | "redis" | "libsql">
|
| Exclude<ServiceType, "application" | "redis">
|
||||||
| "web-server";
|
| "web-server";
|
||||||
backupType?: "database" | "compose";
|
backupType?: "database" | "compose";
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,8 @@ export const ShowBackups = ({
|
|||||||
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
libsql: () =>
|
||||||
|
api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
||||||
"web-server": () => api.user.getBackups.useQuery(),
|
"web-server": () => api.user.getBackups.useQuery(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -83,6 +85,7 @@ export const ShowBackups = ({
|
|||||||
mongo: api.backup.manualBackupMongo.useMutation(),
|
mongo: api.backup.manualBackupMongo.useMutation(),
|
||||||
mysql: api.backup.manualBackupMySql.useMutation(),
|
mysql: api.backup.manualBackupMySql.useMutation(),
|
||||||
postgres: api.backup.manualBackupPostgres.useMutation(),
|
postgres: api.backup.manualBackupPostgres.useMutation(),
|
||||||
|
libsql: api.backup.manualBackupLibsql.useMutation(),
|
||||||
"web-server": api.backup.manualBackupWebServer.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"];
|
type DbType = z.infer<typeof mySchema>["type"];
|
||||||
|
|
||||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||||
libsql: "ghcr.io/tursodatabase/libsql-server:latest",
|
libsql: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||||
mongo: "mongo:7",
|
mongo: "mongo:7",
|
||||||
mariadb: "mariadb:11",
|
mariadb: "mariadb:11",
|
||||||
mysql: "mysql:8",
|
mysql: "mysql:8",
|
||||||
@@ -104,7 +104,7 @@ const mySchema = z
|
|||||||
type: z.literal("libsql"),
|
type: z.literal("libsql"),
|
||||||
dockerImage: z
|
dockerImage: z
|
||||||
.string()
|
.string()
|
||||||
.default("ghcr.io/tursodatabase/libsql-server:latest"),
|
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||||
databaseUser: z.string().default("libsql"),
|
databaseUser: z.string().default("libsql"),
|
||||||
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
||||||
sqldPrimaryUrl: z.string().optional(),
|
sqldPrimaryUrl: z.string().optional(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
|
|||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -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";
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1079,6 +1079,13 @@
|
|||||||
"when": 1773940853496,
|
"when": 1773940853496,
|
||||||
"tag": "0153_even_morlocks",
|
"tag": "0153_even_morlocks",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 154,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773942481550,
|
||||||
|
"tag": "0154_tan_living_mummy",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+5
-9
@@ -13,7 +13,7 @@ import superjson from "superjson";
|
|||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
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 { ShowExternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-external-libsql-credentials";
|
||||||
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
|
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
|
||||||
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
|
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
|
||||||
@@ -276,14 +276,10 @@ const Libsql = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBottomlessReplication
|
<ShowBackups
|
||||||
libsqlId={libsqlId}
|
id={libsqlId}
|
||||||
enableBottomlessReplication={
|
databaseType="libsql"
|
||||||
data?.enableBottomlessReplication || false
|
backupType="database"
|
||||||
}
|
|
||||||
bottomlessReplicationDestinationId={
|
|
||||||
data?.bottomlessReplicationDestinationId
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
findBackupById,
|
findBackupById,
|
||||||
findComposeByBackupId,
|
findComposeByBackupId,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
|
findLibsqlByBackupId,
|
||||||
|
findLibsqlById,
|
||||||
findMariadbByBackupId,
|
findMariadbByBackupId,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findMongoByBackupId,
|
findMongoByBackupId,
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
keepLatestNBackups,
|
keepLatestNBackups,
|
||||||
removeBackupById,
|
removeBackupById,
|
||||||
removeScheduleBackup,
|
removeScheduleBackup,
|
||||||
|
runLibsqlBackup,
|
||||||
runMariadbBackup,
|
runMariadbBackup,
|
||||||
runMongoBackup,
|
runMongoBackup,
|
||||||
runMySqlBackup,
|
runMySqlBackup,
|
||||||
@@ -36,6 +39,7 @@ import {
|
|||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import {
|
import {
|
||||||
restoreComposeBackup,
|
restoreComposeBackup,
|
||||||
|
restoreLibsqlBackup,
|
||||||
restoreMariadbBackup,
|
restoreMariadbBackup,
|
||||||
restoreMongoBackup,
|
restoreMongoBackup,
|
||||||
restoreMySqlBackup,
|
restoreMySqlBackup,
|
||||||
@@ -82,6 +86,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
input.mysqlId ||
|
input.mysqlId ||
|
||||||
input.mariadbId ||
|
input.mariadbId ||
|
||||||
input.mongoId ||
|
input.mongoId ||
|
||||||
|
input.libsqlId ||
|
||||||
input.composeId;
|
input.composeId;
|
||||||
if (serviceId) {
|
if (serviceId) {
|
||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
@@ -103,6 +108,8 @@ export const backupRouter = createTRPCRouter({
|
|||||||
serverId = backup.mongo.serverId;
|
serverId = backup.mongo.serverId;
|
||||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||||
serverId = backup.mariadb.serverId;
|
serverId = backup.mariadb.serverId;
|
||||||
|
} else if (databaseType === "libsql" && backup.libsql?.serverId) {
|
||||||
|
serverId = backup.libsql.serverId;
|
||||||
} else if (
|
} else if (
|
||||||
backup.backupType === "compose" &&
|
backup.backupType === "compose" &&
|
||||||
backup.compose?.serverId
|
backup.compose?.serverId
|
||||||
@@ -154,6 +161,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
backup.mysqlId ||
|
backup.mysqlId ||
|
||||||
backup.mariadbId ||
|
backup.mariadbId ||
|
||||||
backup.mongoId ||
|
backup.mongoId ||
|
||||||
|
backup.libsqlId ||
|
||||||
backup.composeId;
|
backup.composeId;
|
||||||
if (serviceId) {
|
if (serviceId) {
|
||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
@@ -173,6 +181,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
existing.mysqlId ||
|
existing.mysqlId ||
|
||||||
existing.mariadbId ||
|
existing.mariadbId ||
|
||||||
existing.mongoId ||
|
existing.mongoId ||
|
||||||
|
existing.libsqlId ||
|
||||||
existing.composeId;
|
existing.composeId;
|
||||||
if (serviceId) {
|
if (serviceId) {
|
||||||
await checkServicePermissionAndAccess(ctx, 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")
|
manualBackupWebServer: withPermission("backup", "create")
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -536,6 +572,12 @@ export const backupRouter = createTRPCRouter({
|
|||||||
queue.push(log);
|
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") {
|
if (input.databaseType === "web-server") {
|
||||||
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
||||||
queue.push(log);
|
queue.push(log);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { generateAppName } from ".";
|
|||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { destinations } from "./destination";
|
import { destinations } from "./destination";
|
||||||
|
import { libsql } from "./libsql";
|
||||||
import { mariadb } from "./mariadb";
|
import { mariadb } from "./mariadb";
|
||||||
import { mongo } from "./mongo";
|
import { mongo } from "./mongo";
|
||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
@@ -26,6 +27,7 @@ export const databaseType = pgEnum("databaseType", [
|
|||||||
"mysql",
|
"mysql",
|
||||||
"mongo",
|
"mongo",
|
||||||
"web-server",
|
"web-server",
|
||||||
|
"libsql",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||||
@@ -74,6 +76,12 @@ export const backups = pgTable("backup", {
|
|||||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
libsqlId: text("libsqlId").references(
|
||||||
|
(): AnyPgColumn => libsql.libsqlId,
|
||||||
|
{
|
||||||
|
onDelete: "cascade",
|
||||||
|
},
|
||||||
|
),
|
||||||
userId: text("userId").references(() => user.id),
|
userId: text("userId").references(() => user.id),
|
||||||
// Only for compose backups
|
// Only for compose backups
|
||||||
metadata: jsonb("metadata").$type<
|
metadata: jsonb("metadata").$type<
|
||||||
@@ -118,6 +126,10 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
|||||||
fields: [backups.mongoId],
|
fields: [backups.mongoId],
|
||||||
references: [mongo.mongoId],
|
references: [mongo.mongoId],
|
||||||
}),
|
}),
|
||||||
|
libsql: one(libsql, {
|
||||||
|
fields: [backups.libsqlId],
|
||||||
|
references: [libsql.libsqlId],
|
||||||
|
}),
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [backups.userId],
|
fields: [backups.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
@@ -137,11 +149,12 @@ const createSchema = createInsertSchema(backups, {
|
|||||||
database: z.string().min(1),
|
database: z.string().min(1),
|
||||||
schedule: z.string(),
|
schedule: z.string(),
|
||||||
keepLatestCount: z.number().optional(),
|
keepLatestCount: z.number().optional(),
|
||||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"]),
|
||||||
postgresId: z.string().optional(),
|
postgresId: z.string().optional(),
|
||||||
mariadbId: z.string().optional(),
|
mariadbId: z.string().optional(),
|
||||||
mysqlId: z.string().optional(),
|
mysqlId: z.string().optional(),
|
||||||
mongoId: z.string().optional(),
|
mongoId: z.string().optional(),
|
||||||
|
libsqlId: z.string().optional(),
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
metadata: z.any().optional(),
|
metadata: z.any().optional(),
|
||||||
});
|
});
|
||||||
@@ -157,6 +170,7 @@ export const apiCreateBackup = createSchema.pick({
|
|||||||
mysqlId: true,
|
mysqlId: true,
|
||||||
postgresId: true,
|
postgresId: true,
|
||||||
mongoId: true,
|
mongoId: true,
|
||||||
|
libsqlId: true,
|
||||||
databaseType: true,
|
databaseType: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
backupType: true,
|
backupType: true,
|
||||||
@@ -192,7 +206,7 @@ export const apiUpdateBackup = createSchema
|
|||||||
|
|
||||||
export const apiRestoreBackup = z.object({
|
export const apiRestoreBackup = z.object({
|
||||||
databaseId: z.string(),
|
databaseId: z.string(),
|
||||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server", "libsql"]),
|
||||||
backupType: z.enum(["database", "compose"]),
|
backupType: z.enum(["database", "compose"]),
|
||||||
databaseName: z.string().min(1),
|
databaseName: z.string().min(1),
|
||||||
backupFile: z.string().min(1),
|
backupFile: z.string().min(1),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
|||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { destinations } from "./destination";
|
import { backups } from "./backups";
|
||||||
import { environments } from "./environment";
|
import { environments } from "./environment";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
@@ -43,14 +43,6 @@ export const libsql = pgTable("libsql", {
|
|||||||
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
|
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
|
||||||
sqldPrimaryUrl: text("sqldPrimaryUrl"),
|
sqldPrimaryUrl: text("sqldPrimaryUrl"),
|
||||||
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
|
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
|
||||||
enableBottomlessReplication: boolean("enableBottomlessReplication")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
bottomlessReplicationDestinationId: text(
|
|
||||||
"bottomlessReplicationDestinationId",
|
|
||||||
).references(() => destinations.destinationId, {
|
|
||||||
onDelete: "set null",
|
|
||||||
}),
|
|
||||||
dockerImage: text("dockerImage").notNull(),
|
dockerImage: text("dockerImage").notNull(),
|
||||||
command: text("command"),
|
command: text("command"),
|
||||||
env: text("env"),
|
env: text("env"),
|
||||||
@@ -92,16 +84,12 @@ export const libsqlRelations = relations(libsql, ({ one, many }) => ({
|
|||||||
fields: [libsql.environmentId],
|
fields: [libsql.environmentId],
|
||||||
references: [environments.environmentId],
|
references: [environments.environmentId],
|
||||||
}),
|
}),
|
||||||
//backups: many(backups),
|
backups: many(backups),
|
||||||
mounts: many(mounts),
|
mounts: many(mounts),
|
||||||
server: one(server, {
|
server: one(server, {
|
||||||
fields: [libsql.serverId],
|
fields: [libsql.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
bottomlessReplicationDestination: one(destinations, {
|
|
||||||
fields: [libsql.bottomlessReplicationDestinationId],
|
|
||||||
references: [destinations.destinationId],
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(libsql, {
|
const createSchema = createInsertSchema(libsql, {
|
||||||
@@ -119,9 +107,7 @@ const createSchema = createInsertSchema(libsql, {
|
|||||||
sqldNode: z.enum(sqldNode.enumValues),
|
sqldNode: z.enum(sqldNode.enumValues),
|
||||||
sqldPrimaryUrl: z.string().nullable(),
|
sqldPrimaryUrl: z.string().nullable(),
|
||||||
enableNamespaces: z.boolean().default(false),
|
enableNamespaces: z.boolean().default(false),
|
||||||
enableBottomlessReplication: z.boolean().default(false),
|
dockerImage: z.string().default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||||
bottomlessReplicationDestinationId: z.string().nullable(),
|
|
||||||
dockerImage: z.string().default("ghcr.io/tursodatabase/libsql-server:latest"),
|
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
memoryReservation: z.string().optional(),
|
memoryReservation: z.string().optional(),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
export type TemplateProps = {
|
export type TemplateProps = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
applicationName: string;
|
applicationName: string;
|
||||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||||
type: "error" | "success";
|
type: "error" | "success";
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export * from "./utils/access-log/types";
|
|||||||
export * from "./utils/access-log/utils";
|
export * from "./utils/access-log/utils";
|
||||||
export * from "./utils/backups/compose";
|
export * from "./utils/backups/compose";
|
||||||
export * from "./utils/backups/index";
|
export * from "./utils/backups/index";
|
||||||
|
export * from "./utils/backups/libsql";
|
||||||
export * from "./utils/backups/mariadb";
|
export * from "./utils/backups/mariadb";
|
||||||
export * from "./utils/backups/mongo";
|
export * from "./utils/backups/mongo";
|
||||||
export * from "./utils/backups/mysql";
|
export * from "./utils/backups/mysql";
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const findBackupById = async (backupId: string) => {
|
|||||||
mysql: true,
|
mysql: true,
|
||||||
mariadb: true,
|
mariadb: true,
|
||||||
mongo: true,
|
mongo: true,
|
||||||
|
libsql: true,
|
||||||
destination: true,
|
destination: true,
|
||||||
compose: true,
|
compose: true,
|
||||||
},
|
},
|
||||||
@@ -72,7 +73,7 @@ export const removeBackupById = async (backupId: string) => {
|
|||||||
|
|
||||||
export const findBackupsByDbId = async (
|
export const findBackupsByDbId = async (
|
||||||
id: string,
|
id: string,
|
||||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
type: "postgres" | "mysql" | "mariadb" | "mongo" | "libsql",
|
||||||
) => {
|
) => {
|
||||||
const result = await db.query.backups.findMany({
|
const result = await db.query.backups.findMany({
|
||||||
where: eq(backups[`${type}Id`], id),
|
where: eq(backups[`${type}Id`], id),
|
||||||
@@ -81,6 +82,7 @@ export const findBackupsByDbId = async (
|
|||||||
mysql: true,
|
mysql: true,
|
||||||
mariadb: true,
|
mariadb: true,
|
||||||
mongo: true,
|
mongo: true,
|
||||||
|
libsql: true,
|
||||||
destination: true,
|
destination: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import {
|
import {
|
||||||
type apiCreateLibsql,
|
type apiCreateLibsql,
|
||||||
|
backups,
|
||||||
buildAppName,
|
buildAppName,
|
||||||
libsql,
|
libsql,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
@@ -9,12 +10,13 @@ import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
|
|||||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import type { z } from "zod";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Libsql = typeof libsql.$inferSelect;
|
export type Libsql = typeof libsql.$inferSelect;
|
||||||
|
|
||||||
export const createLibsql = async (input: typeof apiCreateLibsql._type) => {
|
export const createLibsql = async (input: z.infer<typeof apiCreateLibsql>) => {
|
||||||
const appName = buildAppName("libsql", input.appName);
|
const appName = buildAppName("libsql", input.appName);
|
||||||
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
@@ -59,13 +61,12 @@ export const findLibsqlById = async (libsqlId: string) => {
|
|||||||
},
|
},
|
||||||
mounts: true,
|
mounts: true,
|
||||||
server: true,
|
server: true,
|
||||||
bottomlessReplicationDestination: true,
|
backups: {
|
||||||
// backups: {
|
with: {
|
||||||
// with: {
|
destination: true,
|
||||||
// destination: true,
|
deployments: true,
|
||||||
// deployments: true,
|
},
|
||||||
// },
|
},
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -102,24 +103,24 @@ export const removeLibsqlById = async (libsqlId: string) => {
|
|||||||
return result[0];
|
return result[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const findLibsqlByBackupId = async (backupId: string) => {
|
export const findLibsqlByBackupId = async (backupId: string) => {
|
||||||
// const result = await db
|
const result = await db
|
||||||
// .select({
|
.select({
|
||||||
// ...getTableColumns(libsql),
|
...getTableColumns(libsql),
|
||||||
// })
|
})
|
||||||
// .from(libsql)
|
.from(libsql)
|
||||||
// .innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
|
.innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
|
||||||
// .where(eq(backups.backupId, backupId))
|
.where(eq(backups.backupId, backupId))
|
||||||
// .limit(1);
|
.limit(1);
|
||||||
//
|
|
||||||
// if (!result || !result[0]) {
|
if (!result || !result[0]) {
|
||||||
// throw new TRPCError({
|
throw new TRPCError({
|
||||||
// code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
// message: "Libsql not found",
|
message: "Libsql not found",
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// return result[0];
|
return result[0];
|
||||||
// };
|
};
|
||||||
|
|
||||||
export const deployLibsql = async (
|
export const deployLibsql = async (
|
||||||
libsqlId: string,
|
libsqlId: string,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export const initCronJobs = async () => {
|
|||||||
mariadb: true,
|
mariadb: true,
|
||||||
mysql: true,
|
mysql: true,
|
||||||
mongo: true,
|
mongo: true,
|
||||||
|
libsql: true,
|
||||||
user: true,
|
user: true,
|
||||||
compose: true,
|
compose: true,
|
||||||
},
|
},
|
||||||
@@ -116,7 +117,8 @@ const getServiceAppName = (backup: BackupSchedule): string => {
|
|||||||
backup.postgres?.appName ||
|
backup.postgres?.appName ||
|
||||||
backup.mysql?.appName ||
|
backup.mysql?.appName ||
|
||||||
backup.mariadb?.appName ||
|
backup.mariadb?.appName ||
|
||||||
backup.mongo?.appName;
|
backup.mongo?.appName ||
|
||||||
|
backup.libsql?.appName;
|
||||||
return serviceAppName || backup.appName;
|
return serviceAppName || backup.appName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
|
import {
|
||||||
|
createDeploymentBackup,
|
||||||
|
updateDeploymentStatus,
|
||||||
|
} from "@dokploy/server/services/deployment";
|
||||||
|
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||||
|
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||||
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
|
export const runLibsqlBackup = async (
|
||||||
|
libsql: Libsql,
|
||||||
|
backup: BackupSchedule,
|
||||||
|
) => {
|
||||||
|
const { name, environmentId, appName } = libsql;
|
||||||
|
const environment = await findEnvironmentById(environmentId);
|
||||||
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
|
const deployment = await createDeploymentBackup({
|
||||||
|
backupId: backup.backupId,
|
||||||
|
title: "Initializing Backup",
|
||||||
|
description: "Initializing Backup",
|
||||||
|
});
|
||||||
|
const { prefix } = backup;
|
||||||
|
const destination = backup.destination;
|
||||||
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
|
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
|
try {
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||||
|
|
||||||
|
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
|
|
||||||
|
const backupCommand = getBackupCommand(
|
||||||
|
backup,
|
||||||
|
rcloneCommand,
|
||||||
|
deployment.logPath,
|
||||||
|
);
|
||||||
|
if (libsql.serverId) {
|
||||||
|
await execAsyncRemote(libsql.serverId, backupCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(backupCommand, {
|
||||||
|
shell: "/bin/bash",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendDatabaseBackupNotifications({
|
||||||
|
applicationName: name,
|
||||||
|
projectName: project.name,
|
||||||
|
databaseType: "libsql",
|
||||||
|
type: "success",
|
||||||
|
organizationId: project.organizationId,
|
||||||
|
databaseName: backup.database,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
|
} catch (error) {
|
||||||
|
await sendDatabaseBackupNotifications({
|
||||||
|
applicationName: name,
|
||||||
|
projectName: project.name,
|
||||||
|
databaseType: "libsql",
|
||||||
|
type: "error",
|
||||||
|
// @ts-ignore
|
||||||
|
errorMessage: error?.message || "Error message not provided",
|
||||||
|
organizationId: project.organizationId,
|
||||||
|
databaseName: backup.database,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
|
|||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { keepLatestNBackups } from ".";
|
import { keepLatestNBackups } from ".";
|
||||||
import { runComposeBackup } from "./compose";
|
import { runComposeBackup } from "./compose";
|
||||||
|
import { runLibsqlBackup } from "./libsql";
|
||||||
import { runMariadbBackup } from "./mariadb";
|
import { runMariadbBackup } from "./mariadb";
|
||||||
import { runMongoBackup } from "./mongo";
|
import { runMongoBackup } from "./mongo";
|
||||||
import { runMySqlBackup } from "./mysql";
|
import { runMySqlBackup } from "./mysql";
|
||||||
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
|||||||
mysql,
|
mysql,
|
||||||
mongo,
|
mongo,
|
||||||
mariadb,
|
mariadb,
|
||||||
|
libsql,
|
||||||
compose,
|
compose,
|
||||||
} = backup;
|
} = backup;
|
||||||
scheduleJob(backupId, schedule, async () => {
|
scheduleJob(backupId, schedule, async () => {
|
||||||
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
|||||||
} else if (databaseType === "mariadb" && mariadb) {
|
} else if (databaseType === "mariadb" && mariadb) {
|
||||||
await runMariadbBackup(mariadb, backup);
|
await runMariadbBackup(mariadb, backup);
|
||||||
await keepLatestNBackups(backup, mariadb.serverId);
|
await keepLatestNBackups(backup, mariadb.serverId);
|
||||||
|
} else if (databaseType === "libsql" && libsql) {
|
||||||
|
await runLibsqlBackup(libsql, backup);
|
||||||
|
await keepLatestNBackups(backup, libsql.serverId);
|
||||||
} else if (databaseType === "web-server") {
|
} else if (databaseType === "web-server") {
|
||||||
await runWebServerBackup(backup);
|
await runWebServerBackup(backup);
|
||||||
await keepLatestNBackups(backup);
|
await keepLatestNBackups(backup);
|
||||||
@@ -107,6 +112,10 @@ export const getMongoBackupCommand = (
|
|||||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getLibsqlBackupCommand = (database: string) => {
|
||||||
|
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getServiceContainerCommand = (appName: string) => {
|
export const getServiceContainerCommand = (appName: string) => {
|
||||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||||
};
|
};
|
||||||
@@ -123,12 +132,12 @@ export const getComposeContainerCommand = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
||||||
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
|
const { backupType, postgres, mysql, mariadb, mongo, libsql, compose, serviceName } =
|
||||||
backup;
|
backup;
|
||||||
|
|
||||||
if (backupType === "database") {
|
if (backupType === "database") {
|
||||||
const appName =
|
const appName =
|
||||||
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
|
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName || libsql?.appName;
|
||||||
return getServiceContainerCommand(appName || "");
|
return getServiceContainerCommand(appName || "");
|
||||||
}
|
}
|
||||||
if (backupType === "compose") {
|
if (backupType === "compose") {
|
||||||
@@ -209,6 +218,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "libsql": {
|
||||||
|
if (backupType === "database") {
|
||||||
|
return getLibsqlBackupCommand(backup.database);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Database type not supported: ${databaseType}`);
|
throw new Error(`Database type not supported: ${databaseType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export type LibsqlNested = InferResultType<
|
|||||||
{
|
{
|
||||||
mounts: true;
|
mounts: true;
|
||||||
environment: { with: { project: true } };
|
environment: { with: { project: true } };
|
||||||
bottomlessReplicationDestination: true;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
export const buildLibsql = async (libsql: LibsqlNested) => {
|
export const buildLibsql = async (libsql: LibsqlNested) => {
|
||||||
@@ -36,8 +35,6 @@ export const buildLibsql = async (libsql: LibsqlNested) => {
|
|||||||
command,
|
command,
|
||||||
mounts,
|
mounts,
|
||||||
enableNamespaces,
|
enableNamespaces,
|
||||||
enableBottomlessReplication,
|
|
||||||
bottomlessReplicationDestination,
|
|
||||||
} = libsql;
|
} = libsql;
|
||||||
|
|
||||||
const basicAuth = Buffer.from(
|
const basicAuth = Buffer.from(
|
||||||
@@ -45,20 +42,10 @@ export const buildLibsql = async (libsql: LibsqlNested) => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
).toString("base64");
|
).toString("base64");
|
||||||
|
|
||||||
let defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
|
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
|
||||||
env ? `\n${env}` : ""
|
env ? `\n${env}` : ""
|
||||||
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
|
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
|
||||||
|
|
||||||
// Add bottomless replication environment variables if destination is configured
|
|
||||||
if (enableBottomlessReplication && bottomlessReplicationDestination) {
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_DATABASE_ID="${appName}"`;
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_BUCKET="${bottomlessReplicationDestination.bucket}"`;
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_ENDPOINT="${bottomlessReplicationDestination.endpoint}"`;
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_AWS_SECRET_ACCESS_KEY="${bottomlessReplicationDestination.secretAccessKey}"`;
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_AWS_ACCESS_KEY_ID="${bottomlessReplicationDestination.accessKey}"`;
|
|
||||||
defaultLibsqlEnv += `\nLIBSQL_BOTTOMLESS_AWS_DEFAULT_REGION="${bottomlessReplicationDestination.region}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
RestartPolicy,
|
RestartPolicy,
|
||||||
@@ -92,16 +79,13 @@ export const buildLibsql = async (libsql: LibsqlNested) => {
|
|||||||
if (enableNamespaces) {
|
if (enableNamespaces) {
|
||||||
finalCommand += " --enable-namespaces";
|
finalCommand += " --enable-namespaces";
|
||||||
}
|
}
|
||||||
if (enableBottomlessReplication) {
|
|
||||||
finalCommand += " --enable-bottomless-replication";
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
const settings: CreateServiceOptions = {
|
||||||
Name: appName,
|
Name: appName,
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
ContainerSpec: {
|
ContainerSpec: {
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
Image: "ghcr.io/tursodatabase/libsql-server:latest",
|
Image: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||||
Env: envVariables,
|
Env: envVariables,
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||||
...(finalCommand
|
...(finalCommand
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
|
|||||||
}: {
|
}: {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
applicationName: string;
|
applicationName: string;
|
||||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||||
type: "error" | "success";
|
type: "error" | "success";
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const restoreComposeBackup = async (
|
|||||||
const restoreCommand = getRestoreCommand({
|
const restoreCommand = getRestoreCommand({
|
||||||
appName: appName,
|
appName: appName,
|
||||||
serviceName: backupInput.metadata?.serviceName,
|
serviceName: backupInput.metadata?.serviceName,
|
||||||
type: backupInput.databaseType,
|
type: backupInput.databaseType as "postgres" | "mariadb" | "mysql" | "mongo",
|
||||||
credentials: {
|
credentials: {
|
||||||
database: backupInput.databaseName,
|
database: backupInput.databaseName,
|
||||||
...credentials,
|
...credentials,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { restoreComposeBackup } from "./compose";
|
export { restoreComposeBackup } from "./compose";
|
||||||
|
export { restoreLibsqlBackup } from "./libsql";
|
||||||
export { restoreMariadbBackup } from "./mariadb";
|
export { restoreMariadbBackup } from "./mariadb";
|
||||||
export { restoreMongoBackup } from "./mongo";
|
export { restoreMongoBackup } from "./mongo";
|
||||||
export { restoreMySqlBackup } from "./mysql";
|
export { restoreMySqlBackup } from "./mysql";
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
|
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||||
|
import type { z } from "zod";
|
||||||
|
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|
||||||
|
export const restoreLibsqlBackup = async (
|
||||||
|
libsql: Libsql,
|
||||||
|
destination: Destination,
|
||||||
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
|
emit: (log: string) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, serverId } = libsql;
|
||||||
|
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
|
||||||
|
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||||
|
|
||||||
|
emit("Starting restore...");
|
||||||
|
emit(`Backup path: ${backupPath}`);
|
||||||
|
|
||||||
|
const containerSearch = getServiceContainerCommand(appName);
|
||||||
|
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
|
||||||
|
|
||||||
|
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
|
||||||
|
|
||||||
|
emit(`Executing command: ${command}`);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("Restore completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
emit(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error restoring libsql backup"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user