S3에 파일을 올릴 때 멀티세션 업로드와 서명이 있는 URL을 사용해 보십시오

14523 단어 AWSS3tech
AWS에서 많은 웹 애플리케이션이 실행되고 있다고 생각합니다.이번에는 웹 애플리케이션에서 동영상 등 대용량(5G 바이트를 초과할 수도 있는) 파일, 업로드에 필요한 시스템 구축 시 사용하는 멀티세션 업로드 처리AWS SDK for JavaScript를 적용해 시행한다.

S3에 파일 업로드


S3에 파일을 올릴 때는 보통putObject API 등을 이용해 단일 파일 크기를 5G로 상한으로 처리하지만, 단일 파일 크기가 5G보다 크면 5G보다 작은 업로드 파일로 분할해 업로드한 파일을 S3에 결합한다최대 5TB의 파일을 업로드할 수 있습니다.문서는 faq 등에 기재되어 있다.
예를 들어 AWS SDK를 사용하여 여러 부분 업로드를 할 때 호출하는 방법은 createMultipartUpload, uploadPart, 마지막completeMultipartUpload 순이다.브라우저에서 이 방법을 정상적으로 실행하기 위해서는 호출 권한이 있는 기밀 파일을 브라우저에 불러와야 합니다.
create Multippart Upload와compulete Multipart ipad 방법은 다중 업로드 처리에서 파일을 업로드하는 전후 처리입니다. 이것은 브라우저에서 실행할 수 있을 뿐만 아니라 클라우드에서도 실행할 수 있지만 uploadPart만 실제로 파일 업로드 처리를 할 수 있습니다.브라우저 쪽에서 실행해야 하지만, 대체 방법을 사용하면 브라우저 측이 고르지 않은 상황에서 파일 업로드 처리를 할 수 있습니다.구체적인 처리로 클라우드getSignedUrl에서 방법 등을 사용하여 서명이 있는 URL을 생성하고 생성된 URL을 브라우저에 보냅니다.브라우저에서는 클라우드에서 전송된 서명이 있는 URL을 사용하여fetch API 등을 통해 파일을 업로드할 수 있다.(정확히 말하면 신용카드는 서명이 있는 URL에 포함된다)
이번에는 검증된 구성에서 클라우드 컴퓨팅(Lambda)을 통해 다양한 API 호출을 수행해 브라우저 측에서 신용도를 유지하지 않고 S3에 파일을 업로드하는 것을 실현했다.

전체 이미지


マルチパートアップロードを実施する全体像

구축


다양한 AWS 리소스 제작에 대한 기록은 생략합니다.

