filecache
This commit is contained in:
@@ -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方法第二个参数可以传errCode,reject(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
|
||||
}
|
||||
Reference in New Issue
Block a user