一、文件上傳:從基礎到企業級方案

文件上傳是鴻蒙應用開發的核心場景(如頭像上傳、附件提交、圖片分享),鴻蒙基於沙箱安全機制,要求上傳文件必須先存入應用沙箱目錄(cacheDir),再通過系統 API 提交。以下是圖片上傳、拍照上傳、任意文件上傳的完整實現,附企業級封裝工具類。

1.1 圖片上傳(媒體庫選擇)思路

1、使用Picker選擇媒體庫的圖片與視頻 (返回一個臨時的圖片地址 file:// 咱們可以直接預覽 也可以繼續向後走 拿到服務器地址再預覽)

const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
...

const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then().catch()

2、把圖片拷貝到應用的緩存目錄 (不支持直接通過相冊本地路徑請求接口,僅支持通過緩存目錄上傳 context.cacheDir)

- 2.1 通過 openSync 讀取 媒體庫文件
- 2.2 通過 fileIo.copyFileSync(file.fd媒體文件, copyFilePath沙箱文件)

細節:copyFilePath沙箱中文件生成規則必須唯一 

fileType
fileName   時間戳 + 隨機數1000~9999 + . 後綴  (別忘了含後綴)
copyFilePath

3、上傳文件 request.uploadFile到服務器

request.uploadFile(上下文信息, 匹配對象{
	請求方式method: 'POST'
	請求地址url: '',
	請求頭
	header: {},
	請求參數
	files: [
			{  name: '參數名',  uri: "internal://cache/沙箱目錄下的文件名含後綴",    type:'',filename:'文件名含後綴'   }
	]
})
.then(uploadTask => {
		// uploadTask.on(類型, 回調函數)
		// progress 訂閲上傳任務進度事件,使用callback異步回調。
		// headerReceive 服務器返回的
		// complete 訂閲上傳任務完成
		// fail 訂閲上傳任務失敗
})
- 使用Picker選擇媒體庫的圖片與視頻
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  @State localFilePath:string = ''
  build() {
    Column() {
      Button('拉起媒體庫·選擇照片').onClick(() => {
        const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
        photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 過濾選擇媒體文件類型為IMAGE
        photoSelectOptions.maxSelectNumber = 1; // 選擇媒體文件的最大數目
        let uris: Array<string> = [];
        const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
        photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
          uris = photoSelectResult.photoUris; // 數組 裏面是一個個file 也就是本地媒體庫圖片地址  咱們可以直接預覽
          this.localFilePath = photoSelectResult.photoUris[0]
          console.info('photoViewPicker.select to file succeed and uris are:' + uris);  // file://media/Photo/947/IMG_1742896185_814/IMG_814.jpg
        }).catch((err: BusinessError) => {
          console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
        })
      })

      Image(this.localFilePath).width('100%')
    }
  }
}
- 把圖片拷貝到應用的沙箱目錄

當前上傳應用文件功能,不支持直接上傳本地相冊的文件,僅支持上傳應用緩存文件路徑(cacheDir)下的文件。

原因:考慮到安全,程序不能直接訪問相冊所有圖片 僅僅支持用户選擇後拷貝到自己的沙箱目錄下 隔離性

// 把相冊路徑拷貝到緩存路徑下(沙箱路徑下)
const fileType = 'jpg'
const fileName = Date.now() + '_' + (Math.floor(Math.random() * (99999 - 11111)) + 11111) + '.' + fileType
const copyFilePath = getContext(this).cacheDir + '/' + fileName


const file = fileIo.openSync(this.uris[0], fileIo.OpenMode.READ_ONLY) // file://media/Photo/2/IMG_1735796348_001/00.jpg
fileIo.copyFileSync(file.fd, copyFilePath)
console.info('cacheDir: ' + copyFilePath);
- 上傳圖片到服務器

準備好參數調用request.uploadFile()獲得上傳對象 uploader

給uploader對象註冊progress事件,監聽上傳進度 requestRes.on("progress", (uploadedSize: number, totalSize: number)=>{})

