Merge pull request #3316 from SupSuper/onedrive-api

Update OneDrive Uploader to Microsoft Graph API
This commit is contained in:
Jaex 2018-04-17 17:10:47 +03:00 committed by GitHub
commit a0255f95a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 307 additions and 67 deletions

View file

@ -424,5 +424,13 @@ public static string RemoveQuery(string url)
return url;
}
public static string BuildUri(string root, string path, string query = null)
{
UriBuilder builder = new UriBuilder(root);
builder.Path = path;
builder.Query = query;
return builder.Uri.AbsoluteUri;
}
}
}

View file

@ -290,6 +290,78 @@ protected UploadResult SendRequestFile(string url, Stream data, string fileName,
return result;
}
protected UploadResult SendRequestBytes(string url, Stream data, string fileName, long contentPosition = 0, long contentLength = -1, Dictionary<string, string> args = null,
NameValueCollection headers = null, CookieCollection cookies = null, ResponseType responseType = ResponseType.Text, HttpMethod method = HttpMethod.PUT)
{
UploadResult result = new UploadResult();
IsUploading = true;
StopUploadRequested = false;
try
{
url = URLHelpers.CreateQuery(url, args);
if (contentLength == -1)
{
contentLength = data.Length;
}
contentLength = Math.Min(contentLength, data.Length - contentPosition);
string contentType = Helpers.GetMimeType(fileName);
if (headers == null)
{
headers = new NameValueCollection();
}
long startByte = contentPosition;
long endByte = startByte + contentLength - 1;
long dataLength = data.Length;
headers.Add("Content-Range", $"bytes {startByte}-{endByte}/{dataLength}");
HttpWebRequest request = PrepareWebRequest(method, url, headers, cookies, contentType, contentLength);
using (Stream requestStream = request.GetRequestStream())
{
if (!TransferData(data, requestStream, contentPosition, contentLength))
{
return null;
}
}
using (WebResponse response = request.GetResponse())
{
result.Response = ResponseToString(response, responseType);
}
result.IsSuccess = true;
}
catch (Exception e)
{
if (!StopUploadRequested)
{
string response = AddWebError(e, url);
if (ReturnResponseOnError && e is WebException)
{
result.Response = response;
}
}
}
finally
{
currentRequest = null;
IsUploading = false;
if (VerboseLogs && !string.IsNullOrEmpty(VerboseLogsPath))
{
WriteVerboseLog(url, args, headers, result.Response);
}
}
return result;
}
private HttpWebResponse GetResponse(HttpMethod method, string url, Stream data = null, string contentType = null, Dictionary<string, string> args = null,
NameValueCollection headers = null, CookieCollection cookies = null)
{
@ -397,21 +469,34 @@ private HttpWebRequest PrepareWebRequest(HttpMethod method, string url, NameValu
return request;
}
protected bool TransferData(Stream dataStream, Stream requestStream)
protected bool TransferData(Stream dataStream, Stream requestStream, long dataPosition = 0, long dataLength = -1)
{
if (dataPosition >= dataStream.Length)
{
return true;
}
if (dataStream.CanSeek)
{
dataStream.Position = 0;
dataStream.Position = dataPosition;
}
ProgressManager progress = new ProgressManager(dataStream.Length);
int length = (int)Math.Min(BufferSize, dataStream.Length);
if (dataLength == -1)
{
dataLength = dataStream.Length;
}
dataLength = Math.Min(dataLength, dataStream.Length - dataPosition);
ProgressManager progress = new ProgressManager(dataStream.Length, dataPosition);
int length = (int)Math.Min(BufferSize, dataLength);
byte[] buffer = new byte[length];
int bytesRead;
long bytesRemaining = dataLength;
while (!StopUploadRequested && (bytesRead = dataStream.Read(buffer, 0, length)) > 0)
{
requestStream.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
length = (int)Math.Min(buffer.Length, bytesRemaining);
if (AllowReportProgress && progress.UpdateProgress(bytesRead))
{

View file

@ -27,6 +27,7 @@ You should have received a copy of the GNU General Public License
using ShareX.HelpersLib;
using ShareX.UploadersLib.Properties;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.IO;
@ -42,14 +43,14 @@ public class OneDriveFileUploaderService : FileUploaderService
public override bool CheckConfig(UploadersConfig config)
{
return OAuth2Info.CheckOAuth(config.OneDriveOAuth2Info);
return OAuth2Info.CheckOAuth(config.OneDriveV2OAuth2Info);
}
public override GenericUploader CreateUploader(UploadersConfig config, TaskReferenceHelper taskInfo)
{
return new OneDrive(config.OneDriveOAuth2Info)
return new OneDrive(config.OneDriveV2OAuth2Info)
{
FolderID = config.OneDriveSelectedFolder.id,
FolderID = config.OneDriveV2SelectedFolder.id,
AutoCreateShareableLink = config.OneDriveAutoCreateShareableLink
};
}
@ -59,13 +60,17 @@ public override GenericUploader CreateUploader(UploadersConfig config, TaskRefer
public sealed class OneDrive : FileUploader, IOAuth2
{
private const string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
private const string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
private const int MaxSegmentSize = 64 * 1024 * 1024; // 64 MiB
public OAuth2Info AuthInfo { get; set; }
public string FolderID { get; set; }
public bool AutoCreateShareableLink { get; set; }
public static OneDriveFileInfo RootFolder = new OneDriveFileInfo
{
id = "me/skydrive",
id = "", // empty defaults to root
name = Resources.OneDrive_RootFolder_Root_folder
};
@ -78,11 +83,16 @@ public string GetAuthorizationURL()
{
Dictionary<string, string> args = new Dictionary<string, string>();
args.Add("client_id", AuthInfo.Client_ID);
args.Add("scope", "wl.offline_access wl.skydrive_update");
args.Add("scope", "offline_access files.readwrite");
args.Add("response_type", "code");
args.Add("redirect_uri", Links.URL_CALLBACK);
if (AuthInfo.Proof != null)
{
args.Add("code_challenge", AuthInfo.Proof.CodeChallenge);
args.Add("code_challenge_method", AuthInfo.Proof.ChallengeMethod);
}
return URLHelpers.CreateQuery("https://login.live.com/oauth20_authorize.srf", args);
return URLHelpers.CreateQuery(AuthorizationEndpoint, args);
}
public bool GetAccessToken(string code)
@ -93,8 +103,12 @@ public bool GetAccessToken(string code)
args.Add("client_secret", AuthInfo.Client_Secret);
args.Add("code", code);
args.Add("grant_type", "authorization_code");
if (AuthInfo.Proof != null)
{
args.Add("code_verifier", AuthInfo.Proof.CodeVerifier);
}
string response = SendRequestURLEncoded(HttpMethod.POST, "https://login.live.com/oauth20_token.srf", args);
string response = SendRequestURLEncoded(HttpMethod.POST, TokenEndpoint, args);
if (!string.IsNullOrEmpty(response))
{
@ -117,12 +131,11 @@ public bool RefreshAccessToken()
{
Dictionary<string, string> args = new Dictionary<string, string>();
args.Add("client_id", AuthInfo.Client_ID);
args.Add("redirect_uri", Links.URL_CALLBACK);
args.Add("client_secret", AuthInfo.Client_Secret);
args.Add("refresh_token", AuthInfo.Token.refresh_token);
args.Add("grant_type", "refresh_token");
string response = SendRequestURLEncoded(HttpMethod.POST, "https://login.live.com/oauth20_token.srf", args);
string response = SendRequestURLEncoded(HttpMethod.POST, TokenEndpoint, args);
if (!string.IsNullOrEmpty(response))
{
@ -161,29 +174,79 @@ public bool CheckAuthorization()
return true;
}
private NameValueCollection GetAuthHeaders()
{
NameValueCollection headers = new NameValueCollection();
headers.Add("Authorization", "Bearer " + AuthInfo.Token.access_token);
return headers;
}
private string GetFolderUrl(string folderID)
{
string folderPath;
if (!string.IsNullOrEmpty(folderID))
{
folderPath = URLHelpers.CombineURL("me/drive/items", folderID);
}
else
{
folderPath = "me/drive/root";
}
return folderPath;
}
private string CreateSession(string fileName)
{
string json = JsonConvert.SerializeObject(new
{
item = new Dictionary<string, string>
{
{ "name", fileName },
{ "@microsoft.graph.conflictBehavior", "replace" }
}
});
string folderPath = GetFolderUrl(FolderID);
string url = URLHelpers.BuildUri("https://graph.microsoft.com", $"/v1.0/{folderPath}:/{fileName}:/createUploadSession");
string response = SendRequest(HttpMethod.POST, url, json, ContentTypeJSON, headers: GetAuthHeaders());
OneDriveUploadSession session = JsonConvert.DeserializeObject<OneDriveUploadSession>(response);
if (session != null)
{
return session.uploadUrl;
}
return null;
}
public override UploadResult Upload(Stream stream, string fileName)
{
if (!CheckAuthorization()) return null;
Dictionary<string, string> args = new Dictionary<string, string>();
args.Add("access_token", AuthInfo.Token.access_token);
args.Add("overwrite", "true");
args.Add("downsize_photo_uploads", "false");
string sessionUrl = CreateSession(fileName);
long position = 0;
UploadResult result = new UploadResult();
string folderPath;
if (!string.IsNullOrEmpty(FolderID))
do
{
folderPath = URLHelpers.CombineURL(FolderID, "files");
}
else
{
folderPath = "me/skydrive/files";
}
result = SendRequestBytes(sessionUrl, stream, fileName, position, MaxSegmentSize);
string url = URLHelpers.CreateQuery(URLHelpers.CombineURL("https://apis.live.net/v5.0", folderPath), args);
UploadResult result = SendRequestFile(url, stream, fileName);
if (result.IsSuccess)
{
position += MaxSegmentSize;
}
else
{
SendRequest(HttpMethod.DELETE, sessionUrl);
break;
}
}
while (position < stream.Length);
if (result.IsSuccess)
{
@ -191,11 +254,13 @@ public override UploadResult Upload(Stream stream, string fileName)
if (AutoCreateShareableLink)
{
AllowReportProgress = false;
result.URL = CreateShareableLink(uploadInfo.id);
}
else
{
result.URL = uploadInfo.source;
result.URL = uploadInfo.webUrl;
}
}
@ -204,9 +269,6 @@ public override UploadResult Upload(Stream stream, string fileName)
public string CreateShareableLink(string id, OneDriveLinkType linkType = OneDriveLinkType.Read)
{
Dictionary<string, string> args = new Dictionary<string, string>();
args.Add("access_token", AuthInfo.Token.access_token);
string linkTypeValue;
switch (linkType)
@ -216,39 +278,46 @@ public string CreateShareableLink(string id, OneDriveLinkType linkType = OneDriv
break;
default:
case OneDriveLinkType.Read:
linkTypeValue = "shared_read_link";
linkTypeValue = "view";
break;
case OneDriveLinkType.Edit:
linkTypeValue = "shared_edit_link";
linkTypeValue = "edit";
break;
}
string response = SendRequest(HttpMethod.GET, $"https://apis.live.net/v5.0/{id}/{linkTypeValue}", args);
OneDriveShareableLinkInfo shareableLinkInfo = JsonConvert.DeserializeObject<OneDriveShareableLinkInfo>(response);
if (shareableLinkInfo != null)
string json = JsonConvert.SerializeObject(new
{
return shareableLinkInfo.link;
type = linkTypeValue
});
string response = SendRequest(HttpMethod.POST, $"https://graph.microsoft.com/v1.0/me/drive/items/{id}/createLink", json, ContentTypeJSON,
headers: GetAuthHeaders());
OneDrivePermission permissionInfo = JsonConvert.DeserializeObject<OneDrivePermission>(response);
if (permissionInfo != null && permissionInfo.link != null)
{
return permissionInfo.link.webUrl;
}
return null;
}
public OneDrivePathInfo GetPathInfo(string path)
public OneDriveFileList GetPathInfo(string id)
{
if (!CheckAuthorization()) return null;
string folderPath = GetFolderUrl(id);
Dictionary<string, string> args = new Dictionary<string, string>();
args.Add("access_token", AuthInfo.Token.access_token);
args.Add("select", "id,name");
args.Add("filter", "folder ne null");
if (!path.EndsWith("files")) path += "/files";
string response = SendRequest(HttpMethod.GET, URLHelpers.CombineURL("https://apis.live.net/v5.0", path), args);
string response = SendRequest(HttpMethod.GET, $"https://graph.microsoft.com/v1.0/{folderPath}/children", args, GetAuthHeaders());
if (response != null)
{
OneDrivePathInfo pathInfo = JsonConvert.DeserializeObject<OneDrivePathInfo>(response);
OneDriveFileList pathInfo = JsonConvert.DeserializeObject<OneDriveFileList>(response);
return pathInfo;
}
@ -260,17 +329,29 @@ public class OneDriveFileInfo
{
public string id { get; set; }
public string name { get; set; }
public string source { get; set; }
public string webUrl { get; set; }
}
public class OneDriveShareableLinkInfo
public class OneDrivePermission
{
public string link { get; set; }
public OneDriveShareableLink link { get; set; }
}
public class OneDrivePathInfo
public class OneDriveShareableLink
{
public OneDriveFileInfo[] data { get; set; }
public string webUrl { get; set; }
public string webHtml { get; set; }
}
public class OneDriveFileList
{
public OneDriveFileInfo[] value { get; set; }
}
public class OneDriveUploadSession
{
public string uploadUrl { get; set; }
public string[] nextExpectedRanges { get; set; }
}
public enum OneDriveLinkType

View file

@ -395,7 +395,7 @@ public void LoadSettings()
tvOneDrive.Nodes.Clear();
OneDriveAddFolder(OneDrive.RootFolder, null);
if (OAuth2Info.CheckOAuth(Config.OneDriveOAuth2Info))
if (OAuth2Info.CheckOAuth(Config.OneDriveV2OAuth2Info))
{
oAuth2OneDrive.Status = OAuthLoginStatus.LoginSuccessful;
@ -403,7 +403,7 @@ public void LoadSettings()
}
cbOneDriveCreateShareableLink.Checked = Config.OneDriveAutoCreateShareableLink;
lblOneDriveFolderID.Text = Resources.UploadersConfigForm_LoadSettings_Selected_folder_ + " " + Config.OneDriveSelectedFolder.name;
lblOneDriveFolderID.Text = Resources.UploadersConfigForm_LoadSettings_Selected_folder_ + " " + Config.OneDriveV2SelectedFolder.name;
tvOneDrive.CollapseAll();
#endregion OneDrive
@ -1750,7 +1750,7 @@ private void oAuth2OneDrive_RefreshButtonClicked()
private void oAuth2OneDrive_ClearButtonClicked()
{
Config.OneDriveOAuth2Info = null;
Config.OneDriveV2OAuth2Info = null;
}
private void cbOneDriveCreateShareableLink_CheckedChanged(object sender, EventArgs e)
@ -1764,7 +1764,7 @@ private void tvOneDrive_AfterSelect(object sender, TreeViewEventArgs e)
if (file != null)
{
lblOneDriveFolderID.Text = Resources.UploadersConfigForm_LoadSettings_Selected_folder_ + " " + file.name;
Config.OneDriveSelectedFolder = file;
Config.OneDriveV2SelectedFolder = file;
}
}

View file

@ -711,11 +711,12 @@ public void OneDriveAuthOpen()
try
{
OAuth2Info oauth = new OAuth2Info(APIKeys.OneDriveClientID, APIKeys.OneDriveClientSecret);
oauth.Proof = new OAuth2ProofKey(OAuth2ChallengeMethod.SHA256);
string url = new OneDrive(oauth).GetAuthorizationURL();
if (!string.IsNullOrEmpty(url))
{
Config.OneDriveOAuth2Info = oauth;
Config.OneDriveV2OAuth2Info = oauth;
URLHelpers.OpenURL(url);
DebugHelper.WriteLine("OneDriveAuthOpen - Authorization URL is opened: " + url);
}
@ -734,9 +735,9 @@ public void OneDriveAuthComplete(string code)
{
try
{
if (!string.IsNullOrEmpty(code) && Config.OneDriveOAuth2Info != null)
if (!string.IsNullOrEmpty(code) && Config.OneDriveV2OAuth2Info != null)
{
bool result = new OneDrive(Config.OneDriveOAuth2Info).GetAccessToken(code);
bool result = new OneDrive(Config.OneDriveV2OAuth2Info).GetAccessToken(code);
if (result)
{
@ -763,9 +764,9 @@ public void OneDriveAuthRefresh()
{
try
{
if (OAuth2Info.CheckOAuth(Config.OneDriveOAuth2Info))
if (OAuth2Info.CheckOAuth(Config.OneDriveV2OAuth2Info))
{
bool result = new OneDrive(Config.OneDriveOAuth2Info).RefreshAccessToken();
bool result = new OneDrive(Config.OneDriveV2OAuth2Info).RefreshAccessToken();
if (result)
{
@ -790,10 +791,10 @@ public void OneDriveAuthRefresh()
public void OneDriveListFolders(OneDriveFileInfo fileEntry, TreeNode tnParent)
{
Application.DoEvents();
OneDrive oneDrive = new OneDrive(Config.OneDriveOAuth2Info);
OneDrivePathInfo oneDrivePathInfo = oneDrive.GetPathInfo(fileEntry.id);
OneDrive oneDrive = new OneDrive(Config.OneDriveV2OAuth2Info);
OneDriveFileList oneDrivePathInfo = oneDrive.GetPathInfo(fileEntry.id);
tnParent.Nodes.Clear();
foreach (OneDriveFileInfo folder in oneDrivePathInfo.data.Where(x => x.id.StartsWith("folder.")))
foreach (OneDriveFileInfo folder in oneDrivePathInfo.value)
{
OneDriveAddFolder(folder, tnParent);
}

View file

@ -30,6 +30,7 @@ public class OAuth2Info
public string Client_ID { get; set; }
public string Client_Secret { get; set; }
public OAuth2Token Token { get; set; }
public OAuth2ProofKey Proof { get; set; }
public OAuth2Info(string client_id, string client_secret)
{

View file

@ -0,0 +1,62 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace ShareX.UploadersLib
{
public enum OAuth2ChallengeMethod
{
Plain, SHA256
}
public class OAuth2ProofKey
{
public string CodeVerifier { get; private set; }
public string CodeChallenge { get; private set; }
private OAuth2ChallengeMethod Method;
public string ChallengeMethod
{
get
{
switch (Method)
{
case OAuth2ChallengeMethod.Plain: return "plain";
case OAuth2ChallengeMethod.SHA256: return "S256";
}
return "";
}
}
public OAuth2ProofKey(OAuth2ChallengeMethod method)
{
Method = method;
var buffer = new byte[32];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(buffer);
}
CodeVerifier = CleanBase64(buffer);
CodeChallenge = CodeVerifier;
if (Method == OAuth2ChallengeMethod.SHA256)
{
using (var sha = SHA256.Create())
{
sha.ComputeHash(Encoding.UTF8.GetBytes(CodeVerifier));
CodeChallenge = CleanBase64(sha.Hash);
}
}
}
private string CleanBase64(byte[] buffer)
{
var sb = new StringBuilder(Convert.ToBase64String(buffer));
sb.Replace('+', '-');
sb.Replace('/', '_');
sb.Replace("=", "");
return sb.ToString();
}
}
}

View file

@ -60,9 +60,10 @@ public TimeSpan Remaining
private long speedTest;
private FixedSizedQueue<double> averageSpeed = new FixedSizedQueue<double>(10);
public ProgressManager(long length)
public ProgressManager(long length, long position = 0)
{
Length = length;
Position = position;
startTimer.Start();
smoothTimer.Start();
}

View file

@ -243,6 +243,7 @@
<Compile Include="Helpers\EscapeHelper.cs" />
<Compile Include="Helpers\GoogleOAuth2.cs" />
<Compile Include="Helpers\OAuth\IOAuthBase.cs" />
<Compile Include="Helpers\OAuth\OAuth2ProofKey.cs" />
<Compile Include="Helpers\SSLBypassHelper.cs" />
<Compile Include="BaseServices\URLSharingService.cs" />
<Compile Include="OtherServices\OCRSpace.cs" />

View file

@ -179,8 +179,8 @@ public class UploadersConfig : SettingsBase<UploadersConfig>
#region OneDrive
public OAuth2Info OneDriveOAuth2Info = null;
public OneDriveFileInfo OneDriveSelectedFolder = OneDrive.RootFolder;
public OAuth2Info OneDriveV2OAuth2Info = null;
public OneDriveFileInfo OneDriveV2SelectedFolder = OneDrive.RootFolder;
public bool OneDriveAutoCreateShareableLink = true;
#endregion OneDrive