ですから今回は、 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 件のコメント:
コメントを投稿