Upload file dung lượng lớn tới S3 với Multipart và Presign-url

by khanhcd92
3K views

Nếu như bình thường ta thực hiện single upload tới s3 thì sẽ có 2 cách sau:

  • Upload file lên s3 qua server của chúng ta: Cái này thì reject vì chúng ta phải tốn thời gian upload lên server của mình rồi từ đó mới upload lên S3. Với large file thì gần như là không nên.

  • Upload từ client bằng cách sử dụng presign-url. Thay vì phải qua server trung gian thì chúng ta upload thẳng lên S3. Tốc độ cải thiện rất nhiều vì cách 1 đa phần thời gian tốn ở khâu upload lên server của chúng ta.

Nhưng nói gì thì nói single upload to S3 thì sẽ có những hạn chế sau dưới Maximum size chỉ là 5GB . Điều này thực sự hạn chế. Hơn nữa AWS cũng suggest chúng ta là file >100MB thì nên sử dụng Multipart Upload . Vậy ưu điểm chính của nó là:

  • Maximum size là 5TB
  • Tốc độ upload tốt hơn

Điều đó mình lựa chọn multipart và presign-url cho bài toán upload file dung lượng lớn. Server ở đây mình sử dụng là python. Client thì dùng angular.

part1

Server

Phần server này mình sử dụng kiến trúc serverless để triển khai với ngôn ngữ Python Flask trên môi trường AWS Cloud

Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

Phần Server chủ yếu sẽ có 3 api chính:

  • Đầu tiên là api start-upload, với logic là request tới s3 để lấy uploadId

    @app.route("/start-upload", methods=["GET"])
    def start_upload():
        file_name = request.args.get('file_name')
        response = s3.create_multipart_upload(
            Bucket=BUCKET_NAME, 
            Key=file_name
        )
    
        return jsonify({
            'upload_id': response['UploadId']
        })
    
  • Tiếp theo là api get-upload-url để lấy presignurl cho từng part của file khi upload

    @app.route("/get-upload-url", methods=["GET"])
    def get_upload_url():
         file_name = request.args.get('file_name')
         upload_id = request.args.get('upload_id')
         part_no = request.args.get('part_no')
         signed_url = s3.generate_presigned_url(
             ClientMethod ='upload_part',
             Params = {
                 'Bucket': BUCKET_NAME,
                 'Key': file_name, 
                 'UploadId': upload_id, 
                 'PartNumber': int(part_no)
             }
         )
    
         return jsonify({
             'upload_signed_url': signed_url
         })
    
  • Cuối cùng đây là api để kiểm tra xem việc upload đã hoàn thành chưa.

     @app.route("/complete-upload", methods=["POST"])
     def complete_upload():
         file_name = request.json.get('file_name')
         upload_id = request.json.get('upload_id')
         print(request.json)
         parts = request.json.get('parts')
         response = s3.complete_multipart_upload(
             Bucket = BUCKET_NAME,
             Key = file_name,
             MultipartUpload = {'Parts': parts},
             UploadId= upload_id
         )
         
         return jsonify({
             'data': response
         })
    

Client

Phần client này mình dùng Angular để triển khai 1 trang web upload file đơn giản.

Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

