[intra-mart] IM-LogicDesignerからS3 Presigned URLを用いたファイルアップロード

# はじめに
NTTデータ イントラマートにはIM-LogicDeignerという、intra-mart Accel Platform上でビジネスロジックをローコードで簡単に作成できるアプリケーションが提供されています。作成したロジックはREST APIとして利用可能です。

https://document.intra-mart.jp/library/iap/public/im_logic/im_logic_user_guide/texts/summary/index.html

今回このIM-LogicDesignerを使って、AWSのS3 Presigned URLを用いたファイルアップロードする方法を紹介します。

# S3 Presigned URL（署名付きURL）とは

S3 Presigned URL（署名付きURL）は、バケットポリシーを更新せずに一時的にS3内のオブジェクトへアクセスが可能となる署名付きのURLです。署名付きURLを使用することによって、AWS のセキュリティ認証情報やアクセス許可を持たずに、一時的にファイルのアップロードやダウンロードすることが可能となります。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-presigned-url.html

今回は、IM-LogicDesignerからS3 Presigned URLを使って、S3へファイルアップロードしようとした際に問題となった点と、その解決方法について解説します。

# 背景

今回は、以下の流れでファイルをCSVファイルをアップロードしてみます。
```mermaid
sequenceDiagram
    participant ld as LogicDesigner
    participant sv as 業務サーバ
    participant s3 as S3

    ld ->> sv: アップロード用Presigned URL要求
    sv ->> s3: Presigned URL要求
    s3 ->> sv: Presigned URL発行
    sv ->> ld: アップロード用Presigned URL返却
    ld ->> s3: Presigned URLでファイルアップロード(PUT)
```
実際のPresigned URLの生成はAWS LambdaからBoto3を使って、以下のような形で生成します。
```python
import boto3
s3 = boto3.client("s3")
upload_url = s3.generate_presigned_url(
    ClientMethod = "put_object",
    Params = {
        'Bucket': "bucket_name",
        'Key': "object_key"
    },
    ExpiresIn = 500,
    HttpMethod = "PUT"
)
```
通常はParamsパラメータにContent-TYpeを指定しますが、今回はこれが省略されていました。複数のContent-Typeを許容する目的でPresigned URL生成時にContent-Typeが指定されないということもあるかと思いますが、Presigned URLを使ってファイルアップロードする際には、Content-Typeが一致しないと受け付けません。つまり、この場合はファイルアップロード時にContent-Typeを省略する必要が出てきます。業務サーバ側の処理を変更すれば良いのですが、今回はIM-LogicDesignerからContent-Typeを省略したアクセスができる方法を試してみます。

# 問題点

まずはIM-LogicDesignerでHTTP通信を行う際の利用頻度の高い「ユーザ定義編集 - REST」の仕様を確認してみます。

https://accel-archives.intra-mart.jp/2023-autumn/document/iap/public/im_logic/im_logic_user_guide/texts/guidance/user_definition/ud_edit_rest/index.html

リクエスト時にContent-Typeを指定する項目はなく、状況に合わせて自動付与される仕様です。
以下の条件を満たす場合にはContent-Typeの自動付与は行われませんが、今回はCSVファイルのアップロードをする目的ですので合致しません。
* リクエスト種別：raw
* 入力値の body パラメータの型：binary または byte[]

次に、HttpClientオブジェクトを用いた通信の仕様を確認します。

https://api.intra-mart.jp/iap/apilist-ssjs/doc/platform/HttpClient/index.html

Content-Type省略時には以下の条件によってContent-Typeが指定される仕様です。
* 非マルチパート時：application/x-www-form-urlencoded
* マルチパート時：multipart/form-data


https://api.intra-mart.jp/iap/apilist-ssjs/doc/platform/HttpClient/index.html?_gl=1*69e9yk*_ga*MTg4MDc0MjQzNy4xNzQ2NzU4MDg3*_ga_77M391ZLH6*czE3NDgyNjA5NTYkbzExJGcxJHQxNzQ4MjYxMTAyJGowJGwwJGgw&_ga=2.48513638.1740189660.1748255702-1880742437.1746758087#method-put_6

以上の点から、intra-martの標準機能ではIM-LogicDesignerから外部へのHTTP通信時にContent-Typeの指定を省略することはできない、ということになります。

# 対処方法

intra-martの標準機能では実現できないことがわかったので、IM-LogicDesigner の拡張機能を利用し独自のJavaタスクを生成する方法で対処します。

https://accel-archives.intra-mart.jp/2023-autumn/document/iap/public/im_logic/im_logic_programming_guide/texts/introduction/index.html

まずはJavaでContent-Typeを省略したHTTP通信の動作検証を行います。

```java
   public static void uploadFile(String targetUrl, File file) throws IOException {
       // URLオブジェクトを作成
       URL url = new URL(targetUrl);
       HttpURLConnection connection = (HttpURLConnection) url.openConnection();
       // 接続設定
       connection.setDoOutput(true);
       connection.setRequestMethod("PUT");
       connection.setRequestProperty("Content-Length", String.valueOf(file.length()));

       // ファイルをストリームとして送信
       try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
            FileInputStream fileInputStream = new FileInputStream(file)) {
           byte[] buffer = new byte[4096];
           int bytesRead;
           while ((bytesRead = fileInputStream.read(buffer)) != -1) {
               outputStream.write(buffer, 0, bytesRead);
           }
           outputStream.flush();
       }
       // レスポンスコードを取得
       int responseCode = connection.getResponseCode();
       System.out.println("Response Code: " + responseCode);
       // 接続を切断
       connection.disconnect();
   }
```

この方法で無事にファイルアップロードできました。ポイントは、`curl -X PUT --upload-file file.csv URL`のようにcurlコマンドによるファイルアップロードと同等の処理とするために、ファイルをストリームとして送信する点です。

あとは、これをIM-LogicDesignerの拡張機能として取り込んで完了です。拡張モジュールの作成方法は、以下の手順で実行しintra-martのwarファイルに含めるように再ビルドを実行します。

https://accel-archives.intra-mart.jp/2023-autumn/document/iap/public/im_logic/im_logic_programming_guide/texts/package/index.html

https://accel-archives.intra-mart.jp/2023-autumn/document/iap/public/im_logic/im_logic_programming_guide/texts/flow/index.html

# おわりに
今回は、IM-LogicDesignerからS3 Presigned URLによるファイルアップロードする際に問題となった点の解消方法について説明を行いました。ただし、この問題はPresigned URL生成時にContent-Typeが指定されていないという点に起因した問題です。一般的にはPresigned URL生成時のロジックにて対処するのが通常だと思いますが、都合によってはその対処ができないケースに遭遇することもあるかもしれません。その際のご参考になれば幸いです。