JavaでポストオブジェクトフォームのOSSへのアップロードをシミュレートする方法

**Alibaba Cloud Object Storage Service（OSS）**は、「バケット」と呼ばれるリソース内にオブジェクトを格納します。Post ObjectはHTMLフォームを使って、指定したバケットにファイルをアップロードします。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/simulate-post-object-form-upload-to-oss-in-java_332241)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

#序章
Alibaba Cloud Object Storage Service (OSS)は、クラウド上で大量のデータを保存、バックアップ、アーカイブすることができる使いやすいサービスです。OSSは暗号化された中央リポジトリとして機能し、世界中のファイルに安全にアクセスすることができます。さらに、最大99.9%の可用性が保証されており、グローバルチームや国際的なプロジェクト管理に最適です。

Alibaba Cloud Object Storage Service (OSS)は、「バケット」と呼ばれるリソース内にオブジェクトを安全に保存します。OSSでは、バケットへのフルアクセスが可能で、各バケット内のログやオブジェクトを閲覧することができます。バケット内のオブジェクトの読み取り、書き込み、削除、保存は無制限に行うことができます。OSSの高いパフォーマンスは、複数の読み取り/書き込みを同時にサポートしています。バケットへのデータ転送はSSL経由で行われ、暗号化されています。

Post Objectは、Putの代わりにHTMLフォームを使用して指定したバケットにファイルをアップロードします。これにより、ブラウザ経由でバケットにファイルをアップロードすることが可能になります。Post ObjectをJavaで実装したいという要望は、様々なサポート担当者からの簡単な説明に由来しています。彼らによると、この機能を必要とするユーザーが、公式ドキュメントに沿って機能を実装しようとすると、様々な困難な問題に直面します。これは、公式のコードリファレンスが存在しないために起こります。

#手続きの流れ
ユーザーが行うべき手順を見てみましょう。

- 公式サイトでは、まずHTTPリクエスト構文を提供しています。HTTPリクエストヘッダーと、パラメータを満たすためのメッセージ本文のmultipart/form-data-encodedフォームフィールドの形式です。
- 次に、"file "や "key "などの "required "フォームフィールドを1つ1つフォームフィールドテーブルで紹介します。ユーザーは、「OSSAccessKeyId」、「policy」、「Signature」などのフォームフィールドのうち、どれか一つでも出てきたら要求します。また、RESTリクエストヘッダ、x-oss -meta- * user meta、その他オプションのフォームフィールドも必要です。
- これを投稿すると、一部のフォームフィールドについて、いくつかの特別な使い方や注意点を紹介しています。また、Post PolicyとSignatureの機能とその使用法についての情報のスタックを共有しています。
- ドキュメントはある程度明確になっていますが、理解しづらい概念が多く、問題の調査に苦労します。さらに、実装時にエラーが発生しやすい点についてのハイライトがないため、ユーザーは以下の2つの大きな課題に直面することになります。ユーザーは、2つの大きな課題に遭遇する可能性があり、さらに2つの側面が関係しています。

- multipart/form-data のような MIME タイプのエンコーディングに慣れていないこと。
- Post Object リクエストを解析するための OSS の実装ルールに慣れていないこと。


次に、上述した2つの側面について説明します。

