Tutorial: Direct upload in a HarmonyOS environment

Updated at:

Server-side signing allows your HarmonyOS application to upload files directly to OSS. You can use the PutObject operation from the HarmonyOS client. This process uses a URL signing mechanism on your server to ensure upload security.

Solution overview

The following process shows how to upload a file from a HarmonyOS environment:

image

To enable direct uploads from a HarmonyOS application to OSS, follow these three steps:

  1. Configure OSS: Create a bucket in the OSS console to store files uploaded by users.

  2. Configure the server: Create an instance on the server to obtain a temporary access credential from the STS service. Then, use the temporary access credential to generate a signed URL that authorizes users to upload files for a specific period.

  3. Configure the HarmonyOS client: On the HarmonyOS client, obtain the signature from the ECS instance and construct a PutObject request to upload the file to OSS.

Sample code

The following code snippets show only the key logic. For the complete sample project, see oss-js-sdk-harmony-demo.zip.

Server-side code sample

The server generates a signed URL:

const express = require("express");
const mime = require("mime");
const OSS = require("ali-oss");
const app = express();
const port = 3000; // The port to listen on

app.use(express.json());

app.use(express.urlencoded({ extended: false }));

app.post("/get_sign_url", async (req, res) => {
  const {
    fileName,
    method,
    headers = {},
    queries = {},
    additionalHeaders = [],
  } = req.body; // Parse data from the body
  const client = new OSS({
    region: "yourRegion",
    accessKeyId: "yourstsAccessKey",
    accessKeySecret: "yourstsAccessKeySecret",
    stsToken: "yourSTSToken",
    bucket: "yourBucket",
    authorizationV4: true,
  });

  const reqHeaders = {
    ...headers,
  };

  // Handle the content-type
  if (fileName && method === "PUT") {
    const fileNameSplit = fileName.split(".");

    reqHeaders["content-type"] = mime.getType(
      fileNameSplit.length > 1 ? fileNameSplit[fileNameSplit.length - 1] : ""
    );
  }

  // Generate a V4 signed URL
  const url = await client.signatureUrlV4(
    method,
    300,
    {
      headers: reqHeaders,
      queries,
    },
    fileName,
    additionalHeaders
  );

  res.json({
    url,
    contentType: reqHeaders["content-type"],
  });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

HarmonyOS client code sample

On the client, obtain the signature and use the signed URL to upload the file:

import { http } from '@kit.NetworkKit';
import fs from '@ohos.file.fs';
import { request } from './request';

const serverUrl = 'http://x.x.x.x:3000/get_sign_url'; // The server URL for getting the signed URL

/**
 * Data returned by getSignUrl
 */
export interface ISignUrlResult {
  /** The signed URL */
  url: string;
  /** content-type */
  contentType?: string;
}

/**
 * Gets the signed URL
 * @param fileName The file name
 * @param req The request information used to generate the V4 signed URL
 * @param req.method The request method
 * @param [req.headers] The request header
 * @param [req.queries] The request query parameters
 * @param [req.additionalHeaders] The request headers to be signed
 */
const getSignUrl = async (fileName: string, req: {
  method: 'GET' | 'POST' | 'PUT';
  headers?: Record<string, string | number>;
  queries?: Record<string, string>;
  additionalHeaders?: string[];
}): Promise<ISignUrlResult> => {
  console.info('in getSignUrl');

  try {
    const response = await request(serverUrl, {
      method: http.RequestMethod.POST,
      header: {
        'Content-Type': 'application/json'
      },
      extraData: {
        fileName,
        method: req.method,
        headers: req.headers,
        queries: req.queries,
        additionalHeaders: req.additionalHeaders
      },
      expectDataType: http.HttpDataType.OBJECT
    }, 200);
    const result = response.result as ISignUrlResult;

    console.info('success getSignUrl');

    return result;
  } catch (err) {
    console.info('getSignUrl request error: ' + JSON.stringify(err));

    throw err;
  }
};

/**
 * PutObject
 * @param fileUri The file URI
 */
const putObject = async (fileUri: string): Promise<void> => {
  console.info('in putObject');

  const fileInfo = await fs.open(fileUri, fs.OpenMode.READ_ONLY);
  const fileStat = await fs.stat(fileInfo.fd);
  let signUrlResult: ISignUrlResult;

  console.info('file name: ', fileInfo.name);

  try {
    // Get the signed URL for PutObject
    signUrlResult = await getSignUrl(fileInfo.name, {
      method: 'PUT',
      headers: {
        'Content-Length': fileStat.size
      },
      additionalHeaders: ['Content-Length']
    });
  } catch (e) {
    await fs.close(fileInfo.fd);

    throw e;
  }

  const data = new ArrayBuffer(fileStat.size);

  await fs.read(fileInfo.fd, data);
  await fs.close(fileInfo.fd);

  try {
    // Use the PutObject method to upload the file
    await request(signUrlResult.url, {
      method: http.RequestMethod.PUT,
      header: {
        'Content-Length': fileStat.size,
        'Content-Type': signUrlResult.contentType
      },
      extraData: data
    }, 200);

    console.info('success putObject');
  } catch (err) {
    console.info('putObject request error: ' + JSON.stringify(err));

    throw err;
  }
};

export {
  getSignUrl,
  putObject
};

Procedure

Step 1: Configure OSS

Create a bucket

Create an OSS Bucket to store files for your web application that are uploaded directly from a browser.

  1. Log on to the OSS console.

  2. In the navigation pane on the left, click Buckets, and then click Create Bucket.

  3. In the Create Bucket panel, select Quick Create and configure the parameters as described in the following table.

    Parameter

    Example value

    Bucket Name

    web-direct-upload

    Region

    China (Hangzhou)

  4. Click Create.

Step 2: Configure the server

Part 1: Create an ECS instance and attach a role

Operation 1: Create an ECS instance

Go to the Custom Launch page and create or select the basic resources required to purchase an ECS instance as described in the following sections.

  1. Select a region and billing method

    1. Select a billing method based on your business needs. This tutorial uses the Pay-As-You-Go mode, which offers more operational flexibility.

    2. Select a region as needed for latency. Typically, the closer the physical distance is to the ECS instance, the lower the network latency and the faster the access speed. This tutorial uses China (Hangzhou) as an example.

      image

  1. Create a VPC and a vSwitch

    When you create a VPC, select the same region as the ECS instance and plan the CIDR block based on your business needs. This tutorial uses the creation of a VPC and vSwitch in the China (Hangzhou) region as an example. After you create the VPC and vSwitch, return to the ECS purchase page, refresh the page, and select them.

    Note

    You can create a vSwitch at the same time you create a VPC.

    image

    image

    image

  1. Select an instance type and image

    Select the instance type and image for the instance. The image determines the operating system and version to be installed on the instance. This tutorial uses the instance type ecs.e-c1m1.large, which is cost-effective and meets testing needs. The image is the public image Alibaba Cloud Linux 3.2104 LTS 64-bit.

    image

  1. Select storage

    Select a system disk for the ECS instance and a data disk as needed. This tutorial demonstrates a simple web system setup, which requires only a system disk to store the operating system and does not require a data disk.

    image

  1. Assign a public IP address

    This instance needs to support public network access. To simplify operations, this tutorial directly assigns a public IP address to the instance. You can also associate an EIP with the instance after you create it. For more information, see Associate an EIP with a cloud resource.

    Note
    • If you do not assign a public IP address, you cannot directly access the instance from the Internet using SSH or RDP. You also cannot verify the web service setup on the instance from the Internet.

    • This tutorial uses the Pay-by-traffic bandwidth billing method. With this billing method, you are charged only for the Internet traffic that you use. For more information, see Public bandwidth billing.

    image

  1. Create a security group

    Create a security group for the instance. A security group is a virtual network firewall that controls the inbound and outbound traffic of ECS instances. When you create the security group, you must allow access to the following specified ports to access the ECS instance later.

    Port Range: SSH (22), RDP (3389), HTTP (80), HTTPS (443).

    Note
    • The selected Port Range specifies the ports that must be open for applications running on the ECS instance.

    • By default, the security group rule uses 0.0.0.0/0 as the source, which allows devices from all network segments to access the specified ports. If you know the IP address of the requesting client, we recommend that you set a specific IP range later. For more information, see Modify security group rules.

    image

  1. Create a key pair

    1. A key pair serves as a secure credential to prove your identity when you log on. After you create the key pair, you must download the private key to use when you connect to the ECS instance. After the key pair is created, return to the ECS purchase page, refresh the page, and select the key pair.

    2. The user root has the highest permissions on the operating system. Using root as the username may pose a security risk. We recommend that you select ecs-user as the username.

      Note

      After you create the key pair, the private key is automatically downloaded. Check your browser's download history and save the .pem private key file.

      image

  1. Create and view the ECS instance

    After you create or select the basic resources for the ECS instance, click Create Order. In the dialog box that appears, click Go to Console to view the created ECS instance in the console. Save the following information for later use.

    • Instance ID: Used to find the instance in the instance list.

    • Region: Used to find the instance in the instance list.

    • Public IP Address: Used to verify the deployment of the web service on the ECS instance.

    imageimage

Operation 2: Connect to the ECS instance

  1. On the Instances page of the Elastic Compute Service console, find the ECS instance that you created by region and instance ID. In the Actions column, click Connect.image

  2. In the Connect to Instance dialog box, click Log On Now next to Connect with Workbench.image

  3. In the Log on to Instance dialog box, set Connection Method to Terminal Connection and Authentication Method to SSH Key Pair. Then, enter or upload the private key file that you downloaded when you created the key pair, and click Log on to log on to the ECS instance with the username ecs-user.

    Note

    The private key file was automatically downloaded to your local machine when you created the key pair. Check your browser's download history to find the .pem private key file.

  4. The following page indicates that you have successfully logged on to the ECS instance.image

Step 3: Create a RAM role in Resource Access Management

  1. Go to the Create Role page in the RAM console.

  2. On the Create Role page, select Alibaba Cloud Service.

  3. Select the Current Alibaba Cloud account and click OK.

  4. Enter a role name, such as oss-web-upload, and click OK.

  5. On the RAM role management page, click Copy to save the role ARN.

Operation 4: Create an access policy for file uploads in the RAM console

  1. On the Policies page, click Create Policy.

  2. On the Create Policy page, click Script Editor and replace <BucketName> in the following script with the bucket name web-direct-upload that you created.

    {
      "Version": "1",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "oss:PutObject",
          "Resource": "acs:oss:*:*:<BucketName>/*"
        }
      ]
    }
  3. Then click OK and enter a policy name.

