switch to Microsoft Graph API

This commit is contained in:
skilion 2017-03-11 11:40:19 +01:00
parent 203062fc6b
commit 1beadf2577
3 changed files with 77 additions and 27 deletions

View file

@ -1,4 +1,4 @@
client_id = "000000004C15842F" client_id = "22c49a0d-d21c-4792-aed1-8f163c982546"
sync_dir = "~/OneDrive" sync_dir = "~/OneDrive"
skip_file = ".*|~*" skip_file = ".*|~*"
skip_dir = ".*" skip_dir = ".*"

View file

@ -6,12 +6,12 @@ static import log;
private immutable { private immutable {
string authUrl = "https://login.live.com/oauth20_authorize.srf"; string authUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
string redirectUrl = "https://login.live.com/oauth20_desktop.srf"; // "urn:ietf:wg:oauth:2.0:oob"; string redirectUrl = "https://login.microsoftonline.com/common/oauth2/nativeclient";
string tokenUrl = "https://login.live.com/oauth20_token.srf"; string tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
string driveUrl = "https://api.onedrive.com/v1.0/drive"; string driveUrl = "https://graph.microsoft.com/v1.0/me/drive";
string itemByIdUrl = "https://api.onedrive.com/v1.0/drive/items/"; string itemByIdUrl = "https://graph.microsoft.com/v1.0/me/drive/items/";
string itemByPathUrl = "https://api.onedrive.com/v1.0/drive/root:/"; string itemByPathUrl = "https://graph.microsoft.com/v1.0/me/drive/root:/";
} }
class OneDriveException: Exception class OneDriveException: Exception
@ -79,7 +79,7 @@ final class OneDriveApi
{ {
import std.stdio, std.regex; import std.stdio, std.regex;
char[] response; char[] response;
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=onedrive.readwrite%20offline_access&response_type=code&redirect_uri=" ~ redirectUrl; string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=files.readwrite.all%20offline_access&response_type=code&redirect_uri=" ~ redirectUrl;
log.log("Authorize this app visiting:\n"); log.log("Authorize this app visiting:\n");
write(url, "\n\n", "Enter the response uri: "); write(url, "\n\n", "Enter the response uri: ");
readln(response); readln(response);
@ -105,9 +105,9 @@ final class OneDriveApi
JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken) JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
const(char)[] url = itemByIdUrl ~ id ~ "/view.delta"; const(char)[] url = itemByIdUrl ~ id ~ "/delta";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,remoteItem,parentReference"; url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,remoteItem,parentReference";
if (statusToken) url ~= "?token=" ~ statusToken; if (statusToken) url ~= "&token=" ~ statusToken;
return get(url); return get(url);
} }
@ -115,7 +115,9 @@ final class OneDriveApi
JSONValue viewChangesByPath(const(char)[] path, const(char)[] statusToken) JSONValue viewChangesByPath(const(char)[] path, const(char)[] statusToken)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(path) ~ ":/view.delta"; //string url = itemByPathUrl ~ encodeComponent(path) ~ ":/delta";
// HACK: item by path seems to no be working
string url = "https://graph.microsoft.com/v1.0/me/drive/root/delta";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,remoteItem,parentReference"; url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,remoteItem,parentReference";
if (statusToken) url ~= "&token=" ~ statusToken; if (statusToken) url ~= "&token=" ~ statusToken;
return get(url); return get(url);
@ -138,7 +140,6 @@ final class OneDriveApi
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(remotePath) ~ ":/content"; string url = itemByPathUrl ~ encodeComponent(remotePath) ~ ":/content";
http.addRequestHeader("Content-Type", "application/octet-stream");
if (eTag) http.addRequestHeader("If-Match", eTag); if (eTag) http.addRequestHeader("If-Match", eTag);
else url ~= "?@name.conflictBehavior=fail"; else url ~= "?@name.conflictBehavior=fail";
return upload(localPath, url); return upload(localPath, url);
@ -177,7 +178,7 @@ final class OneDriveApi
JSONValue createUploadSession(const(char)[] path, const(char)[] eTag = null) JSONValue createUploadSession(const(char)[] path, const(char)[] eTag = null)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(path) ~ ":/upload.createSession"; string url = itemByPathUrl ~ encodeComponent(path) ~ ":/createUploadSession";
if (eTag) http.addRequestHeader("If-Match", eTag); if (eTag) http.addRequestHeader("If-Match", eTag);
return post(url, null); return post(url, null);
} }
@ -192,7 +193,8 @@ final class OneDriveApi
} }
http.method = HTTP.Method.put; http.method = HTTP.Method.put;
http.url = uploadUrl; http.url = uploadUrl;
addAccessTokenHeader(); // when using microsoft graph the auth code is different
//addAccessTokenHeader();
import std.conv; import std.conv;
string contentRange = "bytes " ~ to!string(offset) ~ "-" ~ to!string(offset + offsetSize - 1) ~ "/" ~ to!string(fileSize); string contentRange = "bytes " ~ to!string(offset) ~ "-" ~ to!string(offset + offsetSize - 1) ~ "/" ~ to!string(fileSize);
http.addRequestHeader("Content-Range", contentRange); http.addRequestHeader("Content-Range", contentRange);
@ -210,7 +212,8 @@ final class OneDriveApi
JSONValue requestUploadStatus(const(char)[] uploadUrl) JSONValue requestUploadStatus(const(char)[] uploadUrl)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
return get(uploadUrl); // when using microsoft graph the auth code is different
return get(uploadUrl, true);
} }
private void redeemToken(const(char)[] authCode) private void redeemToken(const(char)[] authCode)
@ -254,12 +257,12 @@ final class OneDriveApi
http.addRequestHeader("Authorization", accessToken); http.addRequestHeader("Authorization", accessToken);
} }
private JSONValue get(const(char)[] url) private JSONValue get(const(char)[] url, bool skipToken = false)
{ {
scope(exit) http.clearRequestHeaders(); scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.get; http.method = HTTP.Method.get;
http.url = url; http.url = url;
addAccessTokenHeader(); if (!skipToken) addAccessTokenHeader(); // HACK: requestUploadStatus
auto response = perform(); auto response = perform();
checkHttpCode(response); checkHttpCode(response);
return response; return response;
@ -366,7 +369,15 @@ final class OneDriveApi
} catch (CurlException e) { } catch (CurlException e) {
throw new OneDriveException(e.msg, e); throw new OneDriveException(e.msg, e);
} }
return content.parseJSON(); JSONValue json;
try {
json = content.parseJSON();
} catch (JSONException e) {
e.msg ~= "\n";
e.msg ~= content;
throw e;
}
return json;
} }
private void checkHttpCode() private void checkHttpCode()
@ -383,3 +394,37 @@ final class OneDriveApi
} }
} }
} }
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();
}