multipart/form-dataの詳細な紹介については、[RFC 2388](https://tools.ietf.org/html/rfc2388?spm=a2c65.11461447.0.0.406d6001dqi8xr)を参照してください。ここで注意すべき点がいくつかあります。それらを一つずつ議論していきましょう。

1.最初のポイントでは、"multipart/form-data "リクエストは一連のフィールドを含むとしている。各フィールドは "form-data "タイプのcontent-dispositionヘッダーを持ちます。このヘッダには、フォームフィールドの内容を記述するためのパラメータ "name "も含まれています。したがって、すべてのフィールドはドキュメントで示されている例に似たフォーマットを持ちます。

`Content-Disposition: form-data; name=“your_key"`

注意点：「:」と「;」はどちらもスペースの後に続きます。

2.注意すべき2つ目のポイントは、フォームにユーザーファイルをアップロードする必要がある場合、ファイル名や他のファイル属性がContent-dispositionヘッダーに必要になるかもしれないということです。さらに、フォームフィールド内の任意のMIMEタイプの値に対して、ファイルのコンテンツタイプを識別するために、オプションのContent-Type属性も存在します。したがって、ドキュメントでは、"file "フォームフィールドの例を以下のように列挙しています。

```
Content-Disposition: form-data; name="file"; filename="MyFilename.jpg"
Content-Type: image/jpeg
```

注意: "ファイル名 "の前の"; "には、まだ末尾にスペースがあります。同様に、"Content-Type "の後の"："にも末尾にスペースがあります。

3.3つ目のポイントは、境界線でデータを区切ることです。メインコンテンツと区別するために、複雑な境界線を使ってみましょう。これは、ドキュメントに描かれているように、HTTPヘッダのコンテンツに似た方法で実現できます。

`Content-Type: multipart/form-data; boundary=9431149156168`

4.注意点の4つ目は、各フォームフィールドの構造が決まっているということです。仕様としては、各フォームフィールドは"--"boundary+で始まり、その後にキャリッジリターン(/r/n)が続きます。続いて、フォームフィールドの説明(ポイント1参照)、/r/nの順に来ます。転送したいコンテンツがファイルの場合、ファイル名情報には、キャリッジリターン(/r/n)の後にファイルコンテンツの種類も含まれます(ポイント2参照)。さらに、実際のコンテンツを開始するためのキャリッジリターン(/r/n)がもう一つありますが、これは/r/nで終わらせる必要があります。

5.最後のフォームフィールドが"--"+boundary+"--"で終わることにも注意してください。

6.さらに、HTTPリクエストヘッダとボディ情報を区別するために/r/nマークも必要です(ヘッダと最初のフォームフィールドの分岐点にあります)。これは基本的にはドキュメントのような余分な空白行であり、以下のようになっています。

`Content-Type: multipart/form-data; boundary=9431149156168`

```
--9431149156168
Content-Disposition: form-data; name="key"
```

以上、OSS公式ドキュメントで提供されているリクエスト構文の一般的な説明と、RFC2388標準との比較による関連分析を説明しました。

ここでは、OSSシステムがPOSTオブジェクトリクエストをパースするための処理の一部と関連する注意点の説明を掘り下げていきます。

OSSがPOSTリクエストをパースするための一般的な手順は、参考までに下の図のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/6a15c174-5bb9-c475-c85a-19067726fd45.png)

リクエスト処理の流れを3つのコアステップに要約します。

1、フィールドの境界を区別するためにHTTPリクエストヘッダの境界を解析する。
2、フローが 'file' フォームフィールドに到達するまで、様々なフィールドの内容を解析する。
3、file'フォームフィールドを解析する。

それゆえ、ドキュメントでは、'file'フォームフィールドを「最後のフィールド」に配置することを強調しています。そうでなければ、「file」の後のフォームフィールドは効果を発揮しない可能性があります。必須のフォームフィールド「key」を「file」の後に配置すると、結果は確実にInvalidArgumentになります。

次に、図のような作業の流れを簡単に説明します。

1) POLICY, OSSACCESSKEYID, SIGNATUREの存在を確認します。
このチェックは必須です。POLICY, OSSACCESSKEYID, SIGNATUREの3つのフィールドのうち1つが出現した場合、他の2つのフィールドが必要となります。
2) 権限の有無。
POLICY, OSSACCESSKEYID, SIGNATURE の情報をもとに、Post リクエストの有効性を確認します。
3) ポリシールールチェック
リクエストの各種フォームフィールドの設定がポリシー設定に準拠しているかどうかを確認します。
4) 長さの合法性をチェックします。

これは、Postリクエストボディの全長に制限があるため、オプションフィールドの長さを確認することを目的としています。
5) ParseFileでParseContentTypeを解析します。
ファイル」フィールドのContentTypeフィールドをパースします。このフィールドは必須ではありません。

以上で、OSSに詳しい方の参考と利用のために、OSSでPost Object uploadを実装したJavaコード(Mavenプロジェクト)で締めくくります。