Operation 5: Grant permissions to the RAM role in the RAM console

  1. On the Roles page, find the target RAM role and click Grant Permission in the Actions column.

  2. On the Grant Permission page, select Custom Policy and choose the custom permission policy you created.

  3. Click Confirm New Authorization.

Operation 6: Attach the RAM role to the ECS instance

  1. Go to the Instances page of the ECS console. At the top of the page, select the region where the ECS instance is located. Then, find the target instance, click the image icon in the Actions column, and select Attach/Detach RAM Role.

    image

  2. In the Attach/Detach RAM Role dialog box, select the target RAM Role to attach the RAM role to the ECS instance.

    image

Note

The generated temporary identity credential is used to generate a signed URL in the next step. If you already have a temporary identity credential, you can skip to the next step, Part 3: Generate a signed URL on the server.

Part 2: Generate a temporary access credential on the server

Operation 1: Configure dependencies on the ECS server

Run the following commands to install the dependencies required to obtain a temporary access credential.

Python

  1. Install Python 3.

  2. Run the following commands to install the Credentials tool.

sudo pip install oss2
sudo pip install alibabacloud_credentials

Java

In your Maven project, import the following dependencies.

<!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>credentials-java</artifactId>
    <version>0.3.4</version>
</dependency>

<dependency>
    <groupId>com.aliyun.kms</groupId>
    <artifactId>kms-transfer-client</artifactId>
    <version>0.1.0</version>
