gongxiangp2/lib/url-rewrite.ts

651 lines
25 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// LEGACY_READ_ONLY: 旧域名仅用于历史链接展示与下载兼容。
/**
* 存储 URL 重写工具
*
* 目标:即使数据库里存的是 http://localhost:9000/... 这样的地址,
* 也要在「返回给前端之前」自动替换为实际可访问的 MinIO 地址。
*
* 注意:这里处理的是资源文件(图片、视频等)的存储地址,不是应用访问地址。
* - 应用访问地址:https://sc.jiansun.vip/(当前主站)
* - 旧域名:https://3d.yydd.ai/(当前仅保留跳转)
* - 资源存储地址:https://s3.jiansun.vip(当前主入口)
*
* 智能选择策略(针对资源文件地址):
* - 内网用户:使用内网IP地址(如 http://192.168.58.20:9000)
* - 外网用户:使用公网域名(如 https://s3.jiansun.vip)
*/
import { NextRequest } from 'next/server'
import { detectIsIntranet } from './network-detection'
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1'])
/**
* 获取 MinIO 基础地址(根据用户网络位置智能选择)
* @param isIntranet 用户是否在内网
* @returns MinIO 基础地址配置
*/
function getSafeMinioBase(isIntranet: boolean = false): { protocol: string; host: string; port: string } | null {
const publicDomain = (process.env.S3_PUBLIC_DOMAIN || '').trim()
const serverIp = (process.env.SERVER_IP || '').trim()
const minioPort = (process.env.MINIO_API_PORT || '9000').trim()
const appUrl = (process.env.APP_PUBLIC_URL || process.env.NEXTAUTH_URL || '').trim()
let host: string | null = null
let port: string = minioPort || '9000'
let protocol: string = 'http:'
// 智能选择策略:
// - 内网用户:优先使用 SERVER_IP(内网地址),走内网网络(最高优先级)
// - 外网用户:使用 S3_PUBLIC_DOMAIN(公网域名),走公网网络
// 注意:内网用户的判断必须优先,避免内网用户被错误地使用公网域名
if (isIntranet) {
// 内网用户:优先使用 SERVER_IP(内网地址)
if (serverIp && !LOCAL_HOSTS.has(serverIp)) {
// 有配置SERVER_IP,使用内网IP地址(最高优先级,直接返回,不继续执行)
host = serverIp
protocol = 'http:'
port = minioPort || '9000'
return {
protocol,
host,
port,
}
}
// 如果没有SERVER_IP,尝试从S3_ENDPOINT推断内网地址
const s3Endpoint = (process.env.S3_ENDPOINT || '').trim()
if (s3Endpoint) {
try {
const endpointUrl = new URL(s3Endpoint)
if (endpointUrl.hostname && !LOCAL_HOSTS.has(endpointUrl.hostname)) {
// 使用S3_ENDPOINT的内网地址
host = endpointUrl.hostname
protocol = endpointUrl.protocol || 'http:'
port = endpointUrl.port || minioPort || '9000'
return {
protocol,
host,
port,
}
}
} catch {
// ignore
}
}
}
// 外网用户或未配置内网IP:使用公网域名
if (publicDomain) {
try {
// 兼容未带协议的配置,默认视为 https,避免浏览器 https 页面出现 mixed-content
const normalized = publicDomain.match(/^https?:\/\//i) ? publicDomain : `https://${publicDomain}`
const url = new URL(normalized)
host = url.hostname
protocol = url.protocol
// 如果配置了公网域名且非本地主机,使用公网域名
if (host && !LOCAL_HOSTS.has(host)) {
// 公网域名必须使用 HTTPS,强制升级协议和端口,避免混合内容错误
protocol = 'https:'
port = '443' // 公网域名强制使用443端口,忽略配置中的端口(如果有的话)
return {
protocol,
host,
port,
}
}
} catch {
// ignore
}
}
// 如果既没有公网域名,也没有内网IP,尝试使用 SERVER_IP 作为后备
if (serverIp && !LOCAL_HOSTS.has(serverIp)) {
host = serverIp
protocol = 'http:'
port = minioPort || '9000'
}
// 如果没有 SERVER_IP,再从 APP_PUBLIC_URL / NEXTAUTH_URL 推断
if (!host && appUrl) {
try {
const url = new URL(appUrl)
if (!LOCAL_HOSTS.has(url.hostname)) {
host = url.hostname
protocol = url.protocol
// 如果appUrl是https,但S3可能是http,这里保持http
// 因为MinIO通常使用http
if (host === serverIp) {
protocol = 'http:'
port = minioPort || '9000'
} else {
port = url.port || (protocol === 'https:' ? '443' : '80')
}
}
} catch {
// ignore
}
}
if (!host) {
return null
}
return {
protocol,
host,
port,
}
}
// 公网S3域名列表(包含当前主域名和少量历史兼容域名)
const PUBLIC_S3_DOMAINS = [
's3.yydd.ai',
's3.jiansun.vip',
's3.1080p.hk',
]
/**
* 将存储相关的 URL 中的 localhost/127.0.0.1/内网IP 或公网S3域名替换为可访问的地址
* - 内网用户:使用 SERVER_IP:MINIO_API_PORT(内网地址),即使URL是公网域名也转换为内网地址
* - 外网用户:使用 S3_PUBLIC_DOMAIN(公网域名)
* - 仅在 URL 为 http/https 且 host 为本机、内网IP或公网S3域名时生效
* - 其他情况(如相对路径、第三方 CDN)保持不变
*
* @param url 要重写的 URL
* @param request 可选的 NextRequest 对象,用于检测用户网络位置
* @returns 重写后的 URL
*/
export function rewriteStorageUrlForClient(url?: string | null, request?: NextRequest): string | null {
// 确保只处理字符串类型,其他类型(如布尔值 true)直接返回 null
if (!url || typeof url !== 'string') return null
const trimmed = url.trim()
if (!trimmed) return null
// 私密预览只能从当前应用的鉴权路由读取,不能变成相对页面地址或 CDN 直链。
if (/^uploads\/materials\/[^/]+\/private-previews\//.test(trimmed)) {
return `/api/${trimmed.split('/').map(encodeURIComponent).join('/')}`
}
// 非 http(s) 直接返回(可能是相对路径或 data: URL)
if (!/^https?:\/\//i.test(trimmed)) {
return trimmed
}
// 检测用户是否在内网(如果提供了 request 对象)
const isIntranet = request ? detectIsIntranet(request) : false
const minioBase = getSafeMinioBase(isIntranet)
if (!minioBase) {
// 没有配置时,不做任何修改,避免返回错误地址
console.warn('[URL Rewrite] getSafeMinioBase returned null, isIntranet:', isIntranet, 'SERVER_IP:', process.env.SERVER_IP)
return trimmed
}
// 调试日志:检查内网用户是否使用了正确的内网地址
if (isIntranet && minioBase.host !== process.env.SERVER_IP) {
console.warn('[URL Rewrite] 内网用户但使用了非内网地址:', {
isIntranet,
minioBaseHost: minioBase.host,
expectedServerIp: process.env.SERVER_IP,
url: trimmed
})
}
try {
const parsed = new URL(trimmed)
const hostname = parsed.hostname.toLowerCase()
const isLocalhost = LOCAL_HOSTS.has(hostname)
const isInternalIP = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)
// 检查是否为公网S3域名
const isPublicS3Domain = PUBLIC_S3_DOMAINS.some(domain =>
hostname === domain || hostname.endsWith(`.${domain}`)
)
// 检查URL是否指向旧MinIO服务器(s3.1080p.hk)且使用旧bucket(jsxiangche)
// 旧MinIO服务器和新MinIO服务器是不同的服务器,新MinIO服务器没有jsxiangche bucket
// 所以对于旧MinIO服务器的URL,即使内网用户也应该保持原始URL(通过代理访问)
const isOldMinIOServer = hostname === 's3.1080p.hk' || hostname.endsWith('.s3.1080p.hk')
const isOldBucket = parsed.pathname.startsWith('/jsxiangche/')
const shouldKeepOriginalUrl = isOldMinIOServer && isOldBucket
// 注意:不要自动替换bucket名称
// 旧素材的文件在 jsxiangche bucket中,新素材在 gongxiangpan bucket中
// 应该保持原始URL不变,让MinIO根据实际bucket提供服务
// 如果确实需要迁移,应该在数据迁移层面处理,而不是在URL重写层面
// 如果是 localhost/127.0.0.1/内网IP,需要重写
// 如果是内网用户且URL是公网S3域名,也需要转换为内网MinIO地址
// 但如果是旧MinIO服务器的URL(s3.1080p.hk + jsxiangche),保持原始URL
if ((isLocalhost || isInternalIP || (isIntranet && isPublicS3Domain)) && !shouldKeepOriginalUrl) {
// 根据用户网络位置智能选择地址
parsed.protocol = minioBase.protocol
parsed.hostname = minioBase.host
parsed.port = minioBase.port
// 重要:MinIO使用path-style访问,路径格式为 bucket/key
// 对于内网MinIO地址,路径中的中文字符应该保持未编码状态
// MinIO服务器会自动处理URL编码,或者浏览器在请求时会自动编码
// 如果手动编码,可能导致MinIO无法正确解析路径,返回404错误
try {
// 检查路径是否已经编码过(包含%符号)
const isAlreadyEncoded = /%[0-9A-Fa-f]{2}/.test(parsed.pathname)
if (isAlreadyEncoded) {
// 路径已经编码过,先解码(MinIO期望未编码的路径)
try {
const decodedPath = decodeURIComponent(parsed.pathname)
// 手动构建URL字符串,避免toString()自动编码
const decodedUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${decodedPath}${parsed.search || ''}${parsed.hash || ''}`
return decodedUrl
} catch {
// 解码失败,保持原路径(可能部分编码)
console.warn('[URL Rewrite] Failed to decode already-encoded path:', parsed.pathname)
// 即使解码失败,也要手动构建URL,避免toString()重新编码
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
}
}
// 对于内网MinIO地址,不进行编码,保持原始路径
// 手动构建URL字符串,避免toString()自动编码
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
} catch (error) {
// 处理失败,保持原路径(但记录警告)
console.warn('[URL Rewrite] Failed to process URL path for MinIO:', error, 'pathname:', parsed.pathname)
// 即使处理失败,也要手动构建URL,避免toString()重新编码
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
}
}
// 对于公网S3域名:
// - 外网用户统一改写到当前配置的 S3_PUBLIC_DOMAIN,避免历史数据继续返回旧域名
// - 再处理路径编码,确保中文 key 可以正常访问
// 如果路径已经编码过,先解码(MinIO期望未编码的路径)
// 浏览器在发送HTTP请求时会自动编码,但MinIO服务器需要未编码的路径才能正确解析
if (isPublicS3Domain) {
if (!isIntranet && !shouldKeepOriginalUrl) {
parsed.protocol = minioBase.protocol
parsed.hostname = minioBase.host
parsed.port = minioBase.port
}
// 使用已经创建的parsed对象,不要重复创建
// 检查路径是否已经编码过(包含%符号)
const isAlreadyEncoded = /%[0-9A-Fa-f]{2}/.test(parsed.pathname)
if (isAlreadyEncoded) {
// 路径已经编码过,先解码(MinIO期望未编码的路径)
try {
const decodedPath = decodeURIComponent(parsed.pathname)
// 手动构建URL字符串,避免toString()自动编码
// 格式:protocol://hostname:port/pathname?search#hash
const decodedUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${decodedPath}${parsed.search || ''}${parsed.hash || ''}`
return decodedUrl
} catch {
// 解码失败,保持原路径
console.warn('[URL Rewrite] Failed to decode already-encoded path for public S3:', parsed.pathname)
// 即使解码失败,也要手动构建URL,避免toString()重新编码
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
}
}
// 路径未编码,手动构建URL字符串,避免toString()自动编码
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
}
// 不是 localhost/内网IP/公网S3域名,检查URL路径是否包含中文字符,如果有则编码
// 浏览器可能无法正确处理URL中的中文字符,需要手动编码路径部分
// 重要:如果路径已经编码过(包含%),先解码再重新编码,避免双重编码
try {
// 检查路径是否已经编码过(包含%符号)
const isAlreadyEncoded = /%[0-9A-Fa-f]{2}/.test(parsed.pathname)
if (isAlreadyEncoded) {
// 路径已经编码过,先解码
try {
parsed.pathname = decodeURIComponent(parsed.pathname)
} catch {
// 解码失败,保持原路径(可能部分编码)
console.warn('[URL Rewrite] Failed to decode already-encoded path:', parsed.pathname)
}
}
// 检查路径是否包含非ASCII字符(包括中文)
if (/[^\x00-\x7F]/.test(parsed.pathname)) {
// 路径包含非ASCII字符,需要编码
// 将路径分割成多个部分,分别编码每个部分(保留斜杠)
const pathParts = parsed.pathname.split('/')
const encodedPath = pathParts.map(part => {
if (!part) return part // 空字符串(开头的斜杠)
return encodeURIComponent(part)
}).join('/')
parsed.pathname = encodedPath
return parsed.toString()
}
} catch (encodeError) {
// 编码失败,返回原始URL
console.warn('[URL Rewrite] Failed to encode URL path:', encodeError)
}
// 没有中文字符或编码失败,直接返回
return trimmed
} catch {
// URL 解析失败时保持原样,避免引入新的错误
return trimmed
}
}
/**
* 批量重写素材对象中的所有存储 URL 字段
*
* @param material 素材对象
* @param request 可选的 NextRequest 对象,用于检测用户网络位置
* @returns 重写后的素材对象
*/
export function rewriteMaterialStorageUrlsForClient<T extends Record<string, any>>(
material: T,
request?: NextRequest
): T {
if (!material || typeof material !== 'object') return material
const cloned: any = { ...material }
const urlFields = [
'previewUrl',
'videoCoverUrl',
'videoPreview540Url',
'videoPreviewClipUrl',
'videoSourceUrl',
] as const
urlFields.forEach((field) => {
if (field in cloned) {
let url = rewriteStorageUrlForClient(cloned[field], request)
// 对于s3.yydd.ai和s3.1080p.hk的URL,直接返回原始URL
// Next.js Image组件无法正确处理相对路径的代理URL,所以直接使用原始URL
// 浏览器会自动处理URL编码,中文路径应该可以正常工作
// 如果遇到连接问题,可以考虑使用普通的<img>标签配合代理URL
cloned[field] = url
}
})
// displayImages: string[]
if (Array.isArray(cloned.displayImages)) {
cloned.displayImages = cloned.displayImages.map((item: any) => {
if (typeof item === 'string') {
let url = rewriteStorageUrlForClient(item, request)
// 对于s3.yydd.ai和s3.1080p.hk的URL,直接返回原始URL
// Next.js Image组件无法正确处理相对路径的代理URL
return url
}
return item
})
}
return cloned as T
}
/**
* 将 HTTP/HTTPS URL 转换为通过代理的 URL(解决混合内容和连接问题)
* 对于 s3.yydd.ai 和 s3.1080p.hk,无论 HTTP 还是 HTTPS,都通过代理提供
* 这样可以避免 ERR_CONNECTION_CLOSED 错误(FRP穿透连接不稳定)
*/
export function proxyHttpUrl(url?: string | null): string | null {
if (!url || typeof url !== 'string') return url ?? null
const trimmed = url.trim()
if (!trimmed) return null
// 非 http(s) 直接返回(可能是相对路径或 data: URL)
if (!/^https?:\/\//i.test(trimmed)) {
return trimmed
}
try {
const parsed = new URL(trimmed)
const hostname = parsed.hostname.toLowerCase()
// s3.jiansun.vip 入口已经稳定,并且前面有独立缓存层。
// 这里优先直连,避免所有预览流量继续压在主站 /api/proxy-media 上。
if (hostname === 's3.jiansun.vip') {
if (parsed.protocol !== 'https:') {
parsed.protocol = 'https:'
if (parsed.port === '80') {
parsed.port = ''
}
}
return parsed.toString()
}
// 检查是否为内网MinIO地址(内网地址不应该通过代理)
const isIntranetMinIO = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname) ||
hostname === 'localhost' || hostname === '127.0.0.1'
if (isIntranetMinIO) {
// 内网MinIO地址直接返回,不通过代理(代理API不允许内网地址)
return trimmed
}
// 遗留公网域名列表(仍保留代理兜底)
// 检查是否为公网域名
const isPublicDomain = PUBLIC_S3_DOMAINS.some(domain =>
hostname === domain || hostname.endsWith(`.${domain}`)
)
// 如果是公网域名,无论 HTTP 还是 HTTPS,都通过代理提供
// 这样可以避免 ERR_CONNECTION_CLOSED 错误
if (isPublicDomain) {
// 使用相对路径的代理 API,避免硬编码域名
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`
}
// 只处理 HTTP 协议的其他资源(避免混合内容问题)
if (parsed.protocol === 'http:') {
// 其他 HTTP 资源也通过代理(如果需要)
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`
}
// 其他 HTTPS 资源直接返回
return trimmed
} catch {
// URL 解析失败时保持原样
return trimmed
}
}
/**
* 确保 URL 使用 HTTPS 协议(避免混合内容问题)
* 对于公网域名(如 s3.1080p.hk),强制使用 HTTPS
*/
export function ensureHttpsUrl(url?: string | null): string | null {
if (!url || typeof url !== 'string') return url ?? null
const trimmed = url.trim()
if (!trimmed) return null
// 非 http(s) 直接返回(可能是相对路径或 data: URL)
if (!/^https?:\/\//i.test(trimmed)) {
return trimmed
}
try {
const parsed = new URL(trimmed)
// 如果是 HTTP 协议,检查是否为公网域名
if (parsed.protocol === 'http:') {
const hostname = parsed.hostname.toLowerCase()
// 公网域名列表(需要强制使用 HTTPS)
const publicDomains = [
's3.1080p.hk',
's3.yydd.ai',
's3.jiansun.vip',
'3d.yydd.ai',
'sc.jiansun.vip',
]
// 检查是否为公网域名或非内网地址
const isPublicDomain = publicDomains.some(domain => hostname === domain || hostname.endsWith(`.${domain}`))
const isLocalhost = LOCAL_HOSTS.has(hostname)
const isInternalIP = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)
// 如果是公网域名或非内网地址,强制升级为 HTTPS
if (isPublicDomain || (!isLocalhost && !isInternalIP)) {
parsed.protocol = 'https:'
// 如果原来是 http 且没有指定端口,移除端口(使用默认 443)
if (parsed.port === '80') {
parsed.port = ''
}
return parsed.toString()
}
}
// 已经是 HTTPS 或内网地址,直接返回
return trimmed
} catch {
// URL 解析失败时保持原样
return trimmed
}
}
/**
* 根据URL路径估算文件大小(用于决定是否需要代理)
* 基于文件类型和路径特征进行估算,避免额外的HEAD请求
*
* @param url 文件URL
* @returns 估算的文件大小(字节)
*/
function estimateFileSize(url: string): number {
const lowerUrl = url.toLowerCase()
// 视频文件:通常 >5MB
// 注意:video-cover 是封面图(小文件),不应该匹配这里
if ((lowerUrl.includes('video-preview') ||
lowerUrl.includes('video-clip') ||
lowerUrl.includes('video-540')) &&
!lowerUrl.includes('video-cover')) {
return 10 * 1024 * 1024 // 10MB
}
// .mp4 文件:通常是视频文件
if (lowerUrl.includes('.mp4') && !lowerUrl.includes('video-cover')) {
return 10 * 1024 * 1024 // 10MB
}
// 音频文件:通常 >2MB
if (lowerUrl.includes('audio-source') ||
(lowerUrl.includes('audio-') && !lowerUrl.includes('audio-cover'))) {
return 5 * 1024 * 1024 // 5MB
}
// 音频文件扩展名
if (lowerUrl.includes('.mp3') ||
lowerUrl.includes('.wav') ||
lowerUrl.includes('.flac')) {
return 5 * 1024 * 1024 // 5MB
}
// 展示图:可能较大(1-2MB)
if (lowerUrl.includes('display-') ||
lowerUrl.includes('model-') ||
lowerUrl.includes('detail-')) {
return 1.5 * 1024 * 1024 // 1.5MB
}
// 预览图:通常 <500KB
// 视频封面:通常 <500KB(video-cover 应该在这里)
return 300 * 1024 // 300KB
}
/**
* 统一的媒体URL重写规则(优化版:内网用户不走代理)
*
* 规则:
* - 内网用户:直接使用内网MinIO地址(不走代理,最快)
* - 所有文件都直接访问内网MinIO,不走代理
* - 外网用户:
* - < 1MB:直接访问S3(小文件,影响不大)
* - ≥ 1MB:通过代理(大文件,需要优化Range请求)
*
* @param url 原始URL
* @param request NextRequest对象(用于检测用户网络位置)
* @param fileType 可选的文件类型提示:'image' | 'video' | 'audio' | 'source'
* @returns 重写后的URL
*/
export function rewriteMediaUrlForClient(
url: string | null | undefined,
request?: NextRequest,
fileType?: 'image' | 'video' | 'audio' | 'source'
): string | null {
// 确保只处理字符串类型,其他类型(如布尔值 true)直接返回 null
if (!url || typeof url !== 'string') return null
// 如果URL已经是代理URL(/api/proxy-media?url=...),需要先提取原始URL
// 这种情况不应该发生,但如果发生了,我们需要处理它
if (url.startsWith('/api/proxy-media?url=')) {
try {
const urlParam = url.replace('/api/proxy-media?url=', '')
const decodedUrl = decodeURIComponent(urlParam)
// 递归调用,使用提取的原始URL
return rewriteMediaUrlForClient(decodedUrl, request, fileType)
} catch (e) {
// 如果解析失败,返回原始URL
console.warn('[URL Rewrite] 无法解析代理URL:', url, e)
return url
}
}
// 检测用户网络位置
const isIntranet = request ? detectIsIntranet(request) : false
// 先重写存储URL(内网用户使用内网地址,外网用户使用公网地址)
const rewrittenUrl = rewriteStorageUrlForClient(url, request)
if (!rewrittenUrl) return null
// 内网用户:直接访问内网MinIO,不走代理(最快)
if (isIntranet) {
// 调试日志:检查内网用户的URL是否正确重写为内网地址
if (
rewrittenUrl.includes('s3.yydd.ai') ||
rewrittenUrl.includes('s3.jiansun.vip') ||
rewrittenUrl.includes('s3.1080p.hk')
) {
console.error('[URL Rewrite] 内网用户但URL仍包含公网S3域名:', {
originalUrl: url,
rewrittenUrl,
isIntranet,
serverIp: process.env.SERVER_IP,
fileType
})
}
return rewrittenUrl
}
// 公网图片优先走统一 URL 策略:
// - s3.jiansun.vip:现在优先直连,命中入口缓存
// - 其它遗留公网域名:继续通过代理兜底
if (fileType === 'image') {
return proxyHttpUrl(rewrittenUrl)
}
// 外网用户:按文件大小阈值决定是否走代理
// 注意:不要将内网MinIO地址转换为代理URL(代理API不允许内网地址)
const isIntranetMinIO = rewrittenUrl.includes('192.168.') || rewrittenUrl.includes('localhost') || rewrittenUrl.includes('127.0.0.1')
if (isIntranetMinIO) {
// 如果是内网MinIO地址,直接返回(不应该通过代理)
return rewrittenUrl
}
const PROXY_THRESHOLD = 1 * 1024 * 1024 // 1MB
const estimatedSize = estimateFileSize(url)
// 如果估算大小 >= 1MB,通过代理(优化Range请求处理)
if (estimatedSize >= PROXY_THRESHOLD) {
return proxyHttpUrl(rewrittenUrl)
}
// 小文件:直接访问S3
return rewrittenUrl
}