filecache

This commit is contained in:
cansnow
2025-12-27 07:08:30 +08:00
parent 974d149d25
commit 09c7889525
54 changed files with 10485 additions and 164 deletions
@@ -0,0 +1,25 @@
import { BusinessError } from '@ohos.base';
import { ConfigMTLS, ConfigMTLSOptions, ConfigMTLSSuccess, Certificate } from '../../interface.uts';
import { API_CONFIG_MTLS, ConfigMTLSApiOptions, ConfigMTLSApiProtocol } from '../../protocol.uts';
import { certificates } from './utils.uts';
export const configMTLS: ConfigMTLS = defineAsyncApi<ConfigMTLSOptions, ConfigMTLSSuccess>(
API_CONFIG_MTLS,
(args: ConfigMTLSOptions, executor: ApiExecutor<ConfigMTLSSuccess>) => {
try {
args.certificates.forEach((certificate) => {
const certHosts = certificates.map(cert => cert.host)
const certHostIndex = certHosts.indexOf(certificate.host)
if (certHostIndex > -1) {
certificates.splice(certHostIndex, 1)
}
certificates.push(certificate)
})
executor.resolve()
} catch (error) {
executor.reject((error as BusinessError).message)
}
},
ConfigMTLSApiProtocol,
ConfigMTLSApiOptions
) as ConfigMTLS
@@ -0,0 +1,56 @@
import webview from '@ohos.web.webview';
// 鸿蒙非secure cookie无法保存
// function replaceHttpWithHttps(url: string): string {
// return url.replace(/^http:/, 'https:');
// }
export function getCookie(url: string): Promise<string> {
return webview.WebCookieManager.fetchCookie(url);
}
export function getCookieSync(url: string): string {
return webview.WebCookieManager.fetchCookieSync(url);
}
export function setCookie(url: string, cookies: string[]): Promise<void> {
return Promise.all(cookies.map(cookie => webview.WebCookieManager.configCookie(url, cookie))).then(() => {
return webview.WebCookieManager.saveCookieAsync();
});
}
export function setCookieSync(url: string, cookies: string[]): void {
cookies.forEach(cookie => {
// 不知道什么版本鸿蒙修复了非secure cookie无法保存的问题
try {
webview.WebCookieManager.configCookieSync(url, cookie);
} catch (error) { }
// let hasSecure = false;
// let hasSameSite = false;
// let savedCookie = cookie.split(';').map(cookieItem => {
// const pair = cookieItem.split('=').map(item => item.trim())
// const keyLower = pair[0].toLowerCase();
// if (keyLower === 'secure') {
// hasSecure = true;
// return cookieItem;
// }
// if (keyLower === 'samesite') {
// hasSameSite = true;
// return 'samesite=none';
// }
// return cookieItem
// }).join(';');
// if (!hasSecure) {
// savedCookie += '; secure';
// }
// if (!hasSameSite) {
// savedCookie += '; samesite=none';
// }
// try {
// // https://baidu.com/ 会返回一条 Set-Cookie: __bsi=; max-age=3600; domain=m.baidu.com; path=/(无重定向) m.baidu.com与baidu.com不一致configCookieSync会抛出错误导致崩溃
// webview.WebCookieManager.configCookieSync(replaceHttpWithHttps(url), savedCookie);
// } catch (error) { }
});
webview.WebCookieManager.saveCookieAsync();
}
@@ -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
@@ -0,0 +1,17 @@
export function lookupExt(contentType: string): string | undefined {
const rawContentType = contentType.split(';')[0].trim().toLowerCase()
return (UTSHarmony.getExtensionFromMimeType(rawContentType) as string | null) || undefined
}
export function lookupContentTypeWithUri(uri: string): string | undefined {
const uriArr = uri.split('.')
if (uriArr.length <= 1) {
return undefined
}
const ext = uriArr.pop() as string
return (UTSHarmony.getMimeTypeFromExtension(ext) as string | null) || undefined
}
export function lookupContentType(ext: string): string | undefined {
return (UTSHarmony.getMimeTypeFromExtension(ext) as string | null) || undefined
}
@@ -0,0 +1,360 @@
import http from '@ohos.net.http'
import buffer from '@ohos.buffer';
import {
Emitter,
getCurrentMP,
} from '@dcloudio/uni-runtime'
import {
RequestTask as UniRequestTask,
RequestOptions as UniRequestOptions,
RequestSuccess as UniRequestSuccess,
Request,
RequestTaskOnChunkReceivedCallback,
RequestTaskOnChunkReceivedListenerResult,
RequestTaskOnHeadersReceivedCallback,
RequestTaskOnHeadersReceivedListenerResult,
} from '../../interface.uts'
import {
API_REQUEST,
RequestApiOptions,
RequestApiProtocol,
} from '../../protocol.uts'
import {
getClientCertificate,
parseUrl,
} from './utils.uts'
import {
getCookieSync,
setCookieSync,
} from './cookie.uts'
interface IUniRequestEmitter {
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 IRequestTask {
abort: Function
onChunkReceived: (listener: RequestTaskOnChunkReceivedCallback) => void
offChunkReceived: (listener?: RequestTaskOnChunkReceivedCallback | null) => void
onHeadersReceived: (listener: RequestTaskOnHeadersReceivedCallback) => void
offHeadersReceived: (listener?: RequestTaskOnHeadersReceivedCallback | null) => void
}
class RequestTask implements UniRequestTask {
__v_skip: boolean = true
private _requestTask: IRequestTask
private _requestOnChunkReceiveCallbackId: number = 0
private _requestOnChunkReceiveCallbacks: Map<number, RequestTaskOnChunkReceivedCallback> = new Map()
private _requestOnHeadersReceiveCallbackId: number = 0
private _requestOnHeadersReceiveCallbacks: Map<number, RequestTaskOnHeadersReceivedCallback> = new Map()
constructor(requestTask: IRequestTask) {
this._requestTask = requestTask
}
abort() {
this._requestTask.abort()
}
onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
this._requestTask.onChunkReceived(callback)
this._requestOnChunkReceiveCallbackId++
this._requestOnChunkReceiveCallbacks.set(this._requestOnChunkReceiveCallbackId, callback)
return this._requestOnChunkReceiveCallbackId
}
offChunkReceived(callbackId?: RequestTaskOnChunkReceivedCallback | number | null) {
if (callbackId === undefined || callbackId === null) {
this._requestTask.offChunkReceived()
this._requestOnChunkReceiveCallbacks.clear()
return
}
if (typeof callbackId === 'function') {
this._requestOnChunkReceiveCallbacks.forEach((callback, id) => {
if (callback === callbackId) {
this._requestOnChunkReceiveCallbacks.delete(id)
this._requestTask.offChunkReceived(callback)
return
}
})
return
}
const callback = this._requestOnChunkReceiveCallbacks.get(callbackId)
if (!callback) {
return
}
this._requestOnChunkReceiveCallbacks.delete(callbackId)
this._requestTask.offChunkReceived(callback)
}
onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
this._requestTask.onHeadersReceived(callback)
this._requestOnHeadersReceiveCallbackId++
this._requestOnHeadersReceiveCallbacks.set(this._requestOnHeadersReceiveCallbackId, callback)
return this._requestOnHeadersReceiveCallbackId
}
offHeadersReceived(callbackId?: RequestTaskOnHeadersReceivedCallback | number | null) {
if (callbackId === undefined || callbackId === null) {
this._requestTask.offHeadersReceived()
this._requestOnHeadersReceiveCallbacks.clear()
return
}
if (typeof callbackId === 'function') {
this._requestOnHeadersReceiveCallbacks.forEach((callback, id) => {
if (callback === callbackId) {
this._requestOnHeadersReceiveCallbacks.delete(id)
this._requestTask.offHeadersReceived(callback)
return
}
})
return
}
const callback = this._requestOnHeadersReceiveCallbacks.get(callbackId)
if (!callback) {
return
}
this._requestOnHeadersReceiveCallbacks.delete(callbackId)
this._requestTask.offHeadersReceived(callback)
}
}
/**
* http.request有5MB响应体大小限制
* rcp.Session.fetch无响应体大小限制,但是headers、cookies处理不够完善,另外rcp需要从@hms引用,不是openharmony标准库
* 为突破http.request的限制,现在使用http.requestInStream
*/
export const request = defineTaskApi<UniRequestOptions<Object>, UniRequestSuccess<Object>, UniRequestTask>(
API_REQUEST,
(args: UniRequestOptions<Object>, exec: ApiExecutor<UniRequestSuccess<Object>>) => {
let { header, method, dataType, timeout, url, responseType } = args
let data: ESObject = args.data
header = header || {} as ESObject
if (!header!['Cookie'] && !header!['cookie']) {
header!['Cookie'] = getCookieSync(url);
}
let contentType = ''
// header
const headers = {} as Record<string, Object>
const headerRecord = header as Object as Record<string, string>
const headerKeys = Object.keys(headerRecord)
for (let i = 0; i < headerKeys.length; i++) {
const name = headerKeys[i];
if (name.toLowerCase() === 'content-type') {
contentType = headerRecord[name] as string
}
headers[name.toLowerCase()] = headerRecord[name]
}
if (!contentType && method === 'POST') {
headers['Content-Type'] = 'application/json'
contentType = 'application/json'
}
// url data
if (method === 'GET' && data && typeof data === 'object') {
const dataRecord = data as Record<string, Object>
const query = Object.keys(dataRecord)
.map((key) => {
return (
encodeURIComponent(key) +
'=' +
encodeURIComponent(dataRecord[key] as string | number | boolean)
)
})
.join('&')
url += query ?
(url.indexOf('?') > -1 ? '&' : '?') + query :
''
data = null
} else if (
method !== 'GET' &&
contentType &&
contentType.indexOf('application/json') === 0 &&
data &&
typeof data !== 'string'
) {
data = JSON.stringify(data)
} else if (
method !== 'GET' &&
contentType &&
contentType.indexOf('application/x-www-form-urlencoded') === 0 &&
data &&
typeof data === 'object'
) {
const dataRecord = data as Record<string, Object>
data = Object.keys(dataRecord)
.map((key) => {
return (
encodeURIComponent(key) +
'=' +
encodeURIComponent(dataRecord[key] as number | string | boolean)
)
})
.join('&')
}
const httpRequest = http.createHttp()
const mp = getCurrentMP()
const userAgent = mp.userAgent.fullUserAgent
if (userAgent && headers && !headers!['User-Agent'] && !headers!['user-agent']) {
headers!['User-Agent'] = userAgent
}
const emitter = new Emitter() as IUniRequestEmitter
const requestTask: IRequestTask = {
abort() {
emitter.off('headersReceive')
httpRequest.destroy()
},
onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
emitter.on('headersReceive', callback)
},
offHeadersReceived(callback?: RequestTaskOnHeadersReceivedCallback | null) {
emitter.off('headersReceive', callback || undefined)
},
onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
emitter.on('dataReceive', callback)
},
offChunkReceived(callback?: RequestTaskOnChunkReceivedCallback | null) {
emitter.off('dataReceive', callback || undefined)
},
}
const realRequestTask = new RequestTask(requestTask)
function destroy() {
emitter.off('headersReceive')
httpRequest.destroy()
}
mp.on('beforeClose', destroy)
let latestHeaders: Object | null = null
let lastUrl = url
httpRequest.on('headersReceive', (headers: Object) => {
const realHeaders = headers as Record<string, string | string[]>
const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie']
if (setCookieHeader) {
setCookieSync(lastUrl, setCookieHeader as string[])
}
latestHeaders = headers
const location = realHeaders['location'] || realHeaders['Location']
if (location) {
lastUrl = location as string
}
// TODO headersReceive存在bug,暂不支持回调给用户。
// emitter.emit('headersReceive', headers);
})
let headersReceiveTriggered = false
const bufs = [] as buffer.Buffer[]
httpRequest.on('dataReceive', (data) => {
if (!headersReceiveTriggered) {
headersReceiveTriggered = true
// 注意重定向时会多次触发,但是只需要给用户回调最后一次headers
const headers = {} as UTSJSONObject
const realHeaders = latestHeaders as Record<string, string | string[]>
let cookies = [] as string[]
// 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错
if (realHeaders) {
const keys = Object.keys(realHeaders)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key === 'set-cookie' || key === 'Set-Cookie') {
const setCookieHeader = realHeaders[key] as string[];
// TODO 待确认为什么鸿蒙会返回重复的Set-Cookie,不合常理。使用 https://httpbin.org/cookies/set/nnn/123 这个接口测试
for (let i = setCookieHeader.length - 1; i >= 0; i--) {
const cookie = setCookieHeader[i]
if (setCookieHeader.indexOf(cookie) !== i) {
setCookieHeader.splice(i, 1)
}
}
cookies = setCookieHeader as string[];
headers[key] = Array.isArray(setCookieHeader) ? setCookieHeader.join(',') : setCookieHeader;
} else {
headers[key] = realHeaders[key];
}
}
}
emitter.emit('headersReceive', {
header: headers,
// 鸿蒙不支持在headersReceive时机获取到状态码
statusCode: NaN,
cookies: cookies,
} as RequestTaskOnHeadersReceivedListenerResult);
}
bufs.push(buffer.from(data))
if (args.enableChunked) {
emitter.emit('dataReceive', {
data: data
} as RequestTaskOnChunkReceivedListenerResult)
}
})
httpRequest.requestInStream(
parseUrl(url),
{
header: headers,
method: (method || 'GET').toUpperCase() as http.RequestMethod, // 仅OPTIONS不支持
extraData: data || undefined, // 传空字符串会报错
connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout
readTimeout: timeout ? timeout : undefined,
clientCert: getClientCertificate(url)
} as http.HttpRequestOptions,
(err, statusCode) => {
if (err) {
/**
* TODO abort后此处收到如下错误,待确认是否直接将此错误码转为abort错误
* {"code":2300023,"message":"Failed writing received data to disk/application"}
*
* reject方法第二个参数可以传errCodereject(err.message, { errCode: -1 })
*/
exec.reject(err.message)
} else {
const responseData = buffer.concat(bufs)
let data: ArrayBuffer | string | object = ''
if (responseType === 'arraybuffer') {
data = responseData.buffer
} else {
data = responseData.toString('utf8')
if (dataType === 'json') {
try {
// #ifdef UNI-APP-X
data = globalThis.UTS.JSON.parse(data) || data;
// #endif
// #ifndef UNI-APP-X
data = JSON.parse(data as string);
// #endif
} catch (e) {
// 与微信保持一致,不抛出异常
}
}
}
const headers = latestHeaders as Record<string, string | string[]>
// 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错
const oldCookies = headers ? (headers['Set-Cookie'] || headers['set-cookie'] || []) as string[] : [] as string[]
let newCookies = oldCookies.join(',')
if (newCookies) {
if (headers['Set-Cookie']) {
headers['Set-Cookie'] = newCookies
} else {
headers['set-cookie'] = newCookies
}
}
exec.resolve({
data,
statusCode,
header: latestHeaders!,
cookies: oldCookies,
} as UniRequestSuccess<Object>)
}
realRequestTask.offChunkReceived()
realRequestTask.offHeadersReceived()
httpRequest.destroy() // 调用完毕后必须调用destroy方法
mp.off('beforeClose', destroy)
}
)
return realRequestTask
},
RequestApiProtocol,
RequestApiOptions
) as Request<Object>
@@ -0,0 +1,253 @@
import http from '@ohos.net.http'
import buffer from '@ohos.buffer';
import fs from '@ohos.file.fs'
import {
getRealPath,
Emitter,
getCurrentMP,
} from '@dcloudio/uni-runtime'
import {
UploadTask as UniUploadTask,
UploadFileOptions as UniUploadFileOptions,
UploadFileSuccess as UniUploadFileSuccess,
OnProgressUpdateResult,
UploadFile
} from '../../interface.uts'
import {
API_UPLOAD_FILE,
UploadFileApiOptions,
UploadFileApiProtocol,
} from '../../protocol.uts'
import {
lookupContentTypeWithUri
} from './mime.uts'
import {
getClientCertificate,
parseUrl,
} from './utils.uts'
import {
getCookieSync,
setCookieSync
} from './cookie.uts'
interface IUniUploadFileEmitter {
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 IUploadTask {
abort: Function
onHeadersReceived: Function
offHeadersReceived: Function
onProgressUpdate: Function
offProgressUpdate: Function
}
class UploadTask implements UniUploadTask {
__v_skip: boolean = true
private _uploadTask: IUploadTask
constructor(uploadTask: IUploadTask) {
this._uploadTask = uploadTask
}
abort() {
this._uploadTask.abort()
}
onProgressUpdate(callback: Function) {
this._uploadTask.onProgressUpdate(callback)
}
offProgressUpdate(callback?: Function) {
this._uploadTask.offProgressUpdate(callback)
}
onHeadersReceived(callback: Function) {
this._uploadTask.onHeadersReceived(callback)
}
offHeadersReceived(callback?: Function) {
this._uploadTask.offHeadersReceived(callback)
}
}
function readFile(filePath: string): ArrayBuffer {
const readFilePath = getRealPath(filePath) as string
const file = fs.openSync(readFilePath, fs.OpenMode.READ_ONLY)
const stat = fs.statSync(file.fd)
const data = new ArrayBuffer(stat.size)
fs.readSync(file.fd, data)
fs.closeSync(file.fd)
return data
}
export const uploadFile = defineTaskApi<UniUploadFileOptions, UniUploadFileSuccess, UniUploadTask>(
API_UPLOAD_FILE,
(args: UniUploadFileOptions, exec: ApiExecutor<UniUploadFileSuccess>) => {
let { url, timeout, header, formData, files, filePath, name } = args
header = header || {} as ESObject
if (!header!['Cookie'] && !header!['cookie']) {
header!['Cookie'] = getCookieSync(url);
}
// header
const headers = {} as Record<string, Object>
if (header) {
const headerRecord = header as Object as Record<string, string>
const headerKeys = Object.keys(headerRecord)
for (let i = 0; i < headerKeys.length; i++) {
const name = headerKeys[i]
headers[name.toLowerCase()] = headerRecord[name]
}
}
headers['Content-Type'] = 'multipart/form-data'
const multiFormDataList = [] as Array<http.MultiFormData>
if (formData) {
const formDataRecord = formData as Object as Record<string, Object>
const formDataKeys = Object.keys(formDataRecord)
for (let i = 0; i < formDataKeys.length; i++) {
const name = formDataKeys[i]
multiFormDataList.push({
name,
contentType: 'text/plain',
data: String(formDataRecord[name]),
} as http.MultiFormData)
}
}
try {
if (files && files.length) {
for (let i = 0; i < files.length; i++) {
const { name, uri } = files[i]
multiFormDataList.push({
name: name || 'file', // 鸿蒙的request.uploadFile接口不能为每个文件设置name
contentType: lookupContentTypeWithUri(uri) || 'application/octet-stream',
remoteFileName: uri.split('/').pop() || 'no-name',
/**
* TODO 未联系鸿蒙确认
* chooseImage返回"file://media/Photo/1/xxxx",此uri可以被fs相关接口读取,但是上传时使用会报错
* 使用"file://media/Photo/1/xxxx"、"file:///media/Photo/1/xxxx"、"/media/Photo/1/xxxx"、"media/Photo/1/xxxx"、格式都会报错
* 错误信息:Failed to open/read local data from file/application
*/
// filePath: getRealPath(uri!),
// TODO 调整为异步读取
data: readFile(uri!)
} as http.MultiFormData)
}
} else if (filePath) {
multiFormDataList.push({
name: name || 'file',
contentType: lookupContentTypeWithUri(filePath!) || 'application/octet-stream',
remoteFileName: filePath.split('/').pop() || 'no-name',
data: readFile(filePath!)
} as http.MultiFormData)
}
} catch (error) {
exec.reject((error as Error).message)
return new UploadTask({
abort: () => { },
onHeadersReceived: (callback: Function) => { },
offHeadersReceived: (callback: Function) => { },
onProgressUpdate: (callback: Function) => { },
offProgressUpdate: (callback: Function) => { },
} as IUploadTask)
}
const httpRequest = http.createHttp()
const mp = getCurrentMP()
const userAgent = mp.userAgent.fullUserAgent
if (userAgent && !headers['User-Agent'] && !headers['user-agent']) {
headers['User-Agent'] = userAgent
}
const emitter = new Emitter() as IUniUploadFileEmitter
const uploadTask: IUploadTask = {
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() {
emitter.off('headersReceive')
emitter.off('progress')
httpRequest.destroy()
}
mp.on('beforeClose', destroy)
let lastUrl = url
httpRequest.on('headersReceive', (headers: Object) => {
const realHeaders = headers as Record<string, string | 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('dataSendProgress', ({ sendSize, totalSize }) => {
emitter.emit('progress', {
progress: Math.floor((sendSize / totalSize) * 100),
totalBytesSent: sendSize,
totalBytesExpectedToSend: totalSize,
} as OnProgressUpdateResult)
})
const bufs = [] as buffer.Buffer[]
httpRequest.on('dataReceive', (data) => {
bufs.push(buffer.from(data))
})
httpRequest.requestInStream(
parseUrl(url),
{
header: headers,
method: http.RequestMethod.POST,
connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout
readTimeout: timeout ? timeout : undefined,
multiFormDataList,
expectDataType: http.HttpDataType.STRING,
clientCert: getClientCertificate(url)
} as http.HttpRequestOptions,
(err, statusCode) => {
if (err) {
/**
* TODO abort后此处收到如下错误,待确认是否直接将此错误码转为abort错误
* {"code":2300023,"message":"Failed writing received data to disk/application"}
*/
exec.reject(err.message)
} else {
const responseData = buffer.concat(bufs)
exec.resolve({
data: responseData.toString('utf8'),
statusCode,
} as UniUploadFileSuccess)
}
uploadTask.offHeadersReceived()
uploadTask.offProgressUpdate()
httpRequest.destroy() // 调用完毕后必须调用destroy方法
mp.off('beforeClose', destroy)
}
)
return new UploadTask(uploadTask)
},
UploadFileApiProtocol,
UploadFileApiOptions
) as UploadFile
@@ -0,0 +1,62 @@
import harmonyUrl from '@ohos.url'
import { http } from '@kit.NetworkKit'
import { Certificate } from '../../interface.uts'
import { getRealPath } from '@dcloudio/uni-runtime'
/**
* 鸿蒙url内包含中文时处理有问题
* 例如 /code/version/11?subject=中文测试 在部分服务器上会收到 /code/version/11?subject=道德与æ³
* 如下看起来很怪异的代码仅仅是为了绕过此Bug,待鸿蒙修复后可删除
*/
function needsEncoding(str: string) {
const decoded = decodeURIComponent(str);
if (decoded !== str) {
if (encodeURIComponent(decoded) === str) {
return false;
}
}
return encodeURIComponent(decoded) !== decoded;
}
export function parseUrl(url: string) {
try {
const urlObj = harmonyUrl.URL.parseURL(url);
urlObj.params.forEach((value, key) => {
if (needsEncoding(value)) {
urlObj.params.set(key, value);
}
})
return urlObj.toString()
} catch (error) {
return url
}
}
export const certificates: Certificate[] = []
function getCertType(certPath: string): http.CertType {
const certExt = certPath.split('.').pop()
switch (certExt) {
case 'p12':
return http.CertType.P12
case 'pem':
return http.CertType.PEM
default:
return http.CertType.PEM
}
}
export function getClientCertificate(url: string): http.ClientCert | undefined {
if (certificates.length === 0) return undefined
const urlObj = harmonyUrl.URL.parseURL(url);
const cert = certificates.find((certificate) => certificate.host === urlObj.host)
if (cert) {
return {
certType: getCertType(cert.client!),
certPath: getRealPath(cert.client!),
keyPath: cert.keyPath ?? '',
keyPassword: cert.clientPassword
} as http.ClientCert
}
return undefined
}