xugenyuan

ref |> UAT调通阿里OSS签名直传,意见反馈已添加带图片上传

Signed-off-by: xugenyuan <xugenyuan@wondertek.com.cn>
@@ -14,3 +14,4 @@ export { HostEnum, HostManager } from "./src/main/ets/http/HttpHostManager" @@ -14,3 +14,4 @@ export { HostEnum, HostManager } from "./src/main/ets/http/HttpHostManager"
14 14
15 export { CacheData } from "./src/main/ets/utils/CacheData" 15 export { CacheData } from "./src/main/ets/utils/CacheData"
16 16
  17 +export { HttpParams } from "./src/main/ets/http/HttpCommonParams"
@@ -895,4 +895,13 @@ export class HttpUrlUtils { @@ -895,4 +895,13 @@ export class HttpUrlUtils {
895 return url 895 return url
896 } 896 }
897 897
  898 + static getOSSTokenUrl() {
  899 + let url = HttpUrlUtils.getHost() + HttpUrlUtils.STS_TOKEN_PATH
  900 + return url
  901 + }
  902 +
  903 + static getOSSConfigInfoUrl () {
  904 + let url = HttpUrlUtils.getHost() + HttpUrlUtils.OSS_PARAMS_PATH
  905 + return url
  906 + }
898 } 907 }
@@ -180,6 +180,9 @@ export { TopicDetailData,GroupItem } from './src/main/ets/bean/content/TopicDeta @@ -180,6 +180,9 @@ export { TopicDetailData,GroupItem } from './src/main/ets/bean/content/TopicDeta
180 180
181 export { FeedbackTypeBean } from './src/main/ets/bean/detail/FeedbackTypeBean'; 181 export { FeedbackTypeBean } from './src/main/ets/bean/detail/FeedbackTypeBean';
182 export { FeedBackParams } from './src/main/ets/bean/content/FeedBackParams'; 182 export { FeedBackParams } from './src/main/ets/bean/content/FeedBackParams';
  183 +export { NetLayerOSSToken } from './src/main/ets/bean/oss/NetLayerOSSToken';
  184 +export { NetLayerOSSConfigInfoModel } from './src/main/ets/bean/oss/NetLayerOSSConfigInfoModel';
  185 +
183 186
184 export { NetLayerVoiceRecoginizerToken } from './src/main/ets/bean/voicerecoginizer/NetLayerVoiceRecoginizerToken'; 187 export { NetLayerVoiceRecoginizerToken } from './src/main/ets/bean/voicerecoginizer/NetLayerVoiceRecoginizerToken';
185 188
@@ -5,4 +5,5 @@ export interface FeedBackParams { @@ -5,4 +5,5 @@ export interface FeedBackParams {
5 userName:string 5 userName:string
6 appVersion:string 6 appVersion:string
7 appDevice:string 7 appDevice:string
  8 + imageUrls:Array<string>
8 } 9 }
  1 +export interface NetLayerOSSConfigInfoModel {
  2 +
  3 + bucketName: string; //bucket
  4 + type: string; //对应的场景类型
  5 + endPoint: string; //图片region
  6 + uploadPath: string; //文件存储的路径
  7 + domain: string; //图片对应桶CDN地址
  8 +}
  1 +/*
  2 +{
  3 + securityToken = "CAIS8wF1q6Ft5B2yfSjIr5eAc9eNoJRZ7ayTe3HFj3U8fMVdhvf9pDz2IH9KfnJoBu8esvQ+nWBY5/oalqNJQppiXlf+as99tj6zAowDO9ivgde8yJBZor/HcDHhJnyW9cvWZPqDP7G5U/yxalfCuzZuyL/hD1uLVECkNpv74vwOLK5gPG+CYCFBGc1dKyZ7tcYeLgGxD/u2NQPwiWeiZygB+CgE0Dojufzhm5LEt0GG0gCmm9V4/dqhfsKWCOB3J4p6XtuP2+h7S7HMyiY46WIRq/ks1/Qbpm+b7oDEUwkAv02cUfDd99p0NxV+YqUqxkWsVW8QeJcagAFKgKk8XPz8DfhO47575fFMjkr46tLwuZaaCvORfAdBZS2c/htVmIckl9LBjVeWP4JP4/PNLxmLgKRjyTJ87JPMDY1Uler3FG9dgLaUPU7IESUqyc7lqtroKdaXZhCOdj+yawEow11k6pgF1CjUf8ty/U8Q22//CZ/vGhkYaY0N+A==";
  4 + accessKeySecret = "Ar5Pre9EBACKsKoyGHotM7VwPefqhfCARMYnEbgNWuuS";
  5 + accessKeyId = "STS.NT58m9MKxZnxyWtkuipJqi2VF";
  6 + expiration = "2022-11-25 11:48:36";
  7 + currentTime = "2022-11-25 10:48:36";
  8 + };
  9 + * */
  10 +export interface NetLayerOSSToken {
  11 + securityToken: string;
  12 + accessKeySecret: string;
  13 + accessKeyId: string;
  14 + expiration: string; // "2024-08-15 20:27:29"
  15 + // timeStamp: string;
  16 + currentTime: string; // "2024-08-15 19:27:29"
  17 + type: string;
  18 +}
@@ -14,6 +14,8 @@ import { ProcessUtils } from 'wdRouter/Index'; @@ -14,6 +14,8 @@ import { ProcessUtils } from 'wdRouter/Index';
14 import { TrackConstants, TrackingButton, TrackingPageBrowse } from 'wdTracking/Index'; 14 import { TrackConstants, TrackingButton, TrackingPageBrowse } from 'wdTracking/Index';
15 import inputMethod from '@ohos.inputMethod'; 15 import inputMethod from '@ohos.inputMethod';
16 import { photoPickerUtils } from '../utils/PhotoPickerUtils'; 16 import { photoPickerUtils } from '../utils/PhotoPickerUtils';
  17 +import { OSSConfigSceneType, OSSUploadManager,OSSFileType, UploadResourceParams } from 'wdHwAbility'
  18 +import { it } from '@ohos/hypium';
