要件
別のドメインの画像URLから画像を取得し、zpi形式でダウンロードしたい。
こんな感じ。(twitterの画像URL(https://pbs.twimg.com/~.jpg)をlocalhost上から画像として取得して、zipでDL)
XMLHttpRequestとか使ってクライアントだけで完結させようとすると、Cross Origin Resource Sharing Blockedに引っかかる。
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin 'http://127.0.0.1:9001’ is therefore not allowed access.
なので、サーバーサイド(Node.js)側で画像をURLからbase64に変換。それをクライアントに譲渡し、zipに固めてダウンロードさせる流れにした。
使ったライブラリとかソース
- JSZip – JavaScriptを使って、ファイルをZipで固めてダウンロードできる。
- FileSaver.js
– JSZipの公式サンプルで使っていたから、流用した。
saveAs()でブラウザのダウンロードインターフェースを利用できるようになる。
ダウンロード
$ bower i jszip file-server -S
開発構成
- AngularJS v1.3
- Node.js + Express v4
流れ
コード
処理に関係のない部分は大分端折ってる。
user pageのzipアイコンをクリックすると、
【Client】user.jade
#page-content-wrapper
i.fa.fa-file-archive-o(data-posts="{{userCategoryPosts}}", download-zip='download-zip') {{userCategoryPosts.length}}
そのクリックイベントを受けて、HTTPリクエストを送信。
返ってきたデータ(画像URLからbase64に変換された文字列)を、JSZipをなんやかんやして固めて、saveAs()でダウンロード。
【Client】directive.js
angular.module('myApp.directives', [])
.directive('downloadZip', [ 'toaster', 'DownloadService', function (toaster, DownloadService) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('click', function(event) {
var postsParsed = JSON.parse(attrs.posts);
// zipFolderの名前を定義しておく
var tag = (postsParsed[0].tags.split(","))[2];
var userName = postsParsed[0].userName;
var userScreenName = postsParsed[0].userScreenName;
var zipFolderName = "【" + tag + "】 " + userName + " 【@" + userScreenName + "】.zip";
// DL開始をAngular-toasterで通知
toaster.pop('wait', "Now Zip Downloading ...", '', 3000, 'trustedHtml');
DownloadService.zip(postsParsed)
.success(function(data) {
// zipに固める
var zip = new JSZip();
_.each(data.data, function(file){
zip.file(file.name + '.jpg', file.image, {base64: true});
});
var content = zip.generate({type:"blob"});
saveAs(content, zipFolderName);
// DL終了を通知
toaster.pop('success', "Finished Download", '', 3000, 'trustedHtml');
});
});
}
};
}]);
HTTPリクエストの内容はservice.jsに書いてある。見たまんま。
【Client】service.js
angular.module('myApp.services', [])
.service('DownloadService', function($http) {
return {
zip: function(posts) {
return $http.post('/api/downloadZip', {posts: posts});
}
};
})
Node.js側で、requestモジュール使ってURLから画像ファイルを取得し、それをbase64に変換。変換処理のコードはこれを参考にした。
複数の画像URLに対して行い、全て変換が済んでからresponseを返す必要があるため、シーケンシャルに処理していかなければならない。今回はPromise.allを利用した。
【Server】api.js
exports.downloadZip = function(req, res) {
var loadBase64Image = function (url) {
return new Promise(function(resolve, reject) {
request({
url: url
, encoding: null
}, function (err, res, body) {
if (!err && res.statusCode == 200) {
var base64prefix = 'data:' + res.headers['content-type'] + ';base64,';
var image = body.toString('base64');
return resolve(image + base64prefix);
} else {
return reject(err);
throw new Error('Can not download image');
}
});
});
};
var tasks = [];
_.each(req.body.posts, function(post){
tasks.push(
new Promise(function(resolve, reject) {
loadBase64Image(post.sourceOrigUrl + '?.jpg')
.then(function(imageBase64) {
return resolve({
image: imageBase64
, name: post.tweetIdStr
});
})
.catch(function(error) {
return resolve('');
});
})
);
});
Promise.all(tasks)
.then(function(base64Array) {
base64ArrayCompacted = _.pick(base64Array, _.identity);
res.json({
data: base64ArrayCompacted
});
});
}
参考文献
Node.jsエンジニアなら2014年内に知っておきたいPromise入門 | Tokyo Otaku Mode Blog
node.js - get image from another domain and encode base64 by node js - Stack Overflow