chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

試作:NodeREDからAmazon Rekognisionを使うサンプル(S3アップロード)

背景:プログラミング教材としてAmazon Rekognisonを使う案を検討中、プログラミングの障壁を減らすため、NodeREDで専用Nodeを用意しておき、それらを部品(詳細は知らなくても良い)として組み合わせることでシステムを試作してもらう案
進捗:Rekognison API呼び出しのノード、カメラ撮影ノードはできた
課題:不足しているノード(S3アップロードノード)を作って、仕上げる
取り組み:AWS SDK をJSから呼び出す事例を調べて、S3アップロードノードを作る
結論: S3へのアップロード用Nodeを作った。AWS SDK V3を使う上でいろいろはまったが、コードを修正してむりやり動かした。根本対策としてはTypeScriptで書くのが良いと思われる。
詳細:

普段はBoto3を使っているが、JSからの利用方法が全く分かっていない。AWSの解説ページを見ても、APIにV2,V3があるようでよく分からない。結局先人が公開している記事から引用することに。

nodejsで写真をS3にアップする例(Saturn Cloud様記事より)

const AWS = require('aws-sdk');
const fs = require('fs');

const s3 = new AWS.S3();

const bucketName = <bucket_name>
const fileName = <path_and_file_in_S3>
const localFileName = <path_and_file_in_Local>
const fileData = fs.readFileSync(localFileName);

s3.upload({
  Bucket: bucketName,
  Key: fileName,
  Body: fileData
}, (err, data) => {
  if (err) {
    console.error(err);
  } else {
    console.log(`File uploaded successfully. ${data.Location}`);
  }
});

動くには動いたが、、、これはv2のコードらしかった。動かしてみるとv3に移行しろと怒られる。

node upload.js

(node:25237) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)
File uploaded successfully. https://xxx_rekognition_xxx.s3.amazonaws.com/face001.jpg

同じAPIが使えるかどうかはおいといて、、少なくともパッケージのインストールとロードは以下となるべきのようだった

npm install @aws-sdk/client-s3
import {S3} from "@aws-sdk/client-s3";

Loading the SDK for JavaScript - AWS SDK for JavaScript
必要なパッケージを以下でインストール

npm install @aws-sdk/client-s3

require文をimport文に変えただけだと以下のエラーになった

import {S3} from "@aws-sdk/client-s3";
^^^^^^
SyntaxError: Cannot use import statement outside a module

JSのパッケージ管理がどうなっているのかほとんど理解できていないのだが、、workaroundでrequire文に書き換える*1

//import {S3} from "@aws-sdk/client-s3";
const {s3} = require("@aws-sdk/client-s3");

すると、uload関数のところでエラーになった。やはりそのままでは使えないと。。
理解が深まって検索キーワードも適切になったのか、欲しい情報に辿れるようになってきた。*2
以下はS3へのファイルアップロード(tmokmss様のサンプルコード)

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createReadStream } from 'fs';

const s3 = new S3Client({});

// ローカル上のtest.txtをアップロード
await s3.send(
  new PutObjectCommand({
    Body: createReadStream('test.txt'),
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);

公開していただいているサンプルコードをベースに少し修正

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createReadStream } from 'fs';

const bucketName = <bucket_name>;
const fileName = <path_and_file_in_bucket>;
const localFileName = <local_file_name>;

const s3 = new S3Client({});

console.log("upload file");
const prom =  s3.send(
  new PutObjectCommand({
      Body: createReadStream(localFileName),
      Bucket: bucketName,
      Key: fileName,
  })
);
console.log("exit send");
console.log(prom);
prom.then(resp => {console.log("done"); console.log(resp);},
          resp => {console.log("error"); console.log(resp);});

正常実行時の出力

 $ node upload2.js
upload file
exit send
Promise { <pending> }
done
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'K4FxxxxxxxXC1',
    extendedRequestId: '70BBxxxxxxxxxCuK4u9Yt8=',
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  ETag: '"91cexxxxx1468b"',
  ServerSideEncryption: 'AES256'
}

存在しないバケツ名を指定してわざとエラーを起こさせるとExceptionになるようだった。JSでのExceptionが不勉強なので、catchとか、ちと分からず。しかし、エラー処理用のコールバック?は呼んでくれるようであった。