```
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created by yushuting on 16/4/17.
 */
public class OssPostObject {

    private String postFileName = "your_file";  
Make sure that the file exists at the path indicated in the run code. 
    private String ossEndpoint = "your_endpoint";  
For example: http://oss-cn-shanghai.aliyuncs.com
    private String ossAccessId = "your_accessid";  This is your access AK
    private String ossAccessKey = "your_accesskey";  This is your access AK
    private String objectName = "your_object_name";  This is the object name after you upload the file
    private String bucket = "your_bucket";  Make sure that the bucket you created previously has been created. 

    private void PostObject() throws Exception {

        String filepath=postFileName;
        String urlStr = ossEndpoint.replace("http://", "http://"+bucket+".");  This is the URL for the submitted form is the bucket domain name

        LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
        // key
        String objectName = this.objectName;
        textMap.put("key", objectName);
        // Content-Disposition
        textMap.put("Content-Disposition", "attachment;filename="+filepath);
        // OSSAccessKeyId
        textMap.put("OSSAccessKeyId", ossAccessId);
        // policy
        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, 104857600]]}";
        String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
        textMap.put("policy", encodePolicy);
        // Signature
        String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(ossAccessKey, encodePolicy);
        textMap.put("Signature", signaturecom);

        Map<String, String> fileMap = new HashMap<String, String>();
        fileMap.put("file", filepath);

        String ret = formUpload(urlStr, textMap, fileMap);
        System.out.println("[" + bucket + "] post_object:" + objectName);
        System.out.println("post reponse:" + ret);
    }

    private static String formUpload(String urlStr, Map<String, String> textMap, Map<String, String> fileMap) throws Exception {
        String res = "";
        HttpURLConnection conn = null;
        String BOUNDARY = "9431149156168";
        try {
            URL url = new URL(urlStr);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(30000);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("User-Agent",
                    "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
            conn.setRequestProperty("Content-Type",
                    "multipart/form-data; boundary=" + BOUNDARY);

            OutputStream out = new DataOutputStream(conn.getOutputStream());
            // text
            if (textMap != null) {
                StringBuffer strBuf = new StringBuffer();
                Iterator iter = textMap.entrySet().iterator();
                int i = 0;
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    String inputName = (String) entry.getKey();
                    String inputValue = (String) entry.getValue();
                    if (inputValue == null) {
                        continue;
                    }
                    if (i == 0) {
                        strBuf.append("--").append(BOUNDARY).append(
                                "\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\""
                                + inputName + "\"\r\n\r\n");
                        strBuf.append(inputValue);
                    } else {
                        strBuf.append("\r\n").append("--").append(BOUNDARY).append(
                                "\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\""
                                + inputName + "\"\r\n\r\n");

                        strBuf.append(inputValue);
                    }

                    i++;
                }
                out.write(strBuf.toString().getBytes());
            }

            // file
            if (fileMap != null) {
                Iterator iter = fileMap.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    String inputName = (String) entry.getKey();
                    String inputValue = (String) entry.getValue();
                    if (inputValue == null) {
                        continue;
                    }
                    File file = new File(inputValue);
                    String filename = file.getName();
                    String contentType = new MimetypesFileTypeMap().getContentType(file);
                    if (contentType == null || contentType.equals("")) {
                        contentType = "application/octet-stream";
                    }

                    StringBuffer strBuf = new StringBuffer();
                    strBuf.append("\r\n").append("--").append(BOUNDARY).append(
                            "\r\n");
                    strBuf.append("Content-Disposition: form-data; name=\""
                            + inputName + "\"; filename=\"" + filename
                            + "\"\r\n");
                    strBuf.append("Content-Type: " + contentType + "\r\n\r\n");

                    out.write(strBuf.toString().getBytes());

                    DataInputStream in = new DataInputStream(new FileInputStream(file));
                    int bytes = 0;
                    byte[] bufferOut = new byte[1024];
                    while ((bytes = in.read(bufferOut)) != -1) {
                        out.write(bufferOut, 0, bytes);
                    }
                    in.close();
                }
                StringBuffer strBuf = new StringBuffer();
                out.write(strBuf.toString().getBytes());
            }

            byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
            out.write(endData);
            out.flush();
            out.close();

            // Read the returned data
            StringBuffer strBuf = new StringBuffer();
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    conn.getInputStream()));
            String line = null;
            while ((line = reader.readLine()) != null) {
                strBuf.append(line).append("\n");
            }
            res = strBuf.toString();
            reader.close();
            reader = null;
        } catch (Exception e) {
            System.err.println("Error in sending a POST request:  " + urlStr);
            throw e;
        } finally {
            if (conn != null) {
                conn.disconnect();
                conn = null;
            }
        }
        return res;
    }

    public static void main(String[] args) throws Exception {
        OssPostObject ossPostObject = new OssPostObject();
        ossPostObject.PostObject();
    }

}
```

pom.xmlに以下を追加する必要がありますのでご注意ください。

```
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>2.2.1</version>
</dependency>
```

#結論
Post Objectは、ブラウザに基づいてバケットにファイルをアップロードすることを可能にします。Post Objectのメッセージ本文のエンコードはmultipart/form-dataを利用しています。Post Object の操作では、プログラムはメッセージ本文のフォームフィールドとしてパラメータを転送します。Post Object は、ポリシーの署名を計算するために AccessKeySecret を使用します。POSTフォームフィールドは、パブリックリードライトバケットをアップロードするためのオプションですが、POSTリクエストを制限するためにこのフィールドを使用することをお勧めします。

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*