mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +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
|
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 {
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user