request.uploadFile(上下文信息, 匹配對象{
	請求方式method: 'POST'
	請求地址url: '',
	請求頭
	header: {},
	請求參數
	files: [
			{  name: '參數名',  uri: "internal://cache/沙箱目錄下的文件名含後綴",    type:'',filename:'文件名含後綴'   }
	]
})
.then(uploadTask => {
		// uploadTask.on(類型, 回調函數)
		// progress 訂閲上傳任務進度事件,使用callback異步回調。
		// headerReceive 服務器返回的
		// complete 訂閲上傳任務完成
		// fail 訂閲上傳任務失敗
})
- 示例代碼
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError, request } from '@kit.BasicServicesKit';


export interface UploadFileType {
  database: string;
  url: string;
  preview: string;
}

export interface PostUploadFileResType {
  state: number;
  msg: string;
  data: UploadFileType;
}


@Entry
@Component
struct Index {
  @State localFilePath:string = ''
  build() {
    Column() {
      Button('選擇的照片拷貝到沙箱').onClick(() => {
        // 一、拉起媒體庫選擇照片
        const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
        photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 過濾選擇媒體文件類型為IMAGE
        photoSelectOptions.maxSelectNumber = 1; // 選擇媒體文件的最大數目
        let uris: Array<string> = [];
        const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
        photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
          uris = photoSelectResult.photoUris; // 數組 裏面是一個個file 也就是本地媒體庫圖片地址  咱們可以直接預覽
          this.localFilePath = photoSelectResult.photoUris[0]
          console.info('photoViewPicker.select to file succeed and uris are:' + uris);  // file://media/Photo/947/IMG_1742896185_814/IMG_814.jpg

          // 二、拷貝到沙箱目錄

          const fileType = 'jpg'
          const fileName = Date.now() + '_' + Math.floor(  Math.random() * (9999-1000+1)+1000  ) + '.' + fileType
          const copyFilePath = getContext(this).cacheDir + '/' + fileName
          // - 2.1 通過 openSync 讀取 媒體庫文件
          const file = fileIo.openSync(photoSelectResult.photoUris[0], fileIo.OpenMode.READ_ONLY)
          // - 2.2 通過 fileIo.copyFileSync(file.fd, copyFilePath)
          // fileIo.copyFileSync(原文件唯一標識, 沙箱目錄及文件名)
          fileIo.copyFileSync(file.fd, copyFilePath)
          // 細節:copyFilePath沙箱中文件生成規則必須唯一
          // fileType
          // fileName   時間戳 + 隨機數1000~9999 + . 後綴
          // copyFilePath
          console.log('最終沙箱目錄:', copyFilePath)

          // 三、上傳圖片到服務器
          request.uploadFile(getContext(this), {
            method: 'POST',
            url: 'http://123.56.141.187:8001/upload/create',
            header: {},
            files: [
              {name: 'file', uri:'internal://cache/'+fileName,  type: fileType,filename:fileName }
            ],
            data: []
          })
            .then((uploadTask) => {
              uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
                console.info("upload totalSize:" + totalSize + "  uploadedSize:" + uploadedSize);
              })
              uploadTask.on('headerReceive', (headers:ESObject) => {
                const serverData: PostUploadFileResType = JSON.parse(headers?.body)
                console.log('服務器數據:', serverData.state)
                console.log('服務器數據:', serverData.msg)
                console.log('服務器數據:', serverData.data.database)  // 圖片名
                console.log('服務器數據:', serverData.data.preview)   // 完整網址+圖片
              })
              uploadTask.on('complete', () => {
                console.log('上傳完成')
              })
              uploadTask.on('fail', () => {
                console.log('上傳失敗')
              })
            })
          // 三、上傳圖片到服務器 end
        }).catch((err: BusinessError) => {
          console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
        })
      })

      Image(this.localFilePath).width('100%')
    }
  }
}
1.2 拍照上傳

API參考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V13/js-apis-camerapicker-V13

