abraunegg-onedrive/src/onedrive.d
2015-09-22 16:14:16 +02:00

243 lines
7 KiB
D

import std.datetime, std.exception, std.json, std.net.curl, std.path;
import std.string, std.uni, std.uri;
import config;
private immutable {
string authUrl = "https://login.live.com/oauth20_authorize.srf";
string redirectUrl = "https://login.live.com/oauth20_desktop.srf";
string tokenUrl = "https://login.live.com/oauth20_token.srf";
string itemByIdUrl = "https://api.onedrive.com/v1.0/drive/items/";
string itemByPathUrl = "https://api.onedrive.com/v1.0/drive/root:/";
}
class OneDriveException: Exception
{
@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
{
super(msg, file, line, next);
}
@nogc @safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line, next);
}
}
final class OneDriveApi
{
private string clientId, clientSecret;
private string refreshToken, accessToken;
private SysTime accessTokenExpiration;
private HTTP http;
void delegate(string) onRefreshToken; // called when a new refresh_token is received
this(Config cfg, bool verbose)
{
this.clientId = cfg.get("client_id");
this.clientSecret = cfg.get("client_secret");
http = HTTP();
//http.verbose = verbose;
}
void authorize()
{
import std.stdio, std.regex;
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=wl.offline_access onedrive.readwrite&response_type=code&redirect_url=" ~ redirectUrl;
writeln("Authorize this app visiting:\n");
writeln(url, "\n");
while (true) {
char[] response;
write("Enter the response url: ");
readln(response);
auto c = matchFirst(response, r"(?:code=)(([\w\d]+-){4}[\w\d]+)");
if (!c.empty) {
c.popFront(); // skip the whole match
redeemToken(c.front);
break;
}
}
}
void setRefreshToken(string refreshToken)
{
this.refreshToken = refreshToken;
newToken();
}
string getItemPath(const(char)[] id)
{
checkAccessTokenExpired();
JSONValue response = get(itemByIdUrl ~ id ~ "/?select=name,parentReference");
string path;
try {
path = response["parentReference"]["path"].str;
} catch (JSONException e) {
// root does not have parentReference
return "";
}
path = decodeComponent(path[path.indexOf(':') + 1 .. $]);
return buildNormalizedPath("." ~ path ~ "/" ~ response["name"].str);
}
string getItemId(const(char)[] path)
{
checkAccessTokenExpired();
JSONValue response = get(itemByPathUrl ~ encodeComponent(path) ~ ":/?select=id");
return response["id"].str;
}
// https://dev.onedrive.com/items/view_changes.htm
JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken)
{
checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id ~ "/view.changes";
if (statusToken) url ~= "?token=" ~ statusToken;
return get(url);
}
// https://dev.onedrive.com/items/view_changes.htm
JSONValue viewChangesByPath(const(char)[] path, const(char)[] statusToken)
{
checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(path) ~ ":/view.changes";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,parentReference";
if (statusToken) url ~= "&token=" ~ statusToken;
return get(url);
}
// https://dev.onedrive.com/items/download.htm
void downloadById(const(char)[] id, string saveToPath)
{
checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id ~ "/content";
try {
download(url, saveToPath, http);
} catch (CurlException e) {
import std.file;
if (exists(saveToPath)) remove(saveToPath);
throw new OneDriveException("Download error", e);
}
}
// https://dev.onedrive.com/items/upload_put.htm
JSONValue simpleUpload(string localPath, const(char)[] remotePath, const(char)[] eTag = null)
{
checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(remotePath) ~ ":/content";
if (!eTag) url ~= "?@name.conflictBehavior=fail";
ubyte[] content;
http.onReceive = (ubyte[] data) {
content ~= data;
return data.length;
};
if (eTag) http.addRequestHeader("If-Match", eTag);
http.addRequestHeader("Content-Type", "application/octet-stream");
try {
upload(localPath, url, http);
} catch (ErrnoException e) {
throw e;
} finally {
// remove the headers
setAccessToken(accessToken);
}
checkHttpCode();
return parseJSON(content);
}
// https://dev.onedrive.com/items/update.htm
JSONValue updateById(const(char)[] id, JSONValue data, const(char)[] eTag = null)
{
checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id;
if (eTag) http.addRequestHeader("If-Match", eTag);
http.addRequestHeader("Content-Type", "application/json");
auto result = patch(url, data.toString());
// remove the headers
setAccessToken(accessToken);
return result;
}
//https://dev.onedrive.com/items/delete.htm
void deleteById(const(char)[] id, const(char)[] eTag = null)
{
checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id;
if (eTag) http.addRequestHeader("If-Match", eTag);
del(url, http);
// remove the if-match header
if (eTag) setAccessToken(accessToken);
}
//https://dev.onedrive.com/items/create.htm
JSONValue createByPath(const(char)[] parentPath, JSONValue item)
{
string url = itemByPathUrl ~ encodeComponent(parentPath) ~ ":/children";
http.addRequestHeader("Content-Type", "application/json");
auto result = post(url, item.toString());
// remove the if-match header
setAccessToken(accessToken);
return result;
}
private void redeemToken(const(char)[] authCode)
{
string postData = "client_id=" ~ clientId ~ "&redirect_url=" ~ redirectUrl ~ "&client_secret=" ~ clientSecret;
postData ~= "&code=" ~ authCode ~ "&grant_type=authorization_code";
acquireToken(postData);
}
private void newToken()
{
string postData = "client_id=" ~ clientId ~ "&redirect_url=" ~ redirectUrl ~ "&client_secret=" ~ clientSecret;
postData ~= "&refresh_token=" ~ refreshToken ~ "&grant_type=refresh_token";
acquireToken(postData);
}
private void acquireToken(const(char)[] postData)
{
JSONValue response = post(tokenUrl, postData);
setAccessToken(response["access_token"].str());
refreshToken = response["refresh_token"].str();
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (onRefreshToken) onRefreshToken(refreshToken);
}
private void setAccessToken(string accessToken)
{
http.clearRequestHeaders();
this.accessToken = accessToken;
http.addRequestHeader("Authorization", "bearer " ~ accessToken);
}
private void checkAccessTokenExpired()
{
if (Clock.currTime() >= accessTokenExpiration) {
newToken();
}
}
private auto get(const(char)[] url)
{
return parseJSON(.get(url, http));
}
private auto patch(T)(const(char)[] url, const(T)[] patchData)
{
return parseJSON(.patch(url, patchData, http));
}
private auto post(T)(const(char)[] url, const(T)[] postData)
{
return parseJSON(.post(url, postData, http));
}
private void checkHttpCode()
{
if (http.statusLine.code / 100 != 2) {
throw new OneDriveException(format("HTTP request returned status code %d (%s)", http.statusLine.code, http.statusLine.reason));
}
}
}