Khi có action upload đầu tiên ta sẽ gọi tới function uploadMultipartFile. Function uploadMultipartFile có chức năng với các step sau:

  • Call api start-upload để lấy được uploadId.

    const uploadStartResponse = await this.startUpload({
        fileName: file.name,
        fileType: file.type
    });
    
  • Split file upload thành các chunks, ở đây chia thành 10MB/chunk nhé. Ở đây mình chia là 10MB vì minimum size của Multipart Upload là 10MB.

  • Thực hiện call api get-upload-url để lấy preSignurl cho từng chunk ở trên.

  • Upload các chunks bằng signurl.

  • Khi tất cả các chunks upload xong sẽ thực hiện call api complete-upload để xác nhận với S3 mình đã upload đầy đủ các part. Done.

    try {
        const FILE_CHUNK_SIZE = 10000000; // 10MB
        const fileSize = file.size;
        const NUM_CHUNKS = Math.floor(fileSize / FILE_CHUNK_SIZE) + 1;
        let start, end, blob;
    
        let uploadPartsArray = [];
        let countParts = 0;
    
        let orderData = [];
    
        for (let index = 1; index < NUM_CHUNKS + 1; index++) {
          start = (index - 1) * FILE_CHUNK_SIZE;
          end = (index) * FILE_CHUNK_SIZE;
          blob = (index < NUM_CHUNKS) ? file.slice(start, end) : file.slice(start);
    
          // (1) Generate presigned URL for each part
          const uploadUrlPresigned = await this.getPresignUrl({
            fileName: file.name,
            fileType: file.type,
            partNo: index.toString(),
            uploadId: uploadStartResponse.upload_id
          });
    
          // (2) Puts each file part into the storage server
    
          orderData.push({
            presignedUrl: uploadUrlPresigned.upload_signed_url,
            index: index
          });
    
          const req = new HttpRequest('PUT', uploadUrlPresigned.upload_signed_url, blob, {
            reportProgress: true
          });
    
          this.httpClient
            .request(req)
            .subscribe((event: HttpEvent<any>) => {
              switch (event.type) {
                case HttpEventType.UploadProgress:
                  const percentDone = Math.round(100 * event.loaded / FILE_CHUNK_SIZE);
                  this.uploadProgress$.emit({
                    progress: file.size < FILE_CHUNK_SIZE ? 100 : percentDone,
                    token: tokenEmit
                  });
                  break;
                case HttpEventType.Response:
                  console.log('Done!');
              }
    
              // (3) Calls the CompleteMultipartUpload endpoint in the backend server
    
              if (event instanceof HttpResponse) {
                const currentPresigned = orderData.find(item => item.presignedUrl === event.url);
    
                countParts++;
                uploadPartsArray.push({
                  ETag: event.headers.get('ETag').replace(/[|&;$%@"<>()+,]/g, ''),
                  PartNumber: currentPresigned.index
                });
                if (uploadPartsArray.length === NUM_CHUNKS) {
                  console.log(file.name)
                  console.log(uploadPartsArray)
                  console.log(uploadStartResponse.upload_id)
                  this.httpClient.post(`${this.url}/complete-upload`, {
                    file_name: encodeURIComponent(file.name),
                    parts: uploadPartsArray.sort((a, b) => {
                      return a.PartNumber - b.PartNumber;
                    }),
                    upload_id: uploadStartResponse.upload_id
                  }).toPromise()
                    .then(res => {
                      this.finishedProgress$.emit({
                        data: res
                      });
                    });
                }
              }
            });
        }
      } catch (e) {
        console.log('error: ', e);
      }
    

Có những chú ý sau.

  • Minimum size của Multipart Upload là 10MB. Maximum là 5TB
  • Được upload tối đã 10000 chunks cho 1 file thôi. Ví dụ ở đây mình chia 1 chunk là 10MB thì mình chỉ upload tối đa 200GB. Tương tự 1 chunk là 5GB => upload tối đa là 5TB.

Demo

  • Cài đặt server với serverless. Vào project serverless và gõ lệnh

    sls deploy
    

    Có những chú ý sau:

    • Sau khi cài đặt NodeJS thì cài Serverless framework

      npm i -g serverless
      
    • Cần có account AWS

    • Config ở file serverless đang sử dụng aws profile mà mình đã setup ở local(khanhcd92). Nếu bạn dùng default hoặc cấu hình profile riêng thì hãy thay đổi nó nhé. Cách cấu hình aws profile.

      provider:
        name: aws
        runtime: python3.6
        stage: dev
        region: ap-southeast-1
        profile: khanhcd92
      
  • Thay đường dẫn API endpoint từ output của cài đặt server cho url trong class UploadService của project web

  • Cài đặt thư viện trong project web

    npm i
    
  • Chạy web angular ở local với lệnh

    npm start
    

    part2

  • Kéo file cần upload vào trình duyệt. Chỗ này mình sẽ dùng 1 file có dụng lượng khoảng 75M để upload.

    part3

  • Kiểm tra kết quả trên S3 part4

Chi tiết source code các bạn xem ở đây nhé.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

You may also like