</dependency>

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

Operation 2: Obtain a temporary identity credential on the ECS server

Integrate the STS SDK on your business server to obtain a temporary STS identity credential and return it to the requester.

Python

from alibabacloud_credentials.client import Client as CredClient
from alibabacloud_credentials.models import Config as CredConfig

def main():
    # Configure an ECS RAM role as the access credential.
    credentialConfig = CredConfig(
        type='ecs_ram_role',
        role_name='ecs_role_name'       # Specify the name of the RAM role that is attached to the ECS instance.
    )
    credentialsClient = CredClient(credentialConfig)
    credential = credentialsClient.get_credential()

    accesskeyid = credential.access_key_id            # Get the AccessKey ID.
    accesskeysecret = credential.access_key_secret    # Get the AccessKey secret.
    security_token = credential.security_token        # Get the security token.

    print("stsToken:", security_token)
    print("accesskeyid:", accesskeyid)
    print("accesskeysecret:", accesskeysecret)

if __name__ == "__main__":
    main()

Java

import com.aliyun.credentials.models.CredentialModel;
import com.aliyun.oss.common.auth.Credentials;
import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.DefaultCredentials;
import com.aliyun.oss.common.utils.BinaryUtil;

public class vxDemo {
    public static void main(String[] args) {
    
        // Configure an ECS RAM role as the access credential.
        com.aliyun.credentials.models.Config config = new com.aliyun.credentials.models.Config();
        config.setType("ecs_ram_role");
        config.setRoleName("ecs_role_name");   // Specify the name of the RAM role that is attached to the ECS instance.
        final com.aliyun.credentials.Client credentialsClient = new com.aliyun.credentials.Client(config);
        CredentialsProvider credentialsProvider = new CredentialsProvider() {
            @Override
            public void setCredentials(Credentials credentials) {
            }

            @Override
            public Credentials getCredentials() {
                CredentialModel credential = credentialsClient.getCredential();
                return new DefaultCredentials(credential.getAccessKeyId(), credential.getAccessKeySecret(), credential.getSecurityToken());
            }
        };
        String accessKeyId = credentialsProvider.getCredentials().getAccessKeyId();             //Get the AccessKey ID.
        String secretAccessKey = credentialsProvider.getCredentials().getSecretAccessKey();     //Get the AccessKey secret.
        String securityToken = credentialsProvider.getCredentials().getSecurityToken();         //Get the security token.

        // Print the temporary access credential.
        System.out.println("stsToken:" + securityToken);
        System.out.println("accessKeyId:" + accessKeyId);
        System.out.println("accesskeySecret:"+ secretAccessKey);
    }
}

