♻️ refactor: Use ProxyManager bypass rules instead of custom dispatcher for S3

Co-authored-by: GeorgeDong32 <98630204+GeorgeDong32@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-03 06:28:52 +00:00 committed by GeorgeDong32
parent ea9e7fccda
commit b1e63470a5
3 changed files with 94 additions and 23 deletions

View File

@ -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 {

View File

@ -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()

View File

@ -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
*/