S3とファイルを共通に扱う

タイトルにある通り、S3を扱うNode.jsライブラリです。
すぐ忘れるので、備忘録として残しておきました。
おそらくもっと良いやり方があるかもしれず、いろんなケースに対応できているわけではないように思いますが、各所でカスタマイズするためのベースにはなるかなあと思います。

ファイルとS3どちらも共通に扱えるようにしています。
また、AWS S3の代わりにMinioでも利用できます。

```js:storage.js
'use strict';

const fs = require('fs').promises;
var AWS = require('aws-sdk');
AWS.config.update({
  // region: "ap-northeast-1",
  // minio用
  region: "[Minioのリージョン名]",
});
var s3  = new AWS.S3({
  // minio用
  accessKeyId: '[MinioのアクセスキーID]' ,
  secretAccessKey: '[Minioのシークレットアクセスキー]',
  endpoint: 'http://[Minioのサーバ]:9000',
  s3ForcePathStyle: true, // needed with minio?
  signatureVersion: 'v4'
});

class Storage{
  constructor(folder, mode = "file"){
    this.folder = folder;
    this.mode = mode;
  }

  async createFolder(){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_createFolder();
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_createFolder();
    }
  }

  async s3_createFolder(){
    var params = {
      Bucket: this.folder, 
    };
    return s3.createBucket(params).promise();
  }

  async file_createFolder(){
    return fs.mkdir(this.folder);
  }

  async deleteFolder(){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_deleteFolder();
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_deleteFolder();
    }
  }

  async s3_deleteFolder(){
    await this.deleteAllObjects();
    var params = {
      Bucket: this.folder
    };
    return s3.deleteBucket(params).promise();
  }

  async file_deleteFolder(){
    await this.deleteAllObjects();
    return fs.rmdir(this.folder);
  }

  async deleteObject(fname){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_deleteObject(fname);
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_deleteObject(fname);
    }
  }

  async s3_deleteObject(fname){
    var param_delete = {
      Bucket: this.folder,
      Key: fname
    };
    return s3.deleteObject(param_delete).promise();
  }

  async file_deleteObject(fname){
    return fs.unlink(this.folder + "/" + fname);
  }

  async deleteAllObjects(){
    var list = await this.listObjects();
    for( var i = 0 ; i < list.length ; i++ )
      await this.deleteObject(list[i]);
  }

  async listObjects(){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_listObjects();
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_listObjects();
    }
  }

  async s3_listObjects(){
    var params = {
      Bucket: this.folder,
    };
    var list = [];
    var result;
    do{
      result = await s3.listObjects(params).promise();
      if( result.Contents )
        for( var i = 0 ; i < result.Contents.length ; i++ )
          list.push(result.Contents[i].Key);

      if( result.NextContinuationToken )
        params.ContinuationToken = result.NextContinuationToken;
    }while(result.NextContinuationToken);

    return list;
  }

  async file_listObjects(){
    return fs.readdir(this.folder);
  }

  async loadBinary(fname){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_loadBinary(fname);
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_loadBinary(fname);
    }
  }

  async s3_loadBinary(fname){
    var param_get = {
      Bucket: this.folder,
      Key: fname
    };
    var obj = await s3.getObject(param_get).promise();
    return obj.Body;
  }

  async file_loadBinary(fname){
    return fs.readFile(this.folder + "/" + fname);
  }

  async loadBinaryPartial(fname, offset, length){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_loadBinaryPartial(fname, offset, length);
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_loadBinaryPartial(fname, offset, length);
    }
  }

  async s3_loadBinaryPartial(fname, offset, length){
    var param_get = {
      Bucket: this.folder,
      Key: fname,
      Range: "bytes=" + offset + "-" + (offset + length - 1)
    };
    var obj = await s3.getObject(param_get).promise();
    return obj.Body;
  }

  async file_loadBinaryPartial(fname, offset, length){
    var stat = await fs.lstat(this.folder + "/" + fname);
    if( stat.size < (offset + length) )
      throw "file size short";

    var buffer = new Uint8Array(length);

    var fp = await fs.open(this.folder + "/" + fname);
    var buffer = await fp.read(buffer, 0, length, offset);
    fp.close();
    return buffer.buffer;
  }

  async putBinary(fname, body){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_putBinary(fname, body);
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_putBinary(fname, body);
    }
  }

  async s3_putBinary(fname, body){
    var param_put = {
      Bucket: this.folder,
      Key: fname,
      Body: body
    };

    return s3.putObject(param_put).promise();
  }

  async file_putBinary(fname, body){
    return fs.writeFile(this.folder + "/" + fname, body);
  }

  async isExist(fname){
    if( this.mode == "s3" ){
      // S3用
      return this.s3_isExist(fname);
    }else
    if( this.mode == "file" ){
      // ファイル用
      return this.file_isExist(fname);
    }
  }

  async s3_isExist(fname){
    try{
      var buffer = await this.loadBinaryPartial(fname, 0, 1);
      return true;
    }catch(error){
      if( error.code == "InvalidRange" )
        return true;
      else
        return false;
    }
  }

  async file_isExist(fname){
    try{
      var stat = await fs.lstat(this.folder + "/" + fname);
      return true;
    }catch(error){
      return false;
    }
  }
}

module.exports = Storage;
```

# その他

S3互換のMinioについては以下が参考になります。
　[SwaggerでLambdaのデバッグ環境を作る(7)：AWS S3トリガをデバッグする](https://qiita.com/poruruba/items/67528767d6abf9799ed5)