The generated temporary access credentials (accessKeyId, accessKeySecret, and stsToken) are used to generate the signed URL in the next step.

Part 3: Generate a signed URL on the server

The client sends a POST request to the server that includes the file name, HTTP method, request headers, and query parameters. After the server receives the request, it generates a signed URL using the OSS Client. This URL allows the user to upload the file to OSS.

app.post('/get_sign_url', async (req, res) => {
    const {
        fileName,
        method,
        headers = {},
        queries = {},
        additionalHeaders = []
    } = req.body; // Parse data from the body
    const client = new OSS({
        region: 'yourRegion',
        // Fill in the temporary access credential (accessKeyId, accessKeySecret, and stsToken) returned in Part 2.
        accessKeyId: 'yourstsAccessKey',
        accessKeySecret: 'yourstsAccessKeySecret',
        stsToken: 'yourSTSToken',
        bucket: 'yourBucket',
        authorizationV4: true
    });

    const reqHeaders = {
        ...headers
    };

    // Handle the content-type
    if (fileName && method === 'PUT') {
        const fileNameSplit = fileName.split('.');

        reqHeaders['content-type'] = mime.getType(fileNameSplit.length > 1 ? fileNameSplit[fileNameSplit.length-1] : '');
    }

    // Generate a V4 signed URL
    const url = await client.signatureUrlV4(method, 300, {
        headers: reqHeaders,
        queries
    }, fileName, additionalHeaders);

    res.json({
        url,
        contentType: reqHeaders['content-type']
    });
});

