reimplemented std.net.curl basic methods in order to intercept http status codes

This commit is contained in:
skilion 2015-10-04 16:24:45 +02:00
parent 9da9b7240d
commit e7d493807d

View file

@ -12,15 +12,15 @@ private immutable {
class OneDriveException: Exception class OneDriveException: Exception
{ {
@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) // HTTP status code
{ int code;
super(msg, file, line, next);
}
@nogc @safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__) @safe pure this(int code, string reason, string file = __FILE__, size_t line = __LINE__)
{ {
super(msg, file, line, next); this.code = code;
} string msg = format("HTTP request returned status code %d (%s)", code, reason);
super(msg, file, line, next);
}
} }
final class OneDriveApi final class OneDriveApi
@ -66,28 +66,6 @@ final class OneDriveApi
newToken(); 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 // https://dev.onedrive.com/items/view_changes.htm
JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken) JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken)
{ {
@ -111,14 +89,12 @@ final class OneDriveApi
void downloadById(const(char)[] id, string saveToPath) void downloadById(const(char)[] id, string saveToPath)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id ~ "/content"; scope(failure) {
try {
download(url, saveToPath, http);
} catch (CurlException e) {
import std.file; import std.file;
if (exists(saveToPath)) remove(saveToPath); if (exists(saveToPath)) remove(saveToPath);
throw new OneDriveException("Download error", e);
} }
char[] url = itemByIdUrl ~ id ~ "/content";
download(url, saveToPath);
} }
// https://dev.onedrive.com/items/upload_put.htm // https://dev.onedrive.com/items/upload_put.htm
@ -126,24 +102,10 @@ final class OneDriveApi
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(remotePath) ~ ":/content"; 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"); http.addRequestHeader("Content-Type", "application/octet-stream");
try { if (eTag) http.addRequestHeader("If-Match", eTag);
upload(localPath, url, http); else url ~= "?@name.conflictBehavior=fail";
} catch (ErrnoException e) { return upload(localPath, url);
throw e;
} finally {
// remove the headers
setAccessToken(accessToken);
}
checkHttpCode();
return parseJSON(content);
} }
// https://dev.onedrive.com/items/update.htm // https://dev.onedrive.com/items/update.htm
@ -153,10 +115,7 @@ final class OneDriveApi
char[] url = itemByIdUrl ~ id; char[] url = itemByIdUrl ~ id;
if (eTag) http.addRequestHeader("If-Match", eTag); if (eTag) http.addRequestHeader("If-Match", eTag);
http.addRequestHeader("Content-Type", "application/json"); http.addRequestHeader("Content-Type", "application/json");
auto result = patch(url, data.toString()); return patch(url, data.toString());
// remove the headers
setAccessToken(accessToken);
return result;
} }
//https://dev.onedrive.com/items/delete.htm //https://dev.onedrive.com/items/delete.htm
@ -165,9 +124,7 @@ final class OneDriveApi
checkAccessTokenExpired(); checkAccessTokenExpired();
char[] url = itemByIdUrl ~ id; char[] url = itemByIdUrl ~ id;
if (eTag) http.addRequestHeader("If-Match", eTag); if (eTag) http.addRequestHeader("If-Match", eTag);
del(url, http); del(url);
// remove the if-match header
if (eTag) setAccessToken(accessToken);
} }
//https://dev.onedrive.com/items/create.htm //https://dev.onedrive.com/items/create.htm
@ -175,10 +132,7 @@ final class OneDriveApi
{ {
string url = itemByPathUrl ~ encodeComponent(parentPath) ~ ":/children"; string url = itemByPathUrl ~ encodeComponent(parentPath) ~ ":/children";
http.addRequestHeader("Content-Type", "application/json"); http.addRequestHeader("Content-Type", "application/json");
auto result = post(url, item.toString()); return post(url, item.toString());
// remove the if-match header
setAccessToken(accessToken);
return result;
} }
// https://dev.onedrive.com/items/upload_large_files.htm // https://dev.onedrive.com/items/upload_large_files.htm
@ -187,40 +141,37 @@ final class OneDriveApi
checkAccessTokenExpired(); checkAccessTokenExpired();
string url = itemByPathUrl ~ encodeComponent(path) ~ ":/upload.createSession"; string url = itemByPathUrl ~ encodeComponent(path) ~ ":/upload.createSession";
if (eTag) http.addRequestHeader("If-Match", eTag); if (eTag) http.addRequestHeader("If-Match", eTag);
auto result = post(url, null); return post(url, null);
// remove the if-match header
if (eTag) setAccessToken(accessToken);
return result;
} }
// https://dev.onedrive.com/items/upload_large_files.htm // https://dev.onedrive.com/items/upload_large_files.htm
JSONValue uploadFragment(const(char)[] uploadUrl, string filepath, long offset, long offsetSize, long fileSize) JSONValue uploadFragment(const(char)[] uploadUrl, string filepath, long offset, long offsetSize, long fileSize)
{ {
checkAccessTokenExpired(); checkAccessTokenExpired();
scope(exit) {
http.clearRequestHeaders();
http.onSend = null;
}
http.method = HTTP.Method.put; http.method = HTTP.Method.put;
http.url = uploadUrl; http.url = uploadUrl;
ubyte[] content; addAccessTokenHeader();
http.onReceive = (ubyte[] data) {
content ~= data;
return data.length;
};
auto file = File(filepath, "rb");
file.seek(offset);
http.onSend = data => file.rawRead(data).length;
http.contentLength = offsetSize;
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);
http.perform(); auto file = File(filepath, "rb");
checkHttpCode(); // TODO: retry on 5xx errors file.seek(offset);
// remove the content-range header http.onSend = data => file.rawRead(data).length;
scope(exit) setAccessToken(accessToken); http.contentLength = offsetSize;
return parseJSON(content); auto response = perform();
// TODO: retry on 5xx errors
checkHttpCode();
return response;
} }
// https://dev.onedrive.com/items/upload_large_files.htm // https://dev.onedrive.com/items/upload_large_files.htm
JSONValue requestUploadStatus(const(char)[] uploadUrl) JSONValue requestUploadStatus(const(char)[] uploadUrl)
{ {
checkAccessTokenExpired();
return get(uploadUrl); return get(uploadUrl);
} }
@ -241,19 +192,12 @@ final class OneDriveApi
private void acquireToken(const(char)[] postData) private void acquireToken(const(char)[] postData)
{ {
JSONValue response = post(tokenUrl, postData); JSONValue response = post(tokenUrl, postData);
setAccessToken(response["access_token"].str()); accessToken = "bearer " ~ response["access_token"].str();
refreshToken = response["refresh_token"].str(); refreshToken = response["refresh_token"].str();
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer()); accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (onRefreshToken) onRefreshToken(refreshToken); if (onRefreshToken) onRefreshToken(refreshToken);
} }
private void setAccessToken(string accessToken)
{
http.clearRequestHeaders();
this.accessToken = accessToken;
http.addRequestHeader("Authorization", "bearer " ~ accessToken);
}
private void checkAccessTokenExpired() private void checkAccessTokenExpired()
{ {
if (Clock.currTime() >= accessTokenExpiration) { if (Clock.currTime() >= accessTokenExpiration) {
@ -261,25 +205,126 @@ final class OneDriveApi
} }
} }
private auto get(const(char)[] url) private void addAccessTokenHeader()
{ {
return parseJSON(.get(url, http)); http.addRequestHeader("Authorization", accessToken);
}
private JSONValue get(const(char)[] url)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.get;
http.url = url;
addAccessTokenHeader();
auto response = perform();
checkHttpCode();
return response;
}
private void del(const(char)[] url)
{
scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.del;
http.url = url;
addAccessTokenHeader();
perform();
checkHttpCode();
}
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();
} }
private auto patch(T)(const(char)[] url, const(T)[] patchData) private auto patch(T)(const(char)[] url, const(T)[] patchData)
{ {
return parseJSON(.patch(url, patchData, http)); scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.patch;
http.url = url;
addAccessTokenHeader();
auto response = perform(patchData);
checkHttpCode();
return response;
} }
private auto post(T)(const(char)[] url, const(T)[] postData) private auto post(T)(const(char)[] url, const(T)[] postData)
{ {
return parseJSON(.post(url, postData, http)); scope(exit) http.clearRequestHeaders();
http.method = HTTP.Method.post;
http.url = url;
addAccessTokenHeader();
auto response = perform(postData);
checkHttpCode();
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();
checkHttpCode();
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;
ubyte[] content;
http.onReceive = (ubyte[] data) {
content ~= data;
return data.length;
};
http.perform();
return content.parseJSON();
} }
private void checkHttpCode() private void checkHttpCode()
{ {
if (http.statusLine.code / 100 != 2) { if (http.statusLine.code / 100 != 2) {
throw new OneDriveException(format("HTTP request returned status code %d (%s)", http.statusLine.code, http.statusLine.reason)); throw new OneDriveException(http.statusLine.code, http.statusLine.reason);
} }
} }
} }