XuqmGroup-RNSDK/packages/xwebview/src/XWebViewDownload.ts

162 行
4.4 KiB
TypeScript

2026-05-07 19:39:41 +08:00
import { Platform } from 'react-native'
import RNFetchBlob from 'react-native-blob-util'
import type {
XWebViewDownloadProgress,
XWebViewDownloadRequest,
XWebViewDownloadResult,
} from '@xuqm/rn-common'
function parseContentDispositionFilename(header: string): string | null {
const rfc5987 = header.match(/filename\*=(?:[^']*'[^']*')?([^;\s]+)/i)
if (rfc5987?.[1]) {
try {
return decodeURIComponent(rfc5987[1].replace(/['"]/g, ''))
} catch {}
}
const quoted = header.match(/filename="([^"]+)"/i)
if (quoted?.[1]) return quoted[1].trim()
const plain = header.match(/filename=([^;\s"]+)/i)
if (plain?.[1]) return plain[1].trim()
return null
}
function filenameFromUrl(url: string): string {
try {
const { pathname } = new URL(url)
const parts = pathname.split('/').filter(Boolean)
return parts[parts.length - 1] ? decodeURIComponent(parts[parts.length - 1]) : 'download'
} catch {
const path = url.split('?')[0]
const parts = path.split('/').filter(Boolean)
return parts[parts.length - 1] || 'download'
}
}
export async function fetchDownloadInfo(
url: string,
hintFilename?: string,
): Promise<XWebViewDownloadRequest> {
try {
const res = await fetch(url, { method: 'HEAD' })
const disposition = res.headers.get('content-disposition') ?? ''
const mimeType = res.headers.get('content-type')?.split(';')[0].trim() || undefined
const length = res.headers.get('content-length')
const fileSize = length ? parseInt(length, 10) : undefined
const suggestedFilename =
(hintFilename && hintFilename.trim()) ||
parseContentDispositionFilename(disposition) ||
filenameFromUrl(url)
return { url, suggestedFilename, mimeType, fileSize }
} catch {
return {
url,
suggestedFilename: (hintFilename && hintFilename.trim()) || filenameFromUrl(url),
}
}
}
async function resolveFilePath(
dir: string,
filename: string,
conflict: 'rename' | 'overwrite',
): Promise<string> {
const full = `${dir}/${filename}`
if (conflict === 'overwrite') return full
const exists = await RNFetchBlob.fs.exists(full)
if (!exists) return full
const dot = filename.lastIndexOf('.')
const base = dot >= 0 ? filename.slice(0, dot) : filename
const ext = dot >= 0 ? filename.slice(dot) : ''
let n = 1
let candidate: string
do {
candidate = `${dir}/${base}(${n})${ext}`
n++
} while (await RNFetchBlob.fs.exists(candidate))
return candidate
}
export async function saveBase64File(
base64: string,
filename: string,
savePath: string | undefined,
conflict: 'rename' | 'overwrite',
): Promise<string> {
const { dirs } = RNFetchBlob.fs
const dir =
savePath ??
(Platform.OS === 'android'
? (dirs.DownloadDir ?? dirs.DocumentDir)
: dirs.DocumentDir)
const filePath = await resolveFilePath(dir, filename, conflict)
await RNFetchBlob.fs.writeFile(filePath, base64, 'base64')
return filePath
}
export type DownloadHandle = { cancel: () => void }
export function startDownload(
url: string,
filename: string,
savePath: string | undefined,
conflict: 'rename' | 'overwrite',
onProgress: (p: XWebViewDownloadProgress) => void,
onComplete: (r: XWebViewDownloadResult) => void,
onError: (error: string) => void,
): DownloadHandle {
const { dirs } = RNFetchBlob.fs
const dir =
savePath ??
(Platform.OS === 'android'
? (dirs.DownloadDir ?? dirs.DocumentDir)
: dirs.DocumentDir)
let cancelled = false
let cancelFn = () => {
cancelled = true
}
resolveFilePath(dir, filename, conflict)
.then(filePath => {
if (cancelled) return
const task = RNFetchBlob.config({ path: filePath }).fetch('GET', url)
cancelFn = () => task.cancel()
task.progress({ interval: 300 }, (received: number, total: number) => {
const recv = Number(received)
const tot = Number(total)
onProgress({
url,
filename,
received: recv,
total: tot,
percentage: tot > 0 ? Math.round((recv / tot) * 100) : -1,
})
})
task
.then((res: { path: () => string }) => {
onComplete({ url, filename, filePath: res.path(), fileSize: 0 })
})
.catch((err: Error) => {
if (!String(err?.message ?? '').toLowerCase().includes('cancel')) {
onError(err?.message ?? String(err))
}
})
})
.catch(err => onError(String(err)))
return { cancel: () => cancelFn() }
}