View file

@ -5,21 +5,22 @@ import config, itemdb, onedrive, upload, util;
static import log; static import log;
// threshold after which files will be uploaded using an upload session // threshold after which files will be uploaded using an upload session
private long thresholdFileSize = 10 * 2^^20; // 10 MiB private long thresholdFileSize = 4 * 2^^20; // 4 MiB
private bool isItemFolder(const ref JSONValue item) private bool isItemFolder(const ref JSONValue item)
{ {
return (("folder" in item.object) !is null); return ("folder" in item) != null;
} }
private bool isItemFile(const ref JSONValue item) private bool isItemFile(const ref JSONValue item)
{ {
return (("file" in item.object) !is null); return ("file" in item) != null;
} }
private bool isItemDeleted(const ref JSONValue item) private bool isItemDeleted(const ref JSONValue item)
{ {
return (("deleted" in item.object) !is null); // HACK: fix for https://github.com/skilion/onedrive/issues/157
return ("deleted" in item) || ("fileSystemInfo" !in item);
} }
private bool testCrc32(string path, const(char)[] crc32) private bool testCrc32(string path, const(char)[] crc32)
@ -107,9 +108,15 @@ final class SyncEngine
foreach (item; changes["value"].array) { foreach (item; changes["value"].array) {
applyDifference(item); applyDifference(item);
} }
statusToken = changes["@delta.token"].str; // hack to reuse old code
string url;
if ("@odata.nextLink" in changes) url = changes["@odata.nextLink"].str;
if ("@odata.deltaLink" in changes) url = changes["@odata.deltaLink"].str;
auto c = matchFirst(url, r"(?:token=)([\w\d]+)");
c.popFront(); // skip the whole match
statusToken = c.front;
std.file.write(cfg.statusTokenFilePath, statusToken); std.file.write(cfg.statusTokenFilePath, statusToken);
} while (!((changes.type == JSON_TYPE.OBJECT) && (("@odata.nextLink" in changes) is null))); } while ("@odata.nextLink" in changes);
} catch (ErrnoException e) { } catch (ErrnoException e) {
throw new SyncException(e.msg, e); throw new SyncException(e.msg, e);
} catch (FileException e) { } catch (FileException e) {
@ -477,7 +484,6 @@ final class SyncEngine
} else { } else {
response = session.upload(path, path); response = session.upload(path, path);
} }
saveItem(response);
string id = response["id"].str; string id = response["id"].str;
string cTag = response["cTag"].str; string cTag = response["cTag"].str;
SysTime mtime = timeLastModified(path).toUTC(); SysTime mtime = timeLastModified(path).toUTC();
@ -512,7 +518,6 @@ final class SyncEngine
private void saveItem(JSONValue jsonItem) private void saveItem(JSONValue jsonItem)
{ {
string id = jsonItem["id"].str;
ItemType type; ItemType type;
if (isItemFile(jsonItem)) { if (isItemFile(jsonItem)) {
type = ItemType.file; type = ItemType.file;
@ -522,7 +527,7 @@ final class SyncEngine
assert(0); assert(0);
} }
Item item = { Item item = {
id: id, id: jsonItem["id"].str,
name: jsonItem["name"].str, name: jsonItem["name"].str,
type: type, type: type,
eTag: jsonItem["eTag"].str, eTag: jsonItem["eTag"].str,