From c3dc53bac15802c999c46bcc2eb11fe01b836789 Mon Sep 17 00:00:00 2001
From: shuaiplus <2327005759@qq.com>
Date: Thu, 25 Jun 2026 19:45:09 +0800
Subject: [PATCH] feat: add Cloudflare R2 support with detailed backup
recommendations and localization updates
---
shared/backup-schema.ts | 7 ++--
.../backup-center/BackupDestinationDetail.tsx | 29 +++++++++++++--
.../backup-center/BackupOperationsSidebar.tsx | 35 ++++++++++++++++++-
webapp/src/lib/backup-recommendations.ts | 19 ++++++++--
webapp/src/lib/i18n/locales/en.ts | 10 ++++++
webapp/src/lib/i18n/locales/es.ts | 10 ++++++
webapp/src/lib/i18n/locales/ru.ts | 10 ++++++
webapp/src/lib/i18n/locales/zh-CN.ts | 12 ++++++-
webapp/src/lib/i18n/locales/zh-TW.ts | 10 ++++++
9 files changed, 133 insertions(+), 9 deletions(-)
diff --git a/shared/backup-schema.ts b/shared/backup-schema.ts
index 2478a6f..f1fb591 100644
--- a/shared/backup-schema.ts
+++ b/shared/backup-schema.ts
@@ -9,7 +9,8 @@
export const BACKUP_DEFAULT_TIMEZONE = 'UTC';
export const BACKUP_DEFAULT_RETENTION_COUNT = 30;
export const BACKUP_DEFAULT_S3_REGION = 'auto';
-export const BACKUP_DEFAULT_REMOTE_PATH = 'nodewarden';
+export const BACKUP_DEFAULT_S3_ROOT_PATH = '';
+export const BACKUP_DEFAULT_WEBDAV_REMOTE_PATH = 'nodewarden';
export const BACKUP_DEFAULT_INTERVAL_HOURS = 24;
export const BACKUP_DEFAULT_START_TIME = '03:00';
@@ -109,14 +110,14 @@ export function createDefaultBackupDestinationConfig(type: BackupDestinationType
region: BACKUP_DEFAULT_S3_REGION,
accessKeyId: '',
secretAccessKey: '',
- rootPath: BACKUP_DEFAULT_REMOTE_PATH,
+ rootPath: BACKUP_DEFAULT_S3_ROOT_PATH,
};
}
return {
baseUrl: '',
username: '',
password: '',
- remotePath: BACKUP_DEFAULT_REMOTE_PATH,
+ remotePath: BACKUP_DEFAULT_WEBDAV_REMOTE_PATH,
};
}
diff --git a/webapp/src/components/backup-center/BackupDestinationDetail.tsx b/webapp/src/components/backup-center/BackupDestinationDetail.tsx
index d4b5ac4..615e749 100644
--- a/webapp/src/components/backup-center/BackupDestinationDetail.tsx
+++ b/webapp/src/components/backup-center/BackupDestinationDetail.tsx
@@ -151,6 +151,30 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
);
+ case 'cloudflare-r2':
+ return (
+
+
+
+
+ 3. {t('txt_backup_recommend_cloudflare_r2_step_3')}
+
+
+ 4. {t('txt_backup_recommend_cloudflare_r2_step_4')}
+
+
+ 5. {t('txt_backup_recommend_cloudflare_r2_step_5')}
+
+
+ );
}
}
@@ -172,6 +196,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
{props.selectedRecommendedProvider.id === 'infinicloud' ? t('txt_backup_recommend_infinicloud_summary')
: props.selectedRecommendedProvider.id === 'koofr' ? t('txt_backup_recommend_koofr_summary')
: props.selectedRecommendedProvider.id === 'backblaze-b2' ? t('txt_backup_recommend_backblaze_summary')
+ : props.selectedRecommendedProvider.id === 'cloudflare-r2' ? t('txt_backup_recommend_cloudflare_r2_summary')
: t('txt_backup_recommend_pcloud_summary')}
@@ -412,7 +437,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
className="input"
value={(props.selectedDestination.destination as WebDavBackupDestination).remotePath}
disabled={props.loadingSettings || props.disableWhileBusy}
- placeholder="nodewarden/backups"
+ placeholder="nodewarden"
onInput={(event) => props.onUpdateDestination((destination) => ({
...destination,
destination: {
@@ -529,7 +554,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
className="input"
value={(props.selectedDestination.destination as S3BackupDestination).rootPath}
disabled={props.loadingSettings || props.disableWhileBusy}
- placeholder="nodewarden/backups"
+ placeholder=""
onInput={(event) => props.onUpdateDestination((destination) => ({
...destination,
destination: {
diff --git a/webapp/src/components/backup-center/BackupOperationsSidebar.tsx b/webapp/src/components/backup-center/BackupOperationsSidebar.tsx
index 1b9720c..6568667 100644
--- a/webapp/src/components/backup-center/BackupOperationsSidebar.tsx
+++ b/webapp/src/components/backup-center/BackupOperationsSidebar.tsx
@@ -1,9 +1,12 @@
import { Download, FileUp } from 'lucide-preact';
+import { useEffect, useState } from 'preact/hooks';
import type { RecommendedProvider } from '@/lib/backup-recommendations';
import { hasLinkedStorages } from '@/lib/backup-recommendations';
import { t } from '@/lib/i18n';
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
+const MOBILE_RECOMMENDATIONS_QUERY = '(max-width: 760px)';
+
interface BackupOperationsSidebarProps {
disableWhileBusy: boolean;
exporting: boolean;
@@ -18,7 +21,30 @@ interface BackupOperationsSidebarProps {
onSelectProvider: (providerId: string) => void;
}
+function getDefaultRecommendationsOpen() {
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
+ return true;
+ }
+ return !window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY).matches;
+}
+
export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
+ const [recommendationsOpen, setRecommendationsOpen] = useState(getDefaultRecommendationsOpen);
+ const [recommendationsTouched, setRecommendationsTouched] = useState(false);
+
+ useEffect(() => {
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function' || recommendationsTouched) {
+ return;
+ }
+
+ const media = window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY);
+ const syncOpenState = () => setRecommendationsOpen(!media.matches);
+
+ syncOpenState();
+ media.addEventListener('change', syncOpenState);
+ return () => media.removeEventListener('change', syncOpenState);
+ }, [recommendationsTouched]);
+
return (