import http from '@ohos.net.http' import fs from '@ohos.file.fs' import harmonyUrl from '@ohos.url' import { getEnv, Emitter, getCurrentMP } from '@dcloudio/uni-runtime' import { DownloadTask as UniDownloadTask, DownloadFileOptions as UniDownloadFileOptions, DownloadFileSuccess as UniDownloadFileSuccess, OnProgressDownloadResult, DownloadFile } from '../../interface.uts' import { API_DOWNLOAD_FILE, DownloadFileApiOptions, DownloadFileApiProtocol, } from '../../protocol.uts' import { lookupExt } from './mime.uts' import { getClientCertificate, parseUrl, } from './utils.uts' import { getCookieSync, setCookieSync } from './cookie.uts' interface IUniDownloadFileEmitter { on: (eventName: string, callback: Function) => void once: (eventName: string, callback: Function) => void off: (eventName: string, callback?: Function | null) => void emit: (eventName: string, ...args: (Object | undefined | null)[]) => void } interface IDownloadTask { abort: Function onHeadersReceived: Function offHeadersReceived: Function onProgressUpdate: Function offProgressUpdate: Function } function getPossibleExt(contentType: string, contentDisposition: string, url: string): string { const contentDispositionFileNameMatches = contentDisposition.match(/filename="(.*)"/) const contentDispositionFileName = contentDispositionFileNameMatches ? contentDispositionFileNameMatches[1] : '' const contentDispositionExt = contentDispositionFileName ? contentDispositionFileName.split('.').pop() : '' if (contentDispositionExt) { return contentDispositionExt } const urlPath = harmonyUrl.URL.parseURL(url).pathname const urlExt = urlPath.split('/').pop()?.split('.')[1] || '' if (urlExt) { return urlExt } const contentTypeExt = lookupExt(contentType) return contentTypeExt || '' } /** * TODO 鸿蒙的downloadFile接口也需要传filePath,仍然会遇到content-type -> extension的问题 */ class DownloadTask implements UniDownloadTask { private _downloadTask: IDownloadTask constructor(downloadTask: IDownloadTask) { this._downloadTask = downloadTask } abort() { this._downloadTask.abort() } onProgressUpdate(callback: Function) { this._downloadTask.onProgressUpdate(callback) } offProgressUpdate(callback?: Function) { this._downloadTask.offProgressUpdate(callback) } onHeadersReceived(callback: Function) { this._downloadTask.onHeadersReceived(callback) } offHeadersReceived(callback?: Function) { this._downloadTask.offHeadersReceived(callback) } } let downloadIndex: [string, number] = ['0', 0] function getDownloadFileName(ext: string) { let fileName = Date.now() + '' if (downloadIndex[0] === fileName) { downloadIndex[1]++ if (downloadIndex[1] > 0) { fileName += '-' + downloadIndex[1] } } else { downloadIndex[0] = fileName downloadIndex[1] = 0 } if (ext) { fileName += '.' + ext } return fileName } export const downloadFile = defineTaskApi( API_DOWNLOAD_FILE, (args: UniDownloadFileOptions, exec: ApiExecutor) => { let { url, timeout, header, filePath } = args header = header || {} as ESObject if (!header!['Cookie'] && !header!['cookie']) { header!['Cookie'] = getCookieSync(url); } const httpRequest = http.createHttp() const mp = getCurrentMP() const userAgent = mp.userAgent.fullUserAgent if (userAgent && !header!['User-Agent'] && !header!['user-agent']) { header!['User-Agent'] = userAgent } const emitter = new Emitter() as IUniDownloadFileEmitter const downloadTask: IDownloadTask = { abort() { emitter.off('headersReceive') emitter.off('progress') httpRequest.destroy() }, onHeadersReceived(callback: Function) { emitter.on('headersReceive', callback) }, offHeadersReceived(callback?: Function) { emitter.off('headersReceive', callback) }, onProgressUpdate(callback: Function) { emitter.on('progress', callback) }, offProgressUpdate(callback?: Function) { emitter.off('progress', callback) }, } function destroy() { downloadTask.abort() } mp.on('beforeClose', destroy) let responseContentType = '' let responseContentDisposition = '' let lastUrl = url httpRequest.on('headersReceive', (headers: Object) => { const realHeaders = headers as Record responseContentType = realHeaders['content-type'] as string || realHeaders['Content-Type'] as string || '' responseContentDisposition = realHeaders['content-disposition'] as string || realHeaders['Content-Disposition'] as string || '' const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie'] if (setCookieHeader) { setCookieSync(lastUrl, setCookieHeader as string[]) } const location = realHeaders['location'] || realHeaders['Location'] if (location) { lastUrl = location as string } // TODO headersReceive存在bug,暂不支持回调给用户。注意重定向时会多次触发,但是只需要给用户回调最后一次 // emitter.emit('headersReceive', header); }) httpRequest.on('dataReceiveProgress', ({ receiveSize, totalSize }) => { emitter.emit('progress', { progress: Math.floor((receiveSize / totalSize) * 100), totalBytesWritten: receiveSize, totalBytesExpectedToWrite: totalSize, } as OnProgressDownloadResult) }) const TEMP_PATH = getEnv().TEMP_PATH as string const downloadPath = TEMP_PATH + '/download' if (!fs.accessSync(downloadPath)) { fs.mkdirSync(downloadPath, true) } let stream: fs.Stream let tempFilePath = '' let writePromise = Promise.resolve(0) async function queueWrite(data: ArrayBuffer): Promise { writePromise = writePromise.then(async (total) => { const length = await stream.write(data) return total + length }) return writePromise } httpRequest.on('dataReceive', (data) => { if (!stream) { const ext = getPossibleExt(responseContentType, responseContentDisposition, url) tempFilePath = filePath ? filePath.replace(/^file:\/\//, '') : downloadPath + '/' + getDownloadFileName(ext) stream = fs.createStreamSync(tempFilePath, 'w+') } queueWrite(data) }) httpRequest.requestInStream( parseUrl(url), { header: header ? header : {} as ESObject, method: http.RequestMethod.GET, connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout readTimeout: timeout ? timeout : undefined, clientCert: getClientCertificate(url) } as http.HttpRequestOptions, (err, statusCode) => { // 此回调先于dataEnd回调执行 let finishPromise: Promise = Promise.resolve() if (err) { /** * TODO abort后此处收到如下错误,待确认是否直接将此错误码转为abort错误 * {"code":2300023,"message":"Failed writing received data to disk/application"} */ exec.reject(err.message) } else { finishPromise = writePromise.then(async () => { await stream.flush() await stream.close() exec.resolve({ tempFilePath, statusCode, } as UniDownloadFileSuccess) }).catch((err: Error) => { exec.reject(err.message) }) } finishPromise.then(() => { downloadTask.offHeadersReceived() downloadTask.offProgressUpdate() httpRequest.destroy() // 调用完毕后必须调用destroy方法 mp.off('beforeClose', destroy) }) } ) return new DownloadTask(downloadTask) }, DownloadFileApiProtocol, DownloadFileApiOptions ) as DownloadFile