diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 8e51aef919..ed89369548 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -1,23 +1,48 @@ import { relations } from "drizzle-orm"; -import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; import { organization } from "./account"; import { backups } from "./backups"; +export const DESTINATION_TYPES = [ + "s3", + "ftp", + "sftp", + "gdrive", + "onedrive", + "azure_blob", + "b2", + "webdav", +] as const; + +export type DestinationType = (typeof DESTINATION_TYPES)[number]; + export const destinations = pgTable("destination", { destinationId: text("destinationId") .notNull() .primaryKey() .$defaultFn(() => nanoid()), name: text("name").notNull(), + destinationType: text("destinationType").notNull().default("s3"), provider: text("provider"), - accessKey: text("accessKey").notNull(), - secretAccessKey: text("secretAccessKey").notNull(), - bucket: text("bucket").notNull(), - region: text("region").notNull(), - endpoint: text("endpoint").notNull(), + // S3-compatible fields + accessKey: text("accessKey"), + secretAccessKey: text("secretAccessKey"), + bucket: text("bucket"), + region: text("region"), + endpoint: text("endpoint"), + // FTP/SFTP fields + host: text("host"), + port: text("port"), + username: text("username"), + password: text("password"), + path: text("path"), + // OAuth token fields (Google Drive, OneDrive, Azure) + token: text("token"), + // Additional rclone config options (JSON) + configOptions: jsonb("configOptions"), organizationId: text("organizationId") .notNull() .references(() => organization.id, { onDelete: "cascade" }), diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index f30577a53b..b489b59567 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -77,6 +77,102 @@ export const getS3Credentials = (destination: Destination) => { return rcloneFlags; }; +/** + * Generate rclone flags for any destination type. + * Supports: s3, ftp, sftp, gdrive, onedrive, azure_blob, b2, webdav + */ +export const getRcloneFlags = (destination: Destination): string[] => { + const type = destination.destinationType || "s3"; + + switch (type) { + case "s3": + return getS3Credentials(destination); + + case "ftp": + return [ + `--ftp-host="${destination.host}"`, + `--ftp-port="${destination.port || "21"}"`, + `--ftp-user="${destination.username}"`, + `--ftp-pass="${destination.password}"`, + ]; + + case "sftp": + return [ + `--sftp-host="${destination.host}"`, + `--sftp-port="${destination.port || "22"}"`, + `--sftp-user="${destination.username}"`, + destination.password + ? `--sftp-pass="${destination.password}"` + : "", + ].filter(Boolean); + + case "gdrive": + return [ + `--drive-client-id="${destination.accessKey}"`, + `--drive-client-secret="${destination.secretAccessKey}"`, + `--drive-token="${destination.token}"`, + `--drive-root-folder-id="${destination.path || ""}"`, + ]; + + case "onedrive": + return [ + `--onedrive-client-id="${destination.accessKey}"`, + `--onedrive-client-secret="${destination.secretAccessKey}"`, + `--onedrive-token="${destination.token}"`, + `--onedrive-drive-id="${destination.path || ""}"`, + ]; + + case "azure_blob": + return [ + `--azureblob-account="${destination.accessKey}"`, + `--azureblob-key="${destination.secretAccessKey}"`, + ]; + + case "b2": + return [ + `--b2-account="${destination.accessKey}"`, + `--b2-key="${destination.secretAccessKey}"`, + ]; + + case "webdav": + return [ + `--webdav-url="${destination.endpoint}"`, + `--webdav-user="${destination.username}"`, + `--webdav-pass="${destination.password}"`, + ]; + + default: + return getS3Credentials(destination); + } +}; + +/** + * Get the rclone destination path for any type. + */ +export const getRcloneDestPath = (destination: Destination): string => { + const type = destination.destinationType || "s3"; + + switch (type) { + case "s3": + return `:s3:${destination.bucket}`; + case "ftp": + case "sftp": + return `:${type}:${destination.path || "/"}`; + case "gdrive": + return `:drive:${destination.path || ""}`; + case "onedrive": + return `:onedrive:${destination.path || ""}`; + case "azure_blob": + return `:azureblob:${destination.bucket}`; + case "b2": + return `:b2:${destination.bucket}`; + case "webdav": + return `:webdav:${destination.path || "/"}`; + default: + return `:s3:${destination.bucket}`; + } +}; + export const getPostgresBackupCommand = ( database: string, databaseUser: string,