♻️ 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 accessKeyId: string
secretAccessKey: string secretAccessKey: string
root?: string root?: string
bypassProxy?: boolean
} | null = null } | null = null
private cachedWebdavConnectionConfig: { private cachedWebdavConnectionConfig: {
@ -120,7 +121,8 @@ class BackupManager {
cachedConfig.bucket === config.bucket && cachedConfig.bucket === config.bucket &&
cachedConfig.accessKeyId === config.accessKeyId && cachedConfig.accessKeyId === config.accessKeyId &&
cachedConfig.secretAccessKey === config.secretAccessKey && 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) const configChanged = !this.isS3ConfigEqual(this.cachedS3ConnectionConfig, config)
if (configChanged || !this.s3Storage) { if (configChanged || !this.s3Storage) {
// Destroy old instance to clean up bypass rules
if (this.s3Storage) {
this.s3Storage.destroy()
}
this.s3Storage = new S3Storage(config) this.s3Storage = new S3Storage(config)
// 只缓存连接相关的配置字段 // 只缓存连接相关的配置字段
this.cachedS3ConnectionConfig = { this.cachedS3ConnectionConfig = {
@ -155,7 +162,8 @@ class BackupManager {
bucket: config.bucket, bucket: config.bucket,
accessKeyId: config.accessKeyId, accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey, secretAccessKey: config.secretAccessKey,
root: config.root root: config.root,
bypassProxy: config.bypassProxy
} }
logger.debug('[BackupManager] Created new S3Storage instance') logger.debug('[BackupManager] Created new S3Storage instance')
} else { } else {

View File

@ -12,6 +12,8 @@ import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher
const logger = loggerService.withContext('ProxyManager') const logger = loggerService.withContext('ProxyManager')
let byPassRules: string[] = [] 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' type HostnameMatchType = 'exact' | 'wildcardSubdomain' | 'generalWildcard'
@ -222,7 +224,10 @@ export const updateByPassRules = (rules: string[]): void => {
byPassRules = rules byPassRules = rules
parsedByPassRules = [] 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) const parsedRule = parseProxyBypassRule(rule)
if (parsedRule) { if (parsedRule) {
parsedByPassRules.push(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) => { export const isByPass = (url: string) => {
if (parsedByPassRules.length === 0) { if (parsedByPassRules.length === 0) {
return false return false
@ -586,6 +618,22 @@ export class ProxyManager {
// set proxy for electron // set proxy for electron
app.setProxy(config) 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() export const proxyManager = new ProxyManager()

View File

@ -6,13 +6,13 @@ import {
PutObjectCommand, PutObjectCommand,
S3Client S3Client
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3'
import { FetchHttpHandler } from '@smithy/fetch-http-handler'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { S3Config } from '@types' import type { S3Config } from '@types'
import * as net from 'net' import * as net from 'net'
import { Agent as UndiciAgent } from 'undici'
import { Readable } from 'stream' import { Readable } from 'stream'
import { proxyManager } from './ProxyManager'
const logger = loggerService.withContext('S3Storage') const logger = loggerService.withContext('S3Storage')
/** /**
@ -37,10 +37,13 @@ export default class S3Storage {
private client: S3Client private client: S3Client
private bucket: string private bucket: string
private root: string private root: string
private endpoint: string
constructor(config: S3Config) { constructor(config: S3Config) {
const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config
this.endpoint = endpoint
const usePathStyle = (() => { const usePathStyle = (() => {
if (!endpoint) return false if (!endpoint) return false
@ -59,23 +62,20 @@ export default class S3Storage {
} }
})() })()
// Conditionally bypass proxy for S3 requests based on user configuration // Use ProxyManager's dynamic bypass rules instead of custom dispatcher
// When bypassProxy is true (default), S3 requests use a direct dispatcher to avoid // When bypassProxy is true (default), add the S3 endpoint to proxy bypass rules
// proxy interference with large file uploads that can cause incomplete transfers // 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" // Error example: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159"
let requestHandler: FetchHttpHandler | undefined if (bypassProxy && endpoint) {
try {
if (bypassProxy) { const url = new URL(endpoint)
const directDispatcher = new UndiciAgent({ const hostname = url.hostname
connect: { // Add the hostname to dynamic bypass rules
timeout: 60000 // 60 second connection timeout 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)
requestHandler = new FetchHttpHandler({ }
requestTimeout: 300000, // 5 minute request timeout for large files
dispatcher: directDispatcher
})
} }
this.client = new S3Client({ this.client = new S3Client({
@ -85,8 +85,7 @@ export default class S3Storage {
accessKeyId: accessKeyId, accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey secretAccessKey: secretAccessKey
}, },
forcePathStyle: usePathStyle, forcePathStyle: usePathStyle
...(requestHandler && { requestHandler })
}) })
this.bucket = bucket this.bucket = bucket
@ -99,6 +98,22 @@ export default class S3Storage {
this.checkConnection = this.checkConnection.bind(this) 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 * root key
*/ */