import http from '@ohos.net.http' import fs from '@ohos.file.fs' import harmonyUrl from '@ohos.url' import { getEnv, Emitter, getCurrentMP, getRealPath, } 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' import { BusinessError } from '@ohos.base' 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 getContentFileName(contentDisposition: string): string { const contentDispositionFileNameMatches = contentDisposition.match(/filename=['"]?(.*?)(['"]|$)/); const contentDispositionFileName = contentDispositionFileNameMatches ? contentDispositionFileNameMatches[1] : ''; return contentDispositionFileName || ''; } interface GetDownloadFilePathOptions { dirName: string fileName: string contentType: string contentDisposition: string url: string downloadPath: string } function fileExists(filePath: string): boolean { try { fs.statSync(filePath) return true } catch (error) { return false } } function getDownloadFilePath(options: GetDownloadFilePathOptions): string { const { dirName, fileName, contentType, contentDisposition, url, downloadPath } = options if (dirName && fileName) { const realFilePath = dirName + '/' + fileName if (fileExists(realFilePath)) { fs.unlinkSync(realFilePath) } if (!fileExists(dirName)) { fs.mkdirSync(dirName, true) } return realFilePath } let realDirName = dirName || downloadPath const contentDispositionFileName = getContentFileName(contentDisposition); const urlFileName = harmonyUrl.URL.parseURL(url).pathname.split('/').pop() let realFileName: string = contentDispositionFileName || urlFileName || '' if (!realFileName) { const ext = lookupExt(contentType) || '' const now = Date.now() realFileName = ext ? '' + now + '.' + ext : '' + now } const lastIndexOfDot = realFileName.lastIndexOf('.') const realFileNameExt = realFileName.substring(lastIndexOfDot + 1) const realFileNameWithoutExt = realFileName.substring(0, lastIndexOfDot) let realFilePath: string = realDirName + '/' + realFileName let number = 1 while (fileExists(realFilePath)) { realFilePath = realDirName + '/' + realFileNameWithoutExt + '(' + number + ').' + realFileNameExt number++ } if (!fileExists(realDirName)) { fs.mkdirSync(realDirName, true) } return decodeURIComponent(realFilePath) } /** * TODO 鸿蒙的downloadFile接口也需要传filePath,仍然会遇到content-type -> extension的问题 */ class DownloadTask implements UniDownloadTask { __v_skip: boolean = true 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) } } export const downloadFile = defineTaskApi( API_DOWNLOAD_FILE, (args: UniDownloadFileOptions, exec: ApiExecutor) => { let { url, timeout, header, filePath } = args let dirName = ''; let fileName = '' if (filePath) { const realPath = getRealPath(filePath) as string if (filePath.endsWith('/')) { dirName = realPath.endsWith('/') ? realPath.slice(0, -1) : realPath; } else { dirName = realPath.substring(0, realPath.lastIndexOf('/')); fileName = realPath.substring(realPath.lastIndexOf('/') + 1); } } 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 CACHE_PATH = getEnv().CACHE_PATH as string const downloadPath = CACHE_PATH + '/uni-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) { tempFilePath = getDownloadFilePath({ dirName, fileName, contentType: responseContentType, contentDisposition: responseContentDisposition, url, downloadPath } as GetDownloadFilePathOptions) try { stream = fs.createStreamSync(tempFilePath, 'w+'); } catch (error) { exec.reject((error as BusinessError).message) } } 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