Step 3: Configure the HarmonyOS client

Part 1: Build an HTTP data request

Use the http module of the @kit.NetworkKit library to create an asynchronous function request that sends HTTP requests.

import { http } from '@kit.NetworkKit';

const request = async (url: string, options: http.HttpRequestOptions, successCode: number[] | number) => {
  const httpRequest = http.createHttp();

  try {
    const httpResponse = await httpRequest.request(url, {
      ...options,
      priority: 1,
      connectTimeout: 60000,
      readTimeout: 60000,
      usingProtocol: http.HttpProtocol.HTTP1_1
    });

    if ((Array.isArray(successCode) && successCode.includes(httpResponse.responseCode)) || httpResponse.responseCode === successCode) {
      const requestID = httpResponse.header['x-oss-request-id'];

      console.info(`request success${requestID ? ', oss request ID: ' + requestID : ''}`);

      return httpResponse;
    } else {
      throw {
        code: httpResponse.responseCode,
        result: httpResponse.result.toString(),
        requestID: httpResponse.header['x-oss-request-id']
      };
    }
  } catch (err) {
    console.info('request error: ' + JSON.stringify(err));

    throw err;
  } finally {
    httpRequest.destroy();
  }
};

export {
  request
};

Part 2: Get the file to upload from the local device

import { common } from '@kit.AbilityKit';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import picker from '@ohos.file.picker';

// Select a file
const fileSelect = async (context: common.Context) => {
  const documentSelectOptions = new picker.DocumentSelectOptions();
  const documentViewPicker = new picker.DocumentViewPicker(context);

  documentSelectOptions.maxSelectNumber = 5;


  const documentSelectResult = await documentViewPicker.select(documentSelectOptions);

  return documentSelectResult;
};

export {
  fileSelect
};

Part 3: Send a request from the client to generate a signed URL

The client sends a request to the server to generate a signed URL. The request body includes the file name, request method, and header. If the request is successful, the signed URL and Content-Type are returned. If the request fails, an error is caught and thrown.

const getSignUrl = async (fileName: string, req: {
  method: 'GET' | 'POST' | 'PUT';
  headers?: Record<string, string | number>;
  queries?: Record<string, string>;
  additionalHeaders?: string[];
}): Promise<ISignUrlResult> => {
  console.info('in getSignUrl');

  try {
    const response = await request(serverUrl, {
      method: http.RequestMethod.POST,
      header: {
        'Content-Type': 'application/json'
      },
      extraData: {
        fileName,
        method: req.method,
        headers: req.headers,
        queries: req.queries,
        additionalHeaders: req.additionalHeaders
      },
      expectDataType: http.HttpDataType.OBJECT
    }, 200);
    const result = response.result as ISignUrlResult;

    console.info('success getSignUrl');

    return result;
  } catch (err) {
    console.info('getSignUrl request error: ' + JSON.stringify(err));

    throw err;
  }
};

Part 4: Upload the file

Obtain the signed URL generated on the server, and then use the PutObject method to upload the file.