import { cameraPicker as picker } from '@kit.CameraKit';
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';

let mContext = getContext(this) as common.Context;

export interface UploadFileType {
  database: string;
  url: string;
  preview: string;
}

export interface PostUploadFileResType {
  state: number;
  msg: string;
  data: UploadFileType | null;
}



@Entry
@Component
struct Index {
  build() {
    Button('拍照').onClick(async () => {
      try {
        const pickerProfile: picker.PickerProfile = {
          cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
        };
        const  pickerResult: picker.PickerResult = await picker.pick(mContext, [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO], pickerProfile);
        console.log("the pick pickerResult is:" + JSON.stringify(pickerResult)); // 返回一個對象, 裏面的【resultUri】本地的媒體地址 file:...
        console.log("the pick pickerResult is:" + pickerResult.resultUri); // file://media/Photo/2418/IMG_1748244688_2173/IMG_20250526_152948.jpg

        // 拷貝到沙箱

        // const fileType = 'jpg'
        const fileType = pickerResult.resultUri.split('.').pop()   // 'file://medxxx250526_48.jpg'.split('.').pop()
        const fileName = Date.now() + '_' + Math.floor(  Math.random() * (9999-1000+1)+1000  ) + '.' + fileType
        const copyFilePath = getContext(this).cacheDir + '/' + fileName
        // - 2.1 通過 openSync 讀取 媒體庫文件
        const file = fileIo.openSync(pickerResult.resultUri, fileIo.OpenMode.READ_ONLY)
        // - 2.2 通過 fileIo.copyFileSync(file.fd, copyFilePath)
        // fileIo.copyFileSync(原文件唯一標識, 沙箱目錄及文件名)
        fileIo.copyFileSync(file.fd, copyFilePath)
        // 細節:copyFilePath沙箱中文件生成規則必須唯一
        // fileType
        // fileName   時間戳 + 隨機數1000~9999 + . 後綴
        // copyFilePath
        console.log('最終沙箱目錄:', copyFilePath)



        // 請求接口(但是接口有問題 超過2m不行  但是手機一拍就是2M)
        request.uploadFile(getContext(this), {
          method: 'POST',
          url: 'http://123.56.141.187:8001/upload/create',
          header: {},
          files: [
            {name: 'file', uri:'internal://cache/'+fileName,  type: fileType,filename:fileName }
          ],
          data: []
        })
          .then((uploadTask) => {
            uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
              console.info("upload totalSize:" + totalSize + "  uploadedSize:" + uploadedSize);
            })
            uploadTask.on('headerReceive', (headers:ESObject) => {
              const serverData: PostUploadFileResType = JSON.parse(headers?.body)
              console.log('服務器數據:', serverData.state)
              console.log('服務器數據:', serverData.msg)
              console.log('服務器數據:', serverData.data?.database)  // 圖片名
              console.log('服務器數據:', serverData.data?.preview)   // 完整網址+圖片
            })
            uploadTask.on('complete', () => {
              console.log('上傳完成')
            })
            uploadTask.on('fail', () => {
              console.log('上傳失敗')
            })
          })
        // 三、上傳圖片到服務器 end
      } catch (error) {
        let err = error as BusinessError;
        console.error(`the pick call failed. error code: ${err.code}`);
      }

    })
  }
}
1.3 其他文件 - 選擇用户文件

http://tmp00002.zhaodashen.cn/hello.txt

http://tmp00002.zhaodashen.cn/hello2.docx

http://tmp00002.zhaodashen.cn/hello3.pdf

http://tmp00002.zhaodashen.cn/hello4.zip

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V13/select-user-file-V13

text/zip/pdf/docx

import  { fileIo, picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError, request } from '@kit.BasicServicesKit';


export interface UploadFileType {
  database: string;
  url: string;
  preview: string;
}

export interface PostUploadFileResType {
  state: number;
  msg: string;
  data: UploadFileType;
}


