abraunegg-onedrive/src/onedrive.d

459 lines
14 KiB
D
Raw Normal View History

2015-12-29 19:38:15 +01:00
import std.net.curl: CurlException, HTTP;
import std.datetime, std.exception, std.file, std.json, std.path;
import std.stdio, std.string, std.uni, std.uri;
2015-09-14 19:21:06 +02:00
import config;
static import log;
2015-09-01 20:45:34 +02:00
2015-12-29 19:38:15 +01:00
2015-09-01 20:45:34 +02:00
private immutable {
2017-03-12 10:30:31 +01:00
string clientId = "22c49a0d-d21c-4792-aed1-8f163c982546";
2017-03-11 11:40:19 +01:00
string authUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
2017-12-27 15:12:38 +01:00
string redirectUrl = "urn:ietf:wg:oauth:2.0:oob";
2017-03-11 11:40:19 +01:00
string tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
string driveUrl = "https://graph.microsoft.com/v1.0/me/drive";
string itemByIdUrl = "https://graph.microsoft.com/v1.0/me/drive/items/";
string itemByPathUrl = "https://graph.microsoft.com/v1.0/me/drive/root:/";
2017-12-27 15:12:38 +01:00
string driveByIdUrl = "https://graph.microsoft.com/v1.0/drives/";
2015-09-01 20:45:34 +02:00
}
class OneDriveException: Exception
{
2017-12-28 15:03:15 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors
2016-12-14 15:17:20 +01:00
int httpStatusCode;
2016-12-13 18:04:16 +01:00
JSONValue error;
2016-12-14 15:17:20 +01:00
@safe pure this(int httpStatusCode, string reason, string file = __FILE__, size_t line = __LINE__)
{
2016-12-14 15:17:20 +01:00
this.httpStatusCode = httpStatusCode;
2016-12-13 18:04:16 +01:00
this.error = error;
2016-12-14 15:17:20 +01:00
string msg = format("HTTP request returned status code %d (%s)", httpStatusCode, reason);
2017-12-28 15:03:15 +01:00
super(msg, file, line);
}
2016-12-13 18:04:16 +01:00
2016-12-14 15:17:20 +01:00
this(int httpStatusCode, string reason, ref const JSONValue error, string file = __FILE__, size_t line = __LINE__)
2016-12-13 18:04:16 +01:00
{
2016-12-14 15:17:20 +01:00
this.httpStatusCode = httpStatusCode;
2016-12-13 18:04:16 +01:00
this.error = error;
string msg = format("HTTP request returned status code %d (%s)\n%s", httpStatusCode, reason, toJSON(error, true));
2017-12-28 15:03:15 +01:00
super(msg, file, line);
}
2015-09-01 20:45:34 +02:00
}
final class OneDriveApi
{
private Config cfg;
2015-09-01 20:45:34 +02:00
private string refreshToken, accessToken;
2015-09-01 22:23:42 +02:00
private SysTime accessTokenExpiration;
/* private */ HTTP http;
2015-09-01 20:45:34 +02:00
2017-05-28 20:14:50 +02:00
// if true, every new access token is printed
bool printAccessToken;
this(Config cfg)
2015-09-01 20:45:34 +02:00
{
this.cfg = cfg;
2015-09-01 20:45:34 +02:00
http = HTTP();
//http.verbose = true;
}
bool init()
{
try {
refreshToken = readText(cfg.refreshTokenFilePath);
} catch (FileException e) {
return authorize();
}
return true;
2015-09-01 20:45:34 +02:00
}
2016-06-30 12:51:44 +02:00
bool authorize()
2015-09-01 20:45:34 +02:00
{
import std.stdio, std.regex;
2016-06-30 12:51:44 +02:00
char[] response;
2017-03-11 14:52:33 +01:00
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=files.readwrite%20files.readwrite.all%20offline_access&response_type=code&redirect_uri=" ~ redirectUrl;
log.log("Authorize this app visiting:\n");
2016-06-30 12:51:44 +02:00
write(url, "\n\n", "Enter the response uri: ");
readln(response);
// match the authorization code
2017-03-12 19:40:38 +01:00
auto c = matchFirst(response, r"(?:code=)([\w\d-]+)");
2016-06-30 12:51:44 +02:00
if (c.empty) {
log.log("Invalid uri");
2016-06-30 12:51:44 +02:00
return false;
2015-09-01 20:45:34 +02:00
}
2016-06-30 12:51:44 +02:00
c.popFront(); // skip the whole match
redeemToken(c.front);
return true;
2015-09-01 20:45:34 +02:00
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get
2016-12-25 16:38:55 +01:00
JSONValue getDefaultDrive()
{
checkAccessTokenExpired();
return get(driveUrl);
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get
JSONValue getDefaultRoot()
{
checkAccessTokenExpired();
return get(driveUrl ~ "/root");
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta
JSONValue viewChangesById(const(char)[] driveId, const(char)[] id, const(char)[] deltaLink)
2015-09-01 20:45:34 +02:00
{
2015-09-01 22:23:42 +02:00
checkAccessTokenExpired();
2017-12-27 15:12:38 +01:00
const(char)[] url = deltaLink;
if (url == null) {
url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/delta";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference";
}
2015-09-01 20:45:34 +02:00
return get(url);
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta
JSONValue viewChangesByPath(const(char)[] path, const(char)[] deltaLink)
2015-09-01 20:45:34 +02:00
{
2015-09-01 22:23:42 +02:00
checkAccessTokenExpired();
2017-12-27 15:12:38 +01:00
const(char)[] url = deltaLink;
if (url == null) {
if (path == ".") url = driveUrl ~ "/root/delta";
else url = itemByPathUrl ~ encodeComponent(path) ~ ":/delta";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference";
}
2015-09-01 20:45:34 +02:00
return get(url);
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content
void downloadById(const(char)[] driveId, const(char)[] id, string saveToPath)
2015-09-01 20:45:34 +02:00
{
2015-09-01 22:23:42 +02:00
checkAccessTokenExpired();
scope(failure) {
2015-09-01 20:45:34 +02:00
if (exists(saveToPath)) remove(saveToPath);
}
2017-12-27 15:12:38 +01:00
mkdirRecurse(dirName(saveToPath));
const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content?AVOverride=1";
download(url, saveToPath);
2015-09-01 20:45:34 +02:00
}
2017-12-31 02:30:31 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content
2015-09-05 21:23:43 +02:00
JSONValue simpleUpload(string localPath, const(char)[] remotePath, const(char)[] eTag = null)
2015-09-01 20:45:34 +02:00
{
2015-09-01 22:23:42 +02:00
checkAccessTokenExpired();
2015-09-16 10:29:20 +02:00
string url = itemByPathUrl ~ encodeComponent(remotePath) ~ ":/content";
2017-12-31 02:30:31 +01:00
// TODO: investigate why this fails for remote folders
//if (eTag) http.addRequestHeader("If-Match", eTag);
/*else*/ http.addRequestHeader("If-None-Match", "*");
return upload(localPath, url);
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content
JSONValue simpleUpload(string localPath, string parentDriveId, string parentId, string filename, const(char)[] eTag = null)
{
checkAccessTokenExpired();
string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ ":/" ~ encodeComponent(filename) ~ ":/content";
// TODO: investigate why this fails for remote folders
//if (eTag) http.addRequestHeader("If-Match", eTag);
/*else http.addRequestHeader("If-None-Match", "*");*/
return upload(localPath, url);
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content
JSONValue simpleUploadById(string localPath, string driveId, string id, const(char)[] eTag = null)
{
checkAccessTokenExpired();
string url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content";
if (eTag) http.addRequestHeader("If-Match", eTag);
return upload(localPath, url);
2015-09-01 20:45:34 +02:00
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update
JSONValue updateById(const(char)[] driveId, const(char)[] id, JSONValue data, const(char)[] eTag = null)
2015-09-05 21:23:43 +02:00
{
checkAccessTokenExpired();
2017-12-27 15:12:38 +01:00
const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id;
2015-09-05 21:23:43 +02:00
if (eTag) http.addRequestHeader("If-Match", eTag);
http.addRequestHeader("Content-Type", "application/json");
return patch(url, data.toString());
2015-09-05 21:23:43 +02:00
}
2017-12-27 15:12:38 +01:00
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete
void deleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null)
2015-09-06 10:23:32 +02:00
{
checkAccessTokenExpired();
2017-12-27 15:12:38 +01:00
const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id;
//TODO: investigate why this always fail with 412 (Precondition Failed)
//if (eTag) http.addRequestHeader("If-Match", eTag);
del(url);
2015-09-06 10:23:32 +02:00
}
2015-12-29 19:38:15 +01:00
// https://dev.onedrive.com/items/create.htm
2015-09-06 11:06:13 +02:00
JSONValue createByPath(const(char)[] parentPath, JSONValue item)
{
checkAccessTokenExpired();
2015-09-16 10:29:20 +02:00
string url = itemByPathUrl ~ encodeComponent(parentPath) ~ ":/children";
2017-03-11 13:34:07 +01:00
// HACK
if (parentPath == ".") url = driveUrl ~ "/root/children";
2015-09-06 11:06:13 +02:00
http.addRequestHeader("Content-Type", "application/json");
return post(url, item.toString());
2015-09-06 11:06:13 +02:00
}
// https://dev.onedrive.com/items/upload_large_files.htm
JSONValue createUploadSession(const(char)[] path, const(char)[] eTag = null)
{
checkAccessTokenExpired();
2017-03-11 11:40:19 +01:00
string url = itemByPathUrl ~ encodeComponent(path) ~ ":/createUploadSession";
if (eTag) http.addRequestHeader("If-Match", eTag);
return post(url, null);
}
// https://dev.onedrive.com/items/upload_large_files.htm
JSONValue uploadFragment(const(char)[] uploadUrl, string filepath, long offset, long offsetSize, long fileSize)
{
checkAccessTokenExpired();
scope(exit) {
http.clearRequestHeaders();
http.onSend = null;
}
http.method = HTTP.Method.put;
http.url = uploadUrl;
2017-03-11 11:40:19 +01:00
// when using microsoft graph the auth code is different
//addAccessTokenHeader();
import std.conv;
string contentRange = "bytes " ~ to!string(offset) ~ "-" ~ to!string(offset + offsetSize - 1) ~ "/" ~ to!string(fileSize);
http.addRequestHeader("Content-Range", contentRange);
auto file = File(filepath, "rb");
file.seek(offset);
http.onSend = data => file.rawRead(data).length;
http.contentLength = offsetSize;
auto response = perform();
// TODO: retry on 5xx errors
checkHttpCode();
return response;
}
// https://dev.onedrive.com/items/upload_large_files.htm
JSONValue requestUploadStatus(const(char)[] uploadUrl)
{
checkAccessTokenExpired();
2017-03-11 11:40:19 +01:00
// when using microsoft graph the auth code is different
return get(uploadUrl, true);
}
2015-09-01 20:45:34 +02:00
private void redeemToken(const(char)[] authCode)
{
const(char)[] postData =
"client_id=" ~ clientId ~
"&redirect_uri=" ~ redirectUrl ~
"&code=" ~ authCode ~
"&grant_type=authorization_code";
2015-09-01 20:45:34 +02:00
acquireToken(postData);
}
private void newToken()
{
string postData =
"client_id=" ~ clientId ~
"&redirect_uri=" ~ redirectUrl ~
"&refresh_token=" ~ refreshToken ~
"&grant_type=refresh_token";
2015-09-01 20:45:34 +02:00
acquireToken(postData);
}
private void acquireToken(const(char)[] postData)
{
JSONValue response = post(tokenUrl, postData);
accessToken = "bearer " ~ response["access_token"].str();
2015-09-02 11:20:42 +02:00
refreshToken = response["refresh_token"].str();
2015-09-01 22:23:42 +02:00
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
std.file.write(cfg.refreshTokenFilePath, refreshToken);
2017-05-28 20:14:50 +02:00
if (printAccessToken) writeln("New access token: ", accessToken);
2015-09-01 20:45:34 +02:00
}
2015-09-01 22:23:42 +02:00
private void checkAccessTokenExpired()
{
try {
if (Clock.currTime() >= accessTokenExpiration) {
newToken();
}
} catch (OneDriveException e) {
if (e.httpStatusCode == 400 || e.httpStatusCode == 401) {
e.msg ~= "\nRefresh token invalid, use --logout to authorize the client again";
}
throw e;
2015-09-01 22:23:42 +02:00
}
}
2015-09-05 21:23:43 +02:00
private void addAccessTokenHeader()
2015-09-01 20:45:34 +02:00
{
http.addRequestHeader("Authorization", accessToken);
}
2017-03-11 11:40:19 +01:00
private JSONValue get(const(char)[] url, bool skipToken = false)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.get;
http.url = url;
2017-03-11 11:40:19 +01:00
if (!skipToken) addAccessTokenHeader(); // HACK: requestUploadStatus
auto response = perform();
2016-12-13 18:04:16 +01:00
checkHttpCode(response);
return response;
}
private void del(const(char)[] url)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.del;
http.url = url;
addAccessTokenHeader();
2016-12-13 18:04:16 +01:00
auto response = perform();
checkHttpCode(response);
}
private void download(const(char)[] url, string filename)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.get;
http.url = url;
addAccessTokenHeader();
auto f = File(filename, "wb");
http.onReceive = (ubyte[] data) {
f.rawWrite(data);
return data.length;
};
http.perform();
checkHttpCode();
2015-09-01 20:45:34 +02:00
}
2015-09-05 21:23:43 +02:00
private auto patch(T)(const(char)[] url, const(T)[] patchData)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.patch;
http.url = url;
addAccessTokenHeader();
auto response = perform(patchData);
2016-12-13 18:04:16 +01:00
checkHttpCode(response);
return response;
2015-09-05 21:23:43 +02:00
}
2015-09-01 20:45:34 +02:00
private auto post(T)(const(char)[] url, const(T)[] postData)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.post;
http.url = url;
addAccessTokenHeader();
auto response = perform(postData);
2016-12-13 18:04:16 +01:00
checkHttpCode(response);
return response;
}
private JSONValue upload(string filepath, string url)
{
scope(exit) {
http.clearRequestHeaders();
http.onSend = null;
http.contentLength = 0;
}
http.method = HTTP.Method.put;
http.url = url;
addAccessTokenHeader();
http.addRequestHeader("Content-Type", "application/octet-stream");
auto file = File(filepath, "rb");
http.onSend = data => file.rawRead(data).length;
http.contentLength = file.size;
auto response = perform();
2016-12-13 18:04:16 +01:00
checkHttpCode(response);
return response;
}
private JSONValue perform(const(void)[] sendData)
{
scope(exit) {
http.onSend = null;
http.contentLength = 0;
}
if (sendData) {
http.contentLength = sendData.length;
http.onSend = (void[] buf) {
import std.algorithm: min;
size_t minLen = min(buf.length, sendData.length);
if (minLen == 0) return 0;
buf[0 .. minLen] = sendData[0 .. minLen];
sendData = sendData[minLen .. $];
return minLen;
};
} else {
http.onSend = buf => 0;
}
return perform();
}
private JSONValue perform()
{
scope(exit) http.onReceive = null;
2015-10-04 17:33:48 +02:00
char[] content;
http.onReceive = (ubyte[] data) {
content ~= data;
return data.length;
};
2017-12-28 15:03:15 +01:00
http.perform();
2017-03-11 11:40:19 +01:00
JSONValue json;
try {
json = content.parseJSON();
} catch (JSONException e) {
e.msg ~= "\n";
e.msg ~= content;
throw e;
}
return json;
2015-09-01 20:45:34 +02:00
}
2015-09-06 10:07:18 +02:00
private void checkHttpCode()
2015-09-01 20:45:34 +02:00
{
if (http.statusLine.code / 100 != 2) {
throw new OneDriveException(http.statusLine.code, http.statusLine.reason);
2015-09-01 20:45:34 +02:00
}
}
2016-12-13 18:04:16 +01:00
private void checkHttpCode(ref const JSONValue response)
{
if (http.statusLine.code / 100 != 2) {
throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response);
}
}
2015-09-01 20:45:34 +02:00
}
2017-03-11 11:40:19 +01:00
unittest
{
string configDirName = expandTilde("~/.config/onedrive");
auto cfg = new config.Config(configDirName);
cfg.init();
OneDriveApi onedrive = new OneDriveApi(cfg);
onedrive.init();
std.file.write("/tmp/test", "test");
// simpleUpload
auto item = onedrive.simpleUpload("/tmp/test", "/test");
try {
item = onedrive.simpleUpload("/tmp/test", "/test");
} catch (OneDriveException e) {
assert(e.httpStatusCode == 409);
}
try {
item = onedrive.simpleUpload("/tmp/test", "/test", "123");
} catch (OneDriveException e) {
assert(e.httpStatusCode == 412);
}
item = onedrive.simpleUpload("/tmp/test", "/test", item["eTag"].str);
// deleteById
try {
onedrive.deleteById(item["id"].str, "123");
} catch (OneDriveException e) {
assert(e.httpStatusCode == 412);
}
onedrive.deleteById(item["id"].str, item["eTag"].str);
onedrive.http.shutdown();
2015-09-01 20:45:34 +02:00
}