diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index f331254fdf..8c22c9bd23 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -33,6 +33,7 @@ class BackupManager { accessKeyId: string secretAccessKey: string root?: string + bypassProxy?: boolean } | null = null private cachedWebdavConnectionConfig: { @@ -120,7 +121,8 @@ class BackupManager { cachedConfig.bucket === config.bucket && cachedConfig.accessKeyId === config.accessKeyId && cachedConfig.secretAccessKey === config.secretAccessKey && - cachedConfig.root === config.root + cachedConfig.root === config.root && + cachedConfig.bypassProxy === config.bypassProxy ) } @@ -147,6 +149,11 @@ class BackupManager { const configChanged = !this.isS3ConfigEqual(this.cachedS3ConnectionConfig, config) if (configChanged || !this.s3Storage) { + // Destroy old instance to clean up bypass rules + if (this.s3Storage) { + this.s3Storage.destroy() + } + this.s3Storage = new S3Storage(config) // 只缓存连接相关的配置字段 this.cachedS3ConnectionConfig = { @@ -155,7 +162,8 @@ class BackupManager { bucket: config.bucket, accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, - root: config.root + root: config.root, + bypassProxy: config.bypassProxy } logger.debug('[BackupManager] Created new S3Storage instance') } else { diff --git a/src/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts index a4a2211a20..7d7adcd92d 100644 --- a/src/main/services/ProxyManager.ts +++ b/src/main/services/ProxyManager.ts @@ -12,6 +12,8 @@ import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher const logger = loggerService.withContext('ProxyManager') let byPassRules: string[] = [] +// Dynamic bypass rules that can be added/removed at runtime (e.g., for S3 endpoints) +let dynamicBypassRules: string[] = [] type HostnameMatchType = 'exact' | 'wildcardSubdomain' | 'generalWildcard' @@ -222,7 +224,10 @@ export const updateByPassRules = (rules: string[]): void => { byPassRules = rules parsedByPassRules = [] - for (const rule of rules) { + // Combine static bypass rules with dynamic ones + const allRules = [...rules, ...dynamicBypassRules] + + for (const rule of allRules) { const parsedRule = parseProxyBypassRule(rule) if (parsedRule) { parsedByPassRules.push(parsedRule) @@ -232,6 +237,33 @@ export const updateByPassRules = (rules: string[]): void => { } } +/** + * Add a dynamic bypass rule at runtime (e.g., for S3 endpoints) + * @param rule - The bypass rule to add (e.g., hostname or domain pattern) + */ +export const addDynamicBypassRule = (rule: string): void => { + if (!dynamicBypassRules.includes(rule)) { + dynamicBypassRules.push(rule) + // Re-parse all rules with the new dynamic rule + updateByPassRules(byPassRules) + logger.info(`Added dynamic bypass rule: ${rule}`) + } +} + +/** + * Remove a dynamic bypass rule + * @param rule - The bypass rule to remove + */ +export const removeDynamicBypassRule = (rule: string): void => { + const index = dynamicBypassRules.indexOf(rule) + if (index !== -1) { + dynamicBypassRules.splice(index, 1) + // Re-parse all rules without the removed dynamic rule + updateByPassRules(byPassRules) + logger.info(`Removed dynamic bypass rule: ${rule}`) + } +} + export const isByPass = (url: string) => { if (parsedByPassRules.length === 0) { return false @@ -586,6 +618,22 @@ export class ProxyManager { // set proxy for electron app.setProxy(config) } + + /** + * Add a dynamic bypass rule for a specific endpoint + * @param rule - The bypass rule to add (e.g., hostname or domain pattern) + */ + addDynamicBypassRule(rule: string): void { + addDynamicBypassRule(rule) + } + + /** + * Remove a dynamic bypass rule + * @param rule - The bypass rule to remove + */ + removeDynamicBypassRule(rule: string): void { + removeDynamicBypassRule(rule) + } } export const proxyManager = new ProxyManager() diff --git a/src/main/services/S3Storage.ts b/src/main/services/S3Storage.ts index 73c7ddee2c..333e6fde9b 100644 --- a/src/main/services/S3Storage.ts +++ b/src/main/services/S3Storage.ts @@ -6,13 +6,13 @@ import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3' -import { FetchHttpHandler } from '@smithy/fetch-http-handler' import { loggerService } from '@logger' import type { S3Config } from '@types' import * as net from 'net' -import { Agent as UndiciAgent } from 'undici' import { Readable } from 'stream' +import { proxyManager } from './ProxyManager' + const logger = loggerService.withContext('S3Storage') /** @@ -37,10 +37,13 @@ export default class S3Storage { private client: S3Client private bucket: string private root: string + private endpoint: string constructor(config: S3Config) { const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config + this.endpoint = endpoint + const usePathStyle = (() => { if (!endpoint) return false @@ -59,23 +62,20 @@ export default class S3Storage { } })() - // 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 + // Use ProxyManager's dynamic bypass rules instead of custom dispatcher + // When bypassProxy is true (default), add the S3 endpoint to proxy bypass rules + // 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 - - 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 - }) + if (bypassProxy && endpoint) { + try { + const url = new URL(endpoint) + const hostname = url.hostname + // Add the hostname to dynamic bypass rules + proxyManager.addDynamicBypassRule(hostname) + logger.debug(`[S3Storage] Added S3 endpoint to bypass rules: ${hostname}`) + } catch (e) { + logger.warn(`[S3Storage] Failed to add endpoint to bypass rules: ${endpoint}`, e as Error) + } } this.client = new S3Client({ @@ -85,8 +85,7 @@ export default class S3Storage { accessKeyId: accessKeyId, secretAccessKey: secretAccessKey }, - forcePathStyle: usePathStyle, - ...(requestHandler && { requestHandler }) + forcePathStyle: usePathStyle }) this.bucket = bucket @@ -99,6 +98,22 @@ export default class S3Storage { this.checkConnection = this.checkConnection.bind(this) } + /** + * Clean up resources and remove bypass rules + */ + destroy(): void { + if (this.endpoint) { + try { + const url = new URL(this.endpoint) + const hostname = url.hostname + proxyManager.removeDynamicBypassRule(hostname) + logger.debug(`[S3Storage] Removed S3 endpoint from bypass rules: ${hostname}`) + } catch (e) { + logger.warn(`[S3Storage] Failed to remove endpoint from bypass rules: ${this.endpoint}`, e as Error) + } + } + } + /** * 内部辅助方法,用来拼接带 root 的对象 key */