filecache
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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
|
||||
Reference in New Issue
Block a user