ですから今回は、 gapi を使わないで Chrome Apps から GoogleDrive にアクセスする方法をご紹介したいと思います。
以下はそのサンプルコードです。
プロジェクト構成
manifest.json
background.js
main.html
upload.js
app.js
manifest.json
{ "name": "Google Drive Example App", "version": "0.0.1", "manifest_version": 2, "minimum_chrome_version": "29", "oauth2": { "client_id": "*******.apps.googleusercontent.com", /* generate on Google Developers Console */ "scopes": [ "https://www.googleapis.com/auth/drive" ] }, "icons": { "128": "icon_128.png" }, "key": "*********", /* your app's key */ "app": { "background": { "scripts": ["background.js"] } }, "permissions": [ "identity", "https://ssl.gstatic.com/", "https://www.googleapis.com/", "https://docs.google.com/", "https://accounts.google.com/" ] }
manifest.json の "client_id" については Google Developers Console の "APIと認証" > "認証情報" の "新しいクライアントIDを作成" で予め取得しておく必要があります。
そして "APIと認証" > "API" の "有効なAPI" では "Drive API" を追加しておきます。
manifest.json の "key" については Chromeブラウザの「拡張機能」画面から「拡張機能のパッケージ化...」してcrx化したものをインストールし、インストール先にある manifest.json の中の "key" をコピーしてくればOKです。
background.js
chrome.app.runtime.onLaunched.addListener(function() { chrome.app.window.create('main.html', { 'bounds': { 'width': 400, 'height': 500 } }); });
main.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"></meta> <title>Google Drive Example App</title> <link href="main.css" rel="stylesheet"></link> </head> <body> <button id="btn_auth">auth</button> <div id="div_status">Not Authorized.</div> <hr> filename: <input type="text" id="txt_filename" value="test.txt"><br> content:<br> <textarea id="ta_content"></textarea><br> <button id="btn_upload">upload</button> <hr> <button id="btn_download">download</button><br> content: <div id="div_dl"></div> </body> <script src="upload.js"></script> <script src="app.js"></script> </html>
upload.js (GoogleDriveアップロード用ライブラリ)
/** * Helper for implementing retries with backoff. Initial retry * delay is 1 second, increasing by 2x (+jitter) for subsequent retries * * @constructor */ var RetryHandler = function() { this.interval = 1000; // Start at one second this.maxInterval = 60 * 1000; // Don't wait longer than a minute }; /** * Invoke the function after waiting * * @param {function} fn Function to invoke */ RetryHandler.prototype.retry = function(fn) { setTimeout(fn, this.interval); this.interval = this.nextInterval_(); }; /** * Reset the counter (e.g. after successful request.) */ RetryHandler.prototype.reset = function() { this.interval = 1000; }; /** * Calculate the next wait time. * @return {number} Next wait interval, in milliseconds * * @private */ RetryHandler.prototype.nextInterval_ = function() { var interval = this.interval * 2 + this.getRandomInt_(0, 1000); return Math.min(interval, this.maxInterval); }; /** * Get a random int in the range of min to max. Used to add jitter to wait times. * * @param {number} min Lower bounds * @param {number} max Upper bounds * @private */ RetryHandler.prototype.getRandomInt_ = function(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }; /** * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether * files or in-memory constructs. * * @example * var content = new Blob(["Hello world"], {"type": "text/plain"}); * var uploader = new MediaUploader({ * file: content, * token: accessToken, * onComplete: function(data) { ... } * onError: function(data) { ... } * }); * uploader.upload(); * * @constructor * @param {object} options Hash of options * @param {string} options.token Access token * @param {blob} options.file Blob-like item to upload * @param {string} [options.fileId] ID of file if replacing * @param {object} [options.params] Additional query parameters * @param {string} [options.contentType] Content-type, if overriding the type of the blob. * @param {object} [options.metadata] File metadata * @param {function} [options.onComplete] Callback for when upload is complete * @param {function} [options.onError] Callback if upload fails */ var MediaUploader = function(options) { var noop = function() {}; this.file = options.file; this.contentType = options.contentType || this.file.type || 'application/octet-stream'; this.metadata = options.metadata || { 'title': this.file.name, 'mimeType': this.contentType }; this.token = options.token; this.onComplete = options.onComplete || noop; this.onError = options.onError || noop; this.offset = options.offset || 0; this.chunkSize = options.chunkSize || 0; this.retryHandler = new RetryHandler(); this.url = options.url; if (!this.url) { var params = options.params || {}; params.uploadType = 'resumable'; this.url = this.buildUrl_(options.fileId, params); } // this.httpMethod = this.fileId ? 'PUT' : 'POST'; // bug??? this.httpMethod = options.fileId ? 'PUT' : 'POST'; }; /** * Initiate the upload. */ MediaUploader.prototype.upload = function() { var self = this; var xhr = new XMLHttpRequest(); xhr.open(this.httpMethod, this.url, true); xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('X-Upload-Content-Length', this.file.size); xhr.setRequestHeader('X-Upload-Content-Type', this.contentType); xhr.onload = function(e) { var location = e.target.getResponseHeader('Location'); this.url = location; this.sendFile_(); }.bind(this); xhr.onerror = this.onUploadError_.bind(this); xhr.send(JSON.stringify(this.metadata)); }; /** * Send the actual file content. * * @private */ MediaUploader.prototype.sendFile_ = function() { var content = this.file; var end = this.file.size; if (this.offset || this.chunkSize) { // Only bother to slice the file if we're either resuming or uploading in chunks if (this.chunkSize) { end = Math.min(this.offset + this.chunkSize, this.file.size); } content = content.slice(this.offset, end); } var xhr = new XMLHttpRequest(); xhr.open('PUT', this.url, true); xhr.setRequestHeader('Content-Type', this.contentType); xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size); xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); xhr.onload = this.onContentUploadSuccess_.bind(this); xhr.onerror = this.onContentUploadError_.bind(this); xhr.send(content); }; /** * Query for the state of the file for resumption. * * @private */ MediaUploader.prototype.resume_ = function() { var xhr = new XMLHttpRequest(); xhr.open('PUT', this.url, true); xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size); xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); xhr.onload = this.onContentUploadSuccess_.bind(this); xhr.onerror = this.onContentUploadError_.bind(this); xhr.send(); }; /** * Extract the last saved range if available in the request. * * @param {XMLHttpRequest} xhr Request object */ MediaUploader.prototype.extractRange_ = function(xhr) { var range = xhr.getResponseHeader('Range'); if (range) { this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1; } }; /** * Handle successful responses for uploads. Depending on the context, * may continue with uploading the next chunk of the file or, if complete, * invokes the caller's callback. * * @private * @param {object} e XHR event */ MediaUploader.prototype.onContentUploadSuccess_ = function(e) { if (e.target.status == 200 || e.target.status == 201) { this.onComplete(e.target.response); } else if (e.target.status == 308) { this.extractRange_(e.target); this.retryHandler.reset(); this.sendFile_(); } }; /** * Handles errors for uploads. Either retries or aborts depending * on the error. * * @private * @param {object} e XHR event */ MediaUploader.prototype.onContentUploadError_ = function(e) { if (e.target.status && e.target.status < 500) { this.onError(e.target.response); } else { this.retryHandler.retry(this.resume_.bind(this)); } }; /** * Handles errors for the initial request. * * @private * @param {object} e XHR event */ MediaUploader.prototype.onUploadError_ = function(e) { this.onError(e.target.response); // TODO - Retries for initial upload }; /** * Construct a query string from a hash/object * * @private * @param {object} [params] Key/value pairs for query string * @return {string} query string */ MediaUploader.prototype.buildQuery_ = function(params) { params = params || {}; return Object.keys(params).map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&'); }; /** * Build the drive upload URL * * @private * @param {string} [id] File ID if replacing * @param {object} [params] Query parameters * @return {string} URL */ MediaUploader.prototype.buildUrl_ = function(id, params) { var url = 'https://www.googleapis.com/upload/drive/v2/files/'; if (id) { url += id; } var query = this.buildQuery_(params); if (query) { url += '?' + query; } return url; };
upload.jsはchrome-app-samples / gdriveにあったものをほぼ丸々コピーさせてもらいました。(※ちょっとしたバグを発見したので一部修正しています)
このライブラリがあるだけでアップロード周りの処理がかなりスッキリします。
app.js
var accessToken; var btn_auth = document.getElementById("btn_auth"); var btn_upload = document.getElementById("btn_upload"); var btn_download = document.getElementById("btn_download"); var txt_filename = document.getElementById("txt_filename"); var ta_content = document.getElementById("ta_content"); var div_status = document.getElementById("div_status"); var div_dl = document.getElementById("div_dl"); btn_auth.onclick = function(e) { console.log("btn_auth click"); auth(true, function() { console.log("accessToken", accessToken); div_status.textContent = "Authorized."; }) }; btn_upload.onclick = function(e) { console.log("btn_upload click"); getFileOnGDrive(txt_filename.value, upload); }; btn_download.onclick = function(e) { console.log("btn_download click"); getFileOnGDrive(txt_filename.value, download); }; function auth(interactive, opt_callback) { try { chrome.identity.getAuthToken({ interactive: interactive }, function(token) { if (token) { accessToken = token; opt_callback && opt_callback(); } }); } catch (e) { console.log(e); } }; function upload(file) { var filename; var mimeType = "text/plain"; if (file) { filename = file.title; } else { filename = txt_filename.value; } // create contents. var txt = ta_content.value; var content = new Blob([txt], { "type": mimeType }); var onComplete = function(response) { console.log("upload complete. response=", response); // var json = JSON.parse(response); }; var onError = function(response) { console.log("upload error. response=", response); } // // upload // var upload_opts = { metadata: { title: filename, mimeType: mimeType }, file: content, token: accessToken, onComplete: onComplete, onError: onError }; // if the file has already existed, set the fileId to options param if (file) upload_opts.fileId = file.id; var uploader = new MediaUploader(upload_opts); uploader.upload(); } function download(file) { var xhr = new XMLHttpRequest(); var url = "https://www.googleapis.com/drive/v2/files"; if (file) { // get url for download url = file.downloadUrl; } xhr.open('GET', url); xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); xhr.onreadystatechange = function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // get the file's content. console.log("xhr.responseText", xhr.responseText); div_dl.textContent = xhr.responseText; } else if (xhr.readyState == 4 && xhr.status != 200) { console.error("Error: status=", xhr.status); } }; xhr.onerror = function(e) { console.error("Error: ", e); }; xhr.send(); } function getFileOnGDrive(filename, callback) { var xhr = new XMLHttpRequest(); var url = "https://www.googleapis.com/drive/v2/files"; xhr.open('GET', url); xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); xhr.onreadystatechange = function(e) { if (xhr.readyState == 4 && xhr.status == 200) { var json = JSON.parse(xhr.responseText); var filename = txt_filename.value; var file = getFile(filename, json.items); callback(file); return; } else if (xhr.readyState == 4 && xhr.status != 200) { console.error("Error: status=", xhr.status); } }; xhr.onerror = function(e) { console.error("Error: ", e); }; xhr.send(); // get the file having same filename and create url for downloading it function getFile(filename, items) { var item; for (var len = items.length, i = 0; i < len; i++) { item = items[i]; if (item.title == filename) { return item; } } return null; } }
実行してみよう
サンプルアプリを実行し、以下のステップで動かしてみましょう。
1) authボタンを押してaccessTokenを取得
2) GoogleDrive上に作成するファイル名をtxt_filenameテキストボックスに入力
3) uploadボタンを押して適当な内容でファイルをアップロード
4) downloadボタンを押して 3) でアップしたファイルをダウンロード
Note
GoogleDriveでは同名のファイルをアップロードすると、毎回別ファイルとしてDrive上に重複保存します。ファイル名は同じでもファイルIDが別物だからです。上の例では、同名ファイルの重複保存を防ぐために、uploadの前には必ずDrive上に同名のファイルが無いかチェックして(#getFileOnGDrive)、既に存在する場合はそのファイルIDを使って上書き、無ければ新規作成するようにしています。
参考にさせていただいたコード
chrome-app-samples/samples/gdrive/
追記 2015/03/08
GoogleDriveへのアップロード|ダウンロード処理をライブラリ化してみました。
0 件のコメント:
コメントを投稿