mirror of
https://github.com/abraunegg/onedrive
synced 2024-06-04 15:02:25 +02:00
switch to Microsoft Graph API
This commit is contained in:
parent
203062fc6b
commit
1beadf2577
|
@ -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 = ".*"
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
23
src/sync.d
23
src/sync.d
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue