Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion apps/dokploy/__test__/utils/backups.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
import type { Destination } from "@dokploy/server/services/destination";
import {
getRcloneDestination,
getRcloneDestinationProvider,
getRcloneFlags,
normalizeRclonePath,
normalizeS3Path,
shellQuote,
} from "@dokploy/server/utils/backups/utils";
import { describe, expect, test } from "vitest";

const createDestination = (
overrides: Partial<Destination> = {},
): Destination => ({
destinationId: "destination",
name: "Backups",
provider: "AWS",
accessKey: "access-key",
secretAccessKey: "secret-key",
bucket: "dokploy-backups",
region: "us-east-1",
endpoint: "https://s3.example.com",
additionalFlags: [],
organizationId: "organization",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
...overrides,
});

describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => {
expect(normalizeS3Path("")).toBe("");
Expand Down Expand Up @@ -59,3 +84,122 @@ describe("normalizeS3Path", () => {
expect(normalizeS3Path("instance-backups")).toBe("instance-backups/");
});
});

describe("rclone destination providers", () => {
test("should keep existing S3 providers mapped to the s3 backend", () => {
const destination = createDestination({ provider: "Cloudflare" });

expect(getRcloneDestinationProvider(destination)).toBe("s3");
expect(getRcloneDestination(destination, "app/prefix/backup.sql.gz")).toBe(
":s3:dokploy-backups/app/prefix/backup.sql.gz",
);
expect(getRcloneFlags(destination)).toEqual([
"--s3-provider='Cloudflare'",
"--s3-access-key-id='access-key'",
"--s3-secret-access-key='secret-key'",
"--s3-region='us-east-1'",
"--s3-endpoint='https://s3.example.com'",
"--s3-no-check-bucket",
"--s3-force-path-style",
]);
});

test("should build FTP flags and preserve absolute base paths", () => {
const destination = createDestination({
provider: "ftp",
accessKey: "backup-user",
secretAccessKey: "pa'ss word",
bucket: "/srv/backups",
region: "2121",
endpoint: "ftp.example.com",
});

expect(getRcloneDestinationProvider(destination)).toBe("ftp");
expect(getRcloneDestination(destination, "app/backup.sql.gz")).toBe(
":ftp:/srv/backups/app/backup.sql.gz",
);
expect(getRcloneFlags(destination)).toEqual([
"--ftp-host='ftp.example.com'",
"--ftp-user='backup-user'",
"--ftp-port='2121'",
"--ftp-pass=$(rclone obscure 'pa'\\''ss word')",
]);
});

test("should build SFTP flags with additional rclone options", () => {
const destination = createDestination({
provider: "sftp",
accessKey: "dokploy",
secretAccessKey: "secret",
bucket: "relative/backups",
region: "",
endpoint: "sftp.example.com",
additionalFlags: ["--sftp-known-hosts-file=/root/.ssh/known_hosts"],
});

expect(getRcloneDestination(destination, "app/backup.sql.gz")).toBe(
":sftp:relative/backups/app/backup.sql.gz",
);
expect(getRcloneFlags(destination)).toEqual([
"--sftp-host='sftp.example.com'",
"--sftp-user='dokploy'",
"--sftp-pass=$(rclone obscure 'secret')",
"--sftp-known-hosts-file=/root/.ssh/known_hosts",
]);
});

test("should build Google Drive flags from token JSON", () => {
const token =
'{"access_token":"access","token_type":"Bearer","refresh_token":"refresh","expiry":"2026-01-01T00:00:00Z"}';
const destination = createDestination({
provider: "drive",
accessKey: "client-id",
secretAccessKey: "client-secret",
bucket: "Dokploy Backups",
region: "root-folder-id",
endpoint: token,
});

expect(getRcloneDestination(destination, "app/backup.sql.gz")).toBe(
":drive:Dokploy Backups/app/backup.sql.gz",
);
expect(getRcloneFlags(destination)).toEqual([
"--drive-client-id='client-id'",
"--drive-client-secret='client-secret'",
`--drive-token=${shellQuote(token)}`,
"--drive-root-folder-id='root-folder-id'",
]);
});

test("should build OneDrive flags without optional client credentials", () => {
const token = '{"access_token":"access","refresh_token":"refresh"}';
const destination = createDestination({
provider: "onedrive",
accessKey: "",
secretAccessKey: "",
bucket: "",
region: "drive-id",
endpoint: token,
});

expect(getRcloneDestination(destination, "app/backup.sql.gz")).toBe(
":onedrive:app/backup.sql.gz",
);
expect(getRcloneFlags(destination)).toEqual([
`--onedrive-token=${shellQuote(token)}`,
"--onedrive-drive-id='drive-id'",
]);
});

test("should normalize rclone paths while preserving FTP/SFTP root semantics", () => {
expect(
normalizeRclonePath("/absolute/path/", { preserveLeadingSlash: true }),
).toBe("/absolute/path/");
expect(normalizeRclonePath("/absolute/path/")).toBe("absolute/path/");
});

test("should quote shell values safely", () => {
expect(shellQuote("path/with spaces")).toBe("'path/with spaces'");
expect(shellQuote("value'with'quotes")).toBe("'value'\\''with'\\''quotes'");
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
export const RCLONE_DESTINATION_PROVIDERS = [
{
key: "ftp",
name: "FTP",
},
{
key: "sftp",
name: "SFTP",
},
{
key: "drive",
name: "Google Drive",
},
{
key: "onedrive",
name: "Microsoft OneDrive",
},
] as const;

const RCLONE_DESTINATION_PROVIDER_KEYS = new Set(
RCLONE_DESTINATION_PROVIDERS.map((provider) => provider.key),
);

export const getDestinationProviderType = (provider: string) => {
const normalizedProvider = provider.trim().toLowerCase();
if (
RCLONE_DESTINATION_PROVIDER_KEYS.has(
normalizedProvider as (typeof RCLONE_DESTINATION_PROVIDERS)[number]["key"],
)
) {
return normalizedProvider as (typeof RCLONE_DESTINATION_PROVIDERS)[number]["key"];
}
return "s3";
};

export const S3_PROVIDERS: Array<{
key: string;
name: string;
Expand Down
Loading