nkjmkzk.net

powered by Kazuki Nakajima

Archive for 5月, 2013

CORSサポートを利用したAWS S3へのアップロード方法 – Force.comバージョン

昨年秋にAWS S3はCORSをサポートし、クロスドメインでもjavascriptだけでファイルを容易にアップロードできるようになりました。

CORSとは?についてはクラスメソッドさんのブログで決定版的な詳解がありますのでそちらを参考に。
CORS(Cross-Origin Resource Sharing)について整理してみた

Force.comでも非構造化データはS3にお任せする、というシチュエーションはしばしばあります。そういうケースでS3のCORSに対応したファイルアップロードを実現するVisualforceおよびApexのサンプルコードを作成したので共有しておきます。

Step 1. CORS設定

まず、CORSに対応するバケットを作成しておく必要があります。既存のバケットでも構いません。

AWS Management Consoleにアクセスし、CORS対応させるバケットを選択し、「Properties」タブをクリックします。「Permissions」セクションを展開すると「Edit CORS Configuration」ボタンが現れます。

「Edit CORS Configuration」をクリックするとXMLフォームが開きます。このフォームを下記の通りに設定して保存します。

<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Origin</AllowedHeader>
        <AllowedHeader>Content-Type</AllowedHeader>
        <AllowedHeader>x-amz-storage-class</AllowedHeader>
        <AllowedHeader>x-amz-acl</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

このXMLではどのような操作が許可されるかを定義しています。必要に応じて後ほどパラメータを編集するとして、とりあえずはこれで十分です。

 

Step 2. パッケージをSalesforce組織にインストール

サンプルコードはApexクラス、Visualforceコンポーネント・ページ、カスタム設定、カスタムオブジェクトで構成されています。これらをパッケージにまとめてAppExchangeにアップしておきましたのでこちらをインストールしてください。

AWS S3 CORS Uploader

パッケージをインストールしたらアプリケーションの設定 > 開発 > カスタム設定を開き、AWS S3のManageリンクをクリックします。

「新規」ボタンをクリックします。

必須項目のアクセスキー、シークレットアクセスキー、バケット名を入力して保存します。これらの情報はAWS Management Consoleで確認できます。

次に右上のアプリケーションプルダウンメニューからAWS S3 CORS Uploaderを選択します。S3 UploaderとS3 Objectの二つのタブが現れるはずです。S3 Uploaderタブを選択するとシンプルなアップロードフォームが表示されます。


「ファイルを選択」ボタンをクリックして適当なファイルを選択し、「upload」ボタンをクリックして実際にファイルをアップロードしてみてください。JavascriptがダイレクトにファイルをS3にアップロードします。ポップアップが2回表示され、それぞれ「Upload Succeeded」と「Insert Succeeded」と表示されるはずです。

S3のManagement Consoleでファイルがアップロードされたかどうか確認してみてください。また、S3 Objectタブをクリックすると、アップロードしたファイルのURLが記載されたレコードが作成されているはずです。

このサンプルコードでは、JavacriptがまずS3にファイルをアップロードし、その後にForce.comのデータベースにそのファイルに関する情報を記録するようになっているので上記のような挙動になっています。実際のシチュエーションでも何らかの形でS3に格納したオブジェクトの情報をForce.comに記録しておく必要がでてくると思いますので、その際の参考にしていただければ幸いです。

Step 3. サンプルコードの確認

サンプルがどのように動作するのか確認したところでその中身、ソースコードを見ておきましょう。

肝になっているのはApexクラス:aws_s3の中のget_url_for_updoad(string file_name, string file_type)というメソッドです。

@remoteAction
global static r get_url_for_upload(string file_name, string file_type){
    r r = new r();
    r.status = false;
    aws_s3__c s3 = aws_s3__c.getOrgDefaults();

    if (s3.access_key__c == null || s3.secret_access_key__c == null || s3.bucket__c == null){
        r.message = 'Custom Settings has not been configured.';
        return r;
    }

    if (String.isBlank(file_name)){
        r.message = 'file_name is not set.';
        return r;
    }

    string url;

    // This means that users have to start uploading in 600 seconds since they have loaded the page by default.
    integer default_expiration = 600;

    string http_method = 'PUT';
    string content_md5 = '';

    string content_type = '';
    if (String.isBlank(file_type)){
        r.message = 'file_type is not set.';
        return r;
    }
    content_type = file_type.toLowerCase();

    string expiration;
    if (s3.expiration__c == null){
        expiration = string.valueOf((DateTime.now().getTime() / 1000).intValue() + default_expiration);
    } else {
        expiration = string.valueOf((DateTime.now().getTime() / 1000).intValue() + s3.expiration__c.intValue());
    }

    string canonicalized_amz_headers = '';
    if (!String.isBlank(s3.acl__c)){
        if (storage_classes.contains(s3.acl__c.toLowerCase())){
            canonicalized_amz_headers += 'x-amz-acl:' + s3.acl__c.toLowerCase() + '\n';
        } else {
            r.message = 'ACL value is incorrect.';
            return r;
        }
    }
    if (s3.reduced_redundancy__c == true){
        canonicalized_amz_headers += 'x-amz-storage-class:REDUCED_REDUNDANCY' + '\n';
    }

    string canonicalized_resource;
    if (s3.folder__c == null){
        canonicalized_resource = '/' + s3.bucket__c + '/' + file_name;
    } else {
        canonicalized_resource = '/' + s3.bucket__c + '/' + s3.folder__c.removeStart('/').removeEnd('/') + '/' + file_name;
    }
    string string_to_sign = 
        http_method + '\n' +
        content_md5 + '\n' +
        content_type + '\n' + 
        expiration + '\n' + 
        canonicalized_amz_headers + canonicalized_resource;
    system.debug('string_to_sign = ' + string_to_sign);
    string signature = EncodingUtil.urlEncode(EncodingUtil.base64Encode(Crypto.generateMac('hmacSHA1', blob.valueOf(string_to_sign), blob.valueOf(s3.secret_access_key__c))), 'UTF-8');
    string path = '/';
    if (s3.folder__c != null){
        path += s3.folder__c.removeStart('/').removeEnd('/') + '/';
    }
    url = 'https://' + s3.bucket__c + '.s3.amazonaws.com' + path + file_name + '?AWSAccessKeyId=' + s3.access_key__c + '&Signature=' + signature + '&Expires=' + expiration;

    r.status = true;
    r.message = url;
    return r;
}