const putObject = async (fileUri: string): Promise<void> => {
  console.info('in putObject');

  const fileInfo = await fs.open(fileUri, fs.OpenMode.READ_ONLY);
  const fileStat = await fs.stat(fileInfo.fd);
  let signUrlResult: ISignUrlResult;

  console.info('file name: ', fileInfo.name);

  try {
    // Get the signed URL for PutObject
    signUrlResult = await getSignUrl(fileInfo.name, {
      method: 'PUT',
      headers: {
        'Content-Length': fileStat.size
      },
      additionalHeaders: ['Content-Length']
    });
  } catch (e) {
    await fs.close(fileInfo.fd);

    throw e;
  }

  const data = new ArrayBuffer(fileStat.size);

  await fs.read(fileInfo.fd, data);
  await fs.close(fileInfo.fd);

  try {
    // Use the PutObject method to upload the file
    await request(signUrlResult.url, {
      method: http.RequestMethod.PUT,
      header: {
        'Content-Length': fileStat.size,
        'Content-Type': signUrlResult.contentType
      },
      extraData: data
    }, 200);

    console.info('success putObject');
  } catch (err) {
    console.info('putObject request error: ' + JSON.stringify(err));

    throw err;
  }
};

Result Verification

After you complete the deployment, you can upload files to OSS from the HarmonyOS environment. The result is shown in the following example:

  1. Click the Upload File button to select the file to upload.

  2. On the Buckets page, select the bucket that you created to store user-uploaded files. The files uploaded from the HarmonyOS client are displayed in the file list.

    image

Clean up resources

In this tutorial, you created an ECS instance, an OSS bucket, and a RAM role. After you test the solution, you can delete these resources to avoid further charges or security risks.

Release the ECS instance

If you no longer need this instance, you can release it. After the instance is released, it stops being billed, and its data cannot be recovered. The procedure is as follows:

  1. Return to the Instances page of the Elastic Compute Service console, find the target ECS instance by region and instance ID, and click image in the Actions column.

  2. Select Release.image

  3. Confirm that the instance is correct, select Release Now, and click Next.

  4. Confirm the associated resources to be released and understand the related data risks. Then, click Confirm to release the ECS instance.

Note
  • The system disk and the assigned public IP address are released with the instance.

  • Security groups, vSwitches, and VPCs are not released with the instance, but they are free resources. You can choose to delete them based on your business needs.

  • EIPs are not released with the instance and are not free resources. You can choose to delete them based on your business needs.

Delete the bucket

  1. Log on to the OSS console.

  2. Click Buckets, and then click the name of the target bucket.

  3. Delete all files (objects) in the bucket.

  4. In the navigation pane on the left, click Delete Bucket, and then follow the on-screen instructions to delete the bucket.

Delete the RAM role

  1. Log on to the Resource Access Management (RAM) console as a RAM administrator.

  2. In the navigation pane on the left, choose Identity Management > Roles.

  3. On the Roles page, click Delete Role in the Actions column for the target RAM role.

  4. In the Delete Role dialog box, enter the RAM role name, and then click Delete Role.

    Note

    If the RAM role has been granted an access policy, the authorization is revoked when the role is deleted.

FAQ

How do I perform a multipart upload?

If you want to use signed URLs to upload a large file to OSS using multipart upload, you must first initialize the multipart upload. Then, generate a signed URL for each part and return it to the client. The client can use these signed URLs to upload all the parts. After all parts are uploaded, you can merge them to complete the upload. The following code provides a reference for the implementation:

import { http } from '@kit.NetworkKit';
import fs from '@ohos.file.fs';
import { getSignUrl } from './upload';
import { request } from './request';
import { xmlToObj } from './xml';

type TPart = {
  partNum: number;
  etag: string;
};

type TTodoPart = {
  partLength: number;
  partNum: number;
}

/**
 * InitiateMultipartUpload
 * @param fileName The file name
 */
