304 lines
11 KiB
Plaintext
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
|