@Entry
@Component
struct Index {
  build() {
    Button('上傳附件').onClick((event: ClickEvent) => {
      const documentSelectOptions = new picker.DocumentSelectOptions();
      // 選擇文檔的最大數目(可選)。
      documentSelectOptions.maxSelectNumber = 5;
      // 指定選擇的文件或者目錄路徑(可選)。
      // documentSelectOptions.defaultFilePathUri = "file://docs/storage/Users/currentUser/test";
      // 選擇文件的後綴類型['後綴類型描述|後綴類型'](可選,不傳該參數,默認不過濾,即顯示所有文件),若選擇項存在多個後綴名,則每一個後綴名之間用英文逗號進行分隔(可選),後綴類型名不能超過100。此外2in1設備支持通過通配符方式['所有文件(*.*)|.*'],表示為顯示所有文件,手機暫不支持該配置。
      documentSelectOptions.fileSuffixFilters = ['圖片(.png, .jpg)|.png,.jpg', '文檔|.txt', '視頻|.mp4', '.pdf'];
      //選擇是否對指定文件或目錄授權,true為授權,當為true時,defaultFilePathUri為必選參數,拉起文管授權界面;false為非授權(默認為false),拉起常規文管界面(可選),僅支持2in1設備。
      documentSelectOptions.authMode = false;
      // 創建文件選擇器實例
      const documentViewPicker = new picker.DocumentViewPicker(getContext(this));
      documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
        //文件選擇成功後,返回被選中文檔的uri結果集。
        console.info('documentViewPicker.select to file succeed and uris are:' + documentSelectResult);
        // 二、拷貝到沙箱目錄
        // 拷貝到沙箱

        // const fileType = 'jpg'
        const fileType = documentSelectResult[0].split('.').pop()   // 'file://medxxx250526_48.jpg'.split('.').pop()
        const fileName = Date.now() + '_' + Math.floor(  Math.random() * (9999-1000+1)+1000  ) + '.' + fileType
        const copyFilePath = getContext(this).cacheDir + '/' + fileName
        // - 2.1 通過 openSync 讀取 媒體庫文件
        const file = fileIo.openSync(documentSelectResult[0], fileIo.OpenMode.READ_ONLY)
        // - 2.2 通過 fileIo.copyFileSync(file.fd, copyFilePath)
        // fileIo.copyFileSync(原文件唯一標識, 沙箱目錄及文件名)
        fileIo.copyFileSync(file.fd, copyFilePath)
        // 細節:copyFilePath沙箱中文件生成規則必須唯一
        // fileType
        // fileName   時間戳 + 隨機數1000~9999 + . 後綴
        // copyFilePath
        console.log('最終沙箱目錄:', copyFilePath)


        // 請求接口(但是接口有問題 超過2m不行  但是手機一拍就是2M)
        request.uploadFile(getContext(this), {
          method: 'POST',
          url: 'http://123.56.141.187:8001/upload/create',
          header: {},
          files: [
            {name: 'file', uri:'internal://cache/'+fileName,  type: fileType,filename:fileName }
          ],
          data: []
        })
          .then((uploadTask) => {
            uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
              console.info("upload totalSize:" + totalSize + "  uploadedSize:" + uploadedSize);
            })
            uploadTask.on('headerReceive', (headers:ESObject) => {
              const serverData: PostUploadFileResType = JSON.parse(headers?.body)
              console.log('服務器數據:', serverData.state)
              console.log('服務器數據:', serverData.msg)
              console.log('服務器數據:', serverData.data?.database)  // 圖片名
              console.log('服務器數據:', serverData.data?.preview)   // 完整網址+圖片
            })
            uploadTask.on('complete', () => {
              console.log('上傳完成')
            })
            uploadTask.on('fail', () => {
              console.log('上傳失敗')
            })
          })
        // 三、上傳圖片到服務器 end
      }).catch((err: BusinessError) => {
        console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
      })


    })
  }
}

1.4 切片上傳/斷點續傳

後續講解,也可以私聊付費解答

