nkjmkzk.net

powered by Kazuki Nakajima

Archive for the ‘s3’ tag

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 , , , ,

Transloaditを使ってSalesforceから透過的にファイルをAmazon S3にアップロードする

SalesforceにはAttachmentやFilesといったファイル格納機構があります。ただ、そのファイルを外部公開する(認証不要でアクセス可とする)のは少し厄介です。そういう要件がある場合は素直にAmazon S3等にアップした方がよいケースもあるでしょう。

そんなアーキテクチャーでも、ユーザーにはSalesforceを使いながら意識させずにS3にアップロードするような実装をしたいものです。また、アップロードのコードも出来る限りさぼって書きたいですよね。

アップローダーとして使えそうなツールやサービスをいろいろ見ていると、UploadifyPluploadなどがあるなー、と。結果的に私はTransloaditというサービスを使ってみました。

Transloaditは単なるアップロードツールやWidgetという域にとどまらず、画像や動画のエンコーディングという中間処理を得意とするサービスです。そしてもちろんアップロードのためのUIもjQuery Pluginとして提供してくれています。使用感はこちらの動画で確認してみてください。

実際、クロスブラウザ対応などを含め、ファイルのアップロードというのはなにかと問題が発生する意外に難しい処理です。Transloaditはそのあたりのクライアント側の制御をjQuery Pluginを提供することで開発者から解放しつつ、その後の処理もクライアントとファイル格納先(S3等)の中間に入って動作することでお任せすることができるというサービスです。

S3にファイルをアップロードするときにTransloaditはリバースプロキシのような位置づけで機能します。クライアントはjQuery Pluginを使ってファイルをTransloaditにアップロードし、アップロードされたファイルは事前に定義されたTemplateに従って処理されます。最終格納場所(S3)にファイルが保存された後にクライアントはTransloaditからJSONレスポンスでファイルのメタデータを受け取ります。上のデモ動画では、このメタデータからファイルのURLを取得してSalesforceのフィールドに保存しています。

Transloaditのサイトにもこのアップローダーの構築方法が書かれていますが、ここでも簡単に解説しておきます。

まずアカウントを作成します。Transloaditは従量課金の有料サービスですが、100MB/月までは無料で利用できます。

次にTemplateを作成します。Templateはアップロードするファイルをどの順番で、どんな処理をおこなうかを記述したファイルです。これはTransloaditの管理画面から作成することができます。上のデモ動画で定義しているTemplateは下記の通りです。

{
  "steps": {
    "store": {
      "robot": "/s3/store",
      "key": "あなたのAWS KEY",
      "secret": "あなたのAWS SECRET",
      "bucket": "nkjm-sfdc-tokyo"
    }
  }
}

次にアップロードフォームを作成します。これはVisualforceで作成することになります。

<form id="MyForm" action="http://api2.transloadit.com/assemblies" enctype="multipart/form-data" method="POST">
    <input type="hidden" name="params" value='{"auth":{"key":"あなたのAPI KEY"},"template_id":"あなたのTemplate ID","redirect_url":"{!$CurrentPage.URL}"}'></input>
    <input type="file" name="my_file"></input>
    <input type="submit" value="Upload"></input>
</form>

次に同じVisulaforceページにアップローダーとなるjQuery Pluginを追加します。

<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" />
<apex:includeScript value="https://assets.transloadit.com/js/jquery.transloadit2.js" />

<script type="text/javascript">
   // We call .transloadit() after the DOM is initialized:
   j$ = jQuery.noConflict();
   j$(document).ready(function() {
     j$('#MyForm').transloadit({
        wait: true
     });
   });
</script>

次にファイルのアップロードが完了した際にJSONレスポンスを解析して必要な情報をデータベースに保存するApexクラスを作成します。

public class transloadit_uploader {
    public final product__c product;

    public transloadit_uploader(ApexPages.StandardController controller){
      product__c p = (product__c)controller.getRecord();
      if (p.id != null){
          this.product = [select id, name, price__c, code__c, discount_rate__c, photo_url__c, url_to_buy__c, description__c from product__c where id = :p.id];
      }
    }