const initiateMultipartUpload = async (fileName: string) => {
  console.info('in initiateMultipartUpload');

  // Get the signed URL for InitiateMultipartUpload
  const signUrlResult = await getSignUrl(fileName, {
    method: 'POST',
    queries: {
      uploads: null
    }
  });

  try {
    // Use the InitiateMultipartUpload operation to notify OSS to initialize a multipart upload event
    const response = await request(signUrlResult.url, {
      method: http.RequestMethod.POST,
      expectDataType: http.HttpDataType.STRING
    }, 200);
    const result = response.result as string;

    console.info('success initiateMultipartUpload');

    const res = xmlToObj(result) as {
      InitiateMultipartUploadResult: {
        Bucket: string;
        Key: string;
        UploadId: string;
        EncodingType?: string;
      }
    };

    return res.InitiateMultipartUploadResult;
  } catch (err) {
    console.info('initiateMultipartUpload request error: ' + JSON.stringify(err));

    throw err;
  }
};

/**
 * UploadPart
 * @param uploadId The uploadId of the multipart upload
 * @param partNum The partNumber of the multipart upload
 * @param file The file to upload
 * @param length The part size
 * @param offset The position to read the file from
 */
const uploadPart = async (uploadId: string, partNum: number, file: fs.File, length: number, offset: number = 0) => {
  console.info('in uploadPart');

  // Get the signed URL for UploadPart
  const signUrlResult = await getSignUrl(file.name, {
    method: 'PUT',
    headers: {
      'Content-Length': length
    },
    queries: {
      uploadId,
      partNumber: partNum.toString()
    },
    additionalHeaders: ['Content-Length']
  });

  const data = new ArrayBuffer(length);

  await fs.read(file.fd, data, {
    length,
    offset
  });

  try {
    const response = await request(signUrlResult.url, {
      method: http.RequestMethod.PUT,
      header: {
        'Content-Length': length,
        'Content-Type': signUrlResult.contentType
      },
      extraData: data
    }, 200);

    console.info('success uploadPart');

    return response.header['etag'] as string;
  } catch (err) {
    console.info('uploadPart request error: ' + JSON.stringify(err));

    throw err;
  }
};

/**
 * CompleteMultipartUpload
 * @param fileName The file name
 * @param uploadId The uploadId of the multipart upload
 * @param completeAll Specifies whether to list all parts uploaded for the current UploadId
 * @param [parts] The list of parts required for CompleteMultipartUpload
 */
const completeMultipartUpload = async (fileName: string, uploadId: string, completeAll: boolean = false, parts?: TPart[]) => {
  console.info('in completeMultipartUpload');

  if (!completeAll && !parts) {
    throw new Error('completeMultipartUpload needs to pass in parameter parts.');
  }

  const signUrlResult = await getSignUrl(fileName, {
    method: 'POST',
    headers: completeAll ? {
      'x-oss-complete-all': 'yes'
    } : {
      'Content-Type': 'application/xml'
    },
    queries: {
      uploadId
    }
  });

  let xml: string;

  if (!completeAll) {
    const completeParts = parts.concat().sort((a, b) => a.partNum - b.partNum)
      .filter((item, index, arr) => !index || item.partNum !== arr[index - 1].partNum);
    xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';

    completeParts.forEach(item => {
      xml += `<Part>\n<PartNumber>${item.partNum}</PartNumber>\n<ETag>${item.etag}</ETag>\n</Part>\n`
    });
    xml += '</CompleteMultipartUpload>';
  }

  try {
    const result  = await request(signUrlResult.url, {
      method: http.RequestMethod.POST,
      header: completeAll ? {
        'x-oss-complete-all': 'yes'
      } : {
        'Content-Type': 'application/xml'
      },
      extraData: !completeAll ? xml : undefined
    }, 200);
    console.info('success completeMultipartUpload');

    return result;
  } catch (err) {
    console.info('completeMultipartUpload request error: ' + JSON.stringify(err));

    throw err;
  }
};

/**
 * Multipart upload information
 */
interface ICheckpoint {
  /** The uploadId of the multipart upload */
  uploadId: string;
  /** The file URI */
  fileUri: string;
  /** The part size */
  partSize: number;
  /** The parts that have been uploaded */
  doneParts: TPart[];
}

/**
 * Multipart upload
 */