1.5 對象存儲 實戰- 1. 需求

harmonyos教學,鴻蒙系統HarmonyOS打通開發實踐-第1期第1課_服務器

https://cloud.tencent.com/document/product/436/112125

- 2. 由來

明確1:公司開發項目必須有上傳圖片,這些圖片最初就和接口也就是java代碼放在一起

明確2:一台服務器每秒讀寫有上限 =》 就好比廁所坑位 同時蹲坑有上限

明確3:考慮到性能問題 圖片、樣式、接口代碼也就是java代碼、包括數據庫 得放到不同服務器

方案1;每個公司自己搞一個服務器

方案2:用服務器供應商的 (對象存儲產品-專門放圖片視頻等等、雲數據庫)

回答:選擇方案2,還有額外好處例如CDN、例如圖片裁剪水印

CDN

harmonyos教學,鴻蒙系統HarmonyOS打通開發實踐-第1期第1課_上傳_02

大致使用流程

1-下模塊

2-導入模塊

3-修改配置信息

- 3. 騰訊雲

步驟1:註冊賬號 https://cloud.tencent.com/document/product/436/112125

步驟2:個人中心、訪問管理、用户列表、新建用户 -》 點擊進去 操作 API密鑰 (新增就可以看到secretID/secretKey)

https://console.cloud.tencent.com/cam

步驟3:左上角搜索產品對象存儲、創建存儲桶列表 https://console.cloud.tencent.com/cos/bucket

步驟4:根據步驟1、2、3操作 https://cloud.tencent.com/document/product/436/112125

import { CosError, QCloudCredential } from '@tencentcloud/cos';
import { CosXmlBaseService, CosXmlServiceConfig } from '@tencentcloud/cos';
import { PutObjectRequest, UploadTask } from '@tencentcloud/cos';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { CosXmlUploadTaskResult } from '@tencentcloud/cos/src/main/ets/transfer/UploadTask';
import log from '@open/log';

// 獲取臨時密鑰(業務層控制獲取的方式)
let credential : QCloudCredential= new QCloudCredential();
credential.secretID = "AKIDd8UebLvISSJ883qiQxJix0xjKl04zkJg";
credential.secretKey = "HPL6iBVdZIqEfPxLWdp5EGD2lLlHnBNS";
// credential.token = "token";
// startDate和expirationDate均為Date類型,此處通過毫秒時間戳構造Date
// credential.startDate = new Date();
// credential.expirationDate = new Date();

@Entry
@Component
struct Test {

  @State img:string | undefined = ''

  build() {
    Column() {
      Image(this.img).width(300)
      Button('文檔上傳').onClick(async () => {
        log.init({close:false,tag:"✨"})


        let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
        PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
        PhotoSelectOptions.maxSelectNumber = 1;
        let photoPicker = new photoAccessHelper.PhotoViewPicker();
        let photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
        const localPath:string = photoSelectResult.photoUris[0];

        log.info('00000')
        log.info(localPath)


        // 服務配置
        let cosXmlServiceConfig = new CosXmlServiceConfig("ap-beijing"); // ✅
        // 初始化默認的Service
        CosXmlBaseService.initDefaultService(
          // this.context.getApplicationContext(),
          getContext(this),
          cosXmlServiceConfig,
          async () => credential
        )

        // 任何CosXmlRequest都支持這種方式,例如上傳PutObjectRequest、下載GetObjectRequest、刪除DeleteObjectRequest等
        // 以下用上傳進行示例
        let putRequest = new PutObjectRequest("slj-1257200217", "temp1111111.jpg", localPath);  // ✅
        // credential為第一步“初始化密鑰”中獲取到的單次臨時密鑰
        putRequest.credential = credential;
        let task: UploadTask = CosXmlBaseService.default().upload(putRequest);
          task.onResult = {
            onSuccess: (request, result: CosXmlUploadTaskResult) => {
              // const match = result.accessUrl!.match(/user/.*/)
              // todo 上傳成功後的邏輯
              // this.upload({avatar:result.picUploadResult?.originalInfo?.key as string})
              // this.upload({ avatar: match![0] })
              log.info(1111)
              log.info(result.accessUrl)
              this.img = result.accessUrl
            },
            onFail: (request, error: CosError) => {
              // todo 上傳失敗後的邏輯
              log.info(2222)
              log.info(error)
            }
          }
        task.start();
          // ===================
      })
    }
  }
}
- 4. 阿里雲