    public void update_photo_url(){
        string s3_url;
        if (ApexPages.currentPage().getParameters().get('transloadit') != null){
            string result_json = ApexPages.currentPage().getParameters().get('transloadit');
            JSONParser parser = JSON.createParser(result_json);
            while (parser.nextToken() != null) {
                if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'url')) {
                    parser.nextToken();
                    if (parser.getText().contains('s3.amazonaws.com')){
                        s3_url = parser.getText();
                    }
                }
            }
        }
        if (s3_url != null){
            this.product.photo_url__c = s3_url;
            update this.product;
        }
    }
}

最後に、先ほどのVisualforceページに、上記のApexクラスのupdate_photo_url()を最初にキックするように<apex:page>タグにaction=”{!update_photo_url}”を追加します。

<apex:page standardController="sugoidiscount__product__c" extensions="sugoidiscount.transloadit_uploader" action="{!update_photo_url}">

最終的にデモのVisualforceページは下記のようになります。

<apex:page standardController="sugoidiscount__product__c" extensions="sugoidiscount.transloadit_uploader" action="{!update_photo_url}">
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" />
<apex:includeScript value="https://assets.transloadit.com/js/jquery.transloadit2.js" />

<script type="text/javascript">
   // We call .transloadit() after the DOM is initialized:
   j$ = jQuery.noConflict();
   j$(document).ready(function() {
     j$('#MyForm').transloadit({
        wait: true
     });
   });
</script>

<apex:detail />

<apex:pageBlock title="{!$Label.sugoidiscount__upload_photo}" mode="edit">
    <apex:pageBlockSection >
        <form id="MyForm" action="http://api2.transloadit.com/assemblies" enctype="multipart/form-data" method="POST">
            <input type="hidden" name="params" value='{"auth":{"key":"あなたのAPI KEY"},"template_id":"あなたのTemplate ID","redirect_url":"{!$CurrentPage.URL}"}'></input>
            <input type="file" name="my_file"></input>
            <input type="submit" value="Upload"></input>
        </form>
    </apex:pageBlockSection>
    <apex:pageBlockSection >
        <apex:outputpanel id="product_photo" rendered="{! !ISNULL(photo_url)}">
            <img height="200px" src="{!sugoidiscount__product__c.sugoidiscount__photo_url__c}" />
        </apex:outputpanel>
    </apex:pageBlockSection>
</apex:pageBlock>
</apex:page>

エンジョイ。

without comments

Written by 中嶋 一樹

10月 11th, 2012 at 11:58 am

Linux端末からAmazon S3にファイルをアップロードする(s3cmd使用)

思うところあって4GByteのファイルをS3にアップロードしようとしていました。当初一番ユーザーフレンドリーなWeb Consoleを使用してアップロードを試みました。

しかしながら300MByteほどアップロードしたところで原因不明のErrorとなりうまくいきませんでした。AWSブログでも大容量ファイルのアップロードにはいささか改善の余地がある旨が記載されています。

http://aws.typepad.com/aws_japan/2010/11/amazon-s3-multipart-upload.html

同記事内のMultipart Uploadを試みようかと思いましたが、わざわざプログラムを書くほど繰り返し行う作業でもないので何かお手軽なツールはないかと思っていたら、コマンドラインでS3を操作できるs3cmdなんて便利なものがあるのですね。早速インストールして使ってみました。

ダウンロード:
こちらから最新のものを。
http://sourceforge.net/projects/s3tools/files/s3cmd/

インストール:

[root@~]# tar xvfz s3cmd-1.0.0-rc1.tar.gz
[root@~]# cd s3cmd-1.0.0-rc1/
[root@~]# python setup.py install

初期設定:

[root@~]# s3cmd --configure
* AWSのaccess keyとsecret keyが必要です。

アップロード:

[root@~]# s3cmd put -P -rr [ファイル名] s3://[BUCKET]/[ファイル名]
* -P : ファイルのパーミッションをPublic Readableに。
* -rr : ストレージタイプをReduced Redundancy Storageに。

確認:

[root@~]# s3cmd ls s3://[BUCKET]/

ということですごく簡単でよくできているツールでした。
嬉しくなってやたらとS3に格納してしまいそうです。

with 2 comments

Written by 中嶋 一樹

1月 6th, 2011 at 8:39 am

Posted in Uncategorized

Tagged with ,