| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <ctype.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include <string> |
| #include <vector> |
| |
| #include "json/reader.h" |
| #include "json/writer.h" |
| #include "ppapi/c/pp_errors.h" |
| #include "ppapi/cpp/completion_callback.h" |
| #include "ppapi/cpp/instance.h" |
| #include "ppapi/cpp/module.h" |
| #include "ppapi/cpp/url_loader.h" |
| #include "ppapi/cpp/url_request_info.h" |
| #include "ppapi/cpp/url_response_info.h" |
| #include "ppapi/cpp/var.h" |
| #include "ppapi/utility/completion_callback_factory.h" |
| #include "ppapi/utility/threading/simple_thread.h" |
| |
| namespace { |
| |
| // When we upload files, we also upload the metadata at the same time. To do so, |
| // we use the mimetype multipart/related. This mimetype requires specifying a |
| // boundary between the JSON metadata and the file content. |
| const char kBoundary[] = "NACL_BOUNDARY_600673"; |
| |
| // This is a simple implementation of JavaScript's encodeUriComponent. We |
| // assume the data is already UTF-8. See |
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent. |
| std::string EncodeUriComponent(const std::string& s) { |
| char hex[] = "0123456789ABCDEF"; |
| std::string result; |
| for (size_t i = 0; i < s.length(); ++i) { |
| char c = s[i]; |
| if (isalpha(c) || isdigit(c) || strchr("-_.!~*'()", c)) { |
| result += c; |
| } else { |
| result += '%'; |
| result += hex[(c >> 4) & 0xf]; |
| result += hex[c & 0xf]; |
| } |
| } |
| return result; |
| } |
| |
| std::string IntToString(int x) { |
| char buffer[32]; |
| snprintf(&buffer[0], 32, "%d", x); |
| return &buffer[0]; |
| } |
| |
| void AddQueryParameter(std::string* s, |
| const std::string& key, |
| const std::string& value, |
| bool first) { |
| *s += first ? '?' : '&'; |
| *s += EncodeUriComponent(key); |
| *s += '='; |
| *s += EncodeUriComponent(value); |
| } |
| |
| void AddQueryParameter(std::string* s, |
| const std::string& key, |
| int value, |
| bool first) { |
| AddQueryParameter(s, key, IntToString(value), first); |
| } |
| |
| void AddAuthTokenHeader(std::string* s, const std::string& auth_token) { |
| *s += "Authorization: Bearer "; |
| *s += auth_token; |
| *s += "\n"; |
| } |
| |
| void AddHeader(std::string* s, const char* key, const std::string& value) { |
| *s += key; |
| *s += ": "; |
| *s += value; |
| *s += "\n"; |
| } |
| |
| } // namespace |
| |
| // |
| // ReadUrl |
| // |
| struct ReadUrlParams { |
| std::string url; |
| std::string method; |
| std::string request_headers; |
| std::string request_body; |
| }; |
| |
| // This function blocks so it needs to be called off the main thread. |
| int32_t ReadUrl(pp::Instance* instance, |
| const ReadUrlParams& params, |
| std::string* output) { |
| pp::URLRequestInfo url_request(instance); |
| pp::URLLoader url_loader(instance); |
| |
| url_request.SetURL(params.url); |
| url_request.SetMethod(params.method); |
| url_request.SetHeaders(params.request_headers); |
| url_request.SetRecordDownloadProgress(true); |
| if (params.request_body.size()) { |
| url_request.AppendDataToBody(params.request_body.data(), |
| params.request_body.size()); |
| } |
| |
| int32_t result = url_loader.Open(url_request, pp::BlockUntilComplete()); |
| if (result != PP_OK) { |
| return result; |
| } |
| |
| pp::URLResponseInfo url_response = url_loader.GetResponseInfo(); |
| if (url_response.GetStatusCode() != 200) |
| return PP_ERROR_FAILED; |
| |
| output->clear(); |
| |
| int64_t bytes_received = 0; |
| int64_t total_bytes_to_be_received = 0; |
| if (url_loader.GetDownloadProgress(&bytes_received, |
| &total_bytes_to_be_received)) { |
| if (total_bytes_to_be_received > 0) { |
| output->reserve(total_bytes_to_be_received); |
| } |
| } |
| |
| url_request.SetRecordDownloadProgress(false); |
| |
| const int32_t kReadBufferSize = 16 * 1024; |
| uint8_t* buffer_ = new uint8_t[kReadBufferSize]; |
| |
| do { |
| result = url_loader.ReadResponseBody( |
| buffer_, kReadBufferSize, pp::BlockUntilComplete()); |
| if (result > 0) { |
| assert(result <= kReadBufferSize); |
| size_t num_bytes = result; |
| output->insert(output->end(), buffer_, buffer_ + num_bytes); |
| } |
| } while (result > 0); |
| |
| delete[] buffer_; |
| |
| return result; |
| } |
| |
| // |
| // ListFiles |
| // |
| // This is a simplistic implementation of the files.list method defined here: |
| // https://developers.google.com/drive/v2/reference/files/list |
| // |
| struct ListFilesParams { |
| int max_results; |
| std::string page_token; |
| std::string query; |
| }; |
| |
| int32_t ListFiles(pp::Instance* instance, |
| const std::string& auth_token, |
| const ListFilesParams& params, |
| Json::Value* root) { |
| static const char base_url[] = "https://www.googleapis.com/drive/v2/files"; |
| |
| ReadUrlParams p; |
| p.method = "GET"; |
| p.url = base_url; |
| AddQueryParameter(&p.url, "maxResults", params.max_results, true); |
| if (params.page_token.length()) |
| AddQueryParameter(&p.url, "pageToken", params.page_token, false); |
| AddQueryParameter(&p.url, "q", params.query, false); |
| // Request a "partial response". See |
| // https://developers.google.com/drive/performance#partial for more |
| // information. |
| AddQueryParameter(&p.url, "fields", "items(id,downloadUrl)", false); |
| AddAuthTokenHeader(&p.request_headers, auth_token); |
| |
| std::string output; |
| int32_t result = ReadUrl(instance, p, &output); |
| if (result != PP_OK) { |
| return result; |
| } |
| |
| Json::Reader reader(Json::Features::strictMode()); |
| if (!reader.parse(output, *root, false)) { |
| return PP_ERROR_FAILED; |
| } |
| |
| return PP_OK; |
| } |
| |
| // |
| // InsertFile |
| // |
| // This is a simplistic implementation of the files.update and files.insert |
| // methods defined here: |
| // https://developers.google.com/drive/v2/reference/files/insert |
| // https://developers.google.com/drive/v2/reference/files/update |
| // |
| struct InsertFileParams { |
| // If file_id is empty, create a new file (files.insert). If file_id is not |
| // empty, update that file (files.update) |
| std::string file_id; |
| std::string content; |
| std::string description; |
| std::string mime_type; |
| std::string title; |
| }; |
| |
| std::string BuildRequestBody(const InsertFileParams& params) { |
| // This generates the multipart-upload request body for InsertFile. See |
| // https://developers.google.com/drive/manage-uploads#multipart for more |
| // information. |
| std::string result; |
| result += "--"; |
| result += kBoundary; |
| result += "\nContent-Type: application/json; charset=UTF-8\n\n"; |
| |
| Json::Value value(Json::objectValue); |
| if (!params.description.empty()) |
| value["description"] = Json::Value(params.description); |
| |
| if (!params.mime_type.empty()) |
| value["mimeType"] = Json::Value(params.mime_type); |
| |
| if (!params.title.empty()) |
| value["title"] = Json::Value(params.title); |
| |
| Json::FastWriter writer; |
| std::string metadata = writer.write(value); |
| |
| result += metadata; |
| result += "--"; |
| result += kBoundary; |
| result += "\nContent-Type: "; |
| result += params.mime_type; |
| result += "\n\n"; |
| result += params.content; |
| result += "\n--"; |
| result += kBoundary; |
| result += "--"; |
| return result; |
| } |
| |
| int32_t InsertFile(pp::Instance* instance, |
| const std::string& auth_token, |
| const InsertFileParams& params, |
| Json::Value* root) { |
| static const char base_url[] = |
| "https://www.googleapis.com/upload/drive/v2/files"; |
| |
| ReadUrlParams p; |
| p.url = base_url; |
| |
| // If file_id is defined, we are actually updating an existing file. |
| if (!params.file_id.empty()) { |
| p.url += "/"; |
| p.url += params.file_id; |
| p.method = "PUT"; |
| } else { |
| p.method = "POST"; |
| } |
| |
| // We always use the multipart upload interface, but see |
| // https://developers.google.com/drive/manage-uploads for other |
| // options. |
| AddQueryParameter(&p.url, "uploadType", "multipart", true); |
| // Request a "partial response". See |
| // https://developers.google.com/drive/performance#partial for more |
| // information. |
| AddQueryParameter(&p.url, "fields", "id,downloadUrl", false); |
| AddAuthTokenHeader(&p.request_headers, auth_token); |
| AddHeader(&p.request_headers, |
| "Content-Type", |
| std::string("multipart/related; boundary=") + kBoundary + "\n"); |
| p.request_body = BuildRequestBody(params); |
| |
| std::string output; |
| int32_t result = ReadUrl(instance, p, &output); |
| if (result != PP_OK) { |
| return result; |
| } |
| |
| Json::Reader reader(Json::Features::strictMode()); |
| if (!reader.parse(output, *root, false)) { |
| return PP_ERROR_FAILED; |
| } |
| |
| return PP_OK; |
| } |
| |
| // |
| // Instance |
| // |
| class Instance : public pp::Instance { |
| public: |
| Instance(PP_Instance instance); |
| virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]); |
| virtual void HandleMessage(const pp::Var& var_message); |
| |
| void PostMessagef(const char* format, ...); |
| |
| private: |
| void ThreadSetAuthToken(int32_t, const std::string& auth_token); |
| void ThreadRequestThunk(int32_t); |
| bool ThreadRequest(); |
| bool ThreadGetFileMetadata(const char* title, Json::Value* metadata); |
| bool ThreadCreateFile(const char* title, |
| const char* description, |
| const char* content, |
| Json::Value* metadata); |
| bool ThreadUpdateFile(const std::string& file_id, |
| const std::string& content, |
| Json::Value* metadata); |
| bool ThreadDownloadFile(const Json::Value& metadata, std::string* output); |
| bool GetMetadataKey(const Json::Value& metadata, |
| const char* key, |
| std::string* output); |
| |
| pp::SimpleThread worker_thread_; |
| pp::CompletionCallbackFactory<Instance> callback_factory_; |
| std::string auth_token_; |
| bool is_processing_request_; |
| }; |
| |
| Instance::Instance(PP_Instance instance) |
| : pp::Instance(instance), |
| worker_thread_(this), |
| callback_factory_(this), |
| is_processing_request_(false) {} |
| |
| bool Instance::Init(uint32_t /*argc*/, |
| const char * [] /*argn*/, |
| const char * [] /*argv*/) { |
| worker_thread_.Start(); |
| return true; |
| } |
| |
| void Instance::HandleMessage(const pp::Var& var_message) { |
| const char kTokenMessage[] = "token:"; |
| const size_t kTokenMessageLen = strlen(kTokenMessage); |
| const char kGetFileMessage[] = "getFile"; |
| |
| if (!var_message.is_string()) { |
| return; |
| } |
| |
| std::string message = var_message.AsString(); |
| printf("Got message: \"%s\"\n", message.c_str()); |
| if (message.compare(0, kTokenMessageLen, kTokenMessage) == 0) { |
| // Auth token |
| std::string auth_token = message.substr(kTokenMessageLen); |
| worker_thread_.message_loop().PostWork(callback_factory_.NewCallback( |
| &Instance::ThreadSetAuthToken, auth_token)); |
| } else if (message == kGetFileMessage) { |
| // Request |
| if (!is_processing_request_) { |
| is_processing_request_ = true; |
| worker_thread_.message_loop().PostWork( |
| callback_factory_.NewCallback(&Instance::ThreadRequestThunk)); |
| } |
| } |
| } |
| |
| void Instance::PostMessagef(const char* format, ...) { |
| const size_t kBufferSize = 1024; |
| char buffer[kBufferSize]; |
| va_list args; |
| va_start(args, format); |
| vsnprintf(&buffer[0], kBufferSize, format, args); |
| |
| PostMessage(buffer); |
| } |
| |
| void Instance::ThreadSetAuthToken(int32_t /*result*/, |
| const std::string& auth_token) { |
| printf("Got auth token: %s\n", auth_token.c_str()); |
| auth_token_ = auth_token; |
| } |
| |
| void Instance::ThreadRequestThunk(int32_t /*result*/) { |
| ThreadRequest(); |
| is_processing_request_ = false; |
| } |
| |
| bool Instance::ThreadRequest() { |
| static int request_count = 0; |
| static const char kTitle[] = "hello nacl.txt"; |
| Json::Value metadata; |
| std::string output; |
| |
| PostMessagef("log:\n Got request (#%d).\n", ++request_count); |
| PostMessagef("log: Looking for file: \"%s\".\n", kTitle); |
| |
| if (!ThreadGetFileMetadata(kTitle, &metadata)) { |
| PostMessage("log: Not found! Creating a new file...\n"); |
| // No data found, write a new file. |
| static const char kDescription[] = "A file generated by NaCl!"; |
| static const char kInitialContent[] = "Hello, Google Drive!"; |
| |
| if (!ThreadCreateFile(kTitle, kDescription, kInitialContent, &metadata)) { |
| PostMessage("log: Creating the new file failed...\n"); |
| return false; |
| } |
| } else { |
| PostMessage("log: Found it! Downloading the file...\n"); |
| // Found the file, download it's data. |
| if (!ThreadDownloadFile(metadata, &output)) { |
| PostMessage("log: Downloading the file failed...\n"); |
| return false; |
| } |
| |
| // Modify it. |
| output += "\nHello, again Google Drive!"; |
| |
| std::string file_id; |
| if (!GetMetadataKey(metadata, "id", &file_id)) { |
| PostMessage("log: Couldn't find the file id...\n"); |
| return false; |
| } |
| |
| PostMessage("log: Updating the file...\n"); |
| if (!ThreadUpdateFile(file_id, output, &metadata)) { |
| PostMessage("log: Failed to update the file...\n"); |
| return false; |
| } |
| } |
| |
| PostMessage("log: Done!\n"); |
| PostMessage("log: Downloading the newly written file...\n"); |
| if (!ThreadDownloadFile(metadata, &output)) { |
| PostMessage("log: Downloading the file failed...\n"); |
| return false; |
| } |
| |
| PostMessage("log: Done!\n"); |
| PostMessage(output); |
| return true; |
| } |
| |
| bool Instance::ThreadGetFileMetadata(const char* title, Json::Value* metadata) { |
| ListFilesParams p; |
| p.max_results = 1; |
| p.query = "title = \'"; |
| p.query += title; |
| p.query += "\'"; |
| |
| Json::Value root; |
| int32_t result = ListFiles(this, auth_token_, p, &root); |
| if (result != PP_OK) { |
| PostMessagef("log: ListFiles failed with result %d\n", result); |
| return false; |
| } |
| |
| // Extract the first item's metadata. |
| if (!root.isMember("items")) { |
| PostMessage("log: ListFiles returned no items...\n"); |
| return false; |
| } |
| |
| Json::Value items = root["items"]; |
| if (!items.isValidIndex(0)) { |
| PostMessage("log: Expected items[0] to be valid.\n"); |
| return false; |
| } |
| |
| *metadata = items[0U]; |
| return true; |
| } |
| |
| bool Instance::ThreadCreateFile(const char* title, |
| const char* description, |
| const char* content, |
| Json::Value* metadata) { |
| InsertFileParams p; |
| p.content = content; |
| p.description = description; |
| p.mime_type = "text/plain"; |
| p.title = title; |
| |
| int32_t result = InsertFile(this, auth_token_, p, metadata); |
| if (result != PP_OK) { |
| PostMessagef("log: Creating file failed with result %d\n", result); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Instance::ThreadUpdateFile(const std::string& file_id, |
| const std::string& content, |
| Json::Value* metadata) { |
| InsertFileParams p; |
| p.file_id = file_id; |
| p.content = content; |
| p.mime_type = "text/plain"; |
| |
| int32_t result = InsertFile(this, auth_token_, p, metadata); |
| if (result != PP_OK) { |
| PostMessagef("log: Updating file failed with result %d\n", result); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Instance::ThreadDownloadFile(const Json::Value& metadata, |
| std::string* output) { |
| ReadUrlParams p; |
| p.method = "GET"; |
| |
| if (!GetMetadataKey(metadata, "downloadUrl", &p.url)) { |
| return false; |
| } |
| |
| AddAuthTokenHeader(&p.request_headers, auth_token_); |
| |
| int32_t result = ReadUrl(this, p, output); |
| if (result != PP_OK) { |
| PostMessagef("log: Downloading failed with result %d\n", result); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Instance::GetMetadataKey(const Json::Value& metadata, |
| const char* key, |
| std::string* output) { |
| Json::Value value = metadata[key]; |
| if (!value.isString()) { |
| PostMessagef("log: Expected metadata.%s to be a string.\n", key); |
| return false; |
| } |
| |
| *output = value.asString(); |
| return true; |
| } |
| |
| class Module : public pp::Module { |
| public: |
| Module() : pp::Module() {} |
| virtual ~Module() {} |
| |
| virtual pp::Instance* CreateInstance(PP_Instance instance) { |
| return new Instance(instance); |
| } |
| }; |
| |
| namespace pp { |
| |
| Module* CreateModule() { return new ::Module(); } |
| |
| } // namespace pp |