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( API_UPLOAD_FILE, (args: UniUploadFileOptions, exec: ApiExecutor) => { 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 if (header) { const headerRecord = header as Object as Record 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 if (formData) { const formDataRecord = formData as Object as Record 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 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