17 19
18 const TAG = 'FeedBackActivity' 20 const TAG = 'FeedBackActivity'
19 21
@@ -45,6 +47,8 @@ export struct FeedBackActivity { @@ -45,6 +47,8 @@ export struct FeedBackActivity {
45 @State bottomSafeHeight: number = AppStorage.get<number>('bottomSafeHeight') || 0 47 @State bottomSafeHeight: number = AppStorage.get<number>('bottomSafeHeight') || 0
46 @Provide topSafeHeight: number = AppStorage.get<number>('topSafeHeight') || 0 48 @Provide topSafeHeight: number = AppStorage.get<number>('topSafeHeight') || 0
47 49
  50 + @State showLoading: boolean = false
  51 +
48 dialogToast: CustomDialogController = new CustomDialogController({ 52 dialogToast: CustomDialogController = new CustomDialogController({
49 builder: CustomToast({ 53 builder: CustomToast({
50 bgColor:$r("app.color.color_B3000000"), 54 bgColor:$r("app.color.color_B3000000"),
@@ -160,61 +164,61 @@ export struct FeedBackActivity { @@ -160,61 +164,61 @@ export struct FeedBackActivity {
160 columns:5, 164 columns:5,
161 }) { 165 }) {
162 166
163 - // ForEach(this.pics, (feedbackImageItem: PhotoListBean, index: number) => {  
164 - // GridCol({  
165 - // }) {  
166 - // if(1 == feedbackImageItem.itemType){  
167 - // Image($r('app.media.feekback_add'))  
168 - // .width(60)  
169 - // .height(60)  
170 - // .onClick(async (event: ClickEvent) => {  
171 - // if(await FastClickUtil.isMinDelayTime()){  
172 - // return  
173 - // }  
174 - // this.callFilePickerSelectImage();  
175 - // })  
176 - // }else{  
177 - // Stack({alignContent: Alignment.TopEnd}) {  
178 - // Image(feedbackImageItem.picPath)  
179 - // .width(60)  
180 - // .height(60)  
181 - // .borderRadius($r('app.float.margin_1'))  
182 - // .onClick(async (event: ClickEvent) => {  
183 - // if(await FastClickUtil.isMinDelayTime()){  
184 - // return  
185 - // }  
186 - // //查看图片 fixme 去除添加按钮  
187 - // ProcessUtils.gotoMultiPictureListPage(this.pics, index)  
188 - // })  
189 - // Image($r('app.media.icon_feekback_delete'))  
190 - // .width(24)  
191 - // .height(24)  
192 - // .borderRadius($r('app.float.margin_1'))  
193 - // .onClick(async (event: ClickEvent) => {  
194 - // if(await FastClickUtil.isMinDelayTime()){  
195 - // return  
196 - // }  
197 - // let temp: PhotoListBean[] = [] as PhotoListBean[]  
198 - // temp.length = this.pics.length - 1;  
199 - // let tempIndex = 0;  
200 - // for (let index = 0; index < this.pics.length; index++) {  
201 - // const element = this.pics[index];  
202 - // if(!StringUtils.isEmpty(element.picPath) && element.id != feedbackImageItem.id){  
203 - // temp[tempIndex] = element;  
204 - // tempIndex = tempIndex+1  
205 - // }  
206 - // }  
207 - // if(tempIndex < 3){  
208 - // temp[tempIndex] = this.addPic  
209 - // }  
210 - // this.pics = temp  
211 - // })  
212 - // }  
213 - // .width(60)  
214 - // .height(60)  
215 - // }  
216 - // }  
217 - // }) 167 + ForEach(this.pics, (feedbackImageItem: PhotoListBean, index: number) => {
  168 + GridCol({
  169 + }) {
  170 + if(1 == feedbackImageItem.itemType){
  171 + Image($r('app.media.feekback_add'))
  172 + .width(60)
  173 + .height(60)
  174 + .onClick(async (event: ClickEvent) => {
  175 + if(await FastClickUtil.isMinDelayTime()){
  176 + return
  177 + }
  178 + this.callFilePickerSelectImage();
  179 + })
  180 + }else{
  181 + Stack({alignContent: Alignment.TopEnd}) {
  182 + Image(feedbackImageItem.picPath)
  183 + .width(60)
  184 + .height(60)
  185 + .borderRadius($r('app.float.margin_1'))
  186 + .onClick(async (event: ClickEvent) => {
  187 + if(await FastClickUtil.isMinDelayTime()){
  188 + return
  189 + }
  190 + //查看图片 fixme 去除添加按钮
  191 + ProcessUtils.gotoMultiPictureListPage(this.pics, index)
  192 + })
  193 + Image($r('app.media.icon_feekback_delete'))
  194 + .width(24)
  195 + .height(24)
  196 + .borderRadius($r('app.float.margin_1'))
  197 + .onClick(async (event: ClickEvent) => {
  198 + if(await FastClickUtil.isMinDelayTime()){
  199 + return
  200 + }
  201 + let temp: PhotoListBean[] = [] as PhotoListBean[]
  202 + temp.length = this.pics.length - 1;
  203 + let tempIndex = 0;
  204 + for (let index = 0; index < this.pics.length; index++) {
  205 + const element = this.pics[index];
  206 + if(!StringUtils.isEmpty(element.picPath) && element.id != feedbackImageItem.id){
  207 + temp[tempIndex] = element;
  208 + tempIndex = tempIndex+1
  209 + }
  210 + }
  211 + if(tempIndex < 3){
  212 + temp[tempIndex] = this.addPic
  213 + }
  214 + this.pics = temp
  215 + })
  216 + }
  217 + .width(60)
  218 + .height(60)
  219 + }
  220 + }
  221 + })
218 } 222 }
219 .margin({bottom: $r('app.float.vp_12'), right: $r('app.float.vp_12'),left: $r('app.float.vp_12')}) 223 .margin({bottom: $r('app.float.vp_12'), right: $r('app.float.vp_12'),left: $r('app.float.vp_12')})
220 Text(){ 224 Text(){
@@ -426,6 +430,7 @@ export struct FeedBackActivity { @@ -426,6 +430,7 @@ export struct FeedBackActivity {
426 return 430 return
427 } 431 }
428 432
  433 + this.showLoading = true
429 try { 434 try {
430 let feedBackParams: FeedBackParams = { 435 let feedBackParams: FeedBackParams = {
431 //反馈内容 436 //反馈内容
@@ -445,15 +450,36 @@ export struct FeedBackActivity { @@ -445,15 +450,36 @@ export struct FeedBackActivity {
445 feedBackParams.userName = UserDataLocal.getUserName() 450 feedBackParams.userName = UserDataLocal.getUserName()
446 } 451 }
447 452
448 - // //投诉图片  
449 - // if (imageUrl.size() > 0) {  
450 - // String[] str = imageUrl.toArray(new String[imageUrl.size()]);  
451 - // map.set("imageUrls", str);  
452 - // } 453 + //投诉图片
  454 + if (this.pics.length > 0) {
  455 + let inputs = this.pics.filter((item) => {
  456 + return item.picPath && item.picPath.length > 0
  457 + }).map((item) => {
  458 + let i = new UploadResourceParams(item.picPath)
  459 + i.scene = OSSConfigSceneType.feedback
  460 + i.fileType = OSSFileType.image
  461 + return i
  462 + })
  463 + let results = await OSSUploadManager.sharedManager().uploadFiles(inputs)
  464 + if (results.length !== inputs.length) {
  465 + this.showLoading = false
  466 + this.showToastTip('附件上传失败')
  467 + return
  468 + }
  469 +
  470 + let files = results.map((item) => {
  471 + return item.ossFile ?? ""
  472 + })
  473 + feedBackParams.imageUrls = files
  474 + }
  475 +
453 await MultiPictureDetailViewModel.feedBackCommit(feedBackParams) 476 await MultiPictureDetailViewModel.feedBackCommit(feedBackParams)
  477 +
  478 + this.showLoading = false
454 router.back(); 479 router.back();
455 } catch (exception) { 480 } catch (exception) {
456 console.log('请求失败',JSON.stringify(exception)) 481 console.log('请求失败',JSON.stringify(exception))
  482 + this.showLoading = false
457 } 483 }
458 } 484 }
459 485
@@ -6,3 +6,6 @@ export { HWLocationUtils } from './src/main/ets/location/HWLocationUtils' @@ -6,3 +6,6 @@ export { HWLocationUtils } from './src/main/ets/location/HWLocationUtils'
6 export { GetuiPush } from "./src/main/ets/getuiPush/GetuiPush" 6 export { GetuiPush } from "./src/main/ets/getuiPush/GetuiPush"
7 7
8 export {VoiceRecoginizer} from './src/main/ets/voiceRecognizer/VoiceRecoginizer' 8 export {VoiceRecoginizer} from './src/main/ets/voiceRecognizer/VoiceRecoginizer'
  9 +
  10 +export { OSSUploadManager,OSSConfigSceneType,OSSFileType,OSSUploadResult,UploadResourceParams } from "./src/main/ets/aliOSS/OSSUploadManager"
  11 +
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 "wdBean": "file:../../features/wdBean", 15 "wdBean": "file:../../features/wdBean",
16 "wdRouter": "file:../../commons/wdRouter", 16 "wdRouter": "file:../../commons/wdRouter",
17 "wdTracking": "file:../../features/wdTracking", 17 "wdTracking": "file:../../features/wdTracking",
18 - "wdNetwork": "file:../../commons/wdNetwork" 18 + "wdNetwork": "file:../../commons/wdNetwork",
  19 + "@ohos/xml_js": "^1.0.2"
19 } 20 }
20 } 21 }
  1 +
  2 +export class OSSConfig {
  3 + stsToken: string = ''
  4 + bucket: string = ''
  5 + fileName: string = '' // 自定义
  6 + accessKeyId: string = ''
  7 + accessKeySecret: string = ''
  8 +
  9 + customHeaders: Record<string, string | number>
  10 + serverUrl: string = ''
  11 +}
  1 +import { NetLayerOSSConfigInfoModel, NetLayerOSSToken } from 'wdBean'
  2 +import { HostEnum, HttpParams, HttpUrlUtils, ResponseDTO } from 'wdNetwork'
  3 +import { HttpRequest } from 'wdNetwork/src/main/ets/http/HttpRequest'
  4 +import { data } from '@kit.TelephonyKit'
  5 +import { DateTimeUtils, DeviceUtil, Logger } from 'wdKit'
  6 +import { MultipartUpload } from './multipartUpload'
  7 +import { OSSConfig } from './OSSConfig'
  8 +import { http } from '@kit.NetworkKit'
  9 +import { it } from '@ohos/hypium'
  10 +import { putObject } from './upload'
  11 +
  12 +export enum OSSConfigSceneType {
  13 + feedback = "feedback",
  14 +
  15 +}
  16 +
  17 +export enum OSSFileType {
  18 + image = 0,
  19 + video = 1,
  20 +}
  21 +
  22 +export class OSSUploadResult {
  23 + ossFile?: string
  24 +}
  25 +
  26 +export class UploadResourceParams {
  27 + fileUri: string = ''
  28 + scene: OSSConfigSceneType = OSSConfigSceneType.feedback
  29 + fileType: OSSFileType = OSSFileType.image
  30 +
  31 + constructor(fileUri: string) {
  32 + this.fileUri = fileUri
  33 + }
  34 +}
  35 +
  36 +const TAG = "OSSUploadManager"
  37 +
  38 +export class OSSUploadManager {
  39 +
  40 + private ossToken?: NetLayerOSSToken
  41 + private configInfos?: Array<NetLayerOSSConfigInfoModel>
  42 +
  43 + private constructor() { }
  44 + private static manager: OSSUploadManager
  45 + static sharedManager(): OSSUploadManager {
  46 + if (!OSSUploadManager.manager) {
  47 + OSSUploadManager.manager = new OSSUploadManager()
  48 + }
  49 + return OSSUploadManager.manager
  50 + }
  51 +
  52 + uploadFiles(inputs: Array<UploadResourceParams>) : Promise<Array<OSSUploadResult>> {
  53 + return new Promise(async (resolve, fail) => {
  54 + await this.getOssToken()
  55 + await this.getOssConfigInfo()
  56 +
  57 + Promise.allSettled(inputs.map((item): Promise<OSSUploadResult> => {
  58 + return this.uploadFile(item.fileUri, item.scene, item.fileType)
  59 + })).then(res => {
  60 + const failedJobs = res.filter(v => v.status === 'rejected');
  61 +
  62 + if (failedJobs.length > 0) {
  63 + console.info('Failed objects: ' + failedJobs.length);
  64 + resolve([])
  65 + } else {
  66 + console.info('All the objects upload success');
  67 + let results: Array<OSSUploadResult> = [] as Array<OSSUploadResult>
  68 + res.forEach((item) => {
  69 + console.info('>>>>>' + JSON.stringify(item));
  70 + if (item.status === 'fulfilled') {
  71 + results.push(item.value)
  72 + }
  73 + })
  74 + resolve(results)
  75 + }
  76 + })
  77 + })
  78 + }
  79 +
  80 + uploadFile(fileUri: string, scene: OSSConfigSceneType, fileType: OSSFileType) : Promise<OSSUploadResult> {
  81 + return new Promise(async (success, fail) => {
  82 +
  83 + try {
  84 + let tokenModel = await this.getOssToken()
  85 +
  86 + let config = await this.getOssConfigInfo()
  87 + let configModuel = config?.filter((c) => {
  88 + return c.type == scene
  89 + }).pop()
  90 +
  91 + if (!configModuel) {
  92 + Logger.warn(TAG, "配置为空")
  93 + return
  94 + }
  95 +
  96 + //格式样例 zhbj/img/social/2024080215/BA6690B9CF084B3FAEEFA58F100F8D3E.jpg,这里的格式目前和iOS 是保持一致的
  97 + let objectName = configModuel.uploadPath + DateTimeUtils.getCurDate('yyyyMMddHH') + "/" + DeviceUtil.getRandomUUIDForTraceID() + (fileType == OSSFileType.image ? ".jpg" : ".mp4")
  98 + Logger.debug(TAG, `==>> ${fileUri} `)
  99 + Logger.debug(TAG, `==>> endPoint: ${configModuel.endPoint} `)
  100 + Logger.debug(TAG, `==>> bucketName: ${configModuel.bucketName} `)
  101 + Logger.debug(TAG, `==>> objectName: ` + objectName)
  102 + Logger.debug(TAG, JSON.stringify(tokenModel))
  103 + Logger.debug(TAG, JSON.stringify(configModuel))
  104 +
  105 + let result = await this.upload(fileUri, configModuel.endPoint, configModuel.bucketName, objectName)
  106 +
  107 + if (result) {
  108 + let r = {} as OSSUploadResult
  109 + r.ossFile = objectName
  110 +
  111 + let url = configModuel.domain + "/" + objectName
  112 + console.log("result url: " + url)
  113 + success(r)
  114 + return
  115 + }
  116 +
  117 + Logger.error(TAG, "上传失败")
  118 + fail("上传失败")
  119 +
  120 + } catch (e) {
  121 +
  122 + Logger.error(TAG, "上传失败" + JSON.stringify(e))
  123 + fail("上传失败")
  124 + }
  125 + })
  126 + }
  127 +
  128 + private upload(fileUri: string, endpoint:string, bucketName:string, objectName:string) : Promise<boolean> {
  129 +
  130 + return new Promise(async (reslove, fail) => {
  131 +
  132 + if (!this.ossToken || !this.ossToken.securityToken) {
  133 + fail("missing accessKeyId or accessKeySecret or sessionToken")
  134 + return
  135 + }
  136 +
  137 + let config: OSSConfig = {
  138 + stsToken: this.ossToken.securityToken,
  139 + bucket: bucketName,
  140 + fileName: objectName,
  141 + accessKeyId: this.ossToken.accessKeyId,
  142 + accessKeySecret: this.ossToken.accessKeySecret,
  143 + customHeaders: HttpParams.buildHeaders(),
  144 + serverUrl: this.getServerHost() + "/api/harmonyoss/get_sign_url"
  145 + }
  146 +
  147 + let success = false
  148 + try {
  149 + const multipartUpload = new MultipartUpload(config, fileUri);
  150 +
  151 + let result: http.HttpResponse = await multipartUpload.multipartUpload();
  152 + Logger.debug(TAG, "表单上传完成 + " + JSON.stringify(result))
  153 +
  154 + if (result.responseCode == 200) {
  155 + success = true
  156 + }
  157 +
  158 + // await putObject(fileUri, config)
  159 + //
  160 + // success = true
  161 +
  162 + } catch (e) {
  163 + Logger.error(TAG, JSON.stringify(e))
  164 + } finally {
  165 + reslove(success)
  166 + }
  167 + })
  168 + }
  169 +
  170 + private getOssToken():Promise<NetLayerOSSToken | undefined> {
  171 + return new Promise<NetLayerOSSToken | undefined>((success, fail) => {
  172 +
  173 + if (this.ossToken && this.ossToken.securityToken.length > 0) {
  174 +
  175 + // this.ossToken.expiration
  176 + let dateNumber = DateTimeUtils.parseDate(this.ossToken.expiration, DateTimeUtils.PATTERN_DATE_TIME_HYPHEN)
  177 + if (Date.now() < (dateNumber - 60 * 1000)) {
  178 + success(this.ossToken)
  179 + return
  180 + }
  181 + }
  182 +
  183 + HttpRequest.get<ResponseDTO<NetLayerOSSToken>>(
  184 + HttpUrlUtils.getOSSTokenUrl(),
  185 + ).then((res: ResponseDTO<NetLayerOSSToken>) => {
  186 + if (res.code != 0) {
  187 + fail(res.message)
  188 + return
  189 + }
  190 +
  191 + this.ossToken = res.data
  192 + success(res.data)
  193 + }, (error: Error) => {
  194 + fail(error.message)
  195 + })
  196 + })
  197 + }
  198 +
  199 + private getOssConfigInfo() {
  200 + let isAbroad = 0
  201 + return new Promise<Array<NetLayerOSSConfigInfoModel> | undefined>((success, fail) => {
  202 + if (this.configInfos && this.configInfos.length > 0) {
  203 + success(this.configInfos)
  204 + return
  205 + }
  206 + HttpRequest.get<ResponseDTO<Array<NetLayerOSSConfigInfoModel>>> (
  207 + HttpUrlUtils.getOSSConfigInfoUrl()+ '?type=0'
  208 + ).then((res: ResponseDTO<Array<NetLayerOSSConfigInfoModel>>) => {
  209 + if (res.code != 0) {
  210 + fail(res.message)
  211 + return
  212 + }
  213 + this.configInfos = res.data
  214 + success(res.data)
  215 + })
  216 + })
  217 + }
  218 +
  219 + private getServerHost() {
  220 + return HttpUrlUtils.getHost()
  221 + // switch (HttpUrlUtils.getHost()) {
  222 + // case HostEnum.HOST_UAT: {
  223 + // return "https://pd-people-uat.pdnews.cn"
  224 + // }
  225 + // case HostEnum.HOST_DEV: {
  226 + // return "https://pd-people-dev.pdnews.cn"
  227 + // }
  228 + // case HostEnum.HOST_SIT: {
  229 + // return "https://pd-people-sit.pdnews.cn"
  230 + // }
  231 + // }
  232 + // return "https://www.peopleapp.com"
  233 + }
  234 +
  235 +}
  1 +import { http } from '@kit.NetworkKit';
  2 +import fs from '@ohos.file.fs';
  3 +import { getSignUrl } from './upload';
  4 +import { request } from './request';
  5 +import { xmlToObj } from './xml';
  6 +import { OSSConfig } from './OSSConfig';
  7 +
  8 +type TPart = {
  9 + partNum: number;
  10 + etag: string;
  11 +};
  12 +
  13 +type TTodoPart = {
  14 + partLength: number;
  15 + partNum: number;
  16 +}
  17 +
  18 +/**
  19 + * InitiateMultipartUpload
  20 + * @param fileName 文件名
  21 + */
  22 +const initiateMultipartUpload = async (fileName: string, config: OSSConfig) => {
  23 + console.info('in initiateMultipartUpload');
  24 +
  25 + // 获取InitiateMultipartUpload签名URL
  26 + const signUrlResult = await getSignUrl(config.fileName.length > 0 ? config.fileName: fileName, {
  27 + url: config.serverUrl,
  28 + method: 'POST',
  29 + headers: config.customHeaders,
  30 + queries: {
  31 + uploads: null,
  32 + bucket: config.bucket,
  33 + stsToken: config.stsToken,
  34 + accessKeyId: config.accessKeyId,
  35 + accessKeySecret: config.accessKeySecret,
  36 + }
  37 + });
  38 +
  39 + try {
  40 + // 通过InitiateMultipartUpload接口来通知OSS初始化一个Multipart Upload事件
  41 + console.info('url: ' + signUrlResult.url);
  42 + const response = await request(signUrlResult.url, {
  43 + method: http.RequestMethod.POST,
  44 + expectDataType: http.HttpDataType.STRING
  45 + }, 200);
  46 + const result = response.result as string;
  47 +
  48 + console.info('success initiateMultipartUpload');
  49 +
  50 + const res = xmlToObj(result) as {
  51 + InitiateMultipartUploadResult: {
  52 + Bucket: string;
  53 + Key: string;
  54 + UploadId: string;
  55 + EncodingType?: string;
  56 + }
  57 + };
  58 +
  59 + return res.InitiateMultipartUploadResult;
  60 + } catch (err) {
  61 + console.info('initiateMultipartUpload request error: ' + JSON.stringify(err));
  62 +
  63 + throw err;
  64 + }
  65 +};
  66 +
  67 +/**
  68 + * UploadPart
  69 + * @param uploadId 分片上传的uploadId
  70 + * @param partNum 分片上传的partNumber
  71 + * @param file 上传的文件
  72 + * @param length 分片大小
  73 + * @param offset 文件读取位置
  74 + */
  75 +const uploadPart = async (uploadId: string, config: OSSConfig, partNum: number, file: fs.File, length: number, offset: number = 0) => {
  76 + console.info('in uploadPart');
  77 +
  78 + // 获取UploadPart签名URL
  79 + const signUrlResult = await getSignUrl(config.fileName.length > 0 ? config.fileName: file.name, {
  80 + url: config.serverUrl,
  81 + method: 'PUT',
  82 + headers: config.customHeaders,
  83 + extHeaders: {
  84 + 'Content-Length': length
  85 + },
  86 + queries: {
  87 + uploadId,
  88 + partNumber: partNum.toString(),
  89 + bucket: config.bucket,
  90 + stsToken: config.stsToken,
  91 + accessKeyId: config.accessKeyId,
  92 + accessKeySecret: config.accessKeySecret,
  93 + },
  94 + additionalHeaders: ['Content-Length']
  95 + });
  96 +
  97 + const data = new ArrayBuffer(length);
  98 +
  99 + await fs.read(file.fd, data, {
  100 + length,
  101 + offset
  102 + });
  103 +
  104 + try {
  105 + const response = await request(signUrlResult.url, {
  106 + method: http.RequestMethod.PUT,
  107 + header: {
  108 + 'Content-Length': length,
  109 + 'Content-Type': signUrlResult.contentType
  110 + },
  111 + extraData: data
  112 + }, 200);
  113 +
  114 + console.info('success uploadPart');
  115 +
  116 + return response.header['etag'] as string;
  117 + } catch (err) {
  118 + console.info('uploadPart request error: ' + JSON.stringify(err));
  119 +
  120 + throw err;
  121 + }
  122 +};
  123 +
  124 +/**
  125 + * CompleteMultipartUpload
  126 + * @param fileName 文件名
  127 + * @param uploadId 分片上传的uploadId
  128 + * @param completeAll 指定是否列举当前UploadId已上传的所有Part
  129 + * @param [parts] CompleteMultipartUpload所需的Part列表
  130 + */
  131 +const completeMultipartUpload = async (fileName: string, config: OSSConfig, uploadId: string, completeAll: boolean = false, parts?: TPart[]) => {
  132 + console.info('in completeMultipartUpload');
  133 +
  134 + if (!completeAll && !parts) {
  135 + throw new Error('completeMultipartUpload needs to pass in parameter parts.');
  136 + }
  137 +
  138 + const signUrlResult = await getSignUrl(config.fileName.length > 0 ? config.fileName: fileName, {
  139 + url: config.serverUrl,
  140 + method: 'POST',
  141 + headers: config.customHeaders,
  142 + extHeaders: completeAll ? {
  143 + 'x-oss-complete-all': 'yes'
  144 + } : {
  145 + 'Content-Type': 'application/xml'
  146 + },
  147 + queries: {
  148 + uploadId,
  149 + bucket: config.bucket,
  150 + stsToken: config.stsToken,
  151 + accessKeyId: config.accessKeyId,
  152 + accessKeySecret: config.accessKeySecret,
  153 + }
  154 + });
  155 +
  156 + let xml: string;
  157 +
  158 + if (!completeAll) {
  159 + const completeParts = parts.concat().sort((a, b) => a.partNum - b.partNum)
  160 + .filter((item, index, arr) => !index || item.partNum !== arr[index - 1].partNum);
  161 + xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';
  162 +
  163 + completeParts.forEach(item => {
  164 + xml += `<Part>\n<PartNumber>${item.partNum}</PartNumber>\n<ETag>${item.etag}</ETag>\n</Part>\n`
  165 + });
  166 + xml += '</CompleteMultipartUpload>';
  167 + }
  168 +
  169 + try {
  170 + const result = await request(signUrlResult.url, {
  171 + method: http.RequestMethod.POST,
  172 + header: completeAll ? {
  173 + 'x-oss-complete-all': 'yes'
  174 + } : {
  175 + 'Content-Type': 'application/xml'
  176 + },
  177 + extraData: !completeAll ? xml : undefined
  178 + }, 200);
  179 + console.info('success completeMultipartUpload');
  180 +
  181 + return result;
  182 + } catch (err) {
  183 + console.info('completeMultipartUpload request error: ' + JSON.stringify(err));
  184 +
  185 + throw err;
  186 + }
  187 +};
  188 +
  189 +/**
  190 + * 分片上传信息
  191 + */
  192 +interface ICheckpoint {
  193 + /** 分片上传的uploadId */
  194 + uploadId: string;
  195 + /** 文件URI */
  196 + fileUri: string;
  197 + /** 分片大小 */
  198 + partSize: number;
  199 + /** 已经上传完成的分片 */
  200 + doneParts: TPart[];
  201 +}
  202 +
  203 +/**
  204 + * 分片上传
  205 + */
  206 +export class MultipartUpload {
  207 + /** 分片上传信息 */
  208 + private checkpoint: ICheckpoint;
  209 + /** 上传文件 */
  210 + private file: fs.File;
  211 + /** 文件详细属性信息 */
  212 + private fileStat: fs.Stat;
  213 + /** 取消上传标识 */
  214 + private cancelFlag = true;
  215 + /** 并发上传数 */
  216 + private parallel = 5;
  217 + /** 上传队列 */
  218 + private uploadQueue: TTodoPart[] = [];
  219 + /** 当前正在上传数 */
  220 + private uploadingCount = 0;
  221 + /** 上传失败分片信息 */
  222 + private uploadErrors: {
  223 + partNum: number;
  224 + uploadError: Error;
  225 + }[] = [];
  226 + private config: OSSConfig
  227 +
  228 + /**
  229 + * 创建MultipartUpload实例
  230 + * @param [fileUri] 文件URI
  231 + * @param [checkpoint] 分片上传信息
  232 + */
  233 + constructor(config:OSSConfig, fileUri?: string, checkpoint?: ICheckpoint) {
  234 + this.config = config
  235 + if (checkpoint) {
  236 + this.checkpoint = checkpoint;
  237 + this.file = fs.openSync(checkpoint.fileUri, fs.OpenMode.READ_ONLY);
  238 + } else {
  239 + if (!fileUri) {
  240 + throw Error('MultipartUpload need fileUri or checkpoint.');
  241 + }
  242 +
  243 + this.file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
  244 + this.checkpoint = {
  245 + uploadId: '',
  246 + fileUri,
  247 + partSize: 2 ** 20,
  248 + doneParts: []
  249 + };
  250 + }
  251 +
  252 + this.fileStat = fs.statSync(this.file.fd);
  253 + }
  254 +
  255 + private async uploadPart(part: TTodoPart, resolve: () => void) {
  256 + this.uploadingCount++;
  257 +
  258 + const {
  259 + partLength,
  260 + partNum
  261 + } = part;
  262 +
  263 + try {
  264 + const result = await uploadPart(this.checkpoint.uploadId, this.config, partNum, this.file, partLength, (partNum - 1) * this.checkpoint.partSize);
  265 +
  266 + this.checkpoint.doneParts.push({
  267 + partNum: partNum,
  268 + etag: result
  269 + });
  270 + this.uploadingCount--;
  271 +
  272 + if(this.uploadErrors.length < 1) {
  273 + if (this.uploadQueue.length < 1 && this.uploadingCount < 1) {
  274 + resolve();
  275 + } else {
  276 + this.next(resolve);
  277 + }
  278 + }
  279 + } catch (e) {
  280 + this.uploadingCount--;
  281 + this.uploadErrors.push({
  282 + partNum: partNum,
  283 + uploadError: e
  284 + });
  285 + resolve();
  286 + }
  287 + }
  288 +
  289 + private next(resolve: () => void) {
  290 + if (this.cancelFlag) {
  291 + resolve();
  292 + }
  293 +
  294 + if (this.uploadQueue.length > 0 && this.uploadingCount < this.parallel && this.uploadErrors.length < 1) {
  295 + this.uploadPart(this.uploadQueue.shift(), resolve);
  296 + }
  297 + }
  298 +
  299 + /**
  300 + * 执行分片上传
  301 + */
  302 + async multipartUpload() {
  303 + this.cancelFlag = false;
  304 + this.uploadQueue = [];
  305 + this.uploadErrors = [];
  306 +
  307 + if (this.checkpoint.uploadId === '') {
  308 + const initResult = await initiateMultipartUpload(this.file.name, this.config);
  309 +
  310 + this.checkpoint.uploadId = initResult.UploadId;
  311 + }
  312 +
  313 + const partsSum = Math.ceil(this.fileStat.size / this.checkpoint.partSize);
  314 +
  315 + for (let i = 0; i < partsSum; i++) {
  316 + if (this.checkpoint.doneParts.findIndex(v => v.partNum === i + 1) === -1) {
  317 + this.uploadQueue.push({
  318 + partLength: i + 1 === partsSum ? this.fileStat.size % this.checkpoint.partSize : this.checkpoint.partSize,
  319 + partNum: i + 1
  320 + });
  321 + }
  322 + }
  323 +
  324 + const tempCount = Math.min(this.parallel, this.uploadQueue.length);
  325 +
  326 + await new Promise<void>((resolve) => {
  327 + for (let i = 0; i < tempCount; i++) {
  328 + this.next(resolve);
  329 + }
  330 + });
  331 +
  332 + if (this.cancelFlag) {
  333 + throw new Error('MultipartUpload cancel');
  334 + }
  335 +
  336 + if (this.uploadErrors.length) {
  337 + throw new Error('Upload failed parts: ' + this.uploadErrors.map(i => i.partNum).join(','));
  338 + }
  339 +
  340 + return await completeMultipartUpload(this.file.name, this.config, this.checkpoint.uploadId, false, this.checkpoint.doneParts);
  341 + }
  342 +
  343 + cancel() {
  344 + this.cancelFlag = true;
  345 + }
  346 +};
  347 +
  348 +export {
  349 + initiateMultipartUpload,
  350 + uploadPart,
  351 + completeMultipartUpload
  352 +};
  1 +import { http } from '@kit.NetworkKit';
  2 +
  3 +const request = async (url: string, options: http.HttpRequestOptions, successCode: number[] | number) => {
  4 + const httpRequest = http.createHttp();
  5 +
  6 + try {
  7 + console.info('request url: ' + url);
  8 + console.info('request req: ' + JSON.stringify(options));
  9 + const httpResponse = await httpRequest.request(url, {
  10 + ...options,
  11 + priority: 1,
  12 + connectTimeout: 60000,
  13 + readTimeout: 60000,
  14 + usingProtocol: http.HttpProtocol.HTTP1_1
  15 + });
  16 + console.info('request res: ' + JSON.stringify(httpResponse));
  17 +
  18 + if ((Array.isArray(successCode) && successCode.includes(httpResponse.responseCode)) || httpResponse.responseCode === successCode) {
  19 + const requestID = httpResponse.header['x-oss-request-id'];
  20 +
  21 + console.info(`request success${requestID ? ', oss request ID: ' + requestID : ''}`);
  22 +
  23 + return httpResponse;
  24 + } else {
  25 + throw {
  26 + code: httpResponse.responseCode,
  27 + result: httpResponse.result.toString(),
  28 + requestID: httpResponse.header['x-oss-request-id']
  29 + };
  30 + }
  31 + } catch (err) {
  32 + console.info('request error: ' + JSON.stringify(err));
  33 +
  34 + throw err;
  35 + } finally {
  36 + httpRequest.destroy();
  37 + }
  38 +};
  39 +
  40 +export {
  41 + request
  42 +};
  1 +import { http } from '@kit.NetworkKit';
  2 +import fs from '@ohos.file.fs';
  3 +import { request } from './request';
  4 +import { OSSConfig } from './OSSConfig';
  5 +import { JSON } from '@kit.ArkTS';
  6 +
  7 +// const serverUrl = 'http://x.x.x.x:3000/get_sign_url'; // 获取签名URL的服务器URL
  8 +// const serverUrl = 'https://pd-people-uat.pdnews.cn/api/harmonyoss/get_sign_url'; // 获取签名URL的服务器URL
  9 +
  10 +/**
  11 + * getSignUrl返回数据
  12 + */
  13 +export interface ISignUrlResult {
  14 + /** 签名URL */
  15 + url: string;
  16 + /** content-type */
  17 + contentType?: string;
  18 +}
  19 +
  20 +/**
  21 + * 获取签名URL
  22 + * @param fileName 文件名称
  23 + * @param req 用于生成V4签名URL的请求信息
  24 + * @param req.method 请求方式
  25 + * @param [req.headers] 请求头
  26 + * @param [req.queries] 请求查询参数
  27 + * @param [req.additionalHeaders] 加签的请求头
  28 + */
  29 +const getSignUrl = async (fileName: string, req: {
  30 + url: string;
  31 + method: 'GET' | 'POST' | 'PUT';
  32 + headers?: Record<string, string | number>;
  33 + extHeaders?: Record<string, string | number>;
  34 + queries?: Record<string, string>;
  35 + additionalHeaders?: string[];
  36 +}): Promise<ISignUrlResult> => {
  37 + console.info('in getSignUrl form url: ' + req.url);
  38 +
  39 + try {
  40 + let h = req.headers ?? {}
  41 + h['Content-Type'] = 'application/json'
  42 + const response = await request(req.url, {
  43 + method: http.RequestMethod.POST,
  44 + header: h,
  45 + extraData: {
  46 + fileName,
  47 + method: req.method,
  48 + headers: req.extHeaders,
  49 + queries: req.queries,
  50 + additionalHeaders: req.additionalHeaders
  51 + },
  52 + expectDataType: http.HttpDataType.OBJECT
  53 + }, 200);
  54 + const result = response.result as ISignUrlResult;
  55 +
  56 + console.info('success getSignUrl ' + JSON.stringify(result));
  57 +
  58 + return result;
  59 + } catch (err) {
  60 + console.info('getSignUrl request error: ' + JSON.stringify(err));
  61 +
  62 + throw err;
  63 + }
  64 +};
  65 +
  66 +/**
  67 + * PutObject
  68 + * @param fileUri 文件URI
  69 + */
  70 +const putObject = async (fileUri: string, config: OSSConfig): Promise<void> => {
  71 + console.info('in putObject');
  72 +
  73 + const fileInfo = await fs.open(fileUri, fs.OpenMode.READ_ONLY);
  74 + const fileStat = await fs.stat(fileInfo.fd);
  75 + let signUrlResult: ISignUrlResult;
  76 +
  77 + console.info('file name: ', fileInfo.name);
  78 +
  79 + try {
  80 + // 获取PutObject的签名URL
  81 + signUrlResult = await getSignUrl(config.fileName.length > 0 ? config.fileName: fileInfo.name, {
  82 + url: config.serverUrl,
  83 + method: 'PUT',
  84 + headers: config.customHeaders,
  85 + extHeaders: {
  86 + 'Content-Length': fileStat.size
  87 + },
  88 + queries: {
  89 + stsToken: config.stsToken,
  90 + bucket: config.bucket,
  91 + accessKeyId: config.accessKeyId,
  92 + accessKeySecret: config.accessKeySecret,
  93 + },
  94 + additionalHeaders: ['Content-Length']
  95 + });
  96 + } catch (e) {
  97 + await fs.close(fileInfo.fd);
  98 +
  99 + throw e;
  100 + }
  101 +
  102 + const data = new ArrayBuffer(fileStat.size);
  103 +
  104 + await fs.read(fileInfo.fd, data);
  105 + await fs.close(fileInfo.fd);
  106 +
  107 + try {
  108 + // 使用PutObject方法上传文件
  109 + await request(signUrlResult.url, {
  110 + method: http.RequestMethod.PUT,
  111 + header: {
  112 + 'Content-Length': fileStat.size,
  113 + 'Content-Type': signUrlResult.contentType
  114 + },
  115 + extraData: data
  116 + }, 200);
  117 +
  118 + console.info('success putObject');
  119 + } catch (err) {
  120 + console.info('putObject request error: ' + JSON.stringify(err));
  121 +
  122 + throw err;
  123 + }
  124 +};
  125 +
  126 +export {
  127 + getSignUrl,
  128 + putObject
  129 +};
  1 +import convert from '@ohos/xml_js'
  2 +
  3 +const removeTextProperty = (node) => {
  4 + if (node && typeof node === 'object') {
  5 + Object.keys(node).forEach(key => {
  6 + if (node[key] && typeof node[key] === 'object') {
  7 + if ('_text' in node[key]) {
  8 + node[key] = node[key]._text;
  9 + } else {
  10 + removeTextProperty(node[key]);
  11 + }
  12 + }
  13 + });
  14 + }
  15 +
  16 + return node;
  17 +}
  18 +
  19 +const xmlToObj = (xml: string): object => {
  20 + const result = convert.xml2js(xml, {
  21 + compact: true,
  22 + ignoreDeclaration: true,
  23 + ignoreAttributes: true,
  24 + ignoreComment: true,
  25 + ignoreCdata: true
  26 + });
  27 +
  28 + return removeTextProperty(result);
  29 +}
  30 +
  31 +export {
  32 + xmlToObj
  33 +};