https://help.aliyun.com/zh/oss/use-cases/harmonyos-environmental-server-signature-direct-transmission?spm=a2c4g.11186623.help-menu-31815.d_4_1_3.68d0285cYjNNA0&scm=20140722.H_2834342._.OR_help-T_cn~zh-V_1

- 5. 七牛雲

https://developer.qiniu.com/kodo/12669/kodo-harmony-sdk

1.6 周邊語法 統計文件大小
比特(bit)
字節(Bytes)
千字節(KB) 
兆字節(MB)
千兆字節(GB)
太字節(TB)
let stat = fileIo.statSync(copyFilePath);
console.info("get file info succeed, the size of file is " + JSON.stringify(stat));
console.info("get file info succeed, the size of file is " + stat.size/1024/1024);
二、下載模塊、Zip模塊2.1 基礎使用

http://tmp00002.zhaodashen.cn/hello.txt

http://tmp00002.zhaodashen.cn/hello2.docx

http://tmp00002.zhaodashen.cn/hello3.pdf

http://tmp00002.zhaodashen.cn/hello4.zip

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V13/js-apis-request-V13#requestdownloadfile9

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V13/js-apis-zlib-V13

import { BusinessError, request } from '@kit.BasicServicesKit'
import { fileIo as fs } from '@kit.CoreFileKit'
import { zlib } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  private filePath:string = getContext().filesDir + '/'


  @State currentSize:number = 0
  @State totalSize:number = 0


  unlinkFile(fileName:string) {
    if ( fs.accessSync(this.filePath+fileName) ) {
      fs.unlinkSync(this.filePath+fileName)
    }
  }

  downloadFile(url:string) {

    const fileName:string = url.split('/').pop() as string

    // fs模塊檢查文件是否存在 存在就刪除
    this.unlinkFile(fileName)
    // 下載
    request.downloadFile(getContext(), {
      url,
      filePath:  this.filePath + fileName // 字符串變數組 [...,'node-v22.16.0.pkg'] 通過pop彈出最後數據  pop是一個函數 返回彈出的數據
    }).then(uploadTask => {
      uploadTask.on("progress", (receivedSize: number, totalSize: number) => {
        // receivedSize 當前下載字節
        // totalSize 總字節
        console.log('數據:', receivedSize, totalSize)
        this.currentSize = receivedSize
        this.totalSize = totalSize
      })
      uploadTask.on("complete", () => {
        console.log('下載完成')
      })
    })
  }

  build() {
    Column() {
      Button('下載文件txt').onClick(() => {
        this.downloadFile('http://tmp00002.zhaodashen.cn/hello.txt')
      })
      Progress({
        value: this.currentSize,
        total: this.totalSize,
        style:ProgressStyle.Ring
      })
      Button('下載文件node安裝包').onClick(() => {
        this.downloadFile('https://npmmirror.com/mirrors/node/v22.16.0/node-v22.16.0.pkg')
      })

      Button('下載文件zip').onClick(() => {
        this.downloadFile('http://tmp00002.zhaodashen.cn/hello4.zip')
      })


      Button('zip解壓').onClick(() => {
        let inFile =  this.filePath + 'hello4.zip';
        let outFileDir = getContext().cacheDir
        let options: zlib.Options = {
          level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION
        };

        try {
          zlib.decompressFile(inFile, outFileDir, options, (errData: BusinessError) => {
            if (errData !== null) {
              console.error(`errData is errCode:${errData.code}  message:${errData.message}`);
            }
          })
        } catch (errData) {
          let code = (errData as BusinessError).code;
          let message = (errData as BusinessError).message;
          console.error(`errData is errCode:${code}  message:${message}`);
        }
        // .....
      })
    }
  }
}
2.2 封裝使用