nodejsでは動いたので、これをNode-REDのオリジナルノードとして書き換える。
パッケージ管理システム、Node-REDのパッケージ、etcがよく分かっていないのだが、Node-REDでオリジナルノードを作るためJavaScriptを試作したものの、import文を使うとエラーになるようだ。node.jsでimport文を使う時にもエラーが発生して、同じエラー回避策*3を試したが、いずれもNode-REDでは正常には動かなかった。だから、、importを使わず、requireで動かす。*4

■参考URL
How to Upload a File to Amazon S3 with NodeJS | Saturn Cloud Blog
AWS SDK JavaScript v3でS3のファイル操作 チートシート - maybe daily dev notes
nodejs で import がシンタックスエラーを吐く - Qiita

JavaScript モジュール - JavaScript | MDN
import/exportの方が素直な仕様と思えるのだが、なぜnode.jsではモジュールの外では使ってはダメと怒られるのだろうか。。

エラーになるかならないかは、NodeJS側で、対象のファイルをESモジュールとして解釈するか、commonJSモジュールとして解釈するか?によって変わるらしい。エラーを回避する手段も、どう解釈させるかをコントールしているのだろうと理解。ただ、nodejsで動くように変えても、Node-REDでは動かない。これは、解釈のコントール手法がNode-REDには有効に働かないということか。また、ESモジュール/commonJSモジュールの差異を超える別の方法として、TypeScriptで書く方法があるらしい。TypeScriptだとこの問題が発生しないらしい。となると、、Node-REDのオリジナルノードをTypeScriptで書けるのかどうか??ということになる。Node-REDはnode.js上で動いているので、node.jsのレベルでtsが動けばNode-REDでもtsで動かせるのかも・・
少し調べると、TypeScriptでオリジナルノードを作る記事を書いている人がいた。ざっと読むとデータ型の定義が結構大変になるようだ。TypeScriptだけに。。
そもそも、このimport文の問題は、APIをV3に上げる所で発生した。根本解決にはTSで書くべきなのだが、結構手間がかかる。だったら、当面はAPI使うたびに怒られながらも、v2を使う。TSを使いこなせるレベルになってから、TS+V3に移行したい。そう考えると、JSではなくTSを普段使いになれるようにスキルアップすべきかのか・・ JS/TSは変化の激しい今を生きてる言語と再認識しましたっ!!
Node-RED のノードを TypeScript で開発する - ひよこまめ
@types/node-red - npm
Node-RED: Creating a custom node using TypeScript | by Bernardo Belchior | Medium

■追記
import文を使わず、require文で無理やりV3を動かしてみた。以下がソース。一応エラーなくNode-REDに組み込めて、動作はしているようである。
file: s3uploader.js

s3 = require('@aws-sdk/client-s3');
S3Client = s3.S3Client;
PutObjectCommand = s3.PutObjectCommand;
fs = require('fs');

module.exports = (RED) => {
    function S3UpLoader(config) {
        RED.nodes.createNode(this,config);
        var node = this;
        node.on('input', function(msg) {
            s3uploader(node, msg);  
        });
    }
    RED.nodes.registerType("s3uploader",S3UpLoader);
}

function s3uploader(node, msg) {

   const bucketName = <bucket_name>;    //  eg.  'rekognition-bucket'
   const fileName = <path_and_file_in_s3>;
   const localFileName = '<local file name>';

   const s3 = new S3Client({});
   const prom = s3.send(
       new PutObjectCommand({
            Body: fs.createReadStream(localFileName),
            Bucket: bucketName,
            Key: fileName,
       })
   );
   prom.then(resp => {
               msg.payload = "upload done";
               node.send(msg);
             },
             resp => {
               msg.payload = "upload in fail";
               node.send(msg);
             });
    return true;
}

Node-REDでの画面と動作例

■追記
TypeScript入門『サバイバルTypeScript』〜実務で使うなら最低限ここだけはおさえておきたいこと〜
クラス (class) | TypeScript入門『サバイバルTypeScript』
TSで知りたかった事・・変数に対して、新たに定義したClassを指定するのはどうするのか??ー>単純にClass名を指定すれば良い
型指定を省略しても型推論してくれる

*1:回避策: package.jsonに、"type": "module"を加える()

*2:キーワード:aws s3 upload javascript v3、検索期間:1年

*3:2案あって、package.jsonに、"type": "module"を加える方法と、ファイル拡張子をmjsに変える方法

*4:本来のアプローチとしては、パッケージ管理がどうなっているのかをよく理解することで、正しい回避策に到達できるはずですが、、そこまで掘り下げるための情熱が不足している