diff --git a/src/main/services/S3Storage.ts b/src/main/services/S3Storage.ts index ae63e38745..73c7ddee2c 100644 --- a/src/main/services/S3Storage.ts +++ b/src/main/services/S3Storage.ts @@ -39,7 +39,7 @@ export default class S3Storage { private root: string constructor(config: S3Config) { - const { endpoint, region, accessKeyId, secretAccessKey, bucket, root } = config + const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config const usePathStyle = (() => { if (!endpoint) return false @@ -59,22 +59,24 @@ export default class S3Storage { } })() - // Fix for S3 backup failure when using proxy - // Issue: When proxy is enabled, S3 uploads can fail with incomplete writes - // Error: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159" - // Root cause: AWS SDK uses global fetch/undici dispatcher which routes through proxy, - // causing data corruption or incomplete transfers for large files - // Solution: Configure S3Client with a direct dispatcher that bypasses the global proxy - const directDispatcher = new UndiciAgent({ - connect: { - timeout: 60000 // 60 second connection timeout - } - }) + // Conditionally bypass proxy for S3 requests based on user configuration + // When bypassProxy is true (default), S3 requests use a direct dispatcher to avoid + // proxy interference with large file uploads that can cause incomplete transfers + // Error example: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159" + let requestHandler: FetchHttpHandler | undefined - const requestHandler = new FetchHttpHandler({ - requestTimeout: 300000, // 5 minute request timeout for large files - dispatcher: directDispatcher - }) + if (bypassProxy) { + const directDispatcher = new UndiciAgent({ + connect: { + timeout: 60000 // 60 second connection timeout + } + }) + + requestHandler = new FetchHttpHandler({ + requestTimeout: 300000, // 5 minute request timeout for large files + dispatcher: directDispatcher + }) + } this.client = new S3Client({ region, @@ -84,7 +86,7 @@ export default class S3Storage { secretAccessKey: secretAccessKey }, forcePathStyle: usePathStyle, - requestHandler + ...(requestHandler && { requestHandler }) }) this.bucket = bucket diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 36ce60edb6..ac43774771 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3419,6 +3419,10 @@ "help": "When enabled, file data will be skipped during backup, only configuration information will be backed up, significantly reducing backup file size", "label": "Lightweight Backup" }, + "bypassProxy": { + "help": "When enabled, S3 requests bypass the application proxy to prevent data corruption during large file uploads. Recommended to keep enabled unless your S3 endpoint requires proxy access.", + "label": "Bypass Proxy" + }, "syncStatus": { "error": "Sync error: {{message}}", "label": "Sync Status", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0acb816a1c..d3862795eb 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3419,6 +3419,10 @@ "help": "开启后备份时将跳过文件数据,仅备份配置信息,显著减小备份文件体积", "label": "精简备份" }, + "bypassProxy": { + "help": "开启后,S3 请求将绕过应用程序代理,以防止大文件上传时的数据损坏。建议保持开启,除非您的 S3 端点需要通过代理访问。", + "label": "绕过代理" + }, "syncStatus": { "error": "同步错误: {{message}}", "label": "同步状态", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 913a4de5ad..aa9d3f9a67 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3419,6 +3419,10 @@ "help": "開啟後備份時將跳過檔案資料,僅備份設定資訊,顯著減小備份檔案體積", "label": "精簡備份" }, + "bypassProxy": { + "help": "開啟後,S3 請求將繞過應用程式代理,以防止大檔案上傳時的資料損壞。建議保持開啟,除非您的 S3 端點需要透過代理存取。", + "label": "繞過代理" + }, "syncStatus": { "error": "同步錯誤: {{message}}", "label": "同步狀態", diff --git a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx index fbb57cdec6..ec88266c03 100644 --- a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx @@ -31,7 +31,8 @@ const S3Settings: FC = () => { root: s3RootInit = '', syncInterval: s3SyncIntervalInit = 0, maxBackups: s3MaxBackupsInit = 5, - skipBackupFile: s3SkipBackupFileInit = false + skipBackupFile: s3SkipBackupFileInit = false, + bypassProxy: s3BypassProxyInit = true } = s3 const [endpoint, setEndpoint] = useState(s3EndpointInit) @@ -41,6 +42,7 @@ const S3Settings: FC = () => { const [secretAccessKey, setSecretAccessKey] = useState(s3SecretAccessKeyInit) const [root, setRoot] = useState(s3RootInit) const [skipBackupFile, setSkipBackupFile] = useState(s3SkipBackupFileInit) + const [bypassProxy, setBypassProxy] = useState(s3BypassProxyInit) const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [syncInterval, setSyncInterval] = useState(s3SyncIntervalInit) @@ -82,6 +84,11 @@ const S3Settings: FC = () => { dispatch(setS3Partial({ skipBackupFile: value })) } + const onBypassProxyChange = (value: boolean) => { + setBypassProxy(value) + dispatch(setS3Partial({ bypassProxy: value })) + } + const renderSyncStatus = () => { if (!endpoint) return null @@ -261,6 +268,14 @@ const S3Settings: FC = () => { {t('settings.data.s3.skipBackupFile.help')} + + + {t('settings.data.s3.bypassProxy.label')} + + + + {t('settings.data.s3.bypassProxy.help')} + {syncInterval > 0 && ( <> diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 45f521b3df..c24a30d9e7 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -401,7 +401,8 @@ export const initialState: SettingsState = { autoSync: false, syncInterval: 0, maxBackups: 0, - skipBackupFile: false + skipBackupFile: false, + bypassProxy: true }, // Developer mode diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c86e80a157..7d2b5681ce 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -883,6 +883,7 @@ export type S3Config = { autoSync: boolean syncInterval: number maxBackups: number + bypassProxy?: boolean // Whether to bypass proxy for S3 requests (default: true) } export type { Message } from './newMessage'