S3へのアップロードはS3のREST APIにアクセスしているのですが、当然アクセス先のURLが必要になります。そのURLを生成しているのがこのメソッドです。

URLは下記のような構造になっています。

https://[バケット名].s3.amazonaws.com/[バケットを除くファイルへのパス]?AWSAccessKeyId=[アクセスキー]&Signature=[シグネチャー]&Expires=[シグネチャの有効期限]

他のフォーマットもありますが、話を簡単にするためにそこは省略します。

この中で特に生成が難しいのがシグネチャーです。

シグネチャーは下記のフォーマットの文字列をシークレットアクセスキーを鍵にしてHMAC-SHA1のアルゴリズムでハッシュ化し、それをBase64エンコードし、さらにURL用にエンコードしたものになります。

[HTTP Method]\n
[Content-MD5]\n
[Content-Type]\n
[Expiration]\n
[canonicalized-amz-headers]
[canonicalized-amz-resources]

このフォーマットについてさらに詳しく知りたい方はS3の開発者ガイドのこちらのチャプターを参照ください。
REST リクエストの署名と認証

前述のget_url_for_upload()はこの面倒な作業をおこなっているメソッドです。カスタム設定であらかじめセットしたAWSの鍵情報、バケット名と、ユーザーがアップロード用に選択したファイルからこのURLを生成しています。

このURLさえあればあとは簡単です。Visualforceコンポーネント:S3 Uploaderをみてみましょう。

<apex:component controller="aws_s3">
<script>
function s3_upload_file(){
    var file = document.getElementById('s3_file_for_upload').files[0];

    aws_s3.get_url_for_upload(
        file.name,
        file.type,
        function(result, event){
            console.dir(event);
            console.dir(result);
            if (event.status == false){
                alert(event.message);
                return;
            }
            if (result.status == false){
                alert(result.message);
                return
            }
            url = result.message;
            var xhr = new XMLHttpRequest();
            xhr.onload = function(event){
                // You can replace this code to execute your code on upload success.
                console.dir(event);

                if (event.target.status == '200'){
                    alert("Upload Succeeded");
                } else {
                    alert("Upload Failed: " + event.target.statusText);
                    return;
                }
                aws_s3.insert_s3_object(
                    url,
                    function(result, event){
                        console.dir(event);
                        console.dir(result);
                        if (event.status == false){
                            alert(event.message);
                            return;
                        }
                        if (result.status == false){
                            alert(result.message);
                            return
                        }
                        alert("Insert Succeeded.");
                    },
                    {escape:true}
                );
            }
            xhr.open('PUT', url, true);

            // set Content-Type
            xhr.setRequestHeader('Content-Type', file.type);

            // set ACL
            var acl = "{!JSENCODE(config.acl__c)}";
            if (acl != ""){
                xhr.setRequestHeader('x-amz-acl', acl.toLowerCase());
            }

            // set REDUCED_REDUNDANCY
            if ({!config.reduced_redundancy__c} == true){
                xhr.setRequestHeader('x-amz-storage-class','REDUCED_REDUNDANCY');
            }

            xhr.send(file);
        },
        {escape:false}
    );
}
</script>
<input type="file" id="s3_file_for_upload" />
<button onclick="s3_upload_file();">upload</button>
</apex:component>

このコンポーネントはコントローラーとして、get_url_for_upload()メソッドを含むaws_s3を指定しています。

そしてget_url_for_upload()メソッドはJavascript RemotingによってJavascriptから呼び出せるようになっているので、ユーザーがuploadボタンを押した際にaws_s3.get_url_for_upload(ファイル情報とコールバックメソッド)としてget_url_for_upload()メソッドを実行しています。

そしてURLが取得できた後に実行されるコールバックメソッドの中では一般的なXMLHttpRequestオブジェクトを生成し、生成されたURLをもってS3のREST APIにアクセスしています。その際、Content-Type, x-amz-storage-class, x-amz-storage-class等のリクエストヘッダーを必要に応じて付与しています。

そしてさらにこのAPIアクセスのコールバックとして、Force.comデータベースにオブジェクト情報を記録するためのメソッド、aws_s3.insert_s3_object()を実行しています。こちらもJavascript Remotingのメソッドとして、aws_s3クラスの中に定義されているものです。

このコンポーネントの中で実行しているaler()やinsert_s3_object()を実より現実的な処理に置き換えていけば、自身のアプリの中にうまくアップロード処理を組み込めるのではないかと思います。

また、カスタム設定の中にストレージの冗長性を指定するReduced Redundancyや、フォルダ、そして重要なファイルのパーミッションを指定するアクセス制御等の項目を入れてあります。これらの項目をセットすることでオブジェクト格納時のメタ情報を制御することができます。動作を試していただいて、必要に応じてカスタマイズしてみてください。

グッドラック。

without comments

Written by 中嶋 一樹

5月 8th, 2013 at 6:34 pm

Posted in Uncategorized

Tagged with , , , ,