| 1 | // 获取完整的OSS URL |
| 2 | export function getOssUrl(path?: string) { |
| 3 | if (!path) |
| 4 | return '' |
| 5 | if ( |
| 6 | path.startsWith('http') |
| 7 | || path.startsWith('https') |
| 8 | || path.startsWith('ossProxy') |
| 9 | || path.startsWith('/ossProxy/') |
| 10 | || path.startsWith('blob:http') |
| 11 | || path.startsWith('blob:https') |
| 12 | ) { |
| 13 | return path |
| 14 | } |
| 15 | return `${process.env.NEXT_PUBLIC_OSS_URL}${path}` |
| 16 | } |
| 17 | |
| 18 | export interface OssThumbnailOptions { |
| 19 | width?: number |
| 20 | height?: number |
| 21 | quality?: number |
| 22 | } |
| 23 | |
| 24 | const DEFAULT_THUMBNAIL_QUALITY = 75 |
| 25 | const UNSUPPORTED_THUMBNAIL_EXTENSIONS = new Set(['.svg', '.gif', '.mp4', '.mov', '.webm', '.m4v']) |
| 26 | const PROJECT_OSS_HOSTNAMES = new Set(['assets.aitoearn.cn', 'assets.aitoearn.ai']) |
| 27 | |
| 28 | type OssStorageProvider = 'aliyun' | 'cloudflare' |
| 29 | |
| 30 | function getNormalizedDimension(value?: number) { |
| 31 | if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) |
| 32 | return undefined |
| 33 | |
| 34 | return Math.ceil(value) |
| 35 | } |
| 36 | |
| 37 | function getNormalizedQuality(value?: number) { |
| 38 | if (typeof value !== 'number' || !Number.isFinite(value)) |
| 39 | return DEFAULT_THUMBNAIL_QUALITY |
| 40 | |
| 41 | return Math.min(100, Math.max(1, Math.round(value))) |
| 42 | } |
| 43 | |
| 44 | function getUrlExtension(url: URL) { |
| 45 | const lastDotIndex = url.pathname.lastIndexOf('.') |
| 46 | if (lastDotIndex < 0) |
| 47 | return '' |
| 48 | |
| 49 | return url.pathname.slice(lastDotIndex).toLowerCase() |
| 50 | } |
| 51 | |
| 52 | function isProjectOssUrl(url: URL) { |
| 53 | if (PROJECT_OSS_HOSTNAMES.has(url.hostname)) |
| 54 | return true |
| 55 | |
| 56 | const ossUrl = process.env.NEXT_PUBLIC_OSS_URL |
| 57 | if (!ossUrl) |
| 58 | return false |
| 59 | |
| 60 | try { |
| 61 | const ossBaseUrl = new URL(ossUrl) |
| 62 | return url.origin === ossBaseUrl.origin |
| 63 | } |
| 64 | catch { |
| 65 | return false |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | function getOssStorageProvider(url: URL): OssStorageProvider { |
| 70 | if (url.hostname === 'assets.aitoearn.cn') |
| 71 | return 'aliyun' |
| 72 | |
| 73 | if (url.hostname === 'assets.aitoearn.ai') |
| 74 | return 'cloudflare' |
| 75 | |
| 76 | return 'cloudflare' |
| 77 | } |
| 78 | |
| 79 | function canUseOssThumbnail(url: URL) { |
| 80 | if (!isProjectOssUrl(url)) |
| 81 | return false |
| 82 | |
| 83 | if (url.searchParams.has('x-oss-process') || url.pathname.includes('/cdn-cgi/image/')) |
| 84 | return false |
| 85 | |
| 86 | return !UNSUPPORTED_THUMBNAIL_EXTENSIONS.has(getUrlExtension(url)) |
| 87 | } |
| 88 | |
| 89 | function buildAliyunOssThumbnailUrl(url: URL, width?: number, height?: number, quality?: number) { |
| 90 | const resizeOptions = [ |
| 91 | width ? `w_${width}` : '', |
| 92 | height ? `h_${height}` : '', |
| 93 | ].filter(Boolean).join(',') |
| 94 | |
| 95 | const processValue = [ |
| 96 | `image/resize,${resizeOptions}`, |
| 97 | `quality,q_${quality}`, |
| 98 | 'format,webp', |
| 99 | ].join('/') |
| 100 | |
| 101 | const thumbnailUrl = new URL(url.toString()) |
| 102 | thumbnailUrl.searchParams.set('x-oss-process', processValue) |
| 103 | return thumbnailUrl.toString() |
| 104 | } |
| 105 | |
| 106 | export function getOssThumbnailUrl(path?: string, options: OssThumbnailOptions = {}) { |
| 107 | const url = getOssUrl(path) |
| 108 | if (!url) |
| 109 | return '' |
| 110 | |
| 111 | const width = getNormalizedDimension(options.width) |
| 112 | const height = getNormalizedDimension(options.height) |
| 113 | if (!width && !height) |
| 114 | return url |
| 115 | |
| 116 | let parsedUrl: URL |
| 117 | try { |
| 118 | parsedUrl = new URL(url) |
| 119 | } |
| 120 | catch { |
| 121 | return url |
| 122 | } |
| 123 | |
| 124 | if (!canUseOssThumbnail(parsedUrl)) |
| 125 | return url |
| 126 | |
| 127 | if (getOssStorageProvider(parsedUrl) !== 'aliyun') |
| 128 | return url |
| 129 | |
| 130 | const quality = getNormalizedQuality(options.quality) |
| 131 | return buildAliyunOssThumbnailUrl(parsedUrl, width, height, quality) |
| 132 | } |
| 133 | |
| 134 | // 将完整的oss url转为代理的 oss url |
| 135 | export function getOssProxyPath(ossUrl?: string) { |
| 136 | if (!ossUrl) |
| 137 | return '' |
| 138 | |
| 139 | return ossUrl?.replace( |
| 140 | process.env.NEXT_PUBLIC_OSS_URL ?? '', |
| 141 | process.env.NEXT_PUBLIC_OSS_URL_PROXY ?? '', |
| 142 | ) |
| 143 | } |
| 144 |