Skip to content

Commit b1e6347

Browse files
CopilotGeorgeDong32
andcommitted
♻️ refactor: Use ProxyManager bypass rules instead of custom dispatcher for S3
Co-authored-by: GeorgeDong32 <[email protected]>
1 parent ea9e7fc commit b1e6347

File tree

3 files changed

+94
-23
lines changed

3 files changed

+94
-23
lines changed

src/main/services/BackupManager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class BackupManager {
3333
accessKeyId: string
3434
secretAccessKey: string
3535
root?: string
36+
bypassProxy?: boolean
3637
} | null = null
3738

3839
private cachedWebdavConnectionConfig: {
@@ -120,7 +121,8 @@ class BackupManager {
120121
cachedConfig.bucket === config.bucket &&
121122
cachedConfig.accessKeyId === config.accessKeyId &&
122123
cachedConfig.secretAccessKey === config.secretAccessKey &&
123-
cachedConfig.root === config.root
124+
cachedConfig.root === config.root &&
125+
cachedConfig.bypassProxy === config.bypassProxy
124126
)
125127
}
126128

@@ -147,6 +149,11 @@ class BackupManager {
147149
const configChanged = !this.isS3ConfigEqual(this.cachedS3ConnectionConfig, config)
148150

149151
if (configChanged || !this.s3Storage) {
152+
// Destroy old instance to clean up bypass rules
153+
if (this.s3Storage) {
154+
this.s3Storage.destroy()
155+
}
156+
150157
this.s3Storage = new S3Storage(config)
151158
// 只缓存连接相关的配置字段
152159
this.cachedS3ConnectionConfig = {
@@ -155,7 +162,8 @@ class BackupManager {
155162
bucket: config.bucket,
156163
accessKeyId: config.accessKeyId,
157164
secretAccessKey: config.secretAccessKey,
158-
root: config.root
165+
root: config.root,
166+
bypassProxy: config.bypassProxy
159167
}
160168
logger.debug('[BackupManager] Created new S3Storage instance')
161169
} else {

src/main/services/ProxyManager.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher
1212

1313
const logger = loggerService.withContext('ProxyManager')
1414
let byPassRules: string[] = []
15+
// Dynamic bypass rules that can be added/removed at runtime (e.g., for S3 endpoints)
16+
let dynamicBypassRules: string[] = []
1517

1618
type HostnameMatchType = 'exact' | 'wildcardSubdomain' | 'generalWildcard'
1719

@@ -222,7 +224,10 @@ export const updateByPassRules = (rules: string[]): void => {
222224
byPassRules = rules
223225
parsedByPassRules = []
224226

225-
for (const rule of rules) {
227+
// Combine static bypass rules with dynamic ones
228+
const allRules = [...rules, ...dynamicBypassRules]
229+
230+
for (const rule of allRules) {
226231
const parsedRule = parseProxyBypassRule(rule)
227232
if (parsedRule) {
228233
parsedByPassRules.push(parsedRule)
@@ -232,6 +237,33 @@ export const updateByPassRules = (rules: string[]): void => {
232237
}
233238
}
234239

240+
/**
241+
* Add a dynamic bypass rule at runtime (e.g., for S3 endpoints)
242+
* @param rule - The bypass rule to add (e.g., hostname or domain pattern)
243+
*/
244+
export const addDynamicBypassRule = (rule: string): void => {
245+
if (!dynamicBypassRules.includes(rule)) {
246+
dynamicBypassRules.push(rule)
247+
// Re-parse all rules with the new dynamic rule
248+
updateByPassRules(byPassRules)
249+
logger.info(`Added dynamic bypass rule: ${rule}`)
250+
}
251+
}
252+
253+
/**
254+
* Remove a dynamic bypass rule
255+
* @param rule - The bypass rule to remove
256+
*/
257+
export const removeDynamicBypassRule = (rule: string): void => {
258+
const index = dynamicBypassRules.indexOf(rule)
259+
if (index !== -1) {
260+
dynamicBypassRules.splice(index, 1)
261+
// Re-parse all rules without the removed dynamic rule
262+
updateByPassRules(byPassRules)
263+
logger.info(`Removed dynamic bypass rule: ${rule}`)
264+
}
265+
}
266+
235267
export const isByPass = (url: string) => {
236268
if (parsedByPassRules.length === 0) {
237269
return false
@@ -586,6 +618,22 @@ export class ProxyManager {
586618
// set proxy for electron
587619
app.setProxy(config)
588620
}
621+
622+
/**
623+
* Add a dynamic bypass rule for a specific endpoint
624+
* @param rule - The bypass rule to add (e.g., hostname or domain pattern)
625+
*/
626+
addDynamicBypassRule(rule: string): void {
627+
addDynamicBypassRule(rule)
628+
}
629+
630+
/**
631+
* Remove a dynamic bypass rule
632+
* @param rule - The bypass rule to remove
633+
*/
634+
removeDynamicBypassRule(rule: string): void {
635+
removeDynamicBypassRule(rule)
636+
}
589637
}
590638

591639
export const proxyManager = new ProxyManager()

src/main/services/S3Storage.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
PutObjectCommand,
77
S3Client
88
} from '@aws-sdk/client-s3'
9-
import { FetchHttpHandler } from '@smithy/fetch-http-handler'
109
import { loggerService } from '@logger'
1110
import type { S3Config } from '@types'
1211
import * as net from 'net'
13-
import { Agent as UndiciAgent } from 'undici'
1412
import { Readable } from 'stream'
1513

14+
import { proxyManager } from './ProxyManager'
15+
1616
const logger = loggerService.withContext('S3Storage')
1717

1818
/**
@@ -37,10 +37,13 @@ export default class S3Storage {
3737
private client: S3Client
3838
private bucket: string
3939
private root: string
40+
private endpoint: string
4041

4142
constructor(config: S3Config) {
4243
const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config
4344

45+
this.endpoint = endpoint
46+
4447
const usePathStyle = (() => {
4548
if (!endpoint) return false
4649

@@ -59,23 +62,20 @@ export default class S3Storage {
5962
}
6063
})()
6164

62-
// Conditionally bypass proxy for S3 requests based on user configuration
63-
// When bypassProxy is true (default), S3 requests use a direct dispatcher to avoid
64-
// proxy interference with large file uploads that can cause incomplete transfers
65+
// Use ProxyManager's dynamic bypass rules instead of custom dispatcher
66+
// When bypassProxy is true (default), add the S3 endpoint to proxy bypass rules
67+
// to avoid proxy interference with large file uploads that can cause incomplete transfers
6568
// Error example: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159"
66-
let requestHandler: FetchHttpHandler | undefined
67-
68-
if (bypassProxy) {
69-
const directDispatcher = new UndiciAgent({
70-
connect: {
71-
timeout: 60000 // 60 second connection timeout
72-
}
73-
})
74-
75-
requestHandler = new FetchHttpHandler({
76-
requestTimeout: 300000, // 5 minute request timeout for large files
77-
dispatcher: directDispatcher
78-
})
69+
if (bypassProxy && endpoint) {
70+
try {
71+
const url = new URL(endpoint)
72+
const hostname = url.hostname
73+
// Add the hostname to dynamic bypass rules
74+
proxyManager.addDynamicBypassRule(hostname)
75+
logger.debug(`[S3Storage] Added S3 endpoint to bypass rules: ${hostname}`)
76+
} catch (e) {
77+
logger.warn(`[S3Storage] Failed to add endpoint to bypass rules: ${endpoint}`, e as Error)
78+
}
7979
}
8080

8181
this.client = new S3Client({
@@ -85,8 +85,7 @@ export default class S3Storage {
8585
accessKeyId: accessKeyId,
8686
secretAccessKey: secretAccessKey
8787
},
88-
forcePathStyle: usePathStyle,
89-
...(requestHandler && { requestHandler })
88+
forcePathStyle: usePathStyle
9089
})
9190

9291
this.bucket = bucket
@@ -99,6 +98,22 @@ export default class S3Storage {
9998
this.checkConnection = this.checkConnection.bind(this)
10099
}
101100

101+
/**
102+
* Clean up resources and remove bypass rules
103+
*/
104+
destroy(): void {
105+
if (this.endpoint) {
106+
try {
107+
const url = new URL(this.endpoint)
108+
const hostname = url.hostname
109+
proxyManager.removeDynamicBypassRule(hostname)
110+
logger.debug(`[S3Storage] Removed S3 endpoint from bypass rules: ${hostname}`)
111+
} catch (e) {
112+
logger.warn(`[S3Storage] Failed to remove endpoint from bypass rules: ${this.endpoint}`, e as Error)
113+
}
114+
}
115+
}
116+
102117
/**
103118
* 内部辅助方法,用来拼接带 root 的对象 key
104119
*/

0 commit comments

Comments
 (0)