mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
♻️ 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:
parent
ea9e7fccda
commit
b1e63470a5
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user