EC2

  • 웹 서버로 nginx 설치(설치 절차 생략). OS가 Amazon Linux2이면 (여기) [https://dev.classmethod.jp/articles/install-nginx-on-amazon-linux2-from-extras-repository/] 참조)
  • 브라우저가 읽을 수 있는 HTML/JavaScript 만들기
  • 다음은 HTML/JavaScript의 내용
  • <html>
      <head>
        <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
        <script src="https://sdk.amazonaws.com/js/aws-sdk-2.812.0.min.js"></script>
        <script type="text/javascript" src="https://unpkg.com/[email protected]/src/cuid.js"></script>
        <script><!--
          // 分割アップロードの最大ファイル・サイズ
          const FILE_CHUNK_SIZE = 50_000_000;
          // S3 バケット名
          const BUCKET_NAME = 'large-capacity-image-bucket';
    
          $(document).ready(function(){
            // Upload ボタンを押したときの処理
            $('#uploadButton').click(async function() {
              if ($('#uploadFile')[0].files.length == 0) return;
              const file = $('#uploadFile')[0].files[0];
              let objectName = file.name;
              let fileSize = file.size
              let fileCount = Math.ceil(fileSize / FILE_CHUNK_SIZE)
    
              // マルチパートアップロード初期処理の呼び出し
              const uploadId = await createMultipartUpload(objectName);
              // 署名付き URL 作成とそれを使ったファイル・アップロード処理の呼び出し
              const multipartMap = await generateUrlAndUploadParts(uploadId, objectName, file, fileCount);
              // マルチパートアップロード完了処理の呼び出し
              completeMultipartUpload(uploadId, multipartMap,  objectName);
            });
          });
    
    
          // マルチパートアップロード初期処理
          async function createMultipartUpload(objectName) {
            const path = '/api/createMultipartUpload';
            const headers = {
                'Content-Type': 'application/json'
            };
            const body = JSON.stringify({ObjectName: objectName});
            const uploadId = await fetch(path, {
                method: 'POST',
                headers,
                body,
              })
              .then(res => {return res.json()})
              .then(json => {return json.UploadId});
            return uploadId;
          };
    
    
          // 署名付き URL 作成とそれを使ったファイル・アップロード処理
          async function generateUrlAndUploadParts(uploadId, objectName, blob, cnt) {
            const path = '/api/getSignedUrl';
            let commonBody =
              {
                UploadId: uploadId,
                ObjectName: objectName,
              };
    
    
            const multipartMap = {
              Parts: []
            };
    
            let offset = 0;
            let promises = [];
            const PROMISE_CHUNK_SIZE = 5;
            for (let i = 0; i < cnt; i++) {
              const data = await new Promise((resolve)=>{
                  let reader = new FileReader();
                  reader.onload = function(e) {
                  const data = new Uint8Array(e.target.result);
                    resolve(data);
                    reader.abort();
                  }
                  slice = blob.slice(offset, offset + FILE_CHUNK_SIZE, blob.type);
                  offset += FILE_CHUNK_SIZE;
                  reader.readAsArrayBuffer(slice);
                });
    
                console.log((i + 1) + '/' + cnt);
                // 署名付き URL 作成
                const url = await fetch('/api/getSignedUrl', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({...commonBody, PartNumber: i + 1}),
                  })
                .then(res => {return res.json()})
                .then(json => {return json.Url});
    
                // 署名付き URL によるファイル・アップロード
                const headers = {
                  'Content-Type': 'multipart/form-data'
                };
                const res = fetch(url, {
                    method: 'PUT',
                    headers,
                    body: data,
                  });
                promises.push(res);
                if ((i + 1) % PROMISE_CHUNK_SIZE == 0 || (i + 1) == cnt) {
                  await Promise.all(promises)
                  .then((res) => {
                     const baseIndex = (i / PROMISE_CHUNK_SIZE | 0) * PROMISE_CHUNK_SIZE ;
                     for (let j = 0; j < res.length; j++) {
                       multipartMap.Parts.push({
                         ETag: res[j].headers.get('ETag'),
                         PartNumber: baseIndex + j +  1,
                       });
                     }
                     promises.splice(0);
                   });
                }
            }
          return multipartMap;
        }
    
        // マルチパートアップロード完了処理
        async function completeMultipartUpload(uploadId, multipartMap, objectName) {
          const path = '/api/completeMultipartUpload';
          const body =
             {
               UploadId: uploadId,
               ObjectName: objectName,
               MultipartMap: multipartMap,
             };
          await fetch(path, {
              method: 'POST',
              headers: {'Content-Type': 'application/json'},
              body: JSON.stringify({...body}),
            })
            .then(() => {alert("Complete!!");});
        };
    
    //   --> </script>
      </head>
      <body>
        <form>
          <input type="file" id="uploadFile"/>
          <input type="button" id="uploadButton" value="Upload"/>
        </form>
      </body>
    </html>
    

    Lambda

  • 브라우저에서 세 가지fetch API 요청을 처리합니다.
  • 1: SDK의 AWS를 처리합니다.S3 클래스의createMultipad 방법을 실행하여 UploadId
  • 로 돌아가기
  • 2: SDK의 AWS 처리S3 클래스의 getSignedUrl 메서드를 실행하여 서명 URL을 반환합니다
  • .
  • 3: SDK의 AWS 처리S3 클래스의compuleteMultipad를 실행하여 업로드된 파일을 결합시킵니다
  • const AWS = require('aws-sdk')
    const BUCKET_NAME = 'large-capacity-image-bucket'
    
    const s3 = new AWS.S3({signatureVersion: 'v4'});
    
    exports.handler = async (event) => {
        console.log(event)
    
        let path = event.path;
        if (path == '/api/createMultipartUpload') {
          // SDK の AWS.S3 クラスにある createMultipartUpload メソッドを実行して UploadId を返します
          const objectName = JSON.parse(event.body).ObjectName;
          const params = {
            Bucket: BUCKET_NAME,
            Key: objectName,
          }
          const r = await s3.createMultipartUpload(params).promise();
          const response = {
              statusCode: 200,
              statusDescription: '200 OK',
              isBase64Encoded: false,
              headers: {
                  'Content-Type': 'application/json'
              },
              body: JSON.stringify({UploadId: r.UploadId}),
          };
          return response;
    
        } else if (path == '/api/getSignedUrl') {
          // SDK の AWS.S3 クラスにある getSignedUrl メソッドを実行して署名付き URL を返します
          const objectName = JSON.parse(event.body).ObjectName;
          const uploadId = JSON.parse(event.body).UploadId;
          const partNumber = JSON.parse(event.body).PartNumber;
          const params = {
            Bucket: BUCKET_NAME,
            Key: objectName,
            UploadId: uploadId,
            Expires: 60,
            PartNumber: partNumber,
          }
          const url = await s3.getSignedUrl('uploadPart', params);
          const response = {
              statusCode: 200,
              statusDescription: '200 OK',
              isBase64Encoded: false,
              headers: {
                  'Content-Type': 'application/json'
              },
              body: JSON.stringify({Url: url}),
          };
          return response;
    
        } else if (path == '/api/completeMultipartUpload') {
          // SDK の AWS.S3 クラスにある completeMultipartUpload メソッドを実行してアップロードしたファイルを結合します
          const objectName = JSON.parse(event.body).ObjectName;
          const uploadId = JSON.parse(event.body).UploadId;
          const multipartMap = JSON.parse(event.body).MultipartMap;
          const params = {
            Bucket: BUCKET_NAME,
            Key: objectName,
            MultipartUpload: multipartMap,
            UploadId: uploadId,
          };
          const response = await s3.completeMultipartUpload(params).promise()
            .then(res => 
              {
                return {
                    statusCode: 200,
                    statusDescription: '200 OK',
                    isBase64Encoded: false,
                };
              })
            .catch(err => 
              {
                console.log(err);
                return {
                    statusCode: 500,
                    statusDescription: err,
                    isBase64Encoded: false,
                };
              });
              return response;
        }
    };
    
  • 각 방법이 정상적으로 집행될 수 있도록 Lambda의 IAM 스크롤 막대에 다음과 같은 내연 정책을 부여한다(정책명은 마음대로 선택하세요)
  • large-capacity-image-bucket는 이후에 제작된 S3의 통 이름
  • 이다.
  • 멀티세션 업로드 API 실행에 필요한 권한여기.에 대한 참고 가치
  • {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject"
                ],
                "Resource": "arn:aws:s3:::large-capacity-image-bucket/*"
            }
        ]
    }
    

    ALB

  • 같은 영역에서 웹 서버에서 실행되는 EC2와 API를 실행하는 Lambda의 호출을 받아 분배(EC2에서 응용 서버를 이동할 수도 있지만 힘들기 때문에 이번에 Lambda를 이용했다)
  • 여기의 절차 기재는 생략하였으나 HTTPS 통신
  • 을 미리 진행하였다.

    S3

  • large-capacity-image-bucket라는 구간 만들기
  • 다른 도메인의 웹 응용 프로그램에서 요청할 수 있는fetch API로 CORS 설정
  • large-capacity-image-lb-130122881.ap-northeast-1.elb.amazonaws.com 이전에 생성된 ALB 도메인
  • [
       {
           "AllowedHeaders": [
               "*"
           ],
           "AllowedMethods": [
               "PUT",
               "POST"
           ],
           "AllowedOrigins": [
               "https://large-capacity-image-lb-130122881.ap-northeast-1.elb.amazonaws.com"
           ],
           "ExposeHeaders": [
               "ETag"
           ]
       }
    ]
    

    실행


  • 화면(완성된 화면을 올린다. 매우 적막하다)
    ファイル・アップロード画面

  • 브라우저 콘솔
  • 업로드 시작 시
    コンソールログ1
  • 업로드 종료 시
    コンソールログ2
  • 기타


    멀티세션 업로드에서 SDK의compuletemultipad방법이 호출되지 않아 업로드 처리를 끝낼 때 컨트롤러 등에서 볼 수 없는 곳에 분할 업로드된 데이터를 저장합니다.그리고 이쪽 데이터 요금 대상이 되기 때문에 필요 없는 데이터를 삭제해야 합니다.
    한 가지 방법으로는 AWS SDK for JavaScript의listMultipartUploadsabortMultipartUpload 방법의 이용이 있지만, 이를 실행하기 위한 환경 구축, IAM 사용자 제작, 신용을 PC에 설정하고 EC2를 가동하여 IAM 캐릭터를 부여하는 등 여러 가지 번거로움이 있다.따라서 AWS re: Invent 2020 발표AWS CloudShell는 매우 유용합니다.이 옵션을 사용하면 AWS Management Constore에 로그인한 사용자 권한을 사용하여 다양한 AWS 리소스의 API를 호출할 수 있습니다.
    자세한 사용 방법은 링크를 읽어 주십시오. 그러나 클라우드 셸 설정 아래의 코드가 실행하는 일 때문에 필요하지 않은 데이터의 삭제는 쉽게 할 수 있습니다.리소스의 수습(IAM 사용자 삭제와 EC2 삭제 등)을 구현할 필요가 없어 매우 좋습니다.또한 클라우드셸 자체의 비용은 10각/구역까지 무료로 받을 수 있어 좋은 포인트다.
    const AWS = require('aws-sdk')
    const BUCKET_NAME = 'large-capacity-image-bucket'
    
    const s3 = new AWS.S3({signatureVersion: 'v4'});
    
    let listParams = {Bucket: BUCKET_NAME};
    s3.listMultipartUploads(listParams).promise()
      .then(res => {
        console.log(res);
        for (let upload of res.Uploads) {
          console.log(upload);
          let abortParams = {
            Bucket: BUCKET_NAME,
            Key : upload.Key,
            UploadId: upload.UploadId,
          };
          s3.abortMultipartUpload(abortParams).promise()
            .then(res => {
              console.log('delete!');
            });
        }
      });
    

    책임을 면하다


    본 투고는 개인의 견해이지 소속 조직의 공식적인 견해가 아니다.
    게재된 프로그램의 동작도 보장되지 않으니 참고하세요.

    좋은 웹페이지 즐겨찾기