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 ( +
+
+ 1. {t('txt_backup_recommend_cloudflare_r2_step_1_prefix')}{' '} + {t('txt_backup_recommend_cloudflare_r2_bucket_link')} + {t('txt_backup_recommend_cloudflare_r2_step_1_suffix')} +
+
+ 2. {t('txt_backup_recommend_cloudflare_r2_step_2_prefix')}{' '} + {t('txt_backup_recommend_cloudflare_r2_api_link')} + {t('txt_backup_recommend_cloudflare_r2_step_2_suffix')} +
+
+ 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 (