封裝

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError, request } from '@kit.BasicServicesKit';
import  { fileIo, picker } from '@kit.CoreFileKit';
import { cameraPicker } from '@kit.CameraKit';
import { camera } from '@kit.CameraKit';


export interface UploadFileType {
  database: string;
  url: string;
  preview: string;
}

export interface PostUploadFileResType {
  state: number;
  msg: string;
  data: UploadFileType;
}



class FileUploadDownloadUtil {
  // 1 拿到媒體資源本地路徑
  chooseImage():Promise<string[]> {
    return new Promise((resolve, reject) => {
      const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 過濾選擇媒體文件類型為IMAGE
      photoSelectOptions.maxSelectNumber = 1; // 選擇媒體文件的最大數目
      let uris: Array<string> = [];
      const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
      photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
        uris = photoSelectResult.photoUris; // 數組 裏面是一個個file 也就是本地媒體庫圖片地址  咱們可以直接預覽
        // this.localFilePath = photoSelectResult.photoUris[0]
        resolve(uris)
        console.info('photoViewPicker.select to file succeed and uris are:' + uris);  // file://media/Photo/947/IMG_1742896185_814/IMG_814.jpg
      }).catch((err: BusinessError) => {
        reject([])
        console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
      })
    })
  }

  chooseFile():Promise<string[]> {
    return new Promise((resolve, reject) => {
      const documentSelectOptions = new picker.DocumentSelectOptions();
      // 選擇文檔的最大數目(可選)。
      documentSelectOptions.maxSelectNumber = 5;
      // 指定選擇的文件或者目錄路徑(可選)。
      // documentSelectOptions.defaultFilePathUri = "file://docs/storage/Users/currentUser/test";
      // 選擇文件的後綴類型['後綴類型描述|後綴類型'](可選,不傳該參數,默認不過濾,即顯示所有文件),若選擇項存在多個後綴名,則每一個後綴名之間用英文逗號進行分隔(可選),後綴類型名不能超過100。此外2in1設備支持通過通配符方式['所有文件(*.*)|.*'],表示為顯示所有文件,手機暫不支持該配置。
      documentSelectOptions.fileSuffixFilters = ['圖片(.png, .jpg)|.png,.jpg', '文檔|.txt', '視頻|.mp4', '.pdf'];
      //選擇是否對指定文件或目錄授權,true為授權,當為true時,defaultFilePathUri為必選參數,拉起文管授權界面;false為非授權(默認為false),拉起常規文管界面(可選),僅支持2in1設備。
      documentSelectOptions.authMode = false;
      // 創建文件選擇器實例
      const documentViewPicker = new picker.DocumentViewPicker(getContext(this));
      documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
        //文件選擇成功後,返回被選中文檔的uri結果集。
        console.info('documentViewPicker.select to file succeed and uris are:' + documentSelectResult);
        resolve(documentSelectResult)
      }).catch((err: BusinessError) => {
        reject([])
        console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
      })
    })
  }

  chooseCamera():Promise<string> {
    return new Promise(async (resolve, reject) => {

      try {
        const pickerProfile: cameraPicker.PickerProfile = {
          cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
        };
        const  pickerResult: cameraPicker.PickerResult = await cameraPicker.pick(getContext(), [cameraPicker.PickerMediaType.PHOTO, cameraPicker.PickerMediaType.VIDEO], pickerProfile);
        console.log("the pick pickerResult is:" + JSON.stringify(pickerResult)); // 返回一個對象, 裏面的【resultUri】本地的媒體地址 file:...
        console.log("the pick pickerResult is:" + pickerResult.resultUri); // file://media/Photo/2418/IMG_1748244688_2173/IMG_20250526_152948.jpg
        resolve(pickerResult.resultUri)
      } catch (error) {
        reject('')
        let err = error as BusinessError;
        console.error(`the pick call failed. error code: ${err.code}`);
      }

    })
  }

  // 2 拷貝到沙箱
  fileMoveSx(filePath:string):[string, string] {
    const fileType = filePath.split('.').pop() as string
    const fileName = Date.now() + '_' + Math.floor(  Math.random() * (9999-1000+1)+1000  ) + '.' + fileType
    const copyFilePath = getContext(this).cacheDir + '/' + fileName
    // - 2.1 通過 openSync 讀取 媒體庫文件
    const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY)
    // - 2.2 通過 fileIo.copyFileSync(file.fd, copyFilePath)
    // fileIo.copyFileSync(原文件唯一標識, 沙箱目錄及文件名)
    fileIo.copyFileSync(file.fd, copyFilePath)
    // 細節:copyFilePath沙箱中文件生成規則必須唯一
    // fileType
    // fileName   時間戳 + 隨機數1000~9999 + . 後綴
    // copyFilePath
    console.log('最終沙箱目錄:', copyFilePath)
    // return ['文件類型', '文件名稱']
    return [fileType, fileName]
  }

  // 3 上傳文件
  upload(filePath:string):Promise<string>{ // 空字符串代表上傳失敗

    // # 3.1 就是1 媒體本地路徑
    const temp = this.fileMoveSx(filePath)
    const fileType = temp[0]
    const fileName = temp[1]

    // # 3.2 接着請求接口
    return new Promise((resolve, reject) => {
      // 三、上傳圖片到服務器
      request.uploadFile(getContext(this), {
        method: 'POST',
        url: 'http://123.56.141.187:8001/upload/create',
        header: {},
        files: [
          {name: 'file', uri:'internal://cache/'+fileName,  type: fileType,filename:fileName }
        ],
        data: []
      })
        .then((uploadTask) => {
          uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
            console.info("upload totalSize:" + totalSize + "  uploadedSize:" + uploadedSize);
          })
          uploadTask.on('headerReceive', (headers:ESObject) => {
            const serverData: PostUploadFileResType = JSON.parse(headers?.body)
            // console.log('服務器數據:', serverData.state)
            // console.log('服務器數據:', serverData.msg)
            // console.log('服務器數據:', serverData.data?.database)  // 圖片名
            // console.log('服務器數據:', serverData.data?.preview)   // 完整網址+圖片
            resolve(serverData.data?.database)
          })
          uploadTask.on('complete', () => {
            console.log('上傳完成')
          })
          uploadTask.on('fail', () => {
            reject('')
            console.log('上傳失敗')
          })
        })
      // 三、上傳圖片到服務器 end
    })
  }

  // 3 下載文件
  download(url:string, filePath:string):Promise<boolean>{ // 沙箱地址
    if ( fileIo.accessSync(filePath) ) {
      fileIo.unlinkSync(filePath)
    }

    return new Promise((resolve, reject) => {
      // 下載
      request.downloadFile(getContext(), {
        url,
        filePath
      }).then(uploadTask => {
        uploadTask.on("complete", () => {
          console.log('下載完成')
          resolve(true)
        })
        uploadTask.on("fail", () => {
          console.log('下載失敗')
          reject(false)
        })
      })
    })
  }
}

export const fileUploadDownloadUtil = new FileUploadDownloadUtil()

使用

import { fileUploadDownloadUtil } from '../utils/FileUploadDownloadUtil'

@Entry
@Component
struct Index {
  build() {
    Button('上傳圖片').onClick(async () => {
      const filePath:string[] = await fileUploadDownloadUtil.chooseImage()
      if (filePath.length) {
        const serverData = await fileUploadDownloadUtil.upload(filePath[0])
        console.log('- 最終結果:' + serverData)
        console.log('- 最終結果:' + serverData)
        console.log('- 最終結果:' + serverData)
        console.log('- 最終結果:' + serverData)
        console.log('- 最終結果:' + serverData)
      }
    })
  }
}