[AWS] 使用 Uploadify 直接上傳 S3

其實以前知道 S3 可以直接上傳檔案到 Bucket 上面去,只是都沒有專案需要這方面的實作,所以就放置 play 了好一陣子。

直到我夢到一個副本級專案。

AWS S3 Browser-based Uploads Using POST

官方有說,

但是,依照 Amazon AWS 文件的慣例就是,文件基本上只是參考用,實際上會遇到的小毛病,也得要遇到了才知道。

Note

  • MUST BE UTF-8 encoded(不解釋! 由於是使用 Client Browser 的方式去傳遞,所以只要是 FormData 送出的資料都必須要是 UTF-8 encoded,即便像我用 node-browser 這類的東西傳送也是一樣
  • 非檔案內容不得超過 20KB
  • 如果要上傳檔案,file 必須是表單的 最後一項
  • 可以使用特殊變數 ${filename},當你上傳檔案時,這個變數會自動置換成你上傳的檔案名稱

Policy 的規則

除了必須要設定過期時間 expiration 與過濾條件 conditions 之外,過濾條件必須依循以下規則:

  • 必須要有 acl, bucket, $key
  • 除了特定標籤外,也可以任意使用欄位標籤
  • 任何的 欄位標籤 必須使用 start-with 設定
  • 預設的 success_action_status 會回傳 HTTP 201 要注意
  • 如果使用 success_action_redirect 則你的主機需要設定 Access-Control-Allow-Origin
  • 如果你要用 JavaScript 讀取 header,要設定 Access-Control-Expose-Headers

Uploadify HTML5 版本的問題

由於我不會寫 ROR 所以,以下以 PHP 來舉例

<?php
$aws_secret = 'amazon access secret';

$policy = base64_encode(
    '{'.
        '"expiration": "'.date("Y-m-d\TH:i:s\Z", time()+60*30).'",'.
        '"conditions": '.
        '['.
            '{"bucket": "myuploader"},'.
            '{"acl": "public-read"},'.
            '["starts-with", "$key", "static/"],'.
            '["starts-with", "$Content-Type", "image/"],'.
            '["starts-with", "$folder", "static/"],'.
            '["starts-with", "$filename", ""],'.
            '{"success_action_status": "200"},'.
            '["content-length-range", 0, '.(int) $this->AWS['size'].']'.
        ']'.
    '}'
);
$signature = base64_encode(hash_hmac(
    'sha1', str_replace(array("\n","\r"), '', $policy), $aws_secret, true
));

以上的東西請勿照抄!其中, $key 是 S3 檔案結構中上載的鍵值,簡單來說 http://myuploader.s3.amazonaws.com/xxxx 其中的 xxxx 就是 $key 值,它可以是資料夾加上檔案名稱,或是直接是檔案名稱。

然後我們比對一下前端的 JS 需要的部分,

formData           : {
                        'AWSAccessKeyId'            : 'amazon access key',
                        'key'                       : 'static/',
                        'acl'                       : 'public-read',
                        'policy'                    : '<?php echo $policy; ?>',
                        'signature'                 : '<?php echo $signature; ?>',
                        'folder'                    : 'static/',
                        'filename'                  : (new Date()).getTime(),
                        'success_action_status'     : '200'
                     },
fileObjName        : 'file',

以上是 Uploadify 的 HTML5 版本的部分設定(我有付費購買 HTML5 版本的,如果沒有買的人請自行購買!然後,因為 Uploadify 原始碼中,組合 FormData 的順序,是依照 formData 設定的順序來做,這個方法 並不符合 AWS S3 的規則(如果要上傳檔案,file 必須是表單的 最後一項

所以我 Hack 掉了 Uploadify 的原始碼,自己包了一個來用,至於免費的 Flash 版本,要改成 S3 直接上傳網路上有一堆,請自行 Google!

JavaScript 簡易實作程式碼

我們用簡易的 FormData 來說明,

PHP 運算 policysignature 請參考上面的部分,

var __formData  = {
                'AWSAccessKeyId'            : 'amazon access key',
                'key'                       : 'static/'+(new Date()).getTime()+'.jpg',
                'acl'                       : 'public-read',
                'policy'                    : '<?php echo $policy; ?>',
                'signature'                 : '<?php echo $signature; ?>',
                'content-type'              : 'image/jpeg',
                'folder'                    : 'static/',
                'filename'                  : (new Date()).getTime(),
                'success_action_status'     : '200'
             };

var file = document.querySelector('input[type="file"]');
var xhr = file.xhr = new XMLHttpRequest();

var formData = new FormData();

for( x in __formData ) {
    formData.append(x, __formData[x]);
}

/* 檔案一定要在最後才加入 FormData,不然錯誤會噴到行政院! */
formData.append('file', file);

xhr.open('post', 'http://myuploader.s3.amazon.com', true);

xhr.addEventListener('load', function(e) {
    if (this.readyState == 4) {
        if (this.status == 200) {
            if (file.xhr.responseText !== 'Invalid file type.') {
                // 到這裡全部完成
            } else {
                // 就是錯誤
            }
        } else if (this.status == 404) {
            // 找不到太陽餅
        } else if (this.status == 403) {
            // 利大於弊
        } else {
            // 謝謝指教
        }
    }
});

xhr.send(formData);

以上範例不保證實作得出來(誒

S3 bucket CORS Configuration

以下設定僅供參考,

    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <ExposeHeader>ETag</ExposeHeader>
        <ExposeHeader>Location</ExposeHeader>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>

裡面的 <ExposeHeader> 就是我剛剛提到的 Access-Control-Expose-Headers 這個部分,如果你想用 JavaScript 做什麼壞事的話,當然不保證安全就是了,因為 Header 這種東西,聽說也是可以偽造的(茶

小結

這個月 AWS 帳單又要爆了!

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei