2022/8 Node.js S3コピーで注意する5GB

佐々木誠
大阪

S3を利用していると、ファイルを別のフォルダやバケットにコピーしたり、移動するケースがあると思います。

今回は、別のバケットへコピーする機能を作り、本番運用した後に問題が発覚するというしくじりをしました。 そこで、注意喚起含め、問題と解決策について紹介します。

S3コピー

5GB

一般的に、Node.jsでS3ファイルコピーしようとすると、以下のようにcopyObjectを利用します。

await s3.copyObject({
    CopySource: 'src/mdeia/2022.mp4',
    Bucket: 'dest',
    Key: 'media/mp4/2022.mp4'
}).promise();

私もcopyObjectを利用して実装・リリースしました。しかし、しばらく運用していると、一部のファイルがコピーできていないことが発覚しました。

原因は.....copyObjectは5GB以上のファイルはコピーできないというAPI制限があるためでした。

マルチパートアップロード

実は5GB以上のファイルをコピーするには、マルチパートアップロード方式で実装する必要がありました。

マルチパートアップロードするには、メソッドを書き換えるだけは済みません。 画像のように、大まかに5つやることがあります。

マルチパートアップロード

このような流れで、以下のような実装して実現しましたので紹介します(異常系は省いています)

// サイズ取得
const headed = await s3.headObject({
    Bucket: 'src',
    Key: 'mdeia/2022.mp4',
}).promise();

// マルチパートアップロード用ID発行
const created = await s3.createMultipartUpload({
    Bucket: 'dist',
    Key: 'media/mp4/2022.mp4'
}).promise();

// 分割サイズ計算
const parts = [];
const MINIMUM_PART_SIZE = 5242880;
const COPY_PART_SIZE = 50000000;

const limit = Math.floor(headed.ContentLength / COPY_PART_SIZE);
const remainder = headed.ContentLength % COPY_PART_SIZE;

let index, part;

for(index = 0; index < limit; index++){
    const nextIndex = index + 1;
    if(nextIndex === limit && remainder < MINIMUM_PART_SIZE){
        part = `${index * COPY_PART_SIZE}-${(nextIndex) * COPY_PART_SIZE + remainder - 1}`;
    } else {
        part = `${index * COPY_PART_SIZE}-${(nextIndex) * COPY_PART_SIZE - 1}`;
    }
    parts.push(part);
}

if(remainder >= MINIMUM_PART_SIZE){
    parts.push(`${index * COPY_PART_SIZE}-${index * COPY_PART_SIZE + remainder - 1}`);
}

// 分割コピー
const copies = parts.map((part, index)=>{
    return s3.uploadPartCopy({
        UploadId: created.UploadId,
        CopySource: encodeURIComponent('src/mdeia/2022.mp4'),
        CopySourceRange: 'bytes=' + part,
        Bucket: 'dist',
        Key: 'media/mp4/2022.mp4',
        PartNumber: index + 1,
    }).promise();
});
const copied = await Promise.all(copies);

// マルチパートコピー完了
const completed = await s3.completeMultipartUpload({
    UploadId: created.UploadId,
    Bucket: 'dist',
    Key: 'media/mp4/2022.mp4',
    MultipartUpload: {
        Parts: copied.map((result, index)=>{
            return {
                ETag: result.CopyPartResult.ETag,
                PartNumber: index + 1,
            };
        })
    }
}).promise();

アップロードと言っていますが、一旦ローカルにファイルはダウンロードしませんので、ストレージ容量を気にする必要もありません。

また、5GB以下の場合はcopyObjectでコピーするなど使い分けるのが良いと思います。

最後に

最初は1関数をコールするだけでコピーできていたので、このような手順を理解するのに苦労しました。さらに、分割サイズ計算は色々なサイトを参考にしつつもロジックを理解するのに苦戦しました。

開発時には、扱いやすい数MBのファイルでばかりでテストしていたことも反省すべきだと思いました。 動画や肥大化する可能性のあるログファイルなどを扱う際には、設計時に5GBを超える可能性を考慮して頂ければと思います。

こんな記事も読まれています