mirror of
https://github.com/J-D-K/JKSV.git
synced 2026-03-22 01:34:13 -05:00
705 lines
28 KiB
C++
705 lines
28 KiB
C++
#include "remote/GoogleDrive.hpp"
|
|
|
|
#include "logging/logger.hpp"
|
|
#include "remote/Form.hpp"
|
|
#include "remote/URL.hpp"
|
|
#include "remote/remote.hpp"
|
|
#include "strings/strings.hpp"
|
|
#include "stringutil.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
namespace
|
|
{
|
|
// This is the sort-of template for the authorization/token header.
|
|
constexpr std::string_view HEADER_AUTH_BEARER = "Authorization: Bearer ";
|
|
|
|
// Content type headers used.
|
|
constexpr const char *HEADER_CONTENT_TYPE_JSON = "Content-Type: application/json";
|
|
constexpr const char *HEADER_CONTENT_TYPE_FORM = "Content-Type: application/x-www-form-urlencoded";
|
|
|
|
// This is used for upload/patch for grabbing the upload location.
|
|
constexpr std::string_view HEADER_UPLOAD_LOCATION = "Location";
|
|
|
|
// These are API endpoints used in multiple request calls.
|
|
constexpr const char *URL_OAUTH2_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
constexpr const char *URL_DRIVE_FILE_API = "https://www.googleapis.com/drive/v3/files";
|
|
constexpr const char *URL_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3/files";
|
|
|
|
// These are json keys that are used for various requests.
|
|
constexpr const char *JSON_KEY_ACCESS_TOKEN = "access_token";
|
|
constexpr const char *JSON_KEY_CLIENT_ID = "client_id";
|
|
constexpr const char *JSON_KEY_CLIENT_SECRET = "client_secret";
|
|
constexpr const char *JSON_KEY_DEVICE_CODE = "device_code";
|
|
constexpr const char *JSON_KEY_EXPIRES_IN = "expires_in";
|
|
constexpr const char *JSON_KEY_GRANT_TYPE = "grant_type";
|
|
constexpr const char *JSON_KEY_ID = "id";
|
|
constexpr const char *JSON_KEY_INSTALLED = "installed";
|
|
constexpr const char *JSON_KEY_MIMETYPE = "mimeType";
|
|
constexpr const char *JSON_KEY_NAME = "name";
|
|
constexpr const char *JSON_KEY_PARENTS = "parents";
|
|
constexpr const char *JSON_KEY_REFRESH_TOKEN = "refresh_token";
|
|
|
|
/// @brief Folder mimetype string.
|
|
constexpr const char *MIME_TYPE_DIRECTORY = "application/vnd.google-apps.folder";
|
|
} // namespace
|
|
|
|
remote::GoogleDrive::GoogleDrive()
|
|
: Storage("[GD]", true)
|
|
{
|
|
static constexpr const char *STRING_ERROR_READING_CONFIG = "Error reading Google Drive config: %s";
|
|
|
|
// Load the json file.
|
|
json::Object clientJson = json::new_object(json_object_from_file, remote::PATH_GOOGLE_DRIVE_CONFIG.data());
|
|
if (!clientJson)
|
|
{
|
|
logger::log(STRING_ERROR_READING_CONFIG, "Error reading configuration file!");
|
|
return;
|
|
}
|
|
|
|
json_object *installed = json::get_object(clientJson, JSON_KEY_INSTALLED);
|
|
if (!installed)
|
|
{
|
|
logger::log(STRING_ERROR_READING_CONFIG, "Configuration file is malformed!");
|
|
return;
|
|
}
|
|
|
|
json_object *clientId = json_object_object_get(installed, JSON_KEY_CLIENT_ID);
|
|
json_object *clientSecret = json_object_object_get(installed, JSON_KEY_CLIENT_SECRET);
|
|
json_object *refreshToken = json_object_object_get(installed, JSON_KEY_REFRESH_TOKEN);
|
|
if (!clientId || !clientSecret)
|
|
{
|
|
logger::log(STRING_ERROR_READING_CONFIG, "Configuration file is missing required data!");
|
|
return;
|
|
}
|
|
// Grab them.
|
|
m_clientId = json_object_get_string(clientId);
|
|
m_clientSecret = json_object_get_string(clientSecret);
|
|
|
|
// Returning here will make is_initialized return false.
|
|
if (!refreshToken) { return; }
|
|
m_refreshToken = json_object_get_string(refreshToken);
|
|
|
|
if (!GoogleDrive::refresh_token())
|
|
{
|
|
// If refreshing the token failed, this will cause is_initialized to return false and force a re-signin.
|
|
m_refreshToken.clear();
|
|
}
|
|
else if (GoogleDrive::get_root_id() && GoogleDrive::request_listing()) { m_isInitialized = true; }
|
|
}
|
|
|
|
bool remote::GoogleDrive::create_directory(std::string_view name)
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
curl::HeaderList headers = curl::new_header_list();
|
|
curl::append_header(headers, m_authHeader);
|
|
curl::append_header(headers, HEADER_CONTENT_TYPE_JSON);
|
|
|
|
json::Object postJson = json::new_object(json_object_new_object);
|
|
json_object *directoryName = json_object_new_string(name.data());
|
|
json_object *mimeType = json_object_new_string(MIME_TYPE_DIRECTORY);
|
|
json::add_object(postJson, JSON_KEY_NAME, directoryName);
|
|
json::add_object(postJson, JSON_KEY_MIMETYPE, mimeType);
|
|
if (!m_parent.empty())
|
|
{
|
|
// I don't understand why this is an array.
|
|
json_object *parentArray = json_object_new_array();
|
|
json_object *parentId = json_object_new_string(m_parent.c_str());
|
|
json_object_array_add(parentArray, parentId);
|
|
json::add_object(postJson, JSON_KEY_PARENTS, parentArray);
|
|
}
|
|
|
|
std::string response;
|
|
curl::prepare_post(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, headers.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, URL_DRIVE_FILE_API);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, json_object_get_string(postJson.get()));
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!parser)
|
|
{
|
|
logger::log("Error creating directory: Error parsing server response.");
|
|
return false;
|
|
}
|
|
|
|
// This is all I really need from the response.
|
|
json_object *id = json::get_object(parser, JSON_KEY_ID);
|
|
if (!id)
|
|
{
|
|
// This doesn't really mean the request wasn't successful. It just means the new directory couldn't be appended
|
|
// to the list...
|
|
logger::log("Error creating directory: Error finding directory ID in response!");
|
|
return false;
|
|
}
|
|
|
|
m_list.emplace_back(name, json_object_get_string(id), m_parent, 0, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::upload_file(const fslib::Path &source, std::string_view name, sys::ProgressTask *task)
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
fslib::File sourceFile(source, FsOpenMode_Read);
|
|
if (!sourceFile)
|
|
{
|
|
logger::log("Error uploading file: %s", fslib::error::get_string());
|
|
return false;
|
|
}
|
|
|
|
curl::HeaderList headers = curl::new_header_list();
|
|
curl::append_header(headers, m_authHeader);
|
|
curl::append_header(headers, HEADER_CONTENT_TYPE_JSON);
|
|
|
|
// I don't know if I like this much. Looks like I'm using a high level language instead of a real one.
|
|
remote::URL url{URL_DRIVE_UPLOAD_API};
|
|
url.append_parameter("uploadType", "resumable");
|
|
|
|
// Json to post.
|
|
json::Object postJson = json::new_object(json_object_new_object);
|
|
json_object *driveName = json_object_new_string(name.data());
|
|
json::add_object(postJson, JSON_KEY_NAME, driveName);
|
|
if (!m_parent.empty())
|
|
{
|
|
json_object *parentArray = json_object_new_array();
|
|
json_object *parentId = json_object_new_string(m_parent.c_str());
|
|
json_object_array_add(parentArray, parentId);
|
|
json::add_object(postJson, JSON_KEY_PARENTS, parentArray);
|
|
}
|
|
|
|
curl::HeaderArray headerArray;
|
|
curl::prepare_post(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, headers.get());
|
|
curl::set_option(m_curl, CURLOPT_HEADERFUNCTION, curl::write_header_array);
|
|
curl::set_option(m_curl, CURLOPT_HEADERDATA, &headerArray);
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, json_object_get_string(postJson.get()));
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
// Extract the location from the headers.
|
|
std::string location;
|
|
if (!curl::get_header_value(headerArray, HEADER_UPLOAD_LOCATION.data(), location))
|
|
{
|
|
logger::log("Error uploading file: Couldn't extract upload location from headers.");
|
|
return false;
|
|
}
|
|
|
|
const int64_t sourceSize = sourceFile.get_size();
|
|
if (task) { task->reset(static_cast<double>(sourceSize)); }
|
|
|
|
std::string response;
|
|
curl::UploadStruct uploadData = {.source = &sourceFile, .task = task};
|
|
// This is the actual upload. This doesn't need the authentication header to work for some reason?
|
|
curl::prepare_upload(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_URL, location.c_str());
|
|
curl::set_option(m_curl, CURLOPT_READFUNCTION, curl::read_data_from_file);
|
|
curl::set_option(m_curl, CURLOPT_READDATA, &uploadData);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object responseParser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!responseParser)
|
|
{
|
|
logger::log("Error uploading file: response could not be parsed.");
|
|
return false;
|
|
}
|
|
|
|
json_object *id = json::get_object(responseParser, JSON_KEY_ID);
|
|
json_object *filename = json::get_object(responseParser, JSON_KEY_NAME);
|
|
if (!id || !filename)
|
|
{
|
|
logger::log("Error uploading file: server response is missing data required.");
|
|
return false;
|
|
}
|
|
|
|
const char *idString = json_object_get_string(id);
|
|
const char *nameString = json_object_get_string(filename);
|
|
m_list.emplace_back(nameString, idString, m_parent, sourceSize, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::patch_file(remote::Item *file, const fslib::Path &source, sys::ProgressTask *task)
|
|
{
|
|
static constexpr const char *STRING_PATCH_ERROR = "Error patching file: %s";
|
|
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
fslib::File sourceFile(source, FsOpenMode_Read);
|
|
if (!sourceFile)
|
|
{
|
|
logger::log(STRING_PATCH_ERROR, fslib::error::get_string());
|
|
return false;
|
|
}
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader);
|
|
|
|
remote::URL url{URL_DRIVE_UPLOAD_API};
|
|
url.append_path(file->get_id()).append_parameter("uploadType", "resumable");
|
|
|
|
std::string response;
|
|
curl::HeaderArray headerArray;
|
|
curl::reset_handle(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "PATCH");
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_HEADERFUNCTION, curl::write_header_array);
|
|
curl::set_option(m_curl, CURLOPT_HEADERDATA, &headerArray);
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
// This is the target location to upload to.
|
|
std::string location;
|
|
if (!curl::get_header_value(headerArray, HEADER_UPLOAD_LOCATION, location))
|
|
{
|
|
logger::log(STRING_PATCH_ERROR, "Location could not be read from headers!");
|
|
return false;
|
|
}
|
|
|
|
if (task) { task->reset(static_cast<double>(sourceFile.get_size())); }
|
|
|
|
// For some reason, this doesn't need the auth header.
|
|
curl::UploadStruct uploadData = {.source = &sourceFile, .task = task};
|
|
|
|
curl::prepare_upload(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_URL, location.c_str());
|
|
curl::set_option(m_curl, CURLOPT_READFUNCTION, curl::read_data_from_file);
|
|
curl::set_option(m_curl, CURLOPT_READDATA, &uploadData);
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
// Update the file size with the source file size.
|
|
file->set_size(sourceFile.get_size());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::download_file(const remote::Item *file, const fslib::Path &destination, sys::ProgressTask *task)
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
const int64_t itemSize = file->get_size();
|
|
fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write};
|
|
if (!destFile)
|
|
{
|
|
logger::log("Error downloading file: local file could not be opened for writing!");
|
|
return false;
|
|
}
|
|
|
|
if (task) { task->reset(static_cast<double>(itemSize)); }
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader);
|
|
|
|
remote::URL url{URL_DRIVE_FILE_API};
|
|
url.append_path(file->get_id()).append_parameter("alt", "media");
|
|
|
|
auto download = curl::create_download_struct(destFile, task, itemSize);
|
|
|
|
curl::prepare_get(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::download_file_threaded);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, download.get());
|
|
|
|
sys::threadpool::push_job(curl::download_write_thread_function, download);
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
download->writeComplete.acquire();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::delete_item(const remote::Item *item)
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
// Iterator is needed to remove it from the list.
|
|
const std::string_view itemId = item->get_id();
|
|
auto findItem = Storage::find_item_by_id(itemId);
|
|
if (findItem == m_list.end())
|
|
{
|
|
logger::log("Error deleting item: Item not found in list!");
|
|
return false;
|
|
}
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader);
|
|
|
|
remote::URL url{URL_DRIVE_FILE_API};
|
|
url.append_path(itemId);
|
|
|
|
curl::reset_handle(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
// This might be a better way to check?
|
|
long code = curl::get_response_code(m_curl);
|
|
if (code != 204)
|
|
{
|
|
logger::log("Error deleting item from Google Drive: %i.", code);
|
|
return false;
|
|
}
|
|
|
|
// Erase from the master list.
|
|
m_list.erase(findItem);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::rename_item(remote::Item *item, std::string_view newName)
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader);
|
|
curl::append_header(header, HEADER_CONTENT_TYPE_JSON);
|
|
|
|
remote::URL url{URL_DRIVE_FILE_API};
|
|
url.append_path(item->get_id());
|
|
|
|
json::Object post = json::new_object(json_object_new_object);
|
|
json_object *name = json_object_new_string(newName.data());
|
|
json::add_object(post, "name", name);
|
|
|
|
std::string response{};
|
|
const char *postString = json_object_get_string(post.get());
|
|
curl::reset_handle(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "PATCH");
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, HEADER_CONTENT_TYPE_JSON);
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, postString);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
// We're only doing this to check for errors.
|
|
json::Object responseParser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (GoogleDrive::error_occurred(responseParser)) { return false; }
|
|
|
|
item->set_name(newName);
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::sign_in_required() const { return !m_isInitialized || m_refreshToken.empty(); }
|
|
|
|
bool remote::GoogleDrive::get_sign_in_data(std::string &message, std::string &code, std::time_t &expiration, int &wait)
|
|
{
|
|
static constexpr const char *STRING_SIGN_IN_ERROR = "Error requesting sign-in data: %s";
|
|
// This is the the URL for device codes and limited input.
|
|
static constexpr const char *API_URL_DEVICE_CODE = "https://oauth2.googleapis.com/device/code";
|
|
// This is a gimped version of what I used to use.
|
|
static constexpr const char *STRING_DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
|
|
const char *messageTemplate = strings::get_by_name(strings::names::GOOGLE_DRIVE, 0);
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, HEADER_CONTENT_TYPE_FORM);
|
|
|
|
remote::Form post{};
|
|
post.append_parameter(JSON_KEY_CLIENT_ID, m_clientId).append_parameter("scope", STRING_DRIVE_FILE_SCOPE);
|
|
|
|
std::string response;
|
|
curl::prepare_post(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, API_URL_DEVICE_CODE);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, post.get());
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDSIZE, post.length());
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!parser || GoogleDrive::error_occurred(parser)) { return false; }
|
|
|
|
json_object *deviceCode = json::get_object(parser, JSON_KEY_DEVICE_CODE);
|
|
json_object *userCode = json::get_object(parser, "user_code");
|
|
json_object *verificationUrl = json::get_object(parser, "verification_url");
|
|
json_object *expiresIn = json::get_object(parser, "expires_in");
|
|
json_object *interval = json::get_object(parser, "interval");
|
|
// These are required and fatal.
|
|
if (!deviceCode || !userCode || !verificationUrl || !expiresIn || !interval)
|
|
{
|
|
logger::log(STRING_SIGN_IN_ERROR, "Malformed response.");
|
|
return false;
|
|
}
|
|
|
|
message = stringutil::get_formatted_string(messageTemplate,
|
|
json_object_get_string(verificationUrl),
|
|
json_object_get_string(userCode));
|
|
code = json_object_get_string(deviceCode);
|
|
expiration = std::time(NULL) + json_object_get_uint64(expiresIn);
|
|
wait = json_object_get_int64(interval);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::poll_sign_in(std::string_view code)
|
|
{
|
|
static constexpr const char *STRING_ERROR_POLLING = "Error polling Google OAuth2: %s";
|
|
// I'm just gonna save this pre-escaped instead of wasting time escaping it.
|
|
static constexpr const char *STRING_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
|
|
// I'm not sure how else to make this really work with JKSV's task threading system?
|
|
remote::Form post{};
|
|
post.append_parameter(JSON_KEY_CLIENT_ID, m_clientId)
|
|
.append_parameter(JSON_KEY_CLIENT_SECRET, m_clientSecret)
|
|
.append_parameter("device_code", code)
|
|
.append_parameter(JSON_KEY_GRANT_TYPE, STRING_DEVICE_GRANT);
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, HEADER_CONTENT_TYPE_FORM);
|
|
|
|
std::string response;
|
|
curl::prepare_get(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, URL_OAUTH2_TOKEN_URL);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, post.get());
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDSIZE, post.length());
|
|
|
|
if (!curl::perform(m_curl) || response.empty()) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
// This error isn't logged, because that's how you know if the user logged in or not.
|
|
if (!parser || GoogleDrive::error_occurred(parser, false)) { return false; }
|
|
|
|
json_object *accessToken = json::get_object(parser, JSON_KEY_ACCESS_TOKEN);
|
|
json_object *expiresIn = json::get_object(parser, JSON_KEY_EXPIRES_IN);
|
|
json_object *refreshToken = json::get_object(parser, JSON_KEY_REFRESH_TOKEN);
|
|
// All of these are required.
|
|
if (!accessToken || !expiresIn || !refreshToken)
|
|
{
|
|
logger::log(STRING_ERROR_POLLING, "Malformed response or missing data!");
|
|
return false;
|
|
}
|
|
|
|
m_token = json_object_get_string(accessToken);
|
|
m_refreshToken = json_object_get_string(refreshToken);
|
|
m_tokenExpires = std::time(NULL) + json_object_get_uint64(expiresIn);
|
|
m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token;
|
|
|
|
json::Object config = json::new_object(json_object_from_file, remote::PATH_GOOGLE_DRIVE_CONFIG.data());
|
|
if (config)
|
|
{
|
|
// We're going to attach the refresh token to the installed object so nothing bad can happen to it and I don't
|
|
// have to deal with Git issues about it.
|
|
json_object *installed = json::get_object(config, JSON_KEY_INSTALLED);
|
|
json_object *refreshToken = json_object_new_string(m_refreshToken.c_str());
|
|
json_object_object_add(installed, JSON_KEY_REFRESH_TOKEN, refreshToken);
|
|
|
|
fslib::File configFile(remote::PATH_GOOGLE_DRIVE_CONFIG, FsOpenMode_Create | FsOpenMode_Write);
|
|
if (configFile) { configFile << json_object_get_string(config.get()); }
|
|
}
|
|
|
|
if (!GoogleDrive::get_root_id()) { return false; }
|
|
|
|
m_isInitialized = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::get_root_id()
|
|
{
|
|
// This is the only place this is used. V3 of the API doesn't allow you to retrieve this for some reason?
|
|
static constexpr const char *API_URL_ABOUT_ROOT_ID = "https://www.googleapis.com/drive/v2/about?fields=rootFolderId";
|
|
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader);
|
|
|
|
std::string response;
|
|
curl::prepare_get(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, API_URL_ABOUT_ROOT_ID);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!parser) { return false; }
|
|
|
|
json_object *rootId = json::get_object(parser, "rootFolderId");
|
|
if (!rootId)
|
|
{
|
|
logger::log("Error getting drive root directory ID!");
|
|
return false;
|
|
}
|
|
|
|
m_root = json_object_get_string(rootId);
|
|
m_parent = m_root;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::token_is_valid() const noexcept
|
|
{
|
|
// I'm giving this a grace period just to be safe.
|
|
return std::time(NULL) < m_tokenExpires - 10;
|
|
}
|
|
|
|
bool remote::GoogleDrive::refresh_token()
|
|
{
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, HEADER_CONTENT_TYPE_FORM);
|
|
|
|
remote::Form post{};
|
|
post.append_parameter(JSON_KEY_CLIENT_ID, m_clientId)
|
|
.append_parameter(JSON_KEY_CLIENT_SECRET, m_clientSecret)
|
|
.append_parameter(JSON_KEY_REFRESH_TOKEN, m_refreshToken)
|
|
.append_parameter(JSON_KEY_GRANT_TYPE, JSON_KEY_REFRESH_TOKEN);
|
|
|
|
std::string response;
|
|
curl::prepare_post(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, URL_OAUTH2_TOKEN_URL);
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDS, post.get());
|
|
curl::set_option(m_curl, CURLOPT_POSTFIELDSIZE, post.length());
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!parser || GoogleDrive::error_occurred(parser)) { return false; }
|
|
|
|
// These are the only things I care about.
|
|
json_object *accessToken = json::get_object(parser, JSON_KEY_ACCESS_TOKEN);
|
|
json_object *expiresIn = json::get_object(parser, JSON_KEY_EXPIRES_IN);
|
|
if (!accessToken || !expiresIn) { return false; }
|
|
|
|
m_token = json_object_get_string(accessToken);
|
|
m_tokenExpires = std::time(NULL) + json_object_get_uint64(expiresIn);
|
|
m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::request_listing()
|
|
{
|
|
if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; }
|
|
|
|
curl::HeaderList header = curl::new_header_list();
|
|
curl::append_header(header, m_authHeader.c_str());
|
|
|
|
remote::URL url{URL_DRIVE_FILE_API};
|
|
url.append_parameter("fields", "nextPageToken,files(name,id,size,parents,mimeType)")
|
|
.append_parameter("orderBy", "name_natural")
|
|
.append_parameter("pageSize", "256")
|
|
.append_parameter("trashed", "false");
|
|
|
|
std::string response;
|
|
curl::prepare_get(m_curl);
|
|
curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get());
|
|
curl::set_option(m_curl, CURLOPT_URL, url.get());
|
|
curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
|
curl::set_option(m_curl, CURLOPT_WRITEDATA, &response);
|
|
|
|
// This is used as the loop condition.
|
|
json_object *nextPageToken = nullptr;
|
|
do {
|
|
response.clear();
|
|
|
|
if (!curl::perform(m_curl)) { return false; }
|
|
|
|
json::Object parser = json::new_object(json_tokener_parse, response.c_str());
|
|
if (!parser || GoogleDrive::error_occurred(parser) || !GoogleDrive::process_listing(parser))
|
|
{
|
|
logger::log("Error while parseing Google Drive response!");
|
|
return false;
|
|
}
|
|
|
|
nextPageToken = json::get_object(parser, "nextPageToken");
|
|
if (nextPageToken)
|
|
{
|
|
remote::URL nextPage{url};
|
|
nextPage.append_parameter("pageToken", json_object_get_string(nextPageToken));
|
|
curl::set_option(m_curl, CURLOPT_URL, nextPage);
|
|
}
|
|
} while (nextPageToken);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::process_listing(json::Object &json)
|
|
{
|
|
static constexpr const char *STRING_ERROR_PROCESSING = "Error processing Google Drive listing: %s";
|
|
|
|
json_object *files = json::get_object(json, "files");
|
|
if (!files) { return false; }
|
|
|
|
size_t arrayLength = json_object_array_length(files);
|
|
for (size_t i = 0; i < arrayLength; i++)
|
|
{
|
|
json_object *currentFile = json_object_array_get_idx(files, i);
|
|
if (!currentFile) { return false; }
|
|
|
|
json_object *mimeType = json_object_object_get(currentFile, JSON_KEY_MIMETYPE);
|
|
json_object *parents = json_object_object_get(currentFile, JSON_KEY_PARENTS);
|
|
json_object *id = json_object_object_get(currentFile, JSON_KEY_ID);
|
|
json_object *name = json_object_object_get(currentFile, JSON_KEY_NAME);
|
|
json_object *size = json_object_object_get(currentFile, "size");
|
|
if (!mimeType || !parents || !id || !name)
|
|
{
|
|
logger::log(STRING_ERROR_PROCESSING, "Malformed or missing data!");
|
|
continue;
|
|
}
|
|
|
|
// I still think it's stupid this is an array when there can only be one...
|
|
json_object *parent = json_object_array_get_idx(parents, 0);
|
|
if (!parent)
|
|
{
|
|
logger::log(STRING_ERROR_PROCESSING, "Missing parent ID!");
|
|
continue;
|
|
}
|
|
|
|
m_list.emplace_back(json_object_get_string(name),
|
|
json_object_get_string(id),
|
|
json_object_get_string(parent),
|
|
size ? json_object_get_uint64(size) : 0,
|
|
std::strcmp(MIME_TYPE_DIRECTORY, json_object_get_string(mimeType)) == 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool remote::GoogleDrive::error_occurred(json::Object &json, bool log) noexcept
|
|
{
|
|
json_object *error = json::get_object(json, "error");
|
|
if (!error) { return false; }
|
|
|
|
// Google has so many different error response structures. I'm covering two here. Technically,
|
|
// I could grab the code too from the second response, that makes this even more of a headache.
|
|
json_object *description = json::get_object(json, "error_description");
|
|
json_object *message = json_object_object_get(error, "message");
|
|
if (log && (description || message))
|
|
{
|
|
logger::log("Google Drive error: %s.",
|
|
description ? json_object_get_string(description) : json_object_get_string(message));
|
|
}
|
|
|
|
return true;
|
|
}
|