Files
im/uni_modules/network-manage/utssdk/app-harmony/network/downloadFile.uts
T
2025-12-27 07:08:30 +08:00

304 lines
11 KiB
Plaintext

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<UniDownloadFileOptions, UniDownloadFileSuccess, UniDownloadTask>(
API_DOWNLOAD_FILE,
(args: UniDownloadFileOptions, exec: ApiExecutor<UniDownloadFileSuccess>) => {
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<string, string | string[]>
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<number> {
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<void> = 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