Files
2025-12-10 14:38:26 -08:00

4119 lines
192 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "gdrive.h"
#include <variant>
#include <unordered_set> //needed by clang
#include <unordered_map> //
#include <libcurl/curl_wrap.h> //DON'T include <curl/curl.h> directly!
#include <zen/base64.h>
#include <zen/file_access.h>
#include <zen/file_io.h>
#include <zen/file_traverser.h>
#include <zen/guid.h>
#include <zen/http.h>
#include <zen/json.h>
#include <zen/resolve_path.h>
#include <zen/process_exec.h>
#include <zen/socket.h>
#include <zen/shutdown.h>
#include <zen/time.h>
#include <zen/zlib_wrap.h>
#include "abstract_impl.h"
#include "init_curl_libssh2.h"
#include <poll.h>
using namespace zen;
using namespace fff;
using AFS = AbstractFileSystem;
namespace fff
{
struct GdrivePath
{
GdriveLogin gdriveLogin;
AfsPath itemPath; //path relative to drive root
};
struct GdriveRawPath
{
std::string parentId; //Google Drive item IDs are *globally* unique!
Zstring itemName;
};
inline
std::weak_ordering operator<=>(const GdriveRawPath& lhs, const GdriveRawPath& rhs)
{
if (const std::strong_ordering cmp = lhs.parentId <=> rhs.parentId;
cmp != std::strong_ordering::equal)
return cmp;
return compareNativePath(lhs.itemName, rhs.itemName);
}
constinit Global<PathAccessLocker<GdriveRawPath>> globalGdrivePathAccessLocker;
GLOBAL_RUN_ONCE(globalGdrivePathAccessLocker.set(std::make_unique<PathAccessLocker<GdriveRawPath>>()));
template <> std::shared_ptr<PathAccessLocker<GdriveRawPath>> PathAccessLocker<GdriveRawPath>::getGlobalInstance() { return globalGdrivePathAccessLocker.get(); }
template <> Zstring PathAccessLocker<GdriveRawPath>::getItemName(const GdriveRawPath& nativePath) { return nativePath.itemName; }
using PathAccessLock = PathAccessLocker<GdriveRawPath>::Lock; //throw SysError
using PathBlockType = PathAccessLocker<GdriveRawPath>::BlockType;
}
namespace
{
//Google Drive REST API Overview: https://developers.google.com/drive/api/v3/about-sdk
//Google Drive REST API Reference: https://developers.google.com/drive/api/v3/reference
const Zchar* GOOGLE_REST_API_SERVER = Zstr("www.googleapis.com");
constexpr std::chrono::seconds HTTP_SESSION_MAX_IDLE_TIME (20);
constexpr std::chrono::seconds HTTP_SESSION_CLEANUP_INTERVAL(4);
constexpr std::chrono::seconds GDRIVE_SYNC_INTERVAL (5);
const size_t GDRIVE_BLOCK_SIZE_DOWNLOAD = 64 * 1024; //libcurl returns blocks of only 16 kB as returned by recv() even if we request larger blocks via CURLOPT_BUFFERSIZE
const size_t GDRIVE_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks of 64 kB. larger blocksizes set via CURLOPT_UPLOAD_BUFFERSIZE do not seem to make a difference
const size_t GDRIVE_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte]
//stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy()
constexpr ZstringView gdrivePrefix = Zstr("gdrive:");
const char gdriveFolderMimeType [] = "application/vnd.google-apps.folder";
const char gdriveShortcutMimeType[] = "application/vnd.google-apps.shortcut"; //= symbolic link!
const char DB_FILE_DESCR[] = "FreeFileSync";
const int DB_FILE_VERSION = 5; //2021-05-15
std::string getGdriveClientId () { return ""; } // => replace with live credentials
std::string getGdriveClientSecret() { return ""; } //
struct HttpSessionId
{
explicit HttpSessionId(const Zstring& serverName) :
server(serverName) {}
Zstring server;
};
inline
bool operator==(const HttpSessionId& lhs, const HttpSessionId& rhs) { return equalAsciiNoCase(lhs.server, rhs.server); }
}
//exactly the type of case insensitive comparison we need for server names!
//https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs
template<> struct std::hash<HttpSessionId> { size_t operator()(const HttpSessionId& sessionId) const { return StringHashAsciiNoCase()(sessionId.server); } };
namespace
{
Zstring concatenateGdriveFolderPathPhrase(const GdrivePath& gdrivePath); //noexcept
//e.g.: gdrive:/john@gmail.com:SharedDrive/folder/file.txt
std::wstring getGdriveDisplayPath(const GdrivePath& gdrivePath)
{
Zstring displayPath = Zstring(gdrivePrefix) + FILE_NAME_SEPARATOR;
displayPath += utfTo<Zstring>(gdrivePath.gdriveLogin.email);
if (!gdrivePath.gdriveLogin.locationName.empty())
displayPath += Zstr(':') + gdrivePath.gdriveLogin.locationName;
if (!gdrivePath.itemPath.value.empty())
displayPath += FILE_NAME_SEPARATOR + gdrivePath.itemPath.value;
return utfTo<std::wstring>(displayPath);
}
std::wstring formatGdriveErrorRaw(std::string serverResponse)
{
/* e.g.: { "error": { "errors": [{ "domain": "global",
"reason": "invalidSharingRequest",
"message": "Bad Request. User message: \"ACL change not allowed.\"" }],
"code": 400,
"message": "Bad Request" }}
or: { "error": "invalid_client",
"error_description": "Unauthorized" }
or merely: { "error": "invalid_token" } */
trim(serverResponse);
assert(!serverResponse.empty());
if (serverResponse.empty())
return L"<" + _("empty") + L">"; //at least give some indication
try
{
const JsonValue jresponse = parseJson(serverResponse); //throw JsonParsingError
if (const JsonValue* error = getChildFromJsonObject(jresponse, "error"))
{
if (error->type == JsonValue::Type::string)
return utfTo<std::wstring>(error->primVal);
//the inner message is generally more descriptive!
else if (const JsonValue* errors = getChildFromJsonObject(*error, "errors"))
if (errors->type == JsonValue::Type::array && !errors->arrayVal.empty())
if (const JsonValue* message = getChildFromJsonObject(errors->arrayVal[0], "message"))
if (message->type == JsonValue::Type::string)
return utfTo<std::wstring>(message->primVal);
}
}
catch (JsonParsingError&) {} //not JSON?
return utfTo<std::wstring>(serverResponse);
}
AFS::FingerPrint getGdriveFilePrint(const std::string& itemId)
{
assert(!itemId.empty());
//Google Drive item ID is persistent and globally unique! :)
return hashString<AFS::FingerPrint>(itemId);
}
//----------------------------------------------------------------------------------------------------------------
constinit Global<UniSessionCounter> httpSessionCount;
GLOBAL_RUN_ONCE(httpSessionCount.set(createUniSessionCounter()));
UniInitializer globalInitHttp(*httpSessionCount.get());
//----------------------------------------------------------------------------------------------------------------
class HttpSessionManager //reuse (healthy) HTTP sessions globally
{
public:
explicit HttpSessionManager(const Zstring& caCertFilePath) :
caCertFilePath_(caCertFilePath),
sessionCleaner_([this]
{
setCurrentThreadName(Zstr("Session Cleaner[HTTP]"));
runGlobalSessionCleanUp(); //throw ThreadStopRequest
}) {}
void access(const HttpSessionId& sessionId, const std::function<void(HttpSession& session)>& useHttpSession /*throw X*/) //throw SysError, X
{
Protected<HttpSessionManager::HttpSessionCache>& sessionCache = getSessionCache(sessionId);
std::unique_ptr<HttpInitSession> httpSession;
sessionCache.access([&](HttpSessionManager::HttpSessionCache& sessions)
{
//assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use, idle sessions via worker thread)
if (!sessions.empty())
{
httpSession = std::move(sessions.back ());
/**/ sessions.pop_back();
}
});
//create new HTTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem!
if (!httpSession)
httpSession = std::make_unique<HttpInitSession>(sessionId.server, caCertFilePath_); //throw SysError
ZEN_ON_SCOPE_EXIT(
if (isHealthy(httpSession->session)) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!)
sessionCache.access([&](HttpSessionManager::HttpSessionCache& sessions) { sessions.push_back(std::move(httpSession)); }); );
useHttpSession(httpSession->session); //throw X
}
private:
HttpSessionManager (const HttpSessionManager&) = delete;
HttpSessionManager& operator=(const HttpSessionManager&) = delete;
//associate session counting (for initialization/teardown)
struct HttpInitSession
{
HttpInitSession(const Zstring& server, const Zstring& caCertFilePath) :
session(server, true /*useTls*/, caCertFilePath) {}
const std::shared_ptr<UniCounterCookie> cookie{getLibsshCurlUnifiedInitCookie(httpSessionCount)}; //throw SysError
HttpSession session; //life time must be subset of UniCounterCookie
};
static bool isHealthy(const HttpSession& s) { return std::chrono::steady_clock::now() - s.getLastUseTime() <= HTTP_SESSION_MAX_IDLE_TIME; }
using HttpSessionCache = std::vector<std::unique_ptr<HttpInitSession>>;
Protected<HttpSessionCache>& getSessionCache(const HttpSessionId& sessionId)
{
//single global session store per sessionId; life-time bound to globalInstance => never remove a sessionCache!!!
Protected<HttpSessionCache>* sessionCache = nullptr;
globalSessionCache_.access([&](GlobalHttpSessions& sessionsById)
{
sessionCache = &sessionsById[sessionId]; //get or create
});
static_assert(std::is_same_v<GlobalHttpSessions, std::unordered_map<HttpSessionId, Protected<HttpSessionCache>>>, "require std::unordered_map so that the pointers we return remain stable");
return *sessionCache;
}
//run a dedicated clean-up thread => it's unclear when the server let's a connection time out, so we do it preemptively
//context of worker thread:
void runGlobalSessionCleanUp() //throw ThreadStopRequest
{
std::chrono::steady_clock::time_point lastCleanupTime;
for (;;)
{
const auto now = std::chrono::steady_clock::now();
if (now < lastCleanupTime + HTTP_SESSION_CLEANUP_INTERVAL)
interruptibleSleep(lastCleanupTime + HTTP_SESSION_CLEANUP_INTERVAL - now); //throw ThreadStopRequest
lastCleanupTime = std::chrono::steady_clock::now();
std::vector<Protected<HttpSessionCache>*> sessionCaches; //pointers remain stable, thanks to std::unordered_map<>
globalSessionCache_.access([&](GlobalHttpSessions& sessionsByCfg)
{
for (auto& [sessionCfg, idleSession] : sessionsByCfg)
sessionCaches.push_back(&idleSession);
});
for (Protected<HttpSessionCache>* sessionCache : sessionCaches)
for (;;)
{
bool done = false;
sessionCache->access([&](HttpSessionCache& sessions)
{
for (std::unique_ptr<HttpInitSession>& sshSession : sessions)
if (!isHealthy(sshSession->session)) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long
{
sshSession.swap(sessions.back());
/**/ sessions.pop_back(); //run ~HttpSession *inside* the lock! => avoid hitting server limits!
return; //don't hold lock for too long: delete only one session at a time, then yield...
}
done = true;
});
if (done)
break;
std::this_thread::yield();
}
}
}
using GlobalHttpSessions = std::unordered_map<HttpSessionId, Protected<HttpSessionCache>>;
Protected<GlobalHttpSessions> globalSessionCache_;
const Zstring caCertFilePath_;
InterruptibleThread sessionCleaner_;
};
//--------------------------------------------------------------------------------------
constinit Global<HttpSessionManager> globalHttpSessionManager; //caveat: life time must be subset of static UniInitializer!
//--------------------------------------------------------------------------------------
struct GdriveAccess
{
std::string token;
int timeoutSec = 0;
};
//===========================================================================================================================
HttpSession::Result googleHttpsRequest(const Zstring& serverName, const std::string& serverRelPath, //throw SysError, X
const std::vector<std::string>& extraHeaders,
std::vector<CurlOption> extraOptions,
const std::function<void (std::span<const char> buf)>& writeResponse /*throw X*/, //optional
const std::function<size_t(std::span< char> buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream!
const std::function<void(const std::string_view& header)>& receiveHeader /*throw X*/, //optional
int timeoutSec)
{
//https://developers.google.com/drive/api/v3/performance
//"In order to receive a gzip-encoded response you must do two things: Set an Accept-Encoding header, ["gzip" automatically set by HttpSession]
extraOptions.emplace_back(CURLOPT_USERAGENT, "FreeFileSync (gzip)"); //and modify your user agent to contain the string gzip."
const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get();
if (!mgr)
throw SysError(formatSystemError("googleHttpsRequest", L"", L"Function call not allowed during init/shutdown."));
HttpSession::Result httpResult;
mgr->access(HttpSessionId(serverName), [&](HttpSession& session) //throw SysError
{
httpResult = session.perform(serverRelPath, extraHeaders, extraOptions, writeResponse, readRequest, receiveHeader, timeoutSec); //throw SysError, X
});
return httpResult;
}
//try to get a grip on this crazy REST API: - parameters are passed via query string, header, or body, using GET, POST, PUT, PATCH, DELETE, ... it's a dice roll
HttpSession::Result gdriveHttpsRequest(const std::string& serverRelPath, //throw SysError, X
std::vector<std::string> extraHeaders,
const std::vector<CurlOption>& extraOptions,
const std::function<void (std::span<const char> buf)>& writeResponse /*throw X*/, //optional
const std::function<size_t(std::span< char> buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream!
const std::function<void(const std::string_view& header)>& receiveHeader /*throw X*/, //optional
const GdriveAccess& access)
{
extraHeaders.push_back("Authorization: Bearer " + access.token);
return googleHttpsRequest(GOOGLE_REST_API_SERVER, serverRelPath,
extraHeaders,
extraOptions,
writeResponse /*throw X*/,
readRequest /*throw X*/,
receiveHeader /*throw X*/, access.timeoutSec); //throw SysError, X
}
//========================================================================================================
struct GdriveUser
{
std::wstring displayName;
std::string email;
};
GdriveUser getGdriveUser(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/about
const std::string& queryParams = xWwwFormUrlEncode(
{
{"fields", "user/displayName,user/emailAddress"},
});
std::string response;
gdriveHttpsRequest("/drive/v3/about?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
if (const JsonValue* user = getChildFromJsonObject(jresponse, "user"))
{
const std::optional<std::string> displayName = getPrimitiveFromJsonObject(*user, "displayName");
const std::optional<std::string> email = getPrimitiveFromJsonObject(*user, "emailAddress");
if (displayName && email)
return {utfTo<std::wstring>(*displayName), *email};
}
throw SysError(formatGdriveErrorRaw(response));
}
struct GdriveAuthCode
{
std::string code;
std::string redirectUrl;
std::string codeChallenge;
};
struct GdriveAccessToken
{
std::string value;
time_t validUntil = 0; //remaining lifetime of the access token
};
struct GdriveAccessInfo
{
GdriveAccessToken accessToken;
std::string refreshToken;
GdriveUser userInfo;
};
GdriveAccessInfo gdriveExchangeAuthCode(const GdriveAuthCode& authCode, int timeoutSec) //throw SysError
{
//https://developers.google.com/identity/protocols/OAuth2InstalledApp#exchange-authorization-code
const std::string postBuf = xWwwFormUrlEncode(
{
{"code", authCode.code},
{"client_id", getGdriveClientId()},
{"client_secret", getGdriveClientSecret()},
{"redirect_uri", authCode.redirectUrl},
{"grant_type", "authorization_code"},
{"code_verifier", authCode.codeChallenge},
});
std::string response;
googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/token", {} /*extraHeaders*/, {{CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, timeoutSec); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> accessToken = getPrimitiveFromJsonObject(jresponse, "access_token");
const std::optional<std::string> refreshToken = getPrimitiveFromJsonObject(jresponse, "refresh_token");
const std::optional<std::string> expiresIn = getPrimitiveFromJsonObject(jresponse, "expires_in"); //e.g. 3600 seconds
if (!accessToken || !refreshToken || !expiresIn)
throw SysError(formatGdriveErrorRaw(response));
const GdriveUser userInfo = getGdriveUser({*accessToken, timeoutSec}); //throw SysError
return {{*accessToken, std::time(nullptr) + stringTo<time_t>(*expiresIn)}, *refreshToken, userInfo};
}
GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const std::function<void()>& updateGui /*throw X*/, int timeoutSec) //throw SysError, X
{
//spin up a web server to wait for the HTTP GET after Google authentication
const addrinfo hints
{
.ai_flags =
AI_ADDRCONFIG | //no such issue on Linux: https://bugs.chromium.org/p/chromium/issues/detail?id=5234
AI_PASSIVE, //the returned socket addresses will be suitable for bind(2)ing a socket that will accept(2) connections.
.ai_family = AF_UNSPEC, //don't care if AF_INET or AF_INET6
.ai_socktype = SOCK_STREAM, //we *do* care about this one!
};
addrinfo* servinfo = nullptr;
ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo));
//ServiceName == "0": open the next best free port
const int rcGai = ::getaddrinfo(nullptr, //_In_opt_ PCSTR pNodeName
"0", //_In_opt_ PCSTR pServiceName
&hints, //_In_opt_ const ADDRINFOA* pHints
&servinfo); //_Outptr_ PADDRINFOA* ppResult
if (rcGai != 0)
THROW_LAST_SYS_ERROR_GAI(rcGai);
if (!servinfo)
throw SysError(formatSystemError("getaddrinfo", L"" /*errorCode*/, L"No local IP address available"));
const auto getBoundSocket = [](const auto& /*::addrinfo*/ ai)
{
SocketType testSocket = ::socket(ai.ai_family, //int socket_family
SOCK_CLOEXEC |
ai.ai_socktype, //int socket_type
ai.ai_protocol); //int protocol
if (testSocket == invalidSocket)
THROW_LAST_SYS_ERROR_WSA("socket");
ZEN_ON_SCOPE_FAIL(closeSocket(testSocket));
if (::bind(testSocket, ai.ai_addr, static_cast<int>(ai.ai_addrlen)) != 0)
THROW_LAST_SYS_ERROR_WSA("bind");
return testSocket;
};
SocketType socket = invalidSocket;
std::optional<SysError> firstError;
for (const auto* /*::addrinfo*/ si = servinfo; si; si = si->ai_next)
if (si->ai_family == AF_INET ||
si->ai_family == AF_INET6)
try
{
socket = getBoundSocket(*si); //throw SysError; pass ownership
break;
}
catch (const SysError& e) { if (!firstError) firstError = e; }
if (socket == invalidSocket)
{
if (firstError)
throw* firstError;
throw SysError(formatSystemError("getaddrinfo", L"" /*errorCode*/, L"No local IPv4 or IPv6 address available"));
}
ZEN_ON_SCOPE_EXIT(closeSocket(socket));
sockaddr_storage addr = {}; //"sufficiently large to store address information for IPv4 (AF_INET/sockaddr_in) or IPv6 (AF_INET6/sockaddr_in6)"
socklen_t addrLen = sizeof(addr);
if (::getsockname(socket, reinterpret_cast<sockaddr*>(&addr), &addrLen) != 0)
THROW_LAST_SYS_ERROR_WSA("getsockname");
std::string redirectUrl;
if (addr.ss_family == AF_INET)
{
//the socket is not bound to a specific local IP:
// char buf[INET_ADDRSTRLEN] = {}; //inet_ntop -> "0.0.0.0"
// inet_ntop(AF_INET, &reinterpret_cast<const sockaddr_in&>(addr).sin_addr, buf, std::size(buf));
const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port);
redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port);
}
else if (addr.ss_family == AF_INET6) //inet_ntop() == "::"
{
const int port = ntohs(reinterpret_cast<const sockaddr_in6&>(addr).sin6_port);
redirectUrl = "http://[::1]:" + numberTo<std::string>(port);
}
else
throw SysError(formatSystemError("getsockname", L"", L"Unexpected protocol family: " + numberTo<std::wstring>(addr.ss_family)));
if (::listen(socket, SOMAXCONN) != 0)
THROW_LAST_SYS_ERROR_WSA("listen");
//"A code_verifier is a high-entropy cryptographic random string using the unreserved characters:"
//[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length of 43 characters and a maximum length of 128 characters.
std::string codeChallenge = stringEncodeBase64(generateGUID() + generateGUID());
replace(codeChallenge, '+', '-'); //
replace(codeChallenge, '/', '.'); //base64 is almost a perfect fit for code_verifier!
replace(codeChallenge, '=', '_'); //
assert(codeChallenge.size() == 44);
//authenticate Google Drive via browser: https://developers.google.com/identity/protocols/OAuth2InstalledApp#step-2-send-a-request-to-googles-oauth-20-server
const std::string oauthUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + xWwwFormUrlEncode(
{
{"client_id", getGdriveClientId()},
{"redirect_uri", redirectUrl},
{"response_type", "code"},
{"scope", "https://www.googleapis.com/auth/drive"},
{"code_challenge", codeChallenge},
{"code_challenge_method", "plain"},
{"login_hint", gdriveLoginHint},
});
try
{
openWithDefaultApp(utfTo<Zstring>(oauthUrl)); //throw FileError
}
catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError
//process incoming HTTP requests
for (;;)
{
for (;;) //::accept() blocks forever if no client connects (e.g. user just closes the browser window!) => wait for incoming traffic with a time-out via ::select()
{
if (updateGui) updateGui(); //throw X
const int waitTimeMs = 100;
pollfd fds[] = {{socket, POLLIN}};
const char* functionName = "poll";
const int rv = ::poll(fds, std::size(fds), waitTimeMs); //int timeout
if (rv < 0)
THROW_LAST_SYS_ERROR_WSA(functionName);
else if (rv != 0)
break;
//else: time-out!
}
//potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang
const int clientSocket = ::accept4(socket, //int sockfd
nullptr, //sockaddr* addr
nullptr, //socklen_t* addrlen
SOCK_CLOEXEC); //int flags
if (clientSocket == invalidSocket)
THROW_LAST_SYS_ERROR_WSA("accept");
//receive first line of HTTP request
std::string reqLine;
for (;;)
{
const size_t blockSize = 64 * 1024;
reqLine.resize(reqLine.size() + blockSize);
const size_t bytesReceived = tryReadSocket(clientSocket, &*(reqLine.end() - blockSize), blockSize); //throw SysError
reqLine.resize(reqLine.size() - (blockSize - bytesReceived)); //caveat: unsigned arithmetics
if (contains(reqLine, "\r\n"))
{
reqLine = beforeFirst(reqLine, "\r\n", IfNotFoundReturn::none);
break;
}
if (bytesReceived == 0 || reqLine.size() >= 100'000 /*bogus line length*/)
break;
}
//get OAuth2.0 authorization result from Google, either:
std::string code;
std::string error;
//parse header; e.g.: GET http://127.0.0.1:62054/?code=4/ZgBRsB9k68sFzc1Pz1q0__Kh17QK1oOmetySrGiSliXt6hZtTLUlYzm70uElNTH9vt1OqUMzJVeFfplMsYsn4uI HTTP/1.1
const std::vector<std::string_view> statusItems = splitCpy<std::string_view>(reqLine, ' ', SplitOnEmpty::allow); //Method SP Request-URI SP HTTP-Version CRLF
if (statusItems.size() == 3 && statusItems[0] == "GET" && startsWith(statusItems[2], "HTTP/"))
{
for (const auto& [name, value] : xWwwFormUrlDecode(afterFirst(statusItems[1], "?", IfNotFoundReturn::none)))
if (name == "code")
code = value;
else if (name == "error")
error = value; //e.g. "access_denied" => no more detailed error info available :(
} //"add explicit braces to avoid dangling else [-Wdangling-else]"
std::variant<std::monostate, GdriveAccessInfo, SysError> authResult;
//send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line
std::string httpResponse;
if (code.empty() && error.empty()) //parsing error or unrelated HTTP request
httpResponse = "HTTP/1.0 400 Bad Request" "\r\n" "\r\n" "400 Bad Request\n" + reqLine;
else
{
std::string htmlMsg = R"(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE_PLACEHOLDER</title>
<style>
* {
font-family: -apple-system, 'Segoe UI', arial, Tahoma, Helvetica, sans-serif;
text-align: center;
background-color: #eee; }
h1 {
font-size: 45px;
font-weight: 300;
margin: 80px 0 20px 0; }
.descr {
font-size: 21px;
font-weight: 200; }
</style>
</head>
<body>
<h1><img src="https://freefilesync.org/images/FreeFileSync.png" style="vertical-align:middle; height:50px;" alt=""> TITLE_PLACEHOLDER</h1>
<div class="descr">MESSAGE_PLACEHOLDER</div>
</body>
</html>
)";
try
{
if (!error.empty())
throw SysError(replaceCpy(_("Error code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\""));
//do as many login-related tasks as possible while we have the browser as an error output device!
//see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h!
authResult = gdriveExchangeAuthCode({code, redirectUrl, codeChallenge}, timeoutSec); //throw SysError
replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication completed.")));
replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(_("You may close this page now and continue with FreeFileSync.")));
}
catch (const SysError& e)
{
authResult = e;
replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication failed.")));
replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive") + L"\n\n" + e.toString()));
}
httpResponse = "HTTP/1.0 200 OK" "\r\n"
"Content-Type: text/html" "\r\n"
"Content-Length: " + numberTo<std::string>(strLength(htmlMsg)) + "\r\n"
"\r\n" + htmlMsg;
}
for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;)
bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError
shutdownSocketSend(clientSocket); //throw SysError
//---------------------------------------------------------------
if (const SysError* e = std::get_if<SysError>(&authResult))
throw *e;
if (const GdriveAccessInfo* res = std::get_if<GdriveAccessInfo>(&authResult))
return *res;
}
}
GdriveAccessToken gdriveRefreshAccess(const std::string& refreshToken, int timeoutSec) //throw SysError
{
//https://developers.google.com/identity/protocols/OAuth2InstalledApp#offline
const std::string postBuf = xWwwFormUrlEncode(
{
{"refresh_token", refreshToken},
{"client_id", getGdriveClientId()},
{"client_secret", getGdriveClientSecret()},
{"grant_type", "refresh_token"},
});
std::string response;
googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/token", {} /*extraHeaders*/, {{CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, timeoutSec); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> accessToken = getPrimitiveFromJsonObject(jresponse, "access_token");
const std::optional<std::string> expiresIn = getPrimitiveFromJsonObject(jresponse, "expires_in"); //e.g. 3600 seconds
if (!accessToken || !expiresIn)
throw SysError(formatGdriveErrorRaw(response));
return {*accessToken, std::time(nullptr) + stringTo<time_t>(*expiresIn)};
}
void gdriveRevokeAccess(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke
std::string response;
const HttpSession::Result httpResult = googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/revoke?token=" + access.token,
{"Content-Type: application/x-www-form-urlencoded"}, {{ CURLOPT_POSTFIELDS, ""}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access.timeoutSec); //throw SysError
if (httpResult.statusCode != 200)
throw SysError(formatGdriveErrorRaw(response));
}
int64_t gdriveGetMyDriveFreeSpace(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/about
std::string response;
gdriveHttpsRequest("/drive/v3/about?fields=storageQuota", {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
if (const JsonValue* storageQuota = getChildFromJsonObject(jresponse, "storageQuota"))
{
const std::optional<std::string> usage = getPrimitiveFromJsonObject(*storageQuota, "usage");
const std::optional<std::string> limit = getPrimitiveFromJsonObject(*storageQuota, "limit");
if (usage)
{
if (!limit) //"will not be present if the user has unlimited storage."
return std::numeric_limits<int64_t>::max();
const auto bytesUsed = stringTo<int64_t>(*usage);
const auto bytesLimit = stringTo<int64_t>(*limit);
if (0 <= bytesUsed && bytesUsed <= bytesLimit)
return bytesLimit - bytesUsed;
}
}
throw SysError(formatGdriveErrorRaw(response));
}
//instead of the "root" alias Google uses an actual ID in file metadata
std::string /*itemId*/ getMyDriveId(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/get
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "id"},
});
std::string response;
gdriveHttpsRequest("/drive/v3/files/root?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
struct DriveDetails
{
std::string driveId;
Zstring driveName;
};
std::vector<DriveDetails> getSharedDrives(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/drives/list
std::vector<DriveDetails> sharedDrives;
{
std::optional<std::string> nextPageToken;
do
{
std::string queryParams = xWwwFormUrlEncode(
{
{"pageSize", "100"}, //"[1, 100] Default: 10"
{"fields", "nextPageToken,drives(id,name)"},
});
if (nextPageToken)
queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}});
std::string response;
gdriveHttpsRequest("/drive/v3/drives?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
/**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken");
const JsonValue* drives = getChildFromJsonObject (jresponse, "drives");
if (!drives || drives->type != JsonValue::Type::array)
throw SysError(formatGdriveErrorRaw(response));
for (const JsonValue& driveVal : drives->arrayVal)
{
std::optional<std::string> driveId = getPrimitiveFromJsonObject(driveVal, "id");
std::optional<std::string> driveName = getPrimitiveFromJsonObject(driveVal, "name");
if (!driveId || !driveName || driveName->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(driveVal)));
sharedDrives.push_back({std::move(*driveId), utfTo<Zstring>(*driveName)});
}
}
while (nextPageToken);
}
return sharedDrives;
}
struct StarredFolderDetails
{
std::string folderId;
Zstring folderName;
std::string sharedDriveId; //empty if on "My Drive"
};
std::vector<StarredFolderDetails> getStarredFolders(const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/list
std::vector<StarredFolderDetails> starredFolders;
{
std::optional<std::string> nextPageToken;
do
{
std::string queryParams = xWwwFormUrlEncode(
{
{"corpora", "allDrives"}, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list
{"includeItemsFromAllDrives", "true"},
{"pageSize", "1000"}, //"[1, 1000] Default: 100"
{"q", std::string("starred and mimeType = '") + gdriveFolderMimeType + "' and not trashed"},
{"spaces", "drive"},
{"supportsAllDrives", "true"},
{"fields", "nextPageToken,incompleteSearch,files(id,name,driveId)"}, //https://developers.google.com/drive/api/v3/reference/files
});
if (nextPageToken)
queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}});
std::string response;
gdriveHttpsRequest("/drive/v3/files?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
/**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken");
const std::optional<std::string> incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch");
const JsonValue* files = getChildFromJsonObject (jresponse, "files");
if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array)
throw SysError(formatGdriveErrorRaw(response));
for (const JsonValue& childVal : files->arrayVal)
{
assert(childVal.type == JsonValue::Type::object);
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "id");
const std::optional<std::string> itemName = getPrimitiveFromJsonObject(childVal, "name");
const std::optional<std::string> driveId = getPrimitiveFromJsonObject(childVal, "driveId");
if (!itemId || itemId->empty() || !itemName || itemName->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
starredFolders.push_back({*itemId,
utfTo<Zstring>(*itemName),
driveId ? *driveId : ""});
}
}
while (nextPageToken);
}
return starredFolders;
}
enum class GdriveItemType : unsigned char
{
file,
folder,
shortcut,
};
enum class FileOwner : unsigned char
{
none, //"ownedByMe" not populated for items in Shared Drives.
me,
other,
};
struct GdriveItemDetails
{
Zstring itemName;
uint64_t fileSize = 0;
time_t modTime = 0;
//--- minimize padding ---
GdriveItemType type = GdriveItemType::file;
FileOwner owner = FileOwner::none;
//------------------------
std::string targetId; //for GdriveItemType::shortcut: https://developers.google.com/drive/api/v3/shortcuts
std::vector<std::string> parentIds;
bool operator==(const GdriveItemDetails&) const = default;
};
GdriveItemDetails extractItemDetails(const JsonValue& jvalue) //throw SysError
{
assert(jvalue.type == JsonValue::Type::object);
/**/ std::optional<std::string> itemName = getPrimitiveFromJsonObject(jvalue, "name");
const std::optional<std::string> mimeType = getPrimitiveFromJsonObject(jvalue, "mimeType");
const std::optional<std::string> ownedByMe = getPrimitiveFromJsonObject(jvalue, "ownedByMe");
const std::optional<std::string> size = getPrimitiveFromJsonObject(jvalue, "size");
const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(jvalue, "modifiedTime");
const JsonValue* parents = getChildFromJsonObject (jvalue, "parents");
const JsonValue* shortcut = getChildFromJsonObject (jvalue, "shortcutDetails");
if (!itemName || itemName->empty() || !mimeType || !modifiedTime)
throw SysError(formatGdriveErrorRaw(serializeJson(jvalue)));
const GdriveItemType type = *mimeType == gdriveFolderMimeType ? GdriveItemType::folder :
*mimeType == gdriveShortcutMimeType ? GdriveItemType::shortcut :
GdriveItemType::file;
const FileOwner owner = ownedByMe ? (*ownedByMe == "true" ? FileOwner::me : FileOwner::other) : FileOwner::none; //"Not populated for items in Shared Drives"
const uint64_t fileSize = size ? stringTo<uint64_t>(*size) : 0; //not available for folders and shortcuts
//RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IfNotFoundReturn::all));
if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix
throw SysError(L"Modification time is invalid. (" + utfTo<std::wstring>(*modifiedTime) + L')');
const auto [modTime, timeValid] = utcToTimeT(tc);
if (!timeValid)
throw SysError(L"Modification time is invalid. (" + utfTo<std::wstring>(*modifiedTime) + L')');
std::vector<std::string> parentIds;
if (parents) //item without "parents" array is possible! e.g. 1. shared item located in "Shared with me", referenced via a Shortcut 2. root folder under "Computers"
for (const JsonValue& parentVal : parents->arrayVal)
{
if (parentVal.type != JsonValue::Type::string)
throw SysError(formatGdriveErrorRaw(serializeJson(jvalue)));
parentIds.emplace_back(parentVal.primVal);
}
if (!!shortcut != (type == GdriveItemType::shortcut))
throw SysError(formatGdriveErrorRaw(serializeJson(jvalue)));
std::string targetId;
if (shortcut)
{
std::optional<std::string> targetItemId = getPrimitiveFromJsonObject(*shortcut, "targetId");
if (!targetItemId || targetItemId->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(jvalue)));
targetId = std::move(*targetItemId);
//evaluate "targetMimeType" ? don't bother: "The MIME type of a shortcut can become stale"!
}
return {utfTo<Zstring>(*itemName), fileSize, modTime, type, owner, std::move(targetId), std::move(parentIds)};
}
GdriveItemDetails getItemDetails(const std::string& itemId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/get
const std::string& queryParams = xWwwFormUrlEncode(
{
{"fields", "trashed,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId)"},
{"supportsAllDrives", "true"},
});
std::string response;
gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
try
{
const JsonValue jvalue = parseJson(response); //throw JsonParsingError
//careful: do NOT return details about trashed items! they don't exist as far as FFS is concerned!!!
const std::optional<std::string> trashed = getPrimitiveFromJsonObject(jvalue, "trashed");
if (!trashed)
throw SysError(formatGdriveErrorRaw(response));
else if (*trashed == "true")
throw SysError(L"Item has been trashed.");
return extractItemDetails(jvalue); //throw SysError
}
catch (JsonParsingError&) { throw SysError(formatGdriveErrorRaw(response)); }
}
struct GdriveItem
{
std::string itemId;
GdriveItemDetails details;
};
std::vector<GdriveItem> readFolderContent(const std::string& folderId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/list
std::vector<GdriveItem> childItems;
{
std::optional<std::string> nextPageToken;
do
{
std::string queryParams = xWwwFormUrlEncode(
{
{"corpora", "allDrives"}, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list
{"includeItemsFromAllDrives", "true"},
{"pageSize", "1000"}, //"[1, 1000] Default: 100"
{"q", "'" + folderId + "' in parents and not trashed"},
{"spaces", "drive"},
{"supportsAllDrives", "true"},
{"fields", "nextPageToken,incompleteSearch,files(id,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId))"}, //https://developers.google.com/drive/api/v3/reference/files
});
if (nextPageToken)
queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}});
std::string response;
gdriveHttpsRequest("/drive/v3/files?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
/**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken");
const std::optional<std::string> incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch");
const JsonValue* files = getChildFromJsonObject (jresponse, "files");
if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array)
throw SysError(formatGdriveErrorRaw(response));
for (const JsonValue& childVal : files->arrayVal)
{
std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "id");
if (!itemId || itemId->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
GdriveItemDetails itemDetails(extractItemDetails(childVal)); //throw SysError
assert(std::find(itemDetails.parentIds.begin(), itemDetails.parentIds.end(), folderId) != itemDetails.parentIds.end());
childItems.push_back({std::move(*itemId), std::move(itemDetails)});
}
}
while (nextPageToken);
}
return childItems;
}
struct FileChange
{
std::string itemId;
std::optional<GdriveItemDetails> details; //empty if item was deleted/trashed
};
struct DriveChange
{
std::string driveId;
Zstring driveName; //empty if shared drive was deleted
};
struct ChangesDelta
{
std::string newStartPageToken;
std::vector<FileChange> fileChanges;
std::vector<DriveChange> driveChanges;
};
ChangesDelta getChangesDelta(const std::string& sharedDriveId /*empty for "My Drive"*/, const std::string& startPageToken, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/changes/list
ChangesDelta delta;
std::optional<std::string> nextPageToken = startPageToken;
for (;;)
{
std::string queryParams = xWwwFormUrlEncode(
{
{"pageToken", *nextPageToken},
{"fields", "kind,nextPageToken,newStartPageToken,changes(kind,changeType,removed,fileId,file(trashed,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId)),driveId,drive(name))"},
{"includeItemsFromAllDrives", "true"}, //semantics are a mess https://developers.google.com/drive/api/v3/enable-shareddrives https://freefilesync.org/forum/viewtopic.php?t=7827&start=30#p29712
//in short: if driveId is set: required, but blatant lie; only drive-specific file changes returned
// if no driveId set: optional, but blatant lie; only changes to drive objects are returned, but not contained files (with a few exceptions)
{"pageSize", "1000"}, //"[1, 1000] Default: 100"
{"spaces", "drive"},
{"supportsAllDrives", "true"},
//do NOT "restrictToMyDrive": we're also interested in "Shared with me" items, which might be referenced by a shortcut in "My Drive"
});
if (!sharedDriveId.empty())
queryParams += '&' + xWwwFormUrlEncode({{"driveId", sharedDriveId}}); //only allowed for shared drives!
std::string response;
gdriveHttpsRequest("/drive/v3/changes?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
/**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken");
const std::optional<std::string> newStartPageToken = getPrimitiveFromJsonObject(jresponse, "newStartPageToken");
const std::optional<std::string> listKind = getPrimitiveFromJsonObject(jresponse, "kind");
const JsonValue* changes = getChildFromJsonObject (jresponse, "changes");
if (!!nextPageToken == !!newStartPageToken || //there can be only one
!listKind || *listKind != "drive#changeList" ||
!changes || changes->type != JsonValue::Type::array)
throw SysError(formatGdriveErrorRaw(response));
for (const JsonValue& childVal : changes->arrayVal)
{
const std::optional<std::string> kind = getPrimitiveFromJsonObject(childVal, "kind");
const std::optional<std::string> changeType = getPrimitiveFromJsonObject(childVal, "changeType");
const std::optional<std::string> removed = getPrimitiveFromJsonObject(childVal, "removed");
if (!kind || *kind != "drive#change" || !changeType || !removed)
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
if (*changeType == "file")
{
std::optional<std::string> fileId = getPrimitiveFromJsonObject(childVal, "fileId");
if (!fileId || fileId->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
FileChange change;
change.itemId = std::move(*fileId);
if (*removed != "true")
{
const JsonValue* file = getChildFromJsonObject(childVal, "file");
if (!file)
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
const std::optional<std::string> trashed = getPrimitiveFromJsonObject(*file, "trashed");
if (!trashed)
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
if (*trashed != "true")
change.details = extractItemDetails(*file); //throw SysError
}
delta.fileChanges.push_back(std::move(change));
}
else if (*changeType == "drive")
{
std::optional<std::string> driveId = getPrimitiveFromJsonObject(childVal, "driveId");
if (!driveId || driveId->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
DriveChange change;
change.driveId = std::move(*driveId);
if (*removed != "true")
{
const JsonValue* drive = getChildFromJsonObject(childVal, "drive");
if (!drive)
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
const std::optional<std::string> name = getPrimitiveFromJsonObject(*drive, "name");
if (!name || name->empty())
throw SysError(formatGdriveErrorRaw(serializeJson(childVal)));
change.driveName = utfTo<Zstring>(*name);
}
delta.driveChanges.push_back(std::move(change));
}
else assert(false); //no other types (yet!)
}
if (!nextPageToken)
{
delta.newStartPageToken = *newStartPageToken;
return delta;
}
}
}
std::string /*startPageToken*/ getChangesCurrentToken(const std::string& sharedDriveId /*empty for "My Drive"*/, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/changes/getStartPageToken
std::string queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
});
if (!sharedDriveId.empty())
queryParams += '&' + xWwwFormUrlEncode({{"driveId", sharedDriveId}}); //only allowed for shared drives!
std::string response;
gdriveHttpsRequest("/drive/v3/changes/startPageToken?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> startPageToken = getPrimitiveFromJsonObject(jresponse, "startPageToken");
if (!startPageToken)
throw SysError(formatGdriveErrorRaw(response));
return *startPageToken;
}
//- if item is a folder: deletes recursively!!!
//- even deletes a hardlink with multiple parents => use gdriveUnlinkParent() first
void gdriveDeleteItem(const std::string& itemId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/delete
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
});
std::string response;
const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams,
{} /*extraHeaders*/, {{CURLOPT_CUSTOMREQUEST, "DELETE"}}, [&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
if (response.empty() && httpResult.statusCode == 204)
return; //"If successful, this method returns an empty response body"
throw SysError(formatGdriveErrorRaw(response));
}
//item is NOT deleted when last parent is removed: it is just not accessible via the "My Drive" hierarchy but still adds to quota! => use for hard links only!
void gdriveUnlinkParent(const std::string& itemId, const std::string& parentId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/update
const std::string& queryParams = xWwwFormUrlEncode(
{
{"removeParents", parentId},
{"supportsAllDrives", "true"},
{"fields", "id,parents"}, //for test if operation was successful
});
std::string response;
const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, { CURLOPT_POSTFIELDS, "{}"}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
if (response.empty() && httpResult.statusCode == 204)
return; //removing last parent of item not owned by us returns "204 No Content" (instead of 200 + file body)
JsonValue jresponse;
try { jresponse = parseJson(response); /*throw JsonParsingError*/ }
catch (const JsonParsingError&) {}
const std::optional<std::string> id = getPrimitiveFromJsonObject(jresponse, "id"); //id is returned on "success", unlike "parents", see below...
const JsonValue* parents = getChildFromJsonObject(jresponse, "parents");
if (!id || *id != itemId)
throw SysError(formatGdriveErrorRaw(response));
if (parents) //when last parent is removed, Google does NOT return the parents array (not even an empty one!)
if (parents->type != JsonValue::Type::array ||
std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(),
[&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentId; }))
throw SysError(L"gdriveUnlinkParent: Google Drive internal failure"); //user should never see this...
}
//- if item is a folder: trashes recursively!!!
//- a hardlink with multiple parents will NOT be accessible anymore via any of its path aliases!
void gdriveMoveToTrash(const std::string& itemId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/update
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "trashed"},
});
const std::string postBuf = R"({ "trashed": true })";
std::string response;
gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); /*throw JsonParsingError*/ }
catch (const JsonParsingError&) {}
const std::optional<std::string> trashed = getPrimitiveFromJsonObject(jresponse, "trashed");
if (!trashed || *trashed != "true")
throw SysError(formatGdriveErrorRaw(response));
}
//folder name already existing? will (happily) create duplicate => caller must check!
std::string /*folderId*/ gdriveCreateFolderPlain(const Zstring& folderName, const std::string& parentId, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/folder#creating_a_folder
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "id"},
});
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.set("mimeType", gdriveFolderMimeType);
postParams.objectVal.set("name", utfTo<std::string>(folderName));
postParams.objectVal.set("parents", std::vector<JsonValue> {JsonValue(parentId)});
const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
std::string response;
gdriveHttpsRequest("/drive/v3/files?" + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
//shortcut name already existing? will (happily) create duplicate => caller must check!
std::string /*shortcutId*/ gdriveCreateShortcutPlain(const Zstring& shortcutName, const std::string& parentId, const std::string& targetId, const GdriveAccess& access) //throw SysError
{
/* https://developers.google.com/drive/api/v3/shortcuts
- targetMimeType is determined automatically (ignored if passed)
- creating shortcuts to shortcuts fails with "Internal Error" */
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "id"},
});
JsonValue shortcutDetails(JsonValue::Type::object);
shortcutDetails.objectVal.set("targetId", targetId);
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.set("mimeType", gdriveShortcutMimeType);
postParams.objectVal.set("name", utfTo<std::string>(shortcutName));
postParams.objectVal.set("parents", std::vector<JsonValue> {JsonValue(parentId)});
postParams.objectVal.set("shortcutDetails", std::move(shortcutDetails));
const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
std::string response;
gdriveHttpsRequest("/drive/v3/files?" + queryParams, {"Content-Type: application/json; charset=UTF-8"},
{{CURLOPT_POSTFIELDS, postBuf.c_str()}}, [&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
//target name already existing? will (happily) create duplicate items => caller must check!
//can copy files + shortcuts (but fails for folders) + Google-specific file types (.gdoc, .gsheet, .gslides)
std::string /*fileId*/ gdriveCopyFile(const std::string& fileId, const std::string& parentIdTo, const Zstring& newName, time_t newModTime, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/copy
const std::string queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "id"},
});
//more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround:
//RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error
if (modTimeRfc.empty())
throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(newModTime) + L')');
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.set("name", utfTo<std::string>(newName));
postParams.objectVal.set("parents", std::vector<JsonValue> {JsonValue(parentIdTo)});
postParams.objectVal.set("modifiedTime", modTimeRfc);
const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
std::string response;
gdriveHttpsRequest("/drive/v3/files/" + fileId + "/copy?" + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); /*throw JsonParsingError*/ }
catch (const JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
//target name already existing? will (happily) create duplicate items => caller must check!
void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo,
const Zstring& newName, time_t newModTime, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/folder#moving_files_between_folders
std::string queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "name,parents"}, //for test if operation was successful
});
if (parentIdFrom != parentIdTo)
queryParams += '&' + xWwwFormUrlEncode(
{
{"removeParents", parentIdFrom},
{"addParents", parentIdTo},
});
//more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround:
//RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error
if (modTimeRfc.empty())
throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(newModTime) + L')');
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.set("name", utfTo<std::string>(newName));
postParams.objectVal.set("modifiedTime", modTimeRfc);
const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
std::string response;
gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); /*throw JsonParsingError*/ }
catch (const JsonParsingError&) {}
const std::optional<std::string> name = getPrimitiveFromJsonObject(jresponse, "name");
const JsonValue* parents = getChildFromJsonObject(jresponse, "parents");
if (!name || *name != utfTo<std::string>(newName) ||
!parents || parents->type != JsonValue::Type::array)
throw SysError(formatGdriveErrorRaw(response));
if (!std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(),
[&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentIdTo; }))
throw SysError(formatSystemError("gdriveMoveAndRenameItem", L"", L"Google Drive internal failure.")); //user should never see this...
}
#if 0
void setModTime(const std::string& itemId, time_t modTime, const GdriveAccess& access) //throw SysError
{
//https://developers.google.com/drive/api/v3/reference/files/update
//RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
const std::string& modTimeRfc = formatTime<std::string>("%Y-%m-%dT%H:%M:%S.000Z", getUtcTime2(modTime)); //returns empty string on error
if (modTimeRfc.empty())
throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(modTime) + L')');
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"fields", "modifiedTime"},
});
const std::string postBuf = R"({ "modifiedTime": ")" + modTimeRfc + "\" }";
std::string response;
gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError
JsonValue jresponse;
try { jresponse = parseJson(response); /*throw JsonParsingError*/ }
catch (const JsonParsingError&) {}
const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(jresponse, "modifiedTime");
if (!modifiedTime || *modifiedTime != modTimeRfc)
throw SysError(formatGdriveErrorRaw(response));
}
#endif
DEFINE_NEW_SYS_ERROR(SysErrorAbusiveFile)
void gdriveDownloadFileImpl(const std::string& fileId, const std::function<void(const void* buffer, size_t bytesToWrite)>& writeBlock /*throw X*/, //throw SysError, SysErrorAbusiveFile, X
bool acknowledgeAbuse, const GdriveAccess& access)
{
/* https://developers.google.com/drive/api/v3/manage-downloads
doesn't work for Google-specific file types, but Google Backup & Sync still "downloads" them:
- in some JSON-like file format:
{"url": "https://docs.google.com/open?id=FILE_ID", "doc_id": "FILE_ID", "email": "ACCOUNT_EMAIL"}
- adds artificial file extensions: .gdoc, .gsheet, .gslides, ...
- 2022-10-10: In "Google Drive for Desktop" the file content now looks like:
{"":"WARNING! DO NOT EDIT THIS FILE! ANY CHANGES MADE WILL BE LOST!","doc_id":"FILE_ID","resource_key":"","email":"ACCOUNT_EMAIL"} */
std::string queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"alt", "media"},
});
if (acknowledgeAbuse) //apply on demand only! https://freefilesync.org/forum/viewtopic.php?t=7520")
queryParams += '&' + xWwwFormUrlEncode({{"acknowledgeAbuse", "true"}});
std::string headBytes;
bool headBytesWritten = false;
const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + fileId + '?' + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/,
[&](std::span<const char> buf)
/* libcurl feeds us a shitload of tiny kB-sized zlib-decompressed pieces of data!
libcurl's zlib buffer is sized at ridiculous 16 kB!
=> if this ever becomes a perf issue: roll our own zlib decompression! */
{
if (headBytes.size() < 16 * 1024) //don't access writeBlock() yet in case of error! (=> support acknowledgeAbuse retry handling)
headBytes.append(buf.data(), buf.size());
else
{
if (!headBytesWritten)
{
headBytesWritten = true;
writeBlock(headBytes.c_str(), headBytes.size()); //throw X
}
writeBlock(buf.data(), buf.size()); //throw X
}
}, nullptr /*tryReadRequest*/, nullptr /*receiveHeader*/, access); //throw SysError, X
if (httpResult.statusCode / 100 != 2)
{
/* https://freefilesync.org/forum/viewtopic.php?t=7463 => HTTP status code 403 + body:
{ "error": { "errors": [{ "domain": "global",
"reason": "cannotDownloadAbusiveFile",
"message": "This file has been identified as malware or spam and cannot be downloaded." }],
"code": 403,
"message": "This file has been identified as malware or spam and cannot be downloaded." }} */
if (!headBytesWritten && httpResult.statusCode == 403 && contains(headBytes, "\"cannotDownloadAbusiveFile\""))
throw SysErrorAbusiveFile(formatGdriveErrorRaw(headBytes));
throw SysError(formatGdriveErrorRaw(headBytes));
}
if (!headBytesWritten && !headBytes.empty())
writeBlock(headBytes.c_str(), headBytes.size()); //throw X
}
void gdriveDownloadFile(const std::string& fileId, const std::function<void(const void* buffer, size_t bytesToWrite)>& writeBlock /*throw X*/, //throw SysError, X
const GdriveAccess& access)
{
try
{
gdriveDownloadFileImpl(fileId, writeBlock /*throw X*/, false /*acknowledgeAbuse*/, access); //throw SysError, SysErrorAbusiveFile, X
}
catch (SysErrorAbusiveFile&)
{
gdriveDownloadFileImpl(fileId, writeBlock /*throw X*/, true /*acknowledgeAbuse*/, access); //throw SysError, (SysErrorAbusiveFile), X
}
}
#if 0
//file name already existing? => duplicate file created!
//note: Google Drive upload is already transactional!
//upload "small files" (5 MB or less; enforced by Google?) in a single round-trip
std::string /*itemId*/ gdriveUploadSmallFile(const Zstring& fileName, const std::string& parentId, uint64_t streamSize, std::optional<time_t> modTime, //throw SysError, X
const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X; return "bytesToRead" bytes unless end of stream*/,
const GdriveAccess& access)
{
//https://developers.google.com/drive/api/v3/folder#inserting_a_file_in_a_folder
//https://developers.google.com/drive/api/v3/manage-uploads#http_1
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.emplace("name", utfTo<std::string>(fileName));
postParams.objectVal.emplace("parents", std::vector<JsonValue> {JsonValue(parentId)});
if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
{
const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime2(*modTime))); //returns empty string on error
if (modTimeRfc.empty())
throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(*modTime) + L')');
postParams.objectVal.emplace("modifiedTime", modTimeRfc);
}
const std::string& metaDataBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
//allowed chars for border: DIGIT ALPHA ' ( ) + _ , - . / : = ?
const std::string boundaryString = stringEncodeBase64(generateGUID() + generateGUID());
const std::string postBufHead = "--" + boundaryString + "\r\n"
"Content-Type: application/json; charset=UTF-8" "\r\n"
/**/ "\r\n" +
metaDataBuf + "\r\n"
"--" + boundaryString + "\r\n"
"Content-Type: application/octet-stream" "\r\n"
/**/ "\r\n";
const std::string postBufTail = "\r\n--" + boundaryString + "--";
auto readMultipartBlock = [&, headPos = size_t(0), eof = false, tailPos = size_t(0)](void* buffer, size_t bytesToRead) mutable -> size_t
{
const auto bufStart = buffer;
if (headPos < postBufHead.size())
{
const size_t junkSize = std::min<ptrdiff_t>(postBufHead.size() - headPos, bytesToRead);
std::memcpy(buffer, postBufHead.c_str() + headPos, junkSize);
headPos += junkSize;
buffer = static_cast<std::byte*>(buffer) + junkSize;
bytesToRead -= junkSize;
}
if (bytesToRead > 0)
{
if (!eof) //don't assume readBlock() will return streamSize bytes as promised => exhaust and let Google Drive fail if there is a mismatch in Content-Length!
{
const size_t bytesRead = readBlock(buffer, bytesToRead); //throw X; return "bytesToRead" bytes unless end of stream
buffer = static_cast<std::byte*>(buffer) + bytesRead;
bytesToRead -= bytesRead;
if (bytesToRead > 0)
eof = true;
}
if (bytesToRead > 0)
if (tailPos < postBufTail.size())
{
const size_t junkSize = std::min<ptrdiff_t>(postBufTail.size() - tailPos, bytesToRead);
std::memcpy(buffer, postBufTail.c_str() + tailPos, junkSize);
tailPos += junkSize;
buffer = static_cast<std::byte*>(buffer) + junkSize;
bytesToRead -= junkSize;
}
}
return static_cast<std::byte*>(buffer) -
static_cast<std::byte*>(bufStart);
};
TODO:
gzip-compress HTTP request body!
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"uploadType", "multipart"},
});
std::string response;
const HttpSession::Result httpResult = gdriveHttpsRequest("/upload/drive/v3/files?" + queryParams,
{
"Content-Type: multipart/related; boundary=" + boundaryString,
"Content-Length: " + numberTo<std::string>(postBufHead.size() + streamSize + postBufTail.size())
},
{{CURLOPT_POST, 1}}, //otherwise HttpSession::perform() will PUT
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
readMultipartBlock, nullptr /*receiveHeader*/, access); //throw SysError, X
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
#endif
//file name already existing? => duplicate file created!
//note: Google Drive upload is already transactional!
std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::string& parentId, std::optional<time_t> modTime, //throw SysError, X
const std::function<size_t(void* buffer, size_t bytesToRead)>& tryReadBlock /*throw X*/, //returning 0 signals EOF: Posix read() semantics
const GdriveAccess& access)
{
//https://developers.google.com/drive/api/v3/folder#inserting_a_file_in_a_folder
//https://developers.google.com/drive/api/v3/manage-uploads#resumable
//step 1: initiate resumable upload session
std::string uploadUrlRelative;
{
const std::string& queryParams = xWwwFormUrlEncode(
{
{"supportsAllDrives", "true"},
{"uploadType", "resumable"},
});
JsonValue postParams(JsonValue::Type::object);
postParams.objectVal.set("name", utfTo<std::string>(fileName));
postParams.objectVal.set("parents", std::vector<JsonValue> {JsonValue(parentId)});
if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z"
{
const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(*modTime))); //returns empty string on error
if (modTimeRfc.empty())
throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(*modTime) + L')');
postParams.objectVal.set("modifiedTime", modTimeRfc);
}
const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/);
//---------------------------------------------------
std::string uploadUrl;
auto onHeaderData = [&](const std::string_view& header)
{
//"The callback will be called once for each header and only complete header lines are passed on to the callback" (including \r\n at the end)
if (startsWithAsciiNoCase(header, "Location:"))
{
uploadUrl = header;
uploadUrl = afterFirst(uploadUrl, ':', IfNotFoundReturn::none);
trim(uploadUrl);
}
};
std::string response;
const HttpSession::Result httpResult = gdriveHttpsRequest("/upload/drive/v3/files?" + queryParams,
{"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}},
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); },
nullptr /*readRequest*/, onHeaderData, access); //throw SysError
if (httpResult.statusCode != 200)
throw SysError(formatGdriveErrorRaw(response));
if (!startsWith(uploadUrl, "https://www.googleapis.com/"))
throw SysError(L"Invalid upload URL: " + utfTo<std::wstring>(uploadUrl)); //user should never see this
uploadUrlRelative = afterFirst(uploadUrl, "googleapis.com", IfNotFoundReturn::none);
}
//---------------------------------------------------
//step 2: upload file content
//not officially documented, but Google Drive supports compressed file upload when "Content-Encoding: gzip" is set! :)))
InputStreamAsGzip gzipStream(tryReadBlock, GDRIVE_BLOCK_SIZE_UPLOAD); //throw SysError
auto readRequest = [&](std::span<char> buf) { return gzipStream.read(buf.data(), buf.size()); }; //throw SysError, X
std::string response; //don't need "Authorization: Bearer":
googleHttpsRequest(GOOGLE_REST_API_SERVER, uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/,
[&](std::span<const char> buf) { response.append(buf.data(), buf.size()); }, readRequest,
nullptr /*receiveHeader*/, access.timeoutSec); //throw SysError, X
JsonValue jresponse;
try { jresponse = parseJson(response); }
catch (JsonParsingError&) {}
const std::optional<std::string> itemId = getPrimitiveFromJsonObject(jresponse, "id");
if (!itemId)
throw SysError(formatGdriveErrorRaw(response));
return *itemId;
}
class GdriveAccessBuffer //per-user-session & drive! => serialize access (perf: amortized fully buffered!)
{
public:
//GdriveDrivesBuffer constructor calls GdriveAccessBuffer::getAccessToken()
explicit GdriveAccessBuffer(const GdriveAccessInfo& accessInfo) :
accessInfo_(accessInfo) {}
GdriveAccessBuffer(MemoryStreamIn& stream) //throw SysError
{
accessInfo_.accessToken.validUntil = readNumber<int64_t>(stream); //
accessInfo_.accessToken.value = readContainer<std::string>(stream); //
accessInfo_.refreshToken = readContainer<std::string>(stream); //SysErrorUnexpectedEos
accessInfo_.userInfo.displayName = utfTo<std::wstring>(readContainer<std::string>(stream)); //
accessInfo_.userInfo.email = readContainer<std::string>(stream); //
}
void serialize(MemoryStreamOut& stream) const
{
writeNumber<int64_t>(stream, accessInfo_.accessToken.validUntil);
static_assert(sizeof(accessInfo_.accessToken.validUntil) <= sizeof(int64_t)); //ensure cross-platform compatibility!
writeContainer(stream, accessInfo_.accessToken.value);
writeContainer(stream, accessInfo_.refreshToken);
writeContainer(stream, utfTo<std::string>(accessInfo_.userInfo.displayName));
writeContainer(stream, accessInfo_.userInfo.email);
}
//set *before* calling any of the subsequent functions; see GdrivePersistentSessions::accessUserSession()
void setContextTimeout(const std::weak_ptr<int>& timeoutSec) { timeoutSec_ = timeoutSec; }
GdriveAccess getAccessToken() //throw SysError
{
const int timeoutSec = getTimeoutSec();
if (accessInfo_.accessToken.validUntil <= std::time(nullptr) + timeoutSec + 5 /*some leeway*/) //expired/will expire
{
GdriveAccessToken token = gdriveRefreshAccess(accessInfo_.refreshToken, timeoutSec); //throw SysError
//"there are limits on the number of refresh tokens that will be issued"
//Google Drive access token is usually valid for one hour => fail on pathologic user-defined time out:
if (token.validUntil <= std::time(nullptr) + 2 * timeoutSec)
throw SysError(_("Please set up a shorter time out for Google Drive.") + L" [" + _P("1 sec", "%x sec", timeoutSec) + L']');
accessInfo_.accessToken = std::move(token);
}
return {accessInfo_.accessToken.value, timeoutSec};
}
const std::string& getUserEmail() const { return accessInfo_.userInfo.email; }
void update(const GdriveAccessInfo& accessInfo)
{
if (!equalAsciiNoCase(accessInfo.userInfo.email, accessInfo_.userInfo.email))
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
accessInfo_ = accessInfo;
}
private:
GdriveAccessBuffer (const GdriveAccessBuffer&) = delete;
GdriveAccessBuffer& operator=(const GdriveAccessBuffer&) = delete;
int getTimeoutSec() const
{
const std::shared_ptr<int> timeoutSec = timeoutSec_.lock();
assert(timeoutSec);
if (!timeoutSec)
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] GdriveAccessBuffer: Timeout duration was not set.");
return *timeoutSec;
}
GdriveAccessInfo accessInfo_;
std::weak_ptr<int> timeoutSec_;
};
class GdriveDrivesBuffer;
class GdriveFileState //per-user-session! => serialize access (perf: amortized fully buffered!)
{
public:
GdriveFileState(const std::string& driveId, //ID of shared drive or "My Drive": never empty!
const Zstring& sharedDriveName, //*empty* for "My Drive"
GdriveAccessBuffer& accessBuf) : //throw SysError
/* issue getChangesCurrentToken() as the very first Google Drive query! */
lastSyncToken_(getChangesCurrentToken(sharedDriveName.empty() ? std::string() : driveId, accessBuf.getAccessToken())), //throw SysError
driveId_(driveId),
sharedDriveName_(sharedDriveName),
accessBuf_(accessBuf) { assert(!driveId.empty() && sharedDriveName != Zstr("My Drive")); }
GdriveFileState(MemoryStreamIn& stream, GdriveAccessBuffer& accessBuf) : //throw SysError
accessBuf_(accessBuf)
{
lastSyncToken_ = readContainer<std::string>(stream); //
driveId_ = readContainer<std::string>(stream); //SysErrorUnexpectedEos
sharedDriveName_ = utfTo<Zstring>(readContainer<std::string>(stream)); //
for (;;)
{
const std::string folderId = readContainer<std::string>(stream); //SysErrorUnexpectedEos
if (folderId.empty())
break;
folderContents_[folderId].isKnownFolder = true;
}
for (;;)
{
const std::string itemId = readContainer<std::string>(stream); //SysErrorUnexpectedEos
if (itemId.empty())
break;
GdriveItemDetails details = {}; //read in correct sequence!
details.itemName = utfTo<Zstring>(readContainer<std::string>(stream)); //
details.type = readNumber<GdriveItemType>(stream); //
details.owner = readNumber <FileOwner>(stream); //
details.fileSize = readNumber <uint64_t>(stream); //SysErrorUnexpectedEos
details.modTime = static_cast<time_t>(readNumber<int64_t>(stream)); //
details.targetId = readContainer<std::string>(stream); //
size_t parentsCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos
while (parentsCount-- != 0)
details.parentIds.push_back(readContainer<std::string>(stream)); //SysErrorUnexpectedEos
updateItemState(itemId, &details);
}
}
void serialize(MemoryStreamOut& stream) const
{
writeContainer(stream, lastSyncToken_);
writeContainer(stream, driveId_);
writeContainer(stream, utfTo<std::string>(sharedDriveName_));
for (const auto& [folderId, content] : folderContents_)
if (folderId.empty())
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
else if (content.isKnownFolder)
writeContainer(stream, folderId);
writeContainer(stream, std::string()); //sentinel
auto serializeItem = [&](const std::string& itemId, const GdriveItemDetails& details)
{
writeContainer (stream, itemId);
writeContainer (stream, utfTo<std::string>(details.itemName));
writeNumber<GdriveItemType>(stream, details.type);
writeNumber <FileOwner>(stream, details.owner);
writeNumber <uint64_t>(stream, details.fileSize);
writeNumber <int64_t>(stream, details.modTime);
static_assert(sizeof(details.modTime) <= sizeof(int64_t)); //ensure cross-platform compatibility!
writeContainer(stream, details.targetId);
writeNumber(stream, static_cast<uint32_t>(details.parentIds.size()));
for (const std::string& parentId : details.parentIds)
writeContainer(stream, parentId);
};
//serialize + clean up: only save items in "known folders" + items referenced by shortcuts
for (const auto& [folderId, content] : folderContents_)
if (content.isKnownFolder)
for (const auto& itItem : content.childItems)
{
const auto& [itemId, details] = *itItem;
if (itemId.empty())
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
serializeItem(itemId, details);
if (details.type == GdriveItemType::shortcut)
{
if (details.targetId.empty())
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
if (auto it = itemDetails_.find(details.targetId);
it != itemDetails_.end())
serializeItem(details.targetId, it->second);
}
}
writeContainer(stream, std::string()); //sentinel
}
std::string getDriveId() const { return driveId_; }
Zstring getSharedDriveName() const { return sharedDriveName_; } //*empty* for "My Drive"
void setSharedDriveName(const Zstring& sharedDriveName) { sharedDriveName_ = sharedDriveName; }
struct PathStatus
{
std::string existingItemId;
GdriveItemType existingType = GdriveItemType::file;
AfsPath existingPath; //input path =: existingPath + relPath
std::vector<Zstring> relPath; //
};
PathStatus getPathStatus(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
const std::vector<Zstring> relPath = splitCpy(itemPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
if (relPath.empty())
return {locationRootId, GdriveItemType::folder, AfsPath(), {}};
else
return getPathStatusSub(locationRootId, AfsPath(), relPath, followLeafShortcut); //throw SysError
}
std::string /*itemId*/ getItemId(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
const GdriveFileState::PathStatus& ps = getPathStatus(locationRootId, itemPath, followLeafShortcut); //throw SysError
if (ps.relPath.empty())
return ps.existingItemId;
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
}
std::pair<std::string /*itemId*/, GdriveItemDetails> getFileAttributes(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
if (itemPath.value.empty()) //location root not covered by itemDetails_
{
GdriveItemDetails rootDetails
{
.type = GdriveItemType::folder,
//.itemName =... => better leave empty for a root item!
.owner = sharedDriveName_.empty() ? FileOwner::me : FileOwner::none,
};
return {locationRootId, std::move(rootDetails)};
}
const std::string itemId = getItemId(locationRootId, itemPath, followLeafShortcut); //throw SysError
if (auto it = itemDetails_.find(itemId);
it != itemDetails_.end())
return *it;
//itemId was already found! => (must either be a location root) or buffered in itemDetails_
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
}
std::optional<GdriveItemDetails> tryGetBufferedItemDetails(const std::string& itemId) const
{
if (auto it = itemDetails_.find(itemId);
it != itemDetails_.end())
return it->second;
return {};
}
std::optional<std::vector<GdriveItem>> tryGetBufferedFolderContent(const std::string& folderId) const
{
auto it = folderContents_.find(folderId);
if (it == folderContents_.end() || !it->second.isKnownFolder)
return std::nullopt;
std::vector<GdriveItem> childItems;
for (auto itChild : it->second.childItems)
{
const auto& [childId, childDetails] = *itChild;
childItems.push_back({childId, childDetails});
}
return std::move(childItems); //[!] need std::move!
}
//-------------- notifications --------------
using ItemIdDelta = std::unordered_set<std::string>;
struct FileStateDelta //as long as instance exists, GdriveItem will log all changed items
{
FileStateDelta() {}
private:
explicit FileStateDelta(const std::shared_ptr<const ItemIdDelta>& cids) : changedIds(cids) {}
friend class GdriveFileState;
std::shared_ptr<const ItemIdDelta> changedIds; //lifetime is managed by caller; access *only* by GdriveFileState!
};
void notifyFolderContent(const FileStateDelta& stateDelta, const std::string& folderId, const std::vector<GdriveItem>& childItems)
{
folderContents_[folderId].isKnownFolder = true;
for (const GdriveItem& item : childItems)
notifyItemUpdated(stateDelta, item.itemId, &item.details);
//- should we remove parent links for items that are not children of folderId anymore (as of this update)?? => fringe case during first update! (still: maybe trigger sync?)
//- what if there are multiple folder state updates incoming in wrong order!? => notifyItemUpdated() will sort it out!
}
void notifyItemCreated(const FileStateDelta& stateDelta, const GdriveItem& item)
{
notifyItemUpdated(stateDelta, item.itemId, &item.details);
}
void notifyItemUpdated(const FileStateDelta& stateDelta, const GdriveItem& item)
{
notifyItemUpdated(stateDelta, item.itemId, &item.details);
}
void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId)
{
GdriveItemDetails details
{
.itemName = folderName,
.modTime = std::time(nullptr),
.type = GdriveItemType::folder,
.owner = FileOwner::me,
.parentIds{parentId},
};
//avoid needless conflicts due to different Google Drive folder modTime!
if (auto it = itemDetails_.find(folderId); it != itemDetails_.end())
details.modTime = it->second.modTime;
notifyItemUpdated(stateDelta, folderId, &details);
}
void notifyShortcutCreated(const FileStateDelta& stateDelta, const std::string& shortcutId, const Zstring& shortcutName, const std::string& parentId, const std::string& targetId)
{
GdriveItemDetails details
{
.itemName = shortcutName,
.modTime = std::time(nullptr),
.type = GdriveItemType::shortcut,
.owner = FileOwner::me,
.targetId = targetId,
.parentIds{parentId},
};
//avoid needless conflicts due to different Google Drive folder modTime!
if (auto it = itemDetails_.find(shortcutId); it != itemDetails_.end())
details.modTime = it->second.modTime;
notifyItemUpdated(stateDelta, shortcutId, &details);
}
void notifyItemDeleted(const FileStateDelta& stateDelta, const std::string& itemId)
{
notifyItemUpdated(stateDelta, itemId, nullptr);
}
void notifyParentRemoved(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdOld)
{
if (auto it = itemDetails_.find(itemId); it != itemDetails_.end())
{
GdriveItemDetails detailsNew = it->second;
std::erase(detailsNew.parentIds, parentIdOld);
notifyItemUpdated(stateDelta, itemId, &detailsNew);
}
else //conflict!!!
markSyncDue();
}
void notifyMoveAndRename(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo, const Zstring& newName)
{
if (auto it = itemDetails_.find(itemId); it != itemDetails_.end())
{
GdriveItemDetails detailsNew = it->second;
detailsNew.itemName = newName;
std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdFrom || id == parentIdTo; }); //
detailsNew.parentIds.push_back(parentIdTo); //not a duplicate
notifyItemUpdated(stateDelta, itemId, &detailsNew);
}
else //conflict!!!
markSyncDue();
}
private:
GdriveFileState (const GdriveFileState&) = delete;
GdriveFileState& operator=(const GdriveFileState&) = delete;
friend class GdriveDrivesBuffer;
void notifyItemUpdated(const FileStateDelta& stateDelta, const std::string& itemId, const GdriveItemDetails* details)
{
if (!stateDelta.changedIds->contains(itemId)) //no conflicting changes in the meantime?
updateItemState(itemId, details); //=> accept new state data
else //conflict?
{
auto it = itemDetails_.find(itemId);
if (!details == (it == itemDetails_.end()))
if (!details || *details == it->second)
return; //notified changes match our current file state
//else: conflict!!! unclear which has the more recent data!
markSyncDue();
}
}
FileStateDelta registerFileStateDelta()
{
auto deltaPtr = std::make_shared<ItemIdDelta>();
changeLog_.push_back(deltaPtr);
return FileStateDelta(deltaPtr);
}
bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; }
void markSyncDue() { lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; }
void syncWithGoogle() //throw SysError
{
const ChangesDelta delta = getChangesDelta(sharedDriveName_.empty() ? std::string() : driveId_, lastSyncToken_, accessBuf_.getAccessToken()); //throw SysError
for (const FileChange& change : delta.fileChanges)
updateItemState(change.itemId, get(change.details));
lastSyncToken_ = delta.newStartPageToken;
lastSyncTime_ = std::chrono::steady_clock::now();
//good to know: if item is created and deleted between polling for changes it is still reported as deleted by Google!
//Same goes for any other change that is undone in between change notification syncs.
}
PathStatus getPathStatusSub(const std::string& folderId, const AfsPath& folderPath, const std::vector<Zstring>& relPath, bool followLeafShortcut) //throw SysError
{
assert(!relPath.empty());
auto itKnown = folderContents_.find(folderId);
if (itKnown == folderContents_.end() || !itKnown->second.isKnownFolder)
{
notifyFolderContent(registerFileStateDelta(), folderId, readFolderContent(folderId, accessBuf_.getAccessToken())); //throw SysError
//perf: always buffered, except for direct, first-time folder access!
itKnown = folderContents_.find(folderId);
assert(itKnown != folderContents_.end());
if (!itKnown->second.isKnownFolder)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
}
auto itFound = itemDetails_.cend();
for (const DetailsIterator& itChild : itKnown->second.childItems)
//Since Google Drive has no concept of a file path, we have to roll our own "path to ID" mapping => let's use the platform-native style
if (equalNativePath(itChild->second.itemName, relPath.front()))
{
if (itFound != itemDetails_.end())
throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front())));
itFound = itChild;
}
if (itFound == itemDetails_.end())
return {folderId, GdriveItemType::folder, folderPath, relPath}; //always a folder, see check before recursion above
else
{
auto getItemDetailsBuffered = [&](const std::string& itemId) -> const GdriveItemDetails&
{
auto it = itemDetails_.find(itemId);
if (it == itemDetails_.end())
{
notifyItemUpdated(registerFileStateDelta(), {itemId, getItemDetails(itemId, accessBuf_.getAccessToken())}); //throw SysError
//perf: always buffered, except for direct, first-time folder access!
it = itemDetails_.find(itemId);
assert(it != itemDetails_.end());
}
return it->second;
};
const auto& [childId, childDetails] = *itFound;
const AfsPath childItemPath(appendPath(folderPath.value, relPath.front()));
const std::vector<Zstring> childRelPath(relPath.begin() + 1, relPath.end());
if (childRelPath.empty())
{
if (childDetails.type == GdriveItemType::shortcut && followLeafShortcut)
return {childDetails.targetId, getItemDetailsBuffered(childDetails.targetId).type, childItemPath, childRelPath};
else
return {childId, childDetails.type, childItemPath, childRelPath};
}
switch (childDetails.type)
{
case GdriveItemType::file: //parent/file/child-rel-path... => obscure, but possible
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(AFS::getItemName(childItemPath))));
case GdriveItemType::folder:
return getPathStatusSub(childId, childItemPath, childRelPath, followLeafShortcut); //throw SysError
case GdriveItemType::shortcut:
switch (getItemDetailsBuffered(childDetails.targetId).type)
{
case GdriveItemType::file: //parent/file-symlink/child-rel-path... => obscure, but possible
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(AFS::getItemName(childItemPath))));
case GdriveItemType::folder: //parent/folder-symlink/child-rel-path... => always follow
return getPathStatusSub(childDetails.targetId, childItemPath, childRelPath, followLeafShortcut); //throw SysError
case GdriveItemType::shortcut: //should never happen: creating shortcuts to shortcuts fails with "Internal Error"
throw SysError(replaceCpy<std::wstring>(L"Google Drive Shortcut %x is pointing to another Shortcut.", L"%x", fmtPath(AFS::getItemName(childItemPath))));
}
break;
}
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
}
}
void updateItemState(const std::string& itemId, const GdriveItemDetails* details)
{
auto it = itemDetails_.find(itemId);
if (!details == (it == itemDetails_.end()))
if (!details || *details == it->second) //notified changes match our current file state
return; //=> avoid misleading changeLog_ entries after Google Drive sync!!!
//update change logs (and clean up obsolete entries)
std::erase_if(changeLog_, [&](std::weak_ptr<ItemIdDelta>& weakPtr)
{
if (std::shared_ptr<ItemIdDelta> iid = weakPtr.lock())
{
(*iid).insert(itemId);
return false;
}
else
return true;
});
//update file state
if (details)
{
if (it != itemDetails_.end()) //update
{
if (it->second.type != details->type)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); //WTF!?
std::vector<std::string> parentIdsNew = details->parentIds;
std::vector<std::string> parentIdsRemoved = it->second.parentIds;
std::erase_if(parentIdsNew, [&](const std::string& id) { return std::find(it->second.parentIds.begin(), it->second.parentIds.end(), id) != it->second.parentIds.end(); });
std::erase_if(parentIdsRemoved, [&](const std::string& id) { return std::find(details->parentIds.begin(), details->parentIds.end(), id) != details->parentIds.end(); });
for (const std::string& parentId : parentIdsNew)
folderContents_[parentId].childItems.push_back(it); //new insert => no need for duplicate check
for (const std::string& parentId : parentIdsRemoved)
if (auto itP = folderContents_.find(parentId); itP != folderContents_.end())
std::erase(itP->second.childItems, it);
//if all parents are removed, Google Drive will (recursively) delete the item => don't prematurely do this now: wait for change notifications!
//OR: item without parents located in "Shared with me", but referenced via Shortcut => don't remove!!!
it->second = *details;
}
else //create
{
auto itNew = itemDetails_.emplace(itemId, *details).first;
for (const std::string& parentId : details->parentIds)
folderContents_[parentId].childItems.push_back(itNew); //new insert => no need for duplicate check
}
}
else //delete
{
if (it != itemDetails_.end())
{
for (const std::string& parentId : it->second.parentIds) //1. delete from parent folders
if (auto itP = folderContents_.find(parentId); itP != folderContents_.end())
std::erase(itP->second.childItems, it);
itemDetails_.erase(it);
}
if (auto itP = folderContents_.find(itemId); itP != folderContents_.end())
{
//2. delete as parent from child items (don't wait for change notifications of children)
// what if e.g. single change notification "folder removed", then folder reapears,
// and no notifications for child items: possible with Google drive!?
// => no problem: FolderContent::isKnownFolder will be false for this restored folder => only a rescan needed
for (auto itChild : itP->second.childItems)
std::erase(itChild->second.parentIds, itemId);
folderContents_.erase(itP);
}
}
}
using DetailsIterator = std::unordered_map<std::string, GdriveItemDetails>::iterator;
struct FolderContent
{
bool isKnownFolder = false; //:= we've seen its full content at least once; further changes are calculated via change notifications
std::vector<DetailsIterator> childItems;
};
std::unordered_map<std::string /*folderId*/, FolderContent> folderContents_;
std::unordered_map<std::string /*itemId*/, GdriveItemDetails> itemDetails_; //contains ALL known, existing items!
std::string lastSyncToken_; //drive-specific(!) marker corresponding to last sync with Google's change notifications
std::chrono::steady_clock::time_point lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; //... with Google Drive (default: sync is due)
std::vector<std::weak_ptr<ItemIdDelta>> changeLog_; //track changed items since FileStateDelta was created (includes sync with Google + our own intermediate change notifications)
std::string driveId_; //ID of shared drive or "My Drive": never empty!
Zstring sharedDriveName_; //name of shared drive: empty for "My Drive"!
GdriveAccessBuffer& accessBuf_;
};
class GdriveFileStateAtLocation
{
public:
GdriveFileStateAtLocation(GdriveFileState& fileState, const std::string& locationRootId) : fileState_(fileState), locationRootId_(locationRootId) {}
GdriveFileState::PathStatus getPathStatus(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
return fileState_.getPathStatus(locationRootId_, itemPath, followLeafShortcut); //throw SysError
}
std::string /*itemId*/ getItemId(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
return fileState_.getItemId(locationRootId_, itemPath, followLeafShortcut); //throw SysError
}
std::pair<std::string /*itemId*/, GdriveItemDetails> getFileAttributes(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError
{
return fileState_.getFileAttributes(locationRootId_, itemPath, followLeafShortcut); //throw SysError
}
GdriveFileState& all() { return fileState_; }
private:
GdriveFileState& fileState_;
const std::string locationRootId_;
};
class GdriveDrivesBuffer
{
public:
explicit GdriveDrivesBuffer(GdriveAccessBuffer& accessBuf) :
accessBuf_(accessBuf),
myDrive_(getMyDriveId(accessBuf.getAccessToken()), Zstring() /*sharedDriveName*/, accessBuf) {} //throw SysError
GdriveDrivesBuffer(MemoryStreamIn& stream, GdriveAccessBuffer& accessBuf) : //throw SysError
accessBuf_(accessBuf),
myDrive_(stream, accessBuf) //throw SysError
{
size_t sharedDrivesCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos
while (sharedDrivesCount-- != 0)
{
auto fileState = makeSharedRef<GdriveFileState>(stream, accessBuf); //throw SysError
sharedDrives_.emplace(fileState.ref().getDriveId(), fileState);
}
}
void serialize(MemoryStreamOut& stream) const
{
myDrive_.serialize(stream);
writeNumber(stream, static_cast<uint32_t>(sharedDrives_.size()));
for (const auto& [driveId, fileState] : sharedDrives_)
fileState.ref().serialize(stream);
//starredFolders_? no, will be fully restored by syncWithGoogle()
}
std::vector<Zstring /*locationName*/> listLocations() //throw SysError
{
if (syncIsDue())
syncWithGoogle(); //throw SysError
std::vector<Zstring> locationNames;
for (const auto& [driveId, fileState] : sharedDrives_)
locationNames.push_back(fileState.ref().getSharedDriveName());
for (const StarredFolderDetails& sfd : starredFolders_)
locationNames.push_back(sfd.folderName);
return locationNames;
}
std::pair<GdriveFileStateAtLocation, GdriveFileState::FileStateDelta> prepareAccess(const Zstring& locationName) //throw SysError
{
//checking for added/renamed/deleted shared drives *every* GDRIVE_SYNC_INTERVAL is needlessly excessive!
// => check 1. once per FFS run
// 2. on drive access error
if (lastSyncTime_ == std::chrono::steady_clock::time_point())
syncWithGoogle(); //throw SysError
GdriveFileStateAtLocation fileState = [&]
{
try
{
return getFileState(locationName); //throw SysError
}
catch (SysError&)
{
if (syncIsDue())
syncWithGoogle(); //throw SysError
return getFileState(locationName); //throw SysError
}
}();
//manage last sync time here so that "lastSyncToken" remains stable while accessing GdriveFileState in the callback
if (fileState.all().syncIsDue())
fileState.all().syncWithGoogle(); //throw SysError
return {fileState, fileState.all().registerFileStateDelta()};
}
private:
bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; }
void syncWithGoogle() //throw SysError
{
//run in parallel with getSharedDrives()
auto ftStarredFolders = runAsync([access = accessBuf_.getAccessToken() /*throw SysError*/] { return getStarredFolders(access); /*throw SysError*/ });
decltype(sharedDrives_) currentDrives;
//getSharedDrives() should be fast enough to avoid the unjustified complexity of change notifications: https://freefilesync.org/forum/viewtopic.php?t=7827&start=30#p29712
for (const auto& [driveId, driveName] : getSharedDrives(accessBuf_.getAccessToken())) //throw SysError
{
auto fileState = [&, &driveId /*clang bug*/= driveId, &driveName /*clang bug*/= driveName]
{
if (auto it = sharedDrives_.find(driveId);
it != sharedDrives_.end())
{
it->second.ref().setSharedDriveName(driveName);
return it->second;
}
else
return makeSharedRef<GdriveFileState>(driveId, driveName, accessBuf_); //throw SysError
}();
currentDrives.emplace(driveId, fileState);
}
starredFolders_ = ftStarredFolders.get(); //throw SysError //
sharedDrives_.swap(currentDrives); //transaction!
lastSyncTime_ = std::chrono::steady_clock::now(); //...(uhm, mostly, except for setSharedDriveName())
}
GdriveFileStateAtLocation getFileState(const Zstring& locationName) //throw SysError
{
if (locationName.empty())
return {myDrive_, myDrive_.getDriveId()};
GdriveFileState* fileState = nullptr;
std::string locationRootId;
for (auto& [driveId, fileStateRef] : sharedDrives_)
if (equalNativePath(fileStateRef.ref().getSharedDriveName(), locationName))
{
if (fileState)
throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(locationName)));
fileState = &fileStateRef.ref();
locationRootId = driveId;
}
for (const StarredFolderDetails& sfd : starredFolders_)
if (equalNativePath(sfd.folderName, locationName))
{
if (fileState)
throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(locationName)));
if (sfd.sharedDriveId.empty()) //=> My Drive
fileState = &myDrive_;
else
{
auto it = sharedDrives_.find(sfd.sharedDriveId);
if (it == sharedDrives_.end())
break;
fileState = &it->second.ref();
}
locationRootId = sfd.folderId;
}
if (!fileState)
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(locationName)));
return {*fileState, locationRootId};
}
GdriveAccessBuffer& accessBuf_;
std::chrono::steady_clock::time_point lastSyncTime_; //... with Google Drive (default: sync is due)
GdriveFileState myDrive_;
std::unordered_map<std::string /*drive ID*/, SharedRef<GdriveFileState>> sharedDrives_;
std::vector<StarredFolderDetails> starredFolders_;
};
//==========================================================================================
//==========================================================================================
class GdrivePersistentSessions
{
public:
explicit GdrivePersistentSessions(const Zstring& configDirPath) : configDirPath_(configDirPath)
{
onSystemShutdownRegister(onBeforeSystemShutdownCookie_);
}
void saveActiveSessions() //throw FileError
{
std::vector<Protected<SessionHolder>*> protectedSessions; //pointers remain stable, thanks to std::unordered_map<>
globalSessions_.access([&](GlobalSessions& sessions)
{
for (auto& [accountEmail, protectedSession] : sessions)
protectedSessions.push_back(&protectedSession);
});
if (!protectedSessions.empty())
{
createDirectoryIfMissingRecursion(configDirPath_); //throw FileError
std::exception_ptr firstError;
//access each session outside the globalSessions_ lock!
for (Protected<SessionHolder>* protectedSession : protectedSessions)
protectedSession->access([&](SessionHolder& holder)
{
if (holder.session)
try
{
const Zstring dbFilePath = getDbFilePath(holder.session->accessBuf.ref().getUserEmail());
saveSession(dbFilePath, *holder.session); //throw FileError
}
catch (FileError&) { if (!firstError) firstError = std::current_exception(); }
});
if (firstError)
std::rethrow_exception(firstError); //throw FileError
}
}
std::string addUserSession(const std::string& gdriveLoginHint, const std::function<void()>& updateGui /*throw X*/, int timeoutSec) //throw SysError, X
{
const GdriveAccessInfo accessInfo = gdriveAuthorizeAccess(gdriveLoginHint, updateGui, timeoutSec); //throw SysError, X
accessUserSession(accessInfo.userInfo.email, timeoutSec, [&](std::optional<UserSession>& userSession) //throw SysError
{
if (userSession)
userSession->accessBuf.ref().update(accessInfo); //redundant?
else
{
const std::shared_ptr<int> timeoutSec2 = std::make_shared<int>(timeoutSec); //context option: valid only for duration of this call!
auto accessBuf = makeSharedRef<GdriveAccessBuffer>(accessInfo);
accessBuf.ref().setContextTimeout(timeoutSec2); //[!] used by GdriveDrivesBuffer()!
auto drivesBuf = makeSharedRef<GdriveDrivesBuffer>(accessBuf.ref()); //throw SysError
userSession = {accessBuf, drivesBuf};
}
});
return accessInfo.userInfo.email;
}
void removeUserSession(const std::string& accountEmail, int timeoutSec) //throw SysError
{
try
{
accessUserSession(accountEmail, timeoutSec, [&](std::optional<UserSession>& userSession) //throw SysError
{
if (userSession)
gdriveRevokeAccess(userSession->accessBuf.ref().getAccessToken()); //throw SysError
});
}
catch ([[maybe_unused]] const SysError& e) { assert(false); } //best effort: try to invalidate the access token
//=> expected to fail 1. if offline => not worse than removing FFS via "Uninstall Programs" 2. already revoked 3. if DB is corrupted
try
{
//start with deleting the DB file (1. maybe it's corrupted? 2. skip unnecessary lazy-load)
const Zstring dbFilePath = getDbFilePath(accountEmail);
try
{
removeFilePlain(dbFilePath); //throw FileError
}
catch (FileError&)
{
if (itemExists(dbFilePath)) //throw FileError
throw;
}
}
catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //file access errors should be further enriched by context info => SysError
accessUserSession(accountEmail, timeoutSec, [&](std::optional<UserSession>& userSession) //throw SysError
{
userSession.reset();
});
}
std::vector<std::string /*account email*/> listAccounts() //throw SysError
{
std::vector<std::string> emails;
std::vector<Protected<SessionHolder>*> protectedSessions; //pointers remain stable, thanks to std::unordered_map<>
globalSessions_.access([&](GlobalSessions& sessions)
{
for (auto& [accountEmail, protectedSession] : sessions)
protectedSessions.push_back(&protectedSession);
});
//access each session outside the globalSessions_ lock!
for (Protected<SessionHolder>* protectedSession : protectedSessions)
protectedSession->access([&](SessionHolder& holder)
{
if (holder.session)
emails.push_back(holder.session->accessBuf.ref().getUserEmail());
});
//also include available, but not-yet-loaded sessions
try
{
traverseFolder(configDirPath_,
[&](const FileInfo& fi) { if (endsWith(fi.itemName, Zstr(".db"))) emails.push_back(utfTo<std::string>(beforeLast(fi.itemName, Zstr('.'), IfNotFoundReturn::none))); },
[&](const FolderInfo& fi) {},
[&](const SymlinkInfo& si) {}); //throw FileError
}
catch (FileError&)
{
try
{
if (itemExists(configDirPath_)) //throw FileError
throw;
}
catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //file access errors should be further enriched by context info => SysError
}
removeDuplicates(emails, LessAsciiNoCase());
return emails;
}
std::vector<Zstring /*locationName*/> listLocations(const std::string& accountEmail, int timeoutSec) //throw SysError
{
std::vector<Zstring> locationNames;
accessUserSession(accountEmail, timeoutSec, [&](std::optional<UserSession>& userSession) //throw SysError
{
if (!userSession)
throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo<std::wstring>(accountEmail)));
locationNames = userSession->drivesBuf.ref().listLocations(); //throw SysError
});
return locationNames;
}
struct AsyncAccessInfo
{
GdriveAccess access; //don't allow (long-running) web requests while holding the global session lock!
GdriveFileState::FileStateDelta stateDelta;
};
//perf: amortized fully buffered!
AsyncAccessInfo accessGlobalFileState(const GdriveLogin& login, const std::function<void(GdriveFileStateAtLocation& fileState)>& useFileState /*throw X*/) //throw SysError, X
{
GdriveAccess access;
GdriveFileState::FileStateDelta stateDelta;
accessUserSession(login.email, login.timeoutSec, [&](std::optional<UserSession>& userSession) //throw SysError
{
if (!userSession)
throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo<std::wstring>(login.email)));
access = userSession->accessBuf.ref().getAccessToken(); //throw SysError
auto [fileState, stateDelta2] = userSession->drivesBuf.ref().prepareAccess(login.locationName); //throw SysError
stateDelta = std::move(stateDelta2);
useFileState(fileState); //throw X
});
return {access, stateDelta};
}
private:
GdrivePersistentSessions (const GdrivePersistentSessions&) = delete;
GdrivePersistentSessions& operator=(const GdrivePersistentSessions&) = delete;
struct UserSession;
Zstring getDbFilePath(std::string accountEmail) const
{
for (char& c : accountEmail)
c = asciiToLower(c);
//return appendPath(configDirPath_, utfTo<Zstring>(formatAsHexString(getMd5(utfTo<std::string>(accountEmail)))) + Zstr(".db"));
return appendPath(configDirPath_, utfTo<Zstring>(accountEmail) + Zstr(".db"));
}
void accessUserSession(const std::string& accountEmail, int timeoutSec, const std::function<void(std::optional<UserSession>& userSession)>& useSession /*throw X*/) //throw SysError, X
{
Protected<SessionHolder>* protectedSession = nullptr; //pointers remain stable, thanks to std::unordered_map<>
globalSessions_.access([&](GlobalSessions& sessions) { protectedSession = &sessions[accountEmail]; });
protectedSession->access([&](SessionHolder& holder)
{
if (!holder.dbWasLoaded) //let's NOT load the DB files under the globalSessions_ lock, but the session-specific one!
try
{
holder.session = loadSession(getDbFilePath(accountEmail), timeoutSec); //throw SysError
}
catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //GdrivePersistentSessions errors should be further enriched with context info => SysError
holder.dbWasLoaded = true;
const std::shared_ptr<int> timeoutSec2 = std::make_shared<int>(timeoutSec); //context option: valid only for duration of this call!
if (holder.session)
holder.session->accessBuf.ref().setContextTimeout(timeoutSec2);
useSession(holder.session); //throw X
});
}
static void saveSession(const Zstring& dbFilePath, const UserSession& userSession) //throw FileError
{
MemoryStreamOut streamOut;
writeArray(streamOut, DB_FILE_DESCR, sizeof(DB_FILE_DESCR));
writeNumber<int32_t>(streamOut, DB_FILE_VERSION);
MemoryStreamOut streamOutBody;
userSession.accessBuf.ref().serialize(streamOutBody);
userSession.drivesBuf.ref().serialize(streamOutBody);
try
{
streamOut.ref() += compress(streamOutBody.ref(), 3 /*best compression level: see db_file.cpp*/); //throw SysError
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(dbFilePath)), e.toString()); }
setFileContent(dbFilePath, streamOut.ref(), nullptr /*notifyUnbufferedIO*/); //throw FileError
}
static std::optional<UserSession> loadSession(const Zstring& dbFilePath, int timeoutSec) //throw FileError
{
std::string byteStream;
try
{
byteStream = getFileContent(dbFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
}
catch (FileError&)
{
if (itemExists(dbFilePath)) //throw FileError
throw;
return std::nullopt;
}
try
{
MemoryStreamIn streamIn(byteStream);
//-------- file format header --------
char tmp[sizeof(DB_FILE_DESCR)] = {};
readArray(streamIn, &tmp, sizeof(tmp)); //throw SysErrorUnexpectedEos
const std::shared_ptr<int> timeoutSec2 = std::make_shared<int>(timeoutSec); //context option: valid only for duration of this call!
//TODO: remove migration code at some time! 2020-07-03
if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR)))
{
const std::string& uncompressedStream = decompress(byteStream); //throw SysError
MemoryStreamIn streamIn2(uncompressedStream);
//-------- file format header --------
const char DB_FILE_DESCR_OLD[] = "FreeFileSync: Google Drive Database";
char tmp2[sizeof(DB_FILE_DESCR_OLD)] = {};
readArray(streamIn2, &tmp2, sizeof(tmp2)); //throw SysErrorUnexpectedEos
if (!std::equal(std::begin(tmp2), std::end(tmp2), std::begin(DB_FILE_DESCR_OLD)))
throw SysError(_("File content is corrupted.") + L" (invalid header)");
const int version = readNumber<int32_t>(streamIn2); //throw SysErrorUnexpectedEos
if (version != 1 && //TODO: remove migration code at some time! 2019-12-05
version != 2 && //TODO: remove migration code at some time! 2020-06-11
version != 3) //TODO: remove migration code at some time! 2020-07-03
throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo<std::wstring>(version)));
//version 1 + 2: fully discard old state due to missing "ownedByMe" attribute + shortcut support
//version 3: fully discard old state due to revamped shared drive handling
auto accessBuf = makeSharedRef<GdriveAccessBuffer>(streamIn2); //throw SysError
accessBuf.ref().setContextTimeout(timeoutSec2); //not used by GdriveDrivesBuffer(), but let's be consistent
auto drivesBuf = makeSharedRef<GdriveDrivesBuffer>(accessBuf.ref()); //throw SysError
return UserSession{accessBuf, drivesBuf};
}
else
{
if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR)))
throw SysError(_("File content is corrupted.") + L" (invalid header)");
const int version = readNumber<int32_t>(streamIn); //throw SysErrorUnexpectedEos
if (version != 4 &&
version != DB_FILE_VERSION)
throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo<std::wstring>(version)));
const std::string& uncompressedStream = decompress(makeStringView(byteStream.begin() + streamIn.pos(), byteStream.end())); //throw SysError
MemoryStreamIn streamInBody(uncompressedStream);
auto accessBuf = makeSharedRef<GdriveAccessBuffer>(streamInBody); //throw SysError
accessBuf.ref().setContextTimeout(timeoutSec2); //not used by GdriveDrivesBuffer(), but let's be consistent
auto drivesBuf = [&]
{
//TODO: remove migration code at some time! 2021-05-15
if (version <= 4) //fully discard old state due to revamped shared drive handling
return makeSharedRef<GdriveDrivesBuffer>(accessBuf.ref()); //throw SysError
else
return makeSharedRef<GdriveDrivesBuffer>(streamInBody, accessBuf.ref()); //throw SysError
}();
return UserSession{accessBuf, drivesBuf};
}
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot read database file %x."), L"%x", fmtPath(dbFilePath)), e.toString());
}
}
struct UserSession
{
SharedRef<GdriveAccessBuffer> accessBuf;
SharedRef<GdriveDrivesBuffer> drivesBuf;
};
struct SessionHolder
{
bool dbWasLoaded = false;
std::optional<UserSession> session;
};
using GlobalSessions = std::unordered_map<std::string /*Google account email*/, Protected<SessionHolder>, StringHashAsciiNoCase, StringEqualAsciiNoCase>;
Protected<GlobalSessions> globalSessions_;
const Zstring configDirPath_;
const SharedRef<std::function<void()>> onBeforeSystemShutdownCookie_ = makeSharedRef<std::function<void()>>([this]
{
try //let's not lose Google Drive data due to unexpected system shutdown:
{ saveActiveSessions(); } //throw FileError
catch (const FileError& e) { logExtraError(e.toString()); }
});
};
//==========================================================================================
constinit Global<GdrivePersistentSessions> globalGdriveSessions;
//==========================================================================================
GdrivePersistentSessions::AsyncAccessInfo accessGlobalFileState(const GdriveLogin& login, const std::function<void(GdriveFileStateAtLocation& fileState)>& useFileState /*throw X*/) //throw SysError, X
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
return gps->accessGlobalFileState(login, useFileState); //throw SysError, X
throw SysError(formatSystemError("accessGlobalFileState", L"", L"Function call not allowed during init/shutdown."));
}
//==========================================================================================
//==========================================================================================
struct GetDirDetails
{
GetDirDetails(const GdrivePath& folderPath) : folderPath_(folderPath) {}
struct Result
{
std::vector<GdriveItem> childItems;
GdrivePath folderPath;
};
Result operator()() const
{
try
{
std::string folderId;
std::optional<std::vector<GdriveItem>> childItemsBuf;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(folderPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const auto& [itemId, itemDetails] = fileState.getFileAttributes(folderPath_.itemPath, true /*followLeafShortcut*/); //throw SysError
if (itemDetails.type != GdriveItemType::folder) //check(!) or readFolderContent() will return empty (without failing!)
throw SysError(replaceCpy<std::wstring>(L"%x is not a directory.", L"%x", fmtPath(utfTo<Zstring>(itemDetails.itemName))));
folderId = itemId;
childItemsBuf = fileState.all().tryGetBufferedFolderContent(folderId);
});
if (!childItemsBuf)
{
childItemsBuf = readFolderContent(folderId, aai.access); //throw SysError
//buffer new file state ASAP => make sure accessGlobalFileState() has amortized constant access (despite the occasional internal readFolderContent() on non-leaf folders)
accessGlobalFileState(folderPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyFolderContent(aai.stateDelta, folderId, *childItemsBuf);
});
}
for (const GdriveItem& item : *childItemsBuf)
if (item.details.itemName.empty())
throw SysError(L"Folder contains an item without name."); //mostly an issue for FFS's folder traversal, but NOT for globalGdriveSessions!
return {std::move(*childItemsBuf), folderPath_};
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getGdriveDisplayPath(folderPath_))), e.toString()); }
}
private:
GdrivePath folderPath_;
};
struct GetShortcutTargetDetails
{
GetShortcutTargetDetails(const GdrivePath& shortcutPath, const GdriveItemDetails& shortcutDetails) : shortcutPath_(shortcutPath), shortcutDetails_(shortcutDetails) {}
struct Result
{
GdriveItemDetails target;
GdriveItemDetails shortcut;
GdrivePath shortcutPath;
};
Result operator()() const
{
try
{
std::optional<GdriveItemDetails> targetDetailsBuf;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(shortcutPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
targetDetailsBuf = fileState.all().tryGetBufferedItemDetails(shortcutDetails_.targetId);
});
if (!targetDetailsBuf)
{
targetDetailsBuf = getItemDetails(shortcutDetails_.targetId, aai.access); //throw SysError
//buffer new file state ASAP
accessGlobalFileState(shortcutPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyItemUpdated(aai.stateDelta, {shortcutDetails_.targetId, *targetDetailsBuf});
});
}
assert(targetDetailsBuf->targetId.empty());
if (targetDetailsBuf->type == GdriveItemType::shortcut) //should never happen: creating shortcuts to shortcuts fails with "Internal Error"
throw SysError(L"Google Drive Shortcut points to another Shortcut.");
return {std::move(*targetDetailsBuf), shortcutDetails_, shortcutPath_};
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getGdriveDisplayPath(shortcutPath_))), e.toString()); }
}
private:
GdrivePath shortcutPath_;
GdriveItemDetails shortcutDetails_;
};
class SingleFolderTraverser
{
public:
SingleFolderTraverser(const GdriveLogin& gdriveLogin, const std::vector<std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/) :
gdriveLogin_(gdriveLogin), workload_(workload)
{
while (!workload_.empty())
{
auto wi = std::move(workload_. back()); //yes, no strong exception guarantee (std::bad_alloc)
/**/ workload_.pop_back(); //
const auto& [folderPath, cb] = wi;
tryReportingDirError([&] //throw X
{
traverseWithException(folderPath, *cb); //throw FileError, X
}, *cb);
}
}
private:
SingleFolderTraverser (const SingleFolderTraverser&) = delete;
SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete;
void traverseWithException(const AfsPath& folderPath, AFS::TraverserCallback& cb) //throw FileError, X
{
const std::vector<GdriveItem>& childItems = GetDirDetails({gdriveLogin_, folderPath})().childItems; //throw FileError
for (const GdriveItem& item : childItems)
{
const Zstring itemName = utfTo<Zstring>(item.details.itemName);
switch (item.details.type)
{
case GdriveItemType::file:
cb.onFile({itemName, item.details.fileSize, item.details.modTime, getGdriveFilePrint(item.itemId), false /*isFollowedSymlink*/}); //throw X
break;
case GdriveItemType::folder:
if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({itemName, false /*isFollowedSymlink*/})) //throw X
{
const AfsPath afsItemPath(appendPath(folderPath.value, itemName));
workload_.push_back({afsItemPath, std::move(cbSub)});
}
break;
case GdriveItemType::shortcut:
switch (cb.onSymlink({itemName, item.details.modTime})) //throw X
{
case AFS::TraverserCallback::HandleLink::follow:
{
const AfsPath afsItemPath(appendPath(folderPath.value, itemName));
GdriveItemDetails targetDetails = {};
if (!tryReportingItemError([&] //throw X
{
targetDetails = GetShortcutTargetDetails({gdriveLogin_, afsItemPath}, item.details)().target; //throw FileError
}, cb, itemName))
continue;
if (targetDetails.type == GdriveItemType::folder)
{
if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({itemName, true /*isFollowedSymlink*/})) //throw X
workload_.push_back({afsItemPath, std::move(cbSub)});
}
else //a file or named pipe, etc.
cb.onFile({itemName, targetDetails.fileSize, targetDetails.modTime, getGdriveFilePrint(item.details.targetId), true /*isFollowedSymlink*/}); //throw X
}
break;
case AFS::TraverserCallback::HandleLink::skip:
break;
}
break;
}
}
}
const GdriveLogin gdriveLogin_;
std::vector<std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>> workload_;
};
void gdriveTraverseFolderRecursive(const GdriveLogin& gdriveLogin, const std::vector<std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/, size_t) //throw X
{
SingleFolderTraverser dummy(gdriveLogin, workload); //throw X
}
//==========================================================================================
//==========================================================================================
struct InputStreamGdrive : public AFS::InputStream
{
explicit InputStreamGdrive(const GdrivePath& gdrivePath) :
gdrivePath_(gdrivePath)
{
worker_ = InterruptibleThread([asyncStreamOut = this->asyncStreamIn_, gdrivePath]
{
setCurrentThreadName(Zstr("Istream ") + utfTo<Zstring>(getGdriveDisplayPath(gdrivePath)));
try
{
GdriveAccess access;
std::string fileId;
try
{
access = accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileId = fileState.getItemId(gdrivePath.itemPath, true /*followLeafShortcut*/); //throw SysError
}).access;
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); }
try
{
auto writeBlock = [&](const void* buffer, size_t bytesToWrite)
{
asyncStreamOut->write(buffer, bytesToWrite); //throw ThreadStopRequest
};
gdriveDownloadFile(fileId, writeBlock, access); //throw SysError, ThreadStopRequest
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); }
asyncStreamOut->closeStream();
}
catch (FileError&) { asyncStreamOut->setWriteError(std::current_exception()); } //let ThreadStopRequest pass through!
});
}
~InputStreamGdrive()
{
asyncStreamIn_->setReadError(std::make_exception_ptr(ThreadStopRequest()));
}
size_t getBlockSize() override { return GDRIVE_BLOCK_SIZE_DOWNLOAD; } //throw (FileError)
//may return short; only 0 means EOF! CONTRACT: bytesToRead > 0!
size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, (ErrorFileLocked), X
{
const size_t bytesRead = asyncStreamIn_->tryRead(buffer, bytesToRead); //throw FileError
reportBytesProcessed(notifyUnbufferedIO); //throw X
return bytesRead;
//no need for asyncStreamIn_->checkWriteErrors(): once end of stream is reached, asyncStreamOut->closeStream() was called => no errors occured
}
std::optional<AFS::StreamAttributes> tryGetAttributesFast() override //throw FileError
{
AFS::StreamAttributes attr = {};
try
{
accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const auto& [itemId, itemDetails] = fileState.getFileAttributes(gdrivePath_.itemPath, true /*followLeafShortcut*/); //throw SysError
attr.modTime = itemDetails.modTime;
attr.fileSize = itemDetails.fileSize;
attr.filePrint = getGdriveFilePrint(itemId);
});
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))), e.toString()); }
return std::move(attr); //[!]
}
private:
void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X
{
const int64_t bytesDelta = makeSigned(asyncStreamIn_->getTotalBytesWritten()) - totalBytesReported_;
totalBytesReported_ += bytesDelta;
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X
}
const GdrivePath gdrivePath_;
int64_t totalBytesReported_ = 0;
std::shared_ptr<AsyncStreamBuffer> asyncStreamIn_ = std::make_shared<AsyncStreamBuffer>(GDRIVE_STREAM_BUFFER_SIZE);
InterruptibleThread worker_;
};
//==========================================================================================
//already existing: 1. fails or 2. creates duplicate
struct OutputStreamGdrive : public AFS::OutputStreamImpl
{
OutputStreamGdrive(const GdrivePath& gdrivePath,
std::optional<uint64_t> /*streamSize*/,
std::optional<time_t> modTime,
std::unique_ptr<PathAccessLock>&& pal) : //throw SysError
gdrivePath_(gdrivePath)
{
std::promise<AFS::FingerPrint> promFilePrint;
futFilePrint_ = promFilePrint.get_future();
//CAVEAT: if file is already existing, OutputStreamGdrive *constructor* must fail, not OutputStreamGdrive::write(),
// otherwise ~OutputStreamImpl() will delete the already existing file! => don't check asynchronously!
const Zstring fileName = AFS::getItemName(gdrivePath.itemPath);
std::string parentId;
/*const*/ GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus& ps = fileState.getPathStatus(gdrivePath.itemPath, false /*followLeafShortcut*/); //throw SysError
if (ps.relPath.empty())
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(fileName)));
if (ps.relPath.size() > 1) //parent folder missing
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
parentId = ps.existingItemId;
});
worker_ = InterruptibleThread([gdrivePath, modTime, fileName, asyncStreamIn = this->asyncStreamOut_,
pFilePrint = std::move(promFilePrint),
parentId = std::move(parentId),
aai = std::move(aai),
pal = std::move(pal)]() mutable
{
assert(pal); //bind life time to worker thread!
setCurrentThreadName(Zstr("Ostream ") + utfTo<Zstring>(getGdriveDisplayPath(gdrivePath)));
try
{
auto tryReadBlock = [&](void* buffer, size_t bytesToRead) //may return short, only 0 means EOF!
{
return asyncStreamIn->tryRead(buffer, bytesToRead); //throw ThreadStopRequest
};
//for whatever reason, gdriveUploadFile() is slightly faster than gdriveUploadSmallFile()! despite its two roundtrips! even when file sizes are 0!
//=> 1. issue likely on Google's side => 2. persists even after having fixed "Expect: 100-continue"
const std::string fileIdNew = //streamSize && *streamSize < 5 * 1024 * 1024 ?
//gdriveUploadSmallFile(fileName, parentId, *streamSize, modTime, readBlock, aai.access) : //throw SysError, ThreadStopRequest
gdriveUploadFile (fileName, parentId, modTime, tryReadBlock, aai.access); //throw SysError, ThreadStopRequest
assert(asyncStreamIn->getTotalBytesRead() == asyncStreamIn->getTotalBytesWritten());
//already existing: creates duplicate
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
GdriveItem newFileItem
{
.itemId = fileIdNew,
.details{
.itemName = fileName,
.fileSize = asyncStreamIn->getTotalBytesRead(),
.type = GdriveItemType::file,
.owner = FileOwner::me,
}
};
if (modTime) //else: whatever modTime Google Drive selects will be notified after GDRIVE_SYNC_INTERVAL
newFileItem.details.modTime = *modTime;
newFileItem.details.parentIds.push_back(parentId);
accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyItemCreated(aai.stateDelta, newFileItem);
});
pFilePrint.set_value(getGdriveFilePrint(fileIdNew));
}
catch (const SysError& e)
{
FileError fe(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString());
const std::exception_ptr exptr = std::make_exception_ptr(std::move(fe));
asyncStreamIn->setReadError(exptr); //set both!
pFilePrint.set_exception(exptr); //
}
//let ThreadStopRequest pass through!
});
}
~OutputStreamGdrive()
{
if (asyncStreamOut_) //=> cleanup non-finalized output file
{
asyncStreamOut_->setWriteError(std::make_exception_ptr(ThreadStopRequest()));
worker_.join();
try //see removeFilePlain()
{
std::optional<std::string> itemId;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus ps = fileState.getPathStatus(gdrivePath_.itemPath, false /*followLeafShortcut*/); //throw SysError
if (ps.relPath.empty())
itemId = ps.existingItemId;
});
if (itemId)
{
gdriveDeleteItem(*itemId, aai.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyItemDeleted(aai.stateDelta, *itemId);
});
}
}
catch (const SysError& e)
{
logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))) + L"\n\n" + e.toString());
}
}
}
size_t getBlockSize() override { return GDRIVE_BLOCK_SIZE_UPLOAD; } //throw (FileError)
size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0
{
const size_t bytesWritten = asyncStreamOut_->tryWrite(buffer, bytesToWrite); //throw FileError
reportBytesProcessed(notifyUnbufferedIO); //throw X
return bytesWritten;
}
AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X
{
if (!asyncStreamOut_)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
asyncStreamOut_->closeStream();
while (futFilePrint_.wait_for(std::chrono::milliseconds(25)) == std::future_status::timeout)
reportBytesProcessed(notifyUnbufferedIO); //throw X
reportBytesProcessed(notifyUnbufferedIO); //[!] once more, now that *all* bytes were written
AFS::FinalizeResult result;
assert(isReady(futFilePrint_));
result.filePrint = futFilePrint_.get(); //throw FileError
//asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload
asyncStreamOut_.reset(); //output finalized => no more exceptions from here on!
//--------------------------------------------------------------------
//result.errorModTime -> already (successfully) set during file creation
return result;
}
private:
void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X
{
const int64_t bytesDelta = makeSigned(asyncStreamOut_->getTotalBytesRead()) - totalBytesReported_;
totalBytesReported_ += bytesDelta;
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X
}
const GdrivePath gdrivePath_;
int64_t totalBytesReported_ = 0;
std::shared_ptr<AsyncStreamBuffer> asyncStreamOut_ = std::make_shared<AsyncStreamBuffer>(GDRIVE_STREAM_BUFFER_SIZE);
InterruptibleThread worker_;
std::future<AFS::FingerPrint> futFilePrint_;
};
//==========================================================================================
class GdriveFileSystem : public AbstractFileSystem
{
public:
explicit GdriveFileSystem(const GdriveLogin& gdriveLogin) : gdriveLogin_(gdriveLogin) {}
const GdriveLogin& getGdriveLogin() const { return gdriveLogin_; }
Zstring getFolderUrl(const AfsPath& folderPath) const //throw FileError
{
try
{
GdriveFileState::PathStatus ps;
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
ps = fileState.getPathStatus(folderPath, true /*followLeafShortcut*/); //throw SysError
});
if (!ps.relPath.empty())
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
if (ps.existingType != GdriveItemType::folder)
throw SysError(replaceCpy<std::wstring>(L"%x is not a folder.", L"%x", fmtPath(getItemName(folderPath))));
return Zstr("https://drive.google.com/drive/folders/") + utfTo<Zstring>(ps.existingItemId);
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); }
}
private:
GdrivePath getGdrivePath(const AfsPath& itemPath) const { return {gdriveLogin_, itemPath}; }
GdriveRawPath getGdriveRawPath(const AfsPath& itemPath) const //throw SysError
{
const std::optional<AfsPath> parentPath = getParentPath(itemPath);
if (!parentPath)
throw SysError(L"Item is device root");
std::string parentId;
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
parentId = fileState.getItemId(*parentPath, true /*followLeafShortcut*/); //throw SysError
});
return { std::move(parentId), getItemName(itemPath)};
}
Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return concatenateGdriveFolderPathPhrase(getGdrivePath(itemPath)); }
std::vector<Zstring> getPathPhraseAliases(const AfsPath& itemPath) const override { return {getInitPathPhrase(itemPath)}; }
std::wstring getDisplayPath(const AfsPath& itemPath) const override { return getGdriveDisplayPath(getGdrivePath(itemPath)); }
bool isNullFileSystem() const override { return gdriveLogin_.email.empty(); }
std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override
{
const GdriveLogin& lhs = gdriveLogin_;
const GdriveLogin& rhs = static_cast<const GdriveFileSystem&>(afsRhs).gdriveLogin_;
if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.email, rhs.email);
cmp != std::weak_ordering::equivalent)
return cmp;
return compareNativePath(lhs.locationName, rhs.locationName);
}
//----------------------------------------------------------------------------------------------------------------
ItemType getItemType(const AfsPath& itemPath) const override //throw FileError
{
try
{
GdriveFileState::PathStatus ps;
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError
});
if (ps.relPath.empty())
switch (ps.existingType)
{
case GdriveItemType::file: return ItemType::file;
case GdriveItemType::folder: return ItemType::folder;
case GdriveItemType::shortcut: return ItemType::symlink;
}
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(Zstring(ps.relPath.front()))));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); }
}
std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError
{
try
{
GdriveFileState::PathStatus ps;
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError
});
if (ps.relPath.empty())
switch (ps.existingType)
{
case GdriveItemType::file: return ItemType::file;
case GdriveItemType::folder: return ItemType::folder;
case GdriveItemType::shortcut: return ItemType::symlink;
}
return std::nullopt;
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); }
}
//----------------------------------------------------------------------------------------------------------------
//already existing: 1. fails or 2. creates duplicate (unlikely)
void createFolderPlain(const AfsPath& folderPath) const override //throw FileError
{
try
{
//avoid duplicate Google Drive item creation by multiple threads
PathAccessLock pal(getGdriveRawPath(folderPath), PathBlockType::otherWait); //throw SysError
const Zstring folderName = getItemName(folderPath);
std::string parentId;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus& ps = fileState.getPathStatus(folderPath, false /*followLeafShortcut*/); //throw SysError
if (ps.relPath.empty())
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(folderName)));
if (ps.relPath.size() > 1) //parent folder missing
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
parentId = ps.existingItemId;
});
//already existing: creates duplicate
const std::string folderIdNew = gdriveCreateFolderPlain(folderName, parentId, aai.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyFolderCreated(aai.stateDelta, folderIdNew, folderName, parentId);
});
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); }
}
void removeItemPlainImpl(const AfsPath& itemPath, std::optional<GdriveItemType> expectedType, bool permanent /*...or move to trash*/, bool failIfNotExist) const //throw SysError
{
const std::optional<AfsPath> parentPath = getParentPath(itemPath);
if (!parentPath) throw SysError(L"Item is device root");
std::string itemId;
std::optional<std::string> parentIdToUnlink;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError
if (!ps.relPath.empty())
{
if (failIfNotExist)
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
else
return;
}
GdriveItemDetails itemDetails;
std::tie(itemId, itemDetails) = fileState.getFileAttributes(itemPath, false /*followLeafShortcut*/); //throw SysError
assert(std::find(itemDetails.parentIds.begin(), itemDetails.parentIds.end(), fileState.getItemId(*parentPath, true /*followLeafShortcut*/)) != itemDetails.parentIds.end());
if (expectedType && itemDetails.type != *expectedType)
switch (*expectedType)
{
case GdriveItemType::file: throw SysError(L"Item is not a file");
case GdriveItemType::folder: throw SysError(L"Item is not a folder");
case GdriveItemType::shortcut: throw SysError(L"Item is not a shortcut");
}
//hard-link handling applies to shared files as well: 1. it's the right thing (TM) 2. if we're not the owner: deleting would fail
if (itemDetails.parentIds.size() > 1 || itemDetails.owner == FileOwner::other) //FileOwner::other behaves like a followed symlink! i.e. vanishes if owner deletes it!
parentIdToUnlink = fileState.getItemId(*parentPath, true /*followLeafShortcut*/); //throw SysError
});
if (itemId.empty())
return;
if (parentIdToUnlink)
{
gdriveUnlinkParent(itemId, *parentIdToUnlink, aai.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyParentRemoved(aai.stateDelta, itemId, *parentIdToUnlink);
});
}
else
{
if (permanent)
gdriveDeleteItem(itemId, aai.access); //throw SysError
else
gdriveMoveToTrash(itemId, aai.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyItemDeleted(aai.stateDelta, itemId);
});
}
}
void removeFilePlain(const AfsPath& filePath) const override //throw FileError
{
try { removeItemPlainImpl(filePath, GdriveItemType::file, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ }
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); }
}
void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError
{
try { removeItemPlainImpl(linkPath, GdriveItemType::shortcut, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ }
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); }
}
void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError
{
try { removeItemPlainImpl(folderPath, GdriveItemType::folder, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ }
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); }
}
void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError
const std::function<void(const std::wstring& displayPath)>& onBeforeFileDeletion /*throw X*/,
const std::function<void(const std::wstring& displayPath)>& onBeforeSymlinkDeletion/*throw X*/,
const std::function<void(const std::wstring& displayPath)>& onBeforeFolderDeletion /*throw X*/) const override
{
if (onBeforeFolderDeletion) onBeforeFolderDeletion(getDisplayPath(folderPath)); //throw X
try { removeItemPlainImpl(folderPath, GdriveItemType::folder, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ }
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); }
}
//----------------------------------------------------------------------------------------------------------------
AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError
{
//this function doesn't make sense for Google Drive: Shortcuts do not refer by path, but ID!
//even if it were possible to determine a path, doing anything with the target file (e.g. delete + recreate) would break other Shortcuts!
throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(linkPath))), _("Operation not supported by device."));
}
bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError
{
auto getTargetId = [](const GdriveFileSystem& gdriveFs, const AfsPath& linkPath)
{
try
{
std::string targetId;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveFs.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveItemDetails& itemDetails = fileState.getFileAttributes(linkPath, false /*followLeafShortcut*/).second; //throw SysError
if (itemDetails.type != GdriveItemType::shortcut)
throw SysError(L"Not a Google Drive Shortcut.");
targetId = itemDetails.targetId;
});
return targetId;
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(gdriveFs.getDisplayPath(linkPath))), e.toString()); }
};
return getTargetId(*this, linkPathL) == getTargetId(static_cast<const GdriveFileSystem&>(linkPathR.afsDevice.ref()), linkPathR.afsPath);
}
//----------------------------------------------------------------------------------------------------------------
//return value always bound:
std::unique_ptr<InputStream> getInputStream(const AfsPath& filePath) const override //throw FileError, (ErrorFileLocked)
{
return std::make_unique<InputStreamGdrive>(getGdrivePath(filePath));
}
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
//=> actual behavior: 1. fails or 2. creates duplicate (unlikely)
std::unique_ptr<OutputStreamImpl> getOutputStream(const AfsPath& filePath, //throw FileError
std::optional<uint64_t> streamSize,
std::optional<time_t> modTime) const override
{
try
{
//avoid duplicate item creation by multiple threads
auto pal = std::make_unique<PathAccessLock>(getGdriveRawPath(filePath), PathBlockType::otherFail); //throw SysError
//don't block during a potentially long-running file upload!
//already existing: 1. fails or 2. creates duplicate
return std::make_unique<OutputStreamGdrive>(getGdrivePath(filePath), streamSize, modTime, std::move(pal)); //throw SysError
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString());
}
}
//----------------------------------------------------------------------------------------------------------------
void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override
{
gdriveTraverseFolderRecursive(gdriveLogin_, workload, parallelOps); //throw X
}
//----------------------------------------------------------------------------------------------------------------
//symlink handling: follow
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
//=> actual behavior: 1. fails or 2. creates duplicate (unlikely)
FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), (X)
const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override
{
//no native Google Drive file copy => use stream-based file copy:
if (copyFilePermissions)
throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device."));
const GdriveFileSystem& fsTarget = static_cast<const GdriveFileSystem&>(targetPath.afsDevice.ref());
if (!equalAsciiNoCase(gdriveLogin_.email, fsTarget.gdriveLogin_.email))
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
//=> actual behavior: 1. fails or 2. creates duplicate (unlikely)
return copyFileAsStream(sourcePath, attrSource, targetPath, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X
//else: copying files within account works, e.g. between My Drive <-> shared drives
try
{
//avoid duplicate Google Drive item creation by multiple threads (blocking is okay: gdriveCopyFile() should complete instantly!)
PathAccessLock pal(fsTarget.getGdriveRawPath(targetPath.afsPath), PathBlockType::otherWait); //throw SysError
const Zstring itemNameNew = getItemName(targetPath);
std::string itemIdSrc;
GdriveItemDetails itemDetailsSrc;
/*const GdrivePersistentSessions::AsyncAccessInfo aaiSrc =*/ accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
std::tie(itemIdSrc, itemDetailsSrc) = fileState.getFileAttributes(sourcePath, true /*followLeafShortcut*/); //throw SysError
assert(itemDetailsSrc.type == GdriveItemType::file); //Google Drive *should* fail trying to copy folder: "This file cannot be copied by the user."
if (itemDetailsSrc.type != GdriveItemType::file) //=> don't trust + improve error message
throw SysError(replaceCpy<std::wstring>(L"%x is not a file.", L"%x", fmtPath(getItemName(sourcePath))));
});
std::string parentIdTrg;
const GdrivePersistentSessions::AsyncAccessInfo aaiTrg = accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus psTo = fileState.getPathStatus(targetPath.afsPath, false /*followLeafShortcut*/); //throw SysError
if (psTo.relPath.empty())
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(itemNameNew)));
if (psTo.relPath.size() > 1) //parent folder missing
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(psTo.relPath.front())));
parentIdTrg = psTo.existingItemId;
});
//already existing: creates duplicate
const std::string fileIdTrg = gdriveCopyFile(itemIdSrc, parentIdTrg, itemNameNew, itemDetailsSrc.modTime, aaiTrg.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveItem newFileItem
{
.itemId = fileIdTrg,
.details{
.itemName = itemNameNew,
.fileSize = itemDetailsSrc.fileSize,
.modTime = itemDetailsSrc.modTime,
.type = GdriveItemType::file,
.owner = fileState.all().getSharedDriveName().empty() ? FileOwner::me : FileOwner::none,
.parentIds{parentIdTrg},
}
};
fileState.all().notifyItemCreated(aaiTrg.stateDelta, newFileItem);
});
return
{
.fileSize = itemDetailsSrc.fileSize,
.modTime = itemDetailsSrc.modTime,
.sourceFilePrint = getGdriveFilePrint(itemIdSrc),
.targetFilePrint = getGdriveFilePrint(fileIdTrg),
/*.errorModTime = */
};
}
catch (const SysError& e)
{
throw FileError(replaceCpy(replaceCpy(_("Cannot copy file %x to %y."),
L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))),
L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString());
}
}
//symlink handling: follow
//already existing: fail
void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
{
//already existing: 1. fails or 2. creates duplicate (unlikely)
AFS::createFolderPlain(targetPath); //throw FileError
if (copyFilePermissions)
throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device."));
}
//already existing: fail
void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
{
try
{
std::string targetId;
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveItemDetails& itemDetails = fileState.getFileAttributes(sourcePath, false /*followLeafShortcut*/).second; //throw SysError
if (itemDetails.type != GdriveItemType::shortcut)
throw SysError(L"Not a Google Drive Shortcut.");
targetId = itemDetails.targetId;
});
const GdriveFileSystem& fsTarget = static_cast<const GdriveFileSystem&>(targetPath.afsDevice.ref());
//avoid duplicate Google Drive item creation by multiple threads
PathAccessLock pal(fsTarget.getGdriveRawPath(targetPath.afsPath), PathBlockType::otherWait); //throw SysError
const Zstring shortcutName = getItemName(targetPath.afsPath);
std::string parentId;
const GdrivePersistentSessions::AsyncAccessInfo aaiTrg = accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
const GdriveFileState::PathStatus& ps = fileState.getPathStatus(targetPath.afsPath, false /*followLeafShortcut*/); //throw SysError
if (ps.relPath.empty())
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(shortcutName)));
if (ps.relPath.size() > 1) //parent folder missing
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front())));
parentId = ps.existingItemId;
});
//already existing: creates duplicate
const std::string shortcutIdNew = gdriveCreateShortcutPlain(shortcutName, parentId, targetId, aaiTrg.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyShortcutCreated(aaiTrg.stateDelta, shortcutIdNew, shortcutName, parentId, targetId);
});
}
catch (const SysError& e)
{
throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."),
L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))),
L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString());
}
}
//already existing: undefined behavior! (e.g. fail/overwrite)
//=> actual behavior: 1. fails or 2. creates duplicate (unlikely)
void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported
{
if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent)
throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices."));
//note: moving files within account works, e.g. between My Drive <-> shared drives
// BUT: not supported by our model with separate GdriveFileStates; e.g. how to handle complexity of a moved folder (tree)?
try
{
const GdriveFileSystem& fsTarget = static_cast<const GdriveFileSystem&>(pathTo.afsDevice.ref());
//avoid duplicate Google Drive item creation by multiple threads
PathAccessLock pal(fsTarget.getGdriveRawPath(pathTo.afsPath), PathBlockType::otherWait); //throw SysError
const Zstring itemNameOld = getItemName(pathFrom);
const Zstring itemNameNew = getItemName(pathTo);
const std::optional<AfsPath> parentPathFrom = getParentPath(pathFrom);
const std::optional<AfsPath> parentPathTo = getParentPath(pathTo.afsPath);
if (!parentPathFrom) throw SysError(L"Source is device root");
if (!parentPathTo ) throw SysError(L"Target is device root");
std::string itemId;
GdriveItemDetails itemDetails;
std::string parentIdFrom;
std::string parentIdTo;
const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
std::tie(itemId, itemDetails) = fileState.getFileAttributes(pathFrom, false /*followLeafShortcut*/); //throw SysError
parentIdFrom = fileState.getItemId(*parentPathFrom, true /*followLeafShortcut*/); //throw SysError
const GdriveFileState::PathStatus psTo = fileState.getPathStatus(pathTo.afsPath, false /*followLeafShortcut*/); //throw SysError
//e.g. changing file name case only => this is not an "already exists" situation!
//also: hardlink referenced by two different paths, the source one will be unlinked
if (psTo.relPath.empty() && psTo.existingItemId == itemId)
parentIdTo = fileState.getItemId(*parentPathTo, true /*followLeafShortcut*/); //throw SysError
else
{
if (psTo.relPath.empty())
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(itemNameNew)));
if (psTo.relPath.size() > 1) //parent folder missing
throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(psTo.relPath.front())));
parentIdTo = psTo.existingItemId;
}
});
if (parentIdFrom == parentIdTo && itemNameOld == itemNameNew)
return; //nothing to do
//already existing: creates duplicate
gdriveMoveAndRenameItem(itemId, parentIdFrom, parentIdTo, itemNameNew, itemDetails.modTime, aai.access); //throw SysError
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
fileState.all().notifyMoveAndRename(aai.stateDelta, itemId, parentIdFrom, parentIdTo, itemNameNew);
});
}
catch (const SysError& e) { throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); }
}
bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError
//----------------------------------------------------------------------------------------------------------------
FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value
ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value
void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, (X)
{
try
{
const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get();
if (!gps)
throw SysError(formatSystemError("GdriveFileSystem::authenticateAccess", L"", L"Function call not allowed during init/shutdown."));
for (const std::string& accountEmail : gps->listAccounts()) //throw SysError
if (equalAsciiNoCase(accountEmail, gdriveLogin_.email))
return;
const bool allowUserInteraction = static_cast<bool>(requestPassword);
if (allowUserInteraction)
gps->addUserSession(gdriveLogin_.email /*gdriveLoginHint*/, nullptr /*updateGui*/, gdriveLogin_.timeoutSec); //throw SysError
//error messages will be lost if user cancels in dir_exist_async.h! However:
//The most-likely-to-fail parts (web access) are reported by gdriveAuthorizeAccess() via the browser!
else
throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo<std::wstring>(gdriveLogin_.email)));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); }
}
bool hasNativeTransactionalCopy() const override { return true; }
//----------------------------------------------------------------------------------------------------------------
int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available
{
bool onMyDrive = false;
try
{
const GdriveAccess& access = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState)
{ onMyDrive = fileState.all().getSharedDriveName().empty(); }).access; //throw SysError
if (onMyDrive)
return gdriveGetMyDriveFreeSpace(access); //throw SysError
else
return -1;
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); }
}
std::unique_ptr<RecycleSession> createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, (RecycleBinUnavailable)
{
struct RecycleSessionGdrive : public RecycleSession
{
//fails if item is not existing
void moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) override { AFS::moveToRecycleBin(itemPath); } //throw FileError, (RecycleBinUnavailable)
void tryCleanup(const std::function<void(const std::wstring& displayPath)>& notifyDeletionStatus) override {}; //throw FileError
};
return std::make_unique<RecycleSessionGdrive>();
}
//fails if item is not existing
void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, (RecycleBinUnavailable)
{
try
{
removeItemPlainImpl(itemPath, std::nullopt /*expectedType*/, false /*permanent*/, true /*failIfNotExist*/); //throw SysError
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); }
}
const GdriveLogin gdriveLogin_;
};
//===========================================================================================================================
//expects "clean" input data
Zstring concatenateGdriveFolderPathPhrase(const GdrivePath& gdrivePath) //noexcept
{
Zstring emailAndDrive = utfTo<Zstring>(gdrivePath.gdriveLogin.email);
if (!gdrivePath.gdriveLogin.locationName.empty())
emailAndDrive += Zstr(':') + gdrivePath.gdriveLogin.locationName;
Zstring options;
if (gdrivePath.gdriveLogin.timeoutSec != GdriveLogin().timeoutSec)
options += Zstr("|timeout=") + numberTo<Zstring>(gdrivePath.gdriveLogin.timeoutSec);
Zstring itemPath;
if (!gdrivePath.itemPath.value.empty())
itemPath += FILE_NAME_SEPARATOR + gdrivePath.itemPath.value;
if (endsWith(itemPath, Zstr(' ')) && options.empty()) //path phrase concept must survive trimming!
itemPath += FILE_NAME_SEPARATOR;
return Zstring(gdrivePrefix) + FILE_NAME_SEPARATOR + emailAndDrive + itemPath + options;
}
}
void fff::gdriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath)
{
assert(!globalHttpSessionManager.get());
globalHttpSessionManager.set(std::make_unique<HttpSessionManager>(caCertFilePath));
assert(!globalGdriveSessions.get());
globalGdriveSessions.set(std::make_unique<GdrivePersistentSessions>(configDirPath));
}
void fff::gdriveTeardown()
{
try //don't use ~GdrivePersistentSessions() to save! Might never happen, e.g. detached thread waiting for Google Drive authentication; terminated on exit!
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
gps->saveActiveSessions(); //throw FileError
}
catch (const FileError& e) { logExtraError(e.toString()); }
assert(globalGdriveSessions.get());
globalGdriveSessions.set(nullptr);
assert(globalHttpSessionManager.get());
globalHttpSessionManager.set(nullptr);
}
std::string fff::gdriveAddUser(const std::function<void()>& updateGui /*throw X*/, int timeoutSec) //throw FileError, X
{
try
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
return gps->addUserSession("" /*gdriveLoginHint*/, updateGui, timeoutSec); //throw SysError, X
throw SysError(formatSystemError("gdriveAddUser", L"", L"Function call not allowed during init/shutdown."));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive"), e.toString()); }
}
void fff::gdriveRemoveUser(const std::string& accountEmail, int timeoutSec) //throw FileError
{
try
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
return gps->removeUserSession(accountEmail, timeoutSec); //throw SysError
throw SysError(formatSystemError("gdriveRemoveUser", L"", L"Function call not allowed during init/shutdown."));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to disconnect from %x."), L"%x", fmtPath(getGdriveDisplayPath({{accountEmail, Zstr("")}, AfsPath()}))), e.toString()); }
}
std::vector<std::string /*account email*/> fff::gdriveListAccounts() //throw FileError
{
try
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
return gps->listAccounts(); //throw SysError
throw SysError(formatSystemError("gdriveListAccounts", L"", L"Function call not allowed during init/shutdown."));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive"), e.toString()); }
}
std::vector<Zstring /*locationName*/> fff::gdriveListLocations(const std::string& accountEmail, int timeoutSec) //throw FileError
{
try
{
if (const std::shared_ptr<GdrivePersistentSessions> gps = globalGdriveSessions.get())
return gps->listLocations(accountEmail, timeoutSec); //throw SysError
throw SysError(formatSystemError("gdriveListLocations", L"", L"Function call not allowed during init/shutdown."));
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getGdriveDisplayPath({{accountEmail, Zstr("")}, AfsPath()}))), e.toString()); }
}
AfsDevice fff::condenseToGdriveDevice(const GdriveLogin& login) //noexcept
{
//clean up input:
GdriveLogin loginTmp = login;
trim(loginTmp.email);
loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec);
return makeSharedRef<GdriveFileSystem>(loginTmp);
}
GdriveLogin fff::extractGdriveLogin(const AfsDevice& afsDevice) //noexcept
{
if (const auto gdriveDevice = dynamic_cast<const GdriveFileSystem*>(&afsDevice.ref()))
return gdriveDevice ->getGdriveLogin();
assert(false);
return {};
}
Zstring fff::getGoogleDriveFolderUrl(const AbstractPath& folderPath) //throw FileError
{
if (const auto gdriveDevice = dynamic_cast<const GdriveFileSystem*>(&folderPath.afsDevice.ref()))
return gdriveDevice->getFolderUrl(folderPath.afsPath); //throw FileError
//assert(false);
return {};
}
bool fff::acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase) //noexcept
{
Zstring path = expandMacros(itemPathPhrase); //expand before trimming!
trim(path);
return startsWithAsciiNoCase(path, gdrivePrefix);
}
/* syntax: gdrive:\<email>[:<shared drive>]\<relative-path>[|option_name=value]
e.g.: gdrive:\john@gmail.com\folder\file.txt
gdrive:\john@gmail.com:location\folder\file.txt|option_name=value */
AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept
{
Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming!
trim(pathPhrase);
if (startsWithAsciiNoCase(pathPhrase, gdrivePrefix))
pathPhrase = pathPhrase.c_str() + strLength(gdrivePrefix);
trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); });
const ZstringView fullPath = beforeFirst<ZstringView>(pathPhrase, Zstr('|'), IfNotFoundReturn::all);
const ZstringView options = afterFirst<ZstringView>(pathPhrase, Zstr('|'), IfNotFoundReturn::none);
auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; });
const ZstringView emailAndDrive = makeStringView(fullPath.begin(), it);
const AfsPath itemPath = sanitizeDeviceRelativePath({it, fullPath.end()});
GdriveLogin login
{
.email = utfTo<std::string>(beforeFirst(emailAndDrive, Zstr(':'), IfNotFoundReturn::all)),
.locationName = Zstring(afterFirst (emailAndDrive, Zstr(':'), IfNotFoundReturn::none)),
};
split(options, Zstr('|'), [&](ZstringView optPhrase)
{
optPhrase = trimCpy(optPhrase);
if (!optPhrase.empty())
{
if (startsWith(optPhrase, Zstr("timeout=")))
login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none));
else
assert(false);
}
});
return AbstractPath(makeSharedRef<GdriveFileSystem>(login), itemPath);
}