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:
Mauricio Siu
2026-03-19 16:00:39 -06:00
parent a03ec76b6f
commit bb56a0bae8
25 changed files with 8420 additions and 291 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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(),
}
: {

View File

@@ -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>
);
};

View File

@@ -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(),