mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat: 新增 S3 兼容云存储同步 (#1351)
* Add S3 Cloud Sync design document Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV. Hybrid approach: extract shared sync protocol, add independent S3 transport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation design (reqwest + Sig V4) Updated design based on 2026-03-06 draft: switches from rust-s3 crate to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation plan (11 tasks, TDD) Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4 transport, settings, sync/auto-sync modules, Tauri commands, frontend presets/dynamic form, and i18n. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * deps: add hmac crate for S3 Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract sync_protocol.rs from webdav_sync.rs for shared use Move transport-agnostic sync protocol logic (constants, types, snapshot building, manifest validation, artifact verification, snapshot application, utilities) into a new shared sync_protocol module so both WebDAV and the upcoming S3 transport can reuse it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use transport-neutral error keys in sync_protocol Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 transport layer with AWS Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3SyncSettings to AppSettings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync module with upload/download/fetch Implements the S3 sync protocol layer (s3_sync.rs) that combines the shared sync_protocol with the S3 transport. Mirrors the WebDAV sync module structure with independent sync mutex, connection check, upload, download, fetch_remote_info, and sync status persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 auto sync worker with debounce Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync Tauri commands and auto sync worker startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync TypeScript types and API layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync i18n translations (en/zh/ja) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync presets and dynamic form to sync settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve HTTP scheme for S3 custom endpoints (MinIO support) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add live S3 integration tests (env-var driven, --ignored) Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored Verifies test_connection, put_object, get_object, head_object, and 404 handling against a real S3 bucket using the project's own Sig V4 signing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove internal design docs before PR * fix: wire S3 auto-sync to DB hook & sync UI state on async load - P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook so S3 auto-sync worker receives DB change signals (was only wired for WebDAV, leaving S3 worker idle) - P2: Add useEffect to update syncType selector when s3Config loads asynchronously, preventing stale "webdav" default for S3 users * fix: satisfy clippy for s3 sync * fix: address s3 sync review feedback --------- Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
+18
-2
@@ -107,7 +107,7 @@ type View =
|
||||
| "openclawAgents"
|
||||
| "hermesMemory";
|
||||
|
||||
interface WebDavSyncStatusUpdatedPayload {
|
||||
interface SyncStatusUpdatedPayload {
|
||||
source?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
@@ -371,7 +371,7 @@ function App() {
|
||||
}
|
||||
});
|
||||
|
||||
useTauriEvent<WebDavSyncStatusUpdatedPayload | null | undefined>(
|
||||
useTauriEvent<SyncStatusUpdatedPayload | null | undefined>(
|
||||
"webdav-sync-status-updated",
|
||||
async (payload) => {
|
||||
const statusPayload = payload ?? {};
|
||||
@@ -387,6 +387,22 @@ function App() {
|
||||
},
|
||||
);
|
||||
|
||||
useTauriEvent<SyncStatusUpdatedPayload | null | undefined>(
|
||||
"s3-sync-status-updated",
|
||||
async (payload) => {
|
||||
const statusPayload = payload ?? {};
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
if (statusPayload.source !== "auto" || statusPayload.status !== "error") {
|
||||
return;
|
||||
}
|
||||
toast.error(
|
||||
t("settings.s3Sync.autoSyncFailedToast", {
|
||||
error: statusPayload.error || t("common.unknown"),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
useTauriEvent<{ appType: string; providerName: string }>(
|
||||
"proxy-official-warning",
|
||||
(payload) => {
|
||||
|
||||
@@ -415,6 +415,7 @@ export function SettingsPage({
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<WebdavSyncSection
|
||||
config={settings?.webdavSync}
|
||||
s3Config={settings?.s3Sync}
|
||||
settings={settings}
|
||||
onAutoSave={handleAutoSave}
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -193,8 +193,11 @@ export function useSettings(): UseSettingsResult {
|
||||
const sanitizedOpenclawDir = sanitizeDir(
|
||||
mergedSettings.openclawConfigDir,
|
||||
);
|
||||
const { webdavSync: _ignoredWebdavSync, ...restSettings } =
|
||||
mergedSettings;
|
||||
const {
|
||||
webdavSync: _ignoredWebdavSync,
|
||||
s3Sync: _ignoredS3Sync,
|
||||
...restSettings
|
||||
} = mergedSettings;
|
||||
|
||||
const payload: Settings = {
|
||||
...restSettings,
|
||||
@@ -327,8 +330,11 @@ export function useSettings(): UseSettingsResult {
|
||||
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
|
||||
const previousOpencodeDir = sanitizeDir(data?.opencodeConfigDir);
|
||||
const previousOpenclawDir = sanitizeDir(data?.openclawConfigDir);
|
||||
const { webdavSync: _ignoredWebdavSync, ...restSettings } =
|
||||
mergedSettings;
|
||||
const {
|
||||
webdavSync: _ignoredWebdavSync,
|
||||
s3Sync: _ignoredS3Sync,
|
||||
...restSettings
|
||||
} = mergedSettings;
|
||||
|
||||
const payload: Settings = {
|
||||
...restSettings,
|
||||
|
||||
@@ -546,6 +546,107 @@
|
||||
"confirm": "Confirm Upload"
|
||||
}
|
||||
},
|
||||
"syncType": {
|
||||
"label": "Sync Type",
|
||||
"webdav": "WebDAV",
|
||||
"s3": "S3 Compatible"
|
||||
},
|
||||
"s3Sync": {
|
||||
"presets": {
|
||||
"label": "Provider",
|
||||
"awsS3": "AWS S3",
|
||||
"awsS3Hint": "Use IAM Access Key with s3:PutObject/GetObject/HeadObject permissions. Region: us-east-1, ap-northeast-1, etc.",
|
||||
"minio": "MinIO",
|
||||
"minioHint": "Custom endpoint required, e.g. http://minio.local:9000. Region can be 'us-east-1'.",
|
||||
"r2": "Cloudflare R2",
|
||||
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com. Region: 'auto'.",
|
||||
"oss": "Alibaba Cloud OSS",
|
||||
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com. Region: cn-hangzhou, cn-shanghai, etc.",
|
||||
"cos": "Tencent Cloud COS",
|
||||
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com. Region: ap-guangzhou, ap-beijing, etc.",
|
||||
"obs": "Huawei OBS",
|
||||
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com. Region: cn-north-4, cn-east-3, etc.",
|
||||
"custom": "S3 Compatible (Custom)",
|
||||
"customHint": "Any S3-compatible service. Custom endpoint required."
|
||||
},
|
||||
"region": "Region",
|
||||
"regionPlaceholder": "e.g. us-east-1",
|
||||
"bucket": "Bucket",
|
||||
"bucketPlaceholder": "e.g. my-cc-switch-bucket",
|
||||
"accessKeyId": "Access Key ID",
|
||||
"accessKeyIdPlaceholder": "e.g. AKIA...",
|
||||
"secretAccessKey": "Secret Access Key",
|
||||
"secretAccessKeyPlaceholder": "Leave unchanged to keep the existing secret",
|
||||
"endpoint": "Endpoint (optional for AWS)",
|
||||
"endpointHint": "Required for most S3-compatible services",
|
||||
"endpointPlaceholder": "e.g. https://s3-compatible.example.com",
|
||||
"remoteRoot": "Remote Root Directory",
|
||||
"remoteRootDefault": "Default: cc-switch-sync",
|
||||
"profile": "Sync Profile Name",
|
||||
"profileDefault": "Default: default",
|
||||
"autoSync": "Auto Sync",
|
||||
"autoSyncHint": "When enabled, each database change triggers an automatic S3 upload.",
|
||||
"enabled": "Enable S3 Sync",
|
||||
"enabledHint": "Turn on S3 sync for manual and automatic cloud operations.",
|
||||
"mutualExclusionTitle": "Switch Sync Type",
|
||||
"mutualExclusionMessage": "Enabling S3 sync will disable the current WebDAV sync. Continue?",
|
||||
"mutualExclusionMessageReverse": "Enabling WebDAV sync will disable the current S3 sync. Continue?",
|
||||
"mutualExclusionFailed": "Failed to switch sync type: {{error}}",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionOk": "S3 connection successful",
|
||||
"test": "Test Connection",
|
||||
"testing": "Testing...",
|
||||
"testSuccess": "S3 connection successful",
|
||||
"testFailed": "S3 connection failed: {{error}}",
|
||||
"save": "Save Config",
|
||||
"saving": "Saving...",
|
||||
"saveFailed": "Failed to save config: {{error}}",
|
||||
"saveAndTestSuccess": "Config saved, S3 connection OK",
|
||||
"saveAndTestFailed": "Config saved, but S3 connection test failed: {{error}}",
|
||||
"upload": "Upload to Cloud",
|
||||
"uploading": "Uploading...",
|
||||
"uploadSuccess": "Uploaded to S3",
|
||||
"uploadFailed": "S3 upload failed: {{error}}",
|
||||
"autoSyncFailedToast": "S3 auto sync failed: {{error}}",
|
||||
"download": "Download from Cloud",
|
||||
"downloading": "Downloading...",
|
||||
"downloadSuccess": "Downloaded and restored from S3",
|
||||
"downloadFailed": "S3 download failed: {{error}}",
|
||||
"lastSync": "Last sync: {{time}}",
|
||||
"autoSyncLastErrorTitle": "Last S3 auto sync failed",
|
||||
"autoSyncLastErrorHint": "Please check network or S3 settings. Auto sync will retry on future changes.",
|
||||
"missingBucket": "Please enter the S3 bucket",
|
||||
"noRemoteData": "No sync data found in the S3 bucket",
|
||||
"incompatibleVersion": "Remote data is incompatible (version {{version}}). This client supports protocol v2 / db-v6.",
|
||||
"unsaved": "Unsaved",
|
||||
"saved": "Saved",
|
||||
"unsavedChanges": "Please save config first",
|
||||
"saveBeforeSync": "Save configuration first to enable upload/download.",
|
||||
"fetchingRemote": "Fetching remote info...",
|
||||
"fetchRemoteFailed": "Failed to fetch remote info. Please check configuration and network.",
|
||||
"notConfigured": "S3 sync is not configured",
|
||||
"disabled": "S3 sync is disabled",
|
||||
"confirmDownload": {
|
||||
"title": "Restore from S3",
|
||||
"deviceName": "Uploaded by",
|
||||
"createdAt": "Uploaded at",
|
||||
"artifacts": "Contents",
|
||||
"warning": "This will overwrite all local data and skill configurations",
|
||||
"confirm": "Confirm Restore"
|
||||
},
|
||||
"confirmUpload": {
|
||||
"title": "Upload to S3",
|
||||
"content": "The following will be synced to the S3 bucket:",
|
||||
"dbItem": "Database (all provider configs and data)",
|
||||
"skillsItem": "Skills (all custom skills)",
|
||||
"targetPath": "Target object path",
|
||||
"existingData": "Existing cloud data",
|
||||
"deviceName": "Uploaded by",
|
||||
"createdAt": "Uploaded at",
|
||||
"warning": "This will overwrite existing sync data at the S3 path",
|
||||
"confirm": "Confirm Upload"
|
||||
}
|
||||
},
|
||||
"autoReload": "Data refreshed",
|
||||
"languageOptionChinese": "简体中文",
|
||||
"languageOptionTraditionalChinese": "繁體中文",
|
||||
|
||||
@@ -546,6 +546,107 @@
|
||||
"confirm": "アップロードを実行"
|
||||
}
|
||||
},
|
||||
"syncType": {
|
||||
"label": "同期方式",
|
||||
"webdav": "WebDAV",
|
||||
"s3": "S3互換ストレージ"
|
||||
},
|
||||
"s3Sync": {
|
||||
"presets": {
|
||||
"label": "サービス",
|
||||
"awsS3": "AWS S3",
|
||||
"awsS3Hint": "IAM Access Keyを使用。s3:PutObject/GetObject/HeadObject権限が必要です。Region例: us-east-1, ap-northeast-1",
|
||||
"minio": "MinIO",
|
||||
"minioHint": "カスタムEndpointが必要です(例: http://minio.local:9000)。Regionは us-east-1 に設定可能",
|
||||
"r2": "Cloudflare R2",
|
||||
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com、Regionは auto に設定",
|
||||
"oss": "Alibaba Cloud OSS",
|
||||
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com、Region例: cn-hangzhou, cn-shanghai",
|
||||
"cos": "Tencent Cloud COS",
|
||||
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com、Region例: ap-guangzhou, ap-beijing",
|
||||
"obs": "Huawei OBS",
|
||||
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com、Region例: cn-north-4, cn-east-3",
|
||||
"custom": "S3互換サービス(カスタム)",
|
||||
"customHint": "任意のS3互換サービス。カスタムEndpointが必要です"
|
||||
},
|
||||
"region": "リージョン",
|
||||
"regionPlaceholder": "例: us-east-1",
|
||||
"bucket": "バケット",
|
||||
"bucketPlaceholder": "例: my-cc-switch-bucket",
|
||||
"accessKeyId": "Access Key ID",
|
||||
"accessKeyIdPlaceholder": "例: AKIA...",
|
||||
"secretAccessKey": "Secret Access Key",
|
||||
"secretAccessKeyPlaceholder": "既存のシークレットを保持する場合は変更しないでください",
|
||||
"endpoint": "エンドポイント(AWSは空欄可)",
|
||||
"endpointHint": "多くの S3 互換サービスでは必須です",
|
||||
"endpointPlaceholder": "例: https://s3-compatible.example.com",
|
||||
"remoteRoot": "リモートルートディレクトリ",
|
||||
"remoteRootDefault": "デフォルト: cc-switch-sync",
|
||||
"profile": "同期プロファイル名",
|
||||
"profileDefault": "デフォルト: default",
|
||||
"autoSync": "自動同期",
|
||||
"autoSyncHint": "有効にすると、データベース変更のたびに S3 へ自動アップロードします。",
|
||||
"enabled": "S3 同期を有効化",
|
||||
"enabledHint": "S3 のアップロード/ダウンロードと自動同期を有効にします。",
|
||||
"mutualExclusionTitle": "同期方式の切り替え",
|
||||
"mutualExclusionMessage": "S3同期を有効にすると、現在のWebDAV同期が無効になります。続行しますか?",
|
||||
"mutualExclusionMessageReverse": "WebDAV同期を有効にすると、現在のS3同期が無効になります。続行しますか?",
|
||||
"mutualExclusionFailed": "同期方式の切り替えに失敗しました:{{error}}",
|
||||
"testConnection": "接続テスト",
|
||||
"connectionOk": "S3接続成功",
|
||||
"test": "接続テスト",
|
||||
"testing": "テスト中...",
|
||||
"testSuccess": "S3 接続成功",
|
||||
"testFailed": "S3 接続失敗:{{error}}",
|
||||
"save": "設定を保存",
|
||||
"saving": "保存中...",
|
||||
"saveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"saveAndTestSuccess": "設定を保存しました。S3 接続正常です",
|
||||
"saveAndTestFailed": "設定を保存しましたが、S3 接続テストに失敗しました:{{error}}",
|
||||
"upload": "クラウドにアップロード",
|
||||
"uploading": "アップロード中...",
|
||||
"uploadSuccess": "S3 にアップロードしました",
|
||||
"uploadFailed": "S3 アップロードに失敗しました:{{error}}",
|
||||
"autoSyncFailedToast": "S3 自動同期に失敗しました:{{error}}",
|
||||
"download": "クラウドからダウンロード",
|
||||
"downloading": "ダウンロード中...",
|
||||
"downloadSuccess": "S3 からダウンロード・復元しました",
|
||||
"downloadFailed": "S3 ダウンロードに失敗しました:{{error}}",
|
||||
"lastSync": "前回の同期:{{time}}",
|
||||
"autoSyncLastErrorTitle": "前回の S3 自動同期に失敗しました",
|
||||
"autoSyncLastErrorHint": "ネットワークまたは S3 設定を確認してください。次回の変更時に自動再試行されます。",
|
||||
"missingBucket": "S3 バケットを入力してください",
|
||||
"noRemoteData": "S3 バケットに同期データが見つかりません",
|
||||
"incompatibleVersion": "リモートデータに互換性がありません(バージョン {{version}})。このクライアントは protocol v2 / db-v6 をサポートしています。",
|
||||
"unsaved": "未保存",
|
||||
"saved": "保存済み",
|
||||
"unsavedChanges": "先に設定を保存してください",
|
||||
"saveBeforeSync": "アップロード/ダウンロードを有効にするには、先に設定を保存してください。",
|
||||
"fetchingRemote": "リモート情報を取得中...",
|
||||
"fetchRemoteFailed": "リモート情報の取得に失敗しました。設定とネットワークを確認してください。",
|
||||
"notConfigured": "S3同期が設定されていません",
|
||||
"disabled": "S3同期が無効です",
|
||||
"confirmDownload": {
|
||||
"title": "S3 から復元",
|
||||
"deviceName": "アップロード元",
|
||||
"createdAt": "アップロード日時",
|
||||
"artifacts": "内容",
|
||||
"warning": "ローカルのすべてのデータとスキル設定が上書きされます",
|
||||
"confirm": "復元を実行"
|
||||
},
|
||||
"confirmUpload": {
|
||||
"title": "S3 にアップロード",
|
||||
"content": "以下の内容を S3 バケットに同期します:",
|
||||
"dbItem": "データベース(すべてのプロバイダー設定とデータ)",
|
||||
"skillsItem": "スキル(すべてのカスタムスキル)",
|
||||
"targetPath": "保存先オブジェクトパス",
|
||||
"existingData": "クラウドの既存データ",
|
||||
"deviceName": "アップロード元",
|
||||
"createdAt": "アップロード日時",
|
||||
"warning": "この S3 パスにある既存の同期データが上書きされます",
|
||||
"confirm": "アップロードを実行"
|
||||
}
|
||||
},
|
||||
"autoReload": "データを更新しました",
|
||||
"languageOptionChinese": "简体中文",
|
||||
"languageOptionTraditionalChinese": "繁體中文",
|
||||
|
||||
@@ -546,6 +546,107 @@
|
||||
"confirm": "確認上傳"
|
||||
}
|
||||
},
|
||||
"syncType": {
|
||||
"label": "同步方式",
|
||||
"webdav": "WebDAV",
|
||||
"s3": "S3 相容儲存"
|
||||
},
|
||||
"s3Sync": {
|
||||
"presets": {
|
||||
"label": "服務商",
|
||||
"awsS3": "AWS S3",
|
||||
"awsS3Hint": "使用 IAM Access Key,需要 s3:PutObject/GetObject/HeadObject 權限。Region 範例:us-east-1, ap-northeast-1",
|
||||
"minio": "MinIO",
|
||||
"minioHint": "需要自訂 Endpoint,如 http://minio.local:9000。Region 可設為 us-east-1",
|
||||
"r2": "Cloudflare R2",
|
||||
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com,Region 設為 auto",
|
||||
"oss": "阿里雲 OSS",
|
||||
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com,Region 範例:cn-hangzhou, cn-shanghai",
|
||||
"cos": "騰訊雲 COS",
|
||||
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com,Region 範例:ap-guangzhou, ap-beijing",
|
||||
"obs": "華為雲 OBS",
|
||||
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com,Region 範例:cn-north-4, cn-east-3",
|
||||
"custom": "S3 相容服務(自訂)",
|
||||
"customHint": "任意 S3 相容服務,需要自訂 Endpoint"
|
||||
},
|
||||
"region": "區域 (Region)",
|
||||
"regionPlaceholder": "例如 us-east-1",
|
||||
"bucket": "儲存桶 (Bucket)",
|
||||
"bucketPlaceholder": "例如 my-cc-switch-bucket",
|
||||
"accessKeyId": "Access Key ID",
|
||||
"accessKeyIdPlaceholder": "例如 AKIA...",
|
||||
"secretAccessKey": "Secret Access Key",
|
||||
"secretAccessKeyPlaceholder": "留空則保持現有金鑰",
|
||||
"endpoint": "Endpoint(AWS 可留空)",
|
||||
"endpointHint": "多數 S3 相容服務需要填寫",
|
||||
"endpointPlaceholder": "例如 https://s3-compatible.example.com",
|
||||
"remoteRoot": "遠端根目錄",
|
||||
"remoteRootDefault": "預設: cc-switch-sync",
|
||||
"profile": "同步設定檔名稱",
|
||||
"profileDefault": "預設: default",
|
||||
"autoSync": "自動同步",
|
||||
"autoSyncHint": "開啟後每次資料庫變更都會自動上傳到 S3。",
|
||||
"enabled": "啟用 S3 同步",
|
||||
"enabledHint": "開啟後可使用 S3 上傳/下載和自動同步。",
|
||||
"mutualExclusionTitle": "切換同步方式",
|
||||
"mutualExclusionMessage": "啟用 S3 同步將停用目前的 WebDAV 同步,是否繼續?",
|
||||
"mutualExclusionMessageReverse": "啟用 WebDAV 同步將停用目前的 S3 同步,是否繼續?",
|
||||
"mutualExclusionFailed": "切換同步方式失敗:{{error}}",
|
||||
"testConnection": "測試連線",
|
||||
"connectionOk": "S3 連線成功",
|
||||
"test": "測試連線",
|
||||
"testing": "測試中...",
|
||||
"testSuccess": "S3 連線成功",
|
||||
"testFailed": "S3 連線失敗:{{error}}",
|
||||
"save": "儲存設定",
|
||||
"saving": "儲存中...",
|
||||
"saveFailed": "儲存設定失敗:{{error}}",
|
||||
"saveAndTestSuccess": "設定已儲存,S3 連線正常",
|
||||
"saveAndTestFailed": "設定已儲存,但 S3 連線測試失敗:{{error}}",
|
||||
"upload": "上傳至雲端",
|
||||
"uploading": "上傳中...",
|
||||
"uploadSuccess": "已上傳到 S3",
|
||||
"uploadFailed": "S3 上傳失敗:{{error}}",
|
||||
"autoSyncFailedToast": "S3 自動同步失敗:{{error}}",
|
||||
"download": "從雲端下載",
|
||||
"downloading": "下載中...",
|
||||
"downloadSuccess": "已從 S3 下載並還原",
|
||||
"downloadFailed": "S3 下載失敗:{{error}}",
|
||||
"lastSync": "上次同步:{{time}}",
|
||||
"autoSyncLastErrorTitle": "上次 S3 自動同步失敗",
|
||||
"autoSyncLastErrorHint": "請檢查網路或 S3 設定,系統會在後續變更時繼續自動重試。",
|
||||
"missingBucket": "請填寫 S3 儲存桶",
|
||||
"noRemoteData": "S3 儲存桶中沒有找到同步資料",
|
||||
"incompatibleVersion": "遠端資料版本不相容(版本 {{version}}),目前支援協議 v2 / db-v6",
|
||||
"unsaved": "未儲存",
|
||||
"saved": "已儲存",
|
||||
"unsavedChanges": "請先儲存設定",
|
||||
"saveBeforeSync": "請先儲存設定,再使用上傳/下載。",
|
||||
"fetchingRemote": "取得遠端資訊...",
|
||||
"fetchRemoteFailed": "取得遠端資訊失敗,請檢查設定和網路後重試。",
|
||||
"notConfigured": "未設定 S3 同步",
|
||||
"disabled": "S3 同步未啟用",
|
||||
"confirmDownload": {
|
||||
"title": "從 S3 還原",
|
||||
"deviceName": "上傳裝置",
|
||||
"createdAt": "上傳時間",
|
||||
"artifacts": "包含內容",
|
||||
"warning": "還原將覆寫本地所有資料和技能設定",
|
||||
"confirm": "確認還原"
|
||||
},
|
||||
"confirmUpload": {
|
||||
"title": "上傳到 S3",
|
||||
"content": "將同步以下內容到 S3 儲存桶:",
|
||||
"dbItem": "資料庫(所有 Provider 設定和資料)",
|
||||
"skillsItem": "技能包(所有自訂技能)",
|
||||
"targetPath": "目標物件路徑",
|
||||
"existingData": "雲端已有資料",
|
||||
"deviceName": "上傳裝置",
|
||||
"createdAt": "上傳時間",
|
||||
"warning": "將覆寫該 S3 路徑下已有的同步資料",
|
||||
"confirm": "確認上傳"
|
||||
}
|
||||
},
|
||||
"autoReload": "資料已重新整理",
|
||||
"languageOptionChinese": "简体中文",
|
||||
"languageOptionTraditionalChinese": "繁體中文",
|
||||
|
||||
@@ -546,6 +546,107 @@
|
||||
"confirm": "确认上传"
|
||||
}
|
||||
},
|
||||
"syncType": {
|
||||
"label": "同步方式",
|
||||
"webdav": "WebDAV",
|
||||
"s3": "S3 兼容存储"
|
||||
},
|
||||
"s3Sync": {
|
||||
"presets": {
|
||||
"label": "服务商",
|
||||
"awsS3": "AWS S3",
|
||||
"awsS3Hint": "使用 IAM Access Key,需要 s3:PutObject/GetObject/HeadObject 权限。Region 示例:us-east-1, ap-northeast-1",
|
||||
"minio": "MinIO",
|
||||
"minioHint": "需要自定义 Endpoint,如 http://minio.local:9000。Region 可设为 us-east-1",
|
||||
"r2": "Cloudflare R2",
|
||||
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com,Region 设为 auto",
|
||||
"oss": "阿里云 OSS",
|
||||
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com,Region 示例:cn-hangzhou, cn-shanghai",
|
||||
"cos": "腾讯云 COS",
|
||||
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com,Region 示例:ap-guangzhou, ap-beijing",
|
||||
"obs": "华为云 OBS",
|
||||
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com,Region 示例:cn-north-4, cn-east-3",
|
||||
"custom": "S3 兼容服务(自定义)",
|
||||
"customHint": "任意 S3 兼容服务,需要自定义 Endpoint"
|
||||
},
|
||||
"region": "区域 (Region)",
|
||||
"regionPlaceholder": "例如 us-east-1",
|
||||
"bucket": "存储桶 (Bucket)",
|
||||
"bucketPlaceholder": "例如 my-cc-switch-bucket",
|
||||
"accessKeyId": "Access Key ID",
|
||||
"accessKeyIdPlaceholder": "例如 AKIA...",
|
||||
"secretAccessKey": "Secret Access Key",
|
||||
"secretAccessKeyPlaceholder": "留空则保持现有密钥",
|
||||
"endpoint": "Endpoint(AWS 可留空)",
|
||||
"endpointHint": "多数 S3 兼容服务需要填写",
|
||||
"endpointPlaceholder": "例如 https://s3-compatible.example.com",
|
||||
"remoteRoot": "远程根目录",
|
||||
"remoteRootDefault": "默认: cc-switch-sync",
|
||||
"profile": "同步配置名",
|
||||
"profileDefault": "默认: default",
|
||||
"autoSync": "自动同步",
|
||||
"autoSyncHint": "开启后每次数据库变更都会自动上传到 S3。",
|
||||
"enabled": "启用 S3 同步",
|
||||
"enabledHint": "开启后可使用 S3 上传/下载和自动同步。",
|
||||
"mutualExclusionTitle": "切换同步方式",
|
||||
"mutualExclusionMessage": "启用 S3 同步将禁用当前的 WebDAV 同步,是否继续?",
|
||||
"mutualExclusionMessageReverse": "启用 WebDAV 同步将禁用当前的 S3 同步,是否继续?",
|
||||
"mutualExclusionFailed": "切换同步方式失败:{{error}}",
|
||||
"testConnection": "测试连接",
|
||||
"connectionOk": "S3 连接成功",
|
||||
"test": "测试连接",
|
||||
"testing": "测试中...",
|
||||
"testSuccess": "S3 连接成功",
|
||||
"testFailed": "S3 连接失败:{{error}}",
|
||||
"save": "保存配置",
|
||||
"saving": "保存中...",
|
||||
"saveFailed": "保存配置失败:{{error}}",
|
||||
"saveAndTestSuccess": "配置已保存,S3 连接正常",
|
||||
"saveAndTestFailed": "配置已保存,但 S3 连接测试失败:{{error}}",
|
||||
"upload": "上传到云端",
|
||||
"uploading": "上传中...",
|
||||
"uploadSuccess": "已上传到 S3",
|
||||
"uploadFailed": "S3 上传失败:{{error}}",
|
||||
"autoSyncFailedToast": "S3 自动同步失败:{{error}}",
|
||||
"download": "从云端下载",
|
||||
"downloading": "下载中...",
|
||||
"downloadSuccess": "已从 S3 下载并恢复",
|
||||
"downloadFailed": "S3 下载失败:{{error}}",
|
||||
"lastSync": "上次同步:{{time}}",
|
||||
"autoSyncLastErrorTitle": "上次 S3 自动同步失败",
|
||||
"autoSyncLastErrorHint": "请检查网络或 S3 配置,系统会在后续变更时继续自动重试。",
|
||||
"missingBucket": "请填写 S3 存储桶",
|
||||
"noRemoteData": "S3 存储桶中没有找到同步数据",
|
||||
"incompatibleVersion": "远端数据版本不兼容(版本 {{version}}),当前支持协议 v2 / db-v6",
|
||||
"unsaved": "未保存",
|
||||
"saved": "已保存",
|
||||
"unsavedChanges": "请先保存配置",
|
||||
"saveBeforeSync": "请先保存配置,再使用上传/下载。",
|
||||
"fetchingRemote": "获取远端信息...",
|
||||
"fetchRemoteFailed": "获取远端信息失败,请检查配置和网络后重试。",
|
||||
"notConfigured": "未配置 S3 同步",
|
||||
"disabled": "S3 同步未启用",
|
||||
"confirmDownload": {
|
||||
"title": "从 S3 恢复",
|
||||
"deviceName": "上传设备",
|
||||
"createdAt": "上传时间",
|
||||
"artifacts": "包含内容",
|
||||
"warning": "恢复将覆盖本地所有数据和技能配置",
|
||||
"confirm": "确认恢复"
|
||||
},
|
||||
"confirmUpload": {
|
||||
"title": "上传到 S3",
|
||||
"content": "将同步以下内容到 S3 存储桶:",
|
||||
"dbItem": "数据库(所有 Provider 配置和数据)",
|
||||
"skillsItem": "技能包(所有自定义技能)",
|
||||
"targetPath": "目标对象路径",
|
||||
"existingData": "云端已有数据",
|
||||
"deviceName": "上传设备",
|
||||
"createdAt": "上传时间",
|
||||
"warning": "将覆盖该 S3 路径下已有的同步数据",
|
||||
"confirm": "确认上传"
|
||||
}
|
||||
},
|
||||
"autoReload": "数据已刷新",
|
||||
"languageOptionChinese": "简体中文",
|
||||
"languageOptionTraditionalChinese": "繁體中文",
|
||||
|
||||
+40
-1
@@ -1,5 +1,10 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Settings, WebDavSyncSettings, RemoteSnapshotInfo } from "@/types";
|
||||
import type {
|
||||
Settings,
|
||||
WebDavSyncSettings,
|
||||
S3SyncSettings,
|
||||
RemoteSnapshotInfo,
|
||||
} from "@/types";
|
||||
import type { AppId } from "./types";
|
||||
|
||||
export interface ConfigTransferResult {
|
||||
@@ -142,6 +147,40 @@ export const settingsApi = {
|
||||
return await invoke("webdav_sync_fetch_remote_info");
|
||||
},
|
||||
|
||||
// ===== S3 Sync API =====
|
||||
|
||||
async s3TestConnection(
|
||||
settings: S3SyncSettings,
|
||||
preserveEmptyPassword = true,
|
||||
): Promise<WebDavTestResult> {
|
||||
return await invoke("s3_test_connection", {
|
||||
settings,
|
||||
preserveEmptyPassword,
|
||||
});
|
||||
},
|
||||
|
||||
async s3SyncUpload(): Promise<WebDavSyncResult> {
|
||||
return await invoke("s3_sync_upload");
|
||||
},
|
||||
|
||||
async s3SyncDownload(): Promise<WebDavSyncResult> {
|
||||
return await invoke("s3_sync_download");
|
||||
},
|
||||
|
||||
async s3SyncSaveSettings(
|
||||
settings: S3SyncSettings,
|
||||
passwordTouched: boolean,
|
||||
): Promise<{ success: boolean }> {
|
||||
return await invoke("s3_sync_save_settings", {
|
||||
settings,
|
||||
passwordTouched,
|
||||
});
|
||||
},
|
||||
|
||||
async s3SyncFetchRemoteInfo(): Promise<RemoteSnapshotInfo | { empty: true }> {
|
||||
return await invoke("s3_sync_fetch_remote_info");
|
||||
},
|
||||
|
||||
async syncCurrentProvidersLive(): Promise<void> {
|
||||
const result = (await invoke("sync_current_providers_live")) as {
|
||||
success?: boolean;
|
||||
|
||||
@@ -289,6 +289,20 @@ export interface WebDavSyncSettings {
|
||||
status?: WebDavSyncStatus;
|
||||
}
|
||||
|
||||
// S3 同步配置
|
||||
export interface S3SyncSettings {
|
||||
enabled?: boolean;
|
||||
autoSync?: boolean;
|
||||
region?: string;
|
||||
bucket?: string;
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
endpoint?: string;
|
||||
remoteRoot?: string;
|
||||
profile?: string;
|
||||
status?: WebDavSyncStatus;
|
||||
}
|
||||
|
||||
export type RemoteSnapshotLayout = "current" | "legacy";
|
||||
|
||||
// 远端快照信息(下载前预览)
|
||||
@@ -382,6 +396,9 @@ export interface Settings {
|
||||
// ===== WebDAV v2 同步设置 =====
|
||||
webdavSync?: WebDavSyncSettings;
|
||||
|
||||
// ===== S3 同步设置 =====
|
||||
s3Sync?: S3SyncSettings;
|
||||
|
||||
// ===== 备份策略设置 =====
|
||||
// Auto-backup interval in hours (0=disabled, default 24)
|
||||
backupIntervalHours?: number;
|
||||
|
||||
Reference in New Issue
Block a user