export class MultipartUpload {
  /** Multipart upload information */
  private checkpoint: ICheckpoint;
  /** The file to upload */
  private file: fs.File;
  /** Detailed file attribute information */
  private fileStat: fs.Stat;
  /** Flag to cancel the upload */
  private cancelFlag = true;
  /** Number of concurrent uploads */
  private parallel = 5;
  /** The upload queue */
  private uploadQueue: TTodoPart[] = [];
  /** The number of currently uploading parts */
  private uploadingCount = 0;
  /** Information about failed upload parts */
  private uploadErrors: {
    partNum: number;
    uploadError: Error;
  }[] = [];

  /**
   * Creates a MultipartUpload instance
   * @param [fileUri] The file URI
   * @param [checkpoint] The multipart upload information
   */
  constructor(fileUri?: string, checkpoint?: ICheckpoint) {
    if (checkpoint) {
      this.checkpoint = checkpoint;
      this.file = fs.openSync(checkpoint.fileUri, fs.OpenMode.READ_ONLY);
    } else {
      if (!fileUri) {
        throw Error('MultipartUpload need fileUri or checkpoint.');
      }

      this.file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
      this.checkpoint = {
        uploadId: '',
        fileUri,
        partSize: 2 ** 20,
        doneParts: []
      };
    }

    this.fileStat = fs.statSync(this.file.fd);
  }

  private async uploadPart(part: TTodoPart, resolve: () => void) {
    this.uploadingCount++;

    const {
      partLength,
      partNum
    } = part;

    try {
      const result = await uploadPart(this.checkpoint.uploadId, partNum, this.file, partLength, (partNum - 1) * this.checkpoint.partSize);

      this.checkpoint.doneParts.push({
        partNum: partNum,
        etag: result
      });
      this.uploadingCount--;

      if(this.uploadErrors.length < 1) {
        if (this.uploadQueue.length < 1 && this.uploadingCount < 1) {
          resolve();
        } else {
          this.next(resolve);
        }
      }
    } catch (e) {
      this.uploadingCount--;
      this.uploadErrors.push({
        partNum: partNum,
        uploadError: e
      });
      resolve();
    }
  }

  private next(resolve: () => void) {
    if (this.cancelFlag) {
      resolve();
    }

    if (this.uploadQueue.length > 0 && this.uploadingCount < this.parallel && this.uploadErrors.length < 1) {
      this.uploadPart(this.uploadQueue.shift(), resolve);
    }
  }

  /**
   * Executes the multipart upload
   */
  async multipartUpload() {
    this.cancelFlag = false;
    this.uploadQueue = [];
    this.uploadErrors = [];

    if (this.checkpoint.uploadId === '') {
      const initResult = await initiateMultipartUpload(this.file.name);

      this.checkpoint.uploadId = initResult.UploadId;
    }

    const partsSum = Math.ceil(this.fileStat.size / this.checkpoint.partSize);

    for (let i = 0; i < partsSum; i++) {
      if (this.checkpoint.doneParts.findIndex(v => v.partNum === i + 1) === -1) {
        this.uploadQueue.push({
          partLength: i + 1 === partsSum ? this.fileStat.size % this.checkpoint.partSize : this.checkpoint.partSize,
          partNum: i + 1
        });
      }
    }

    const tempCount = Math.min(this.parallel, this.uploadQueue.length);

    await new Promise<void>((resolve) => {
      for (let i = 0; i < tempCount; i++) {
        this.next(resolve);
      }
    });

    if (this.cancelFlag) {
      throw new Error('MultipartUpload cancel');
    }

    if (this.uploadErrors.length) {
      throw new Error('Upload failed parts: ' + this.uploadErrors.map(i => i.partNum).join(','));
    }

    return await completeMultipartUpload(this.file.name, this.checkpoint.uploadId, false, this.checkpoint.doneParts);
  }

  cancel() {
    this.cancelFlag = true;
  }
};

export {
  initiateMultipartUpload,
  uploadPart,
  completeMultipartUpload
};