From 7c6ce14348451cdddd733133bfe06cdd277080cd Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 21 Jan 2025 16:23:07 +0100 Subject: [PATCH] this is the first attempt to fix the currently broken OneDrive implemenation by upgrading to Graph SDK 5. Unfortunately, the SDK is much harder to use because of a missing abstraction for DriveItems on different places (regular folder, special folder, shared folder) and also doesn't work well, see e.g. https://stackoverflow.com/questions/79374963/list-shared-folders-with-graph-sdk or https://stackoverflow.com/questions/79374845/graph-sdk-c-missing-methods-for-working-with-app-folder. --- .../Io/OneDrive2FileStorage.cs | 641 +++++++++++++----- .../Kp2aBusinessLogic.csproj | 5 +- 2 files changed, 456 insertions(+), 190 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs index b57a62e7..a71e9b24 100644 --- a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs @@ -1,30 +1,32 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http.Headers; +using System.Net; +using System.Reflection; using System.Text; -using System.Threading.Tasks; -using Android.App; using Android.Content; -using Android.OS; using Android.Util; -using Java.Net; using keepass2android.Io.ItemLocation; using KeePassLib.Serialization; using KeePassLib.Utility; using Microsoft.Graph; -using Microsoft.Graph.Auth; +using Microsoft.Graph.Drives.Item.Items.Item.CreateUploadSession; +using Microsoft.Graph.Models; using Microsoft.Identity.Client; -using Newtonsoft.Json; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Authentication; + +//there are several "DriveItemItemRequestBuilder" classes in different namespaces. Unfortunately they do not share a common interface anymore. +//Prefix the aliases with Kp2a to indicate that they are non-official aliases. +using Kp2aDriveItemRequestBuilder = Microsoft.Graph.Drives.Item.Items.Item.DriveItemItemRequestBuilder; +using Kp2aSharedDriveItemRequestBuilder = Microsoft.Graph.Shares.Item.Items.Item.DriveItemItemRequestBuilder; +using Kp2aSpecialDriveItemRequestBuilder = Microsoft.Graph.Drives.Item.Special.Item.DriveItemItemRequestBuilder; +//NOTE: even though CustomDriveItemItemRequestBuilder derives from Kp2aDriveItemRequestBuilder, we cannot use polymorphism here because the methods are declared with "new". +//If you cast assign an CustomDriveItemItemRequestBuilder object to a variable declared as Kp2aDriveItemRequestBuilder and then call a method on it, it will fail. + using Exception = System.Exception; -using File = Microsoft.Graph.File; using String = System.String; namespace keepass2android.Io { - namespace ItemLocation + namespace ItemLocation { public class User { @@ -45,9 +47,9 @@ namespace keepass2android.Io } - public class OneDrive2ItemLocation where OneDrive2PrefixContainerType: OneDrive2PrefixContainer, new() + public class OneDrive2ItemLocation where OneDrive2PrefixContainerType : OneDrive2PrefixContainer, new() { - + public User User { get; set; } = new User(); public Share Share { get; set; } = new Share(); public string DriveId { get; set; } @@ -59,7 +61,7 @@ namespace keepass2android.Io { get { - OneDrive2ItemLocation< OneDrive2PrefixContainerType> copy = OneDrive2ItemLocation< OneDrive2PrefixContainerType>.FromString(this.ToString()); + OneDrive2ItemLocation copy = OneDrive2ItemLocation.FromString(this.ToString()); if (copy.LocalPath.Any()) { //pop last: @@ -177,16 +179,16 @@ namespace keepass2android.Io } } - - public abstract class OneDrive2FileStorage : IFileStorage where OneDrive2PrefixContainerType: OneDrive2PrefixContainer, new() + + public abstract class OneDrive2FileStorage : IFileStorage where OneDrive2PrefixContainerType : OneDrive2PrefixContainer, new() { public static IPublicClientApplication _publicClientApp = null; private string ClientID = "8374f801-0f55-407d-80cc-9a04fe86d9b2"; - - + + public abstract IEnumerable Scopes { get; @@ -202,7 +204,7 @@ namespace keepass2android.Io class PathItemBuilder { private readonly string _specialFolder; - public IGraphServiceClient client; + public GraphServiceClient client; public OneDrive2ItemLocation itemLocation; public bool verbose; @@ -211,11 +213,175 @@ namespace keepass2android.Io _specialFolder = specialFolder; } - - public IDriveItemRequestBuilder getPathItem() + /// + /// Wraps the different DriveItemRequestBuilder classes and allows accessing the different types easily + /// + public class DriveItemRequests { - Kp2aLog.Log("getPathItem for " + itemLocation.ToString()); - IDriveItemRequestBuilder pathItem; + + public class DriveItemRequestsResult + { + private readonly DriveItemRequests _req; + + public DriveItemRequestsResult(DriveItemRequests req) + { + _req = req; + } + + public Task Result { get; set; } + + + public DriveItemRequestsResult ForSharedDriveItemRequests(Func> action) + { + if (_req.SharedDriveItemRequestBuilder != null) + { + Result = action(_req.SharedDriveItemRequestBuilder); + } + return this; + } + public DriveItemRequestsResult ForDriveItemRequests(Func> action) + { + if (_req.DriveItemRequestBuilder != null) + { + Result = action(_req.DriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequestsResult ForCustomDriveItemRequests(Func> action) + { + if (_req.CustomDriveItemRequestBuilder != null) + { + Result = action(_req.CustomDriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequestsResult ForSpecialDriveItemRequests(Func> action) + { + if (_req.SpecialDriveItemRequestBuilder != null) + { + Result = action(_req.SpecialDriveItemRequestBuilder); + } + return this; + } + + } + + public class DriveItemRequestsAsyncTask + { + private readonly DriveItemRequests _req; + + public DriveItemRequestsAsyncTask(DriveItemRequests req) + { + _req = req; + Task = Task.CompletedTask; + } + + public Task Task { get; private set; } + + public DriveItemRequestsAsyncTask ForSharedDriveItemRequests(Func action) + { + if (_req.SharedDriveItemRequestBuilder != null) + { + Task = action(_req.SharedDriveItemRequestBuilder); + } + return this; + } + public DriveItemRequestsAsyncTask ForDriveItemRequests(Func action) + { + if (_req.DriveItemRequestBuilder != null) + { + Task = action(_req.DriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequestsAsyncTask ForCustomDriveItemRequests(Func action) + { + if (_req.CustomDriveItemRequestBuilder != null) + { + Task = action(_req.CustomDriveItemRequestBuilder); + } + return this; + } + + + public DriveItemRequestsAsyncTask ForSpecialDriveItemRequests(Func action) + { + if (_req.SpecialDriveItemRequestBuilder != null) + { + Task = action(_req.SpecialDriveItemRequestBuilder); + } + return this; + } + + + } + + public Kp2aSharedDriveItemRequestBuilder? SharedDriveItemRequestBuilder { get; set; } + public Kp2aSpecialDriveItemRequestBuilder? SpecialDriveItemRequestBuilder { get; set; } + + public Kp2aDriveItemRequestBuilder? DriveItemRequestBuilder { get; set; } + + public CustomDriveItemItemRequestBuilder? CustomDriveItemRequestBuilder { get; set; } + + + public DriveItemRequests ForDriveItemRequests(Action action) + { + if (DriveItemRequestBuilder != null) + { + action(DriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequests ForCustomDriveItemRequests(Action action) + { + if (CustomDriveItemRequestBuilder != null) + { + action(CustomDriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequests ForSpecialDriveItemRequests(Action action) + { + if (SpecialDriveItemRequestBuilder!= null) + { + action(SpecialDriveItemRequestBuilder ); + } + return this; + } + + public DriveItemRequests ForSharedDriveItemRequests(Action action) + { + if (SharedDriveItemRequestBuilder != null) + { + action(SharedDriveItemRequestBuilder); + } + return this; + } + + public DriveItemRequestsResult ToAsyncResult() + { + return new DriveItemRequestsResult(this); + } + + public DriveItemRequestsAsyncTask ToAsyncTask() + { + return new DriveItemRequestsAsyncTask(this); + } + + + }; + + + public async Task BuildPathItemAsync() + { + Kp2aLog.Log("buildPathItem for " + itemLocation.ToString()); + DriveItemRequests result = new DriveItemRequests(); + if (!hasShare()) { throw new Exception("Cannot get path item without share"); @@ -223,24 +389,52 @@ namespace keepass2android.Io if ("me".Equals(itemLocation.Share.Id)) { if (verbose) Kp2aLog.Log("Path share is me"); - + if (_specialFolder == null) { if (verbose) Kp2aLog.Log("No special folder. Use drive root."); - pathItem = client.Me.Drive.Root; + + if (itemLocation.LocalPath.Any()) + { + if (verbose) Kp2aLog.Log("LocalPath = " + itemLocation.LocalPathString); + result.CustomDriveItemRequestBuilder = client.Drives[itemLocation.DriveId].Root + .ItemWithPath(itemLocation.LocalPathString); + } + else + { + + result.DriveItemRequestBuilder = client.Drives[itemLocation.DriveId].Items["root"]; + + } + + } else { if (verbose) Kp2aLog.Log("Special folder = " + _specialFolder); - pathItem = client.Me.Drive.Special[_specialFolder]; + + result.SpecialDriveItemRequestBuilder = client.Drives[itemLocation.DriveId].Special[_specialFolder]; + if (itemLocation.LocalPath.Any()) + { + + var child = (await result.SpecialDriveItemRequestBuilder + .GetAsync(configuration => configuration.QueryParameters.Expand = new[] { "children" })) + .Children.FirstOrDefault(c => c.Name == itemLocation.LocalPathString); + + + foreach (var di in client.Drives[itemLocation.DriveId].Items.GetAsync().Result.Value) + { + Kp2aLog.Log("DriveItem: " + di.Name); + + } + + + result.DriveItemRequestBuilder = client.Drives[itemLocation.DriveId].Items[child.Id]; + + } } - if (itemLocation.LocalPath.Any()) - { - if (verbose) Kp2aLog.Log("LocalPath = " + itemLocation.LocalPathString); - pathItem = pathItem.ItemWithPath(itemLocation.LocalPathString); - } } else { @@ -250,7 +444,8 @@ namespace keepass2android.Io String webUrl = itemLocation.Share.WebUrl; if (verbose) Kp2aLog.Log("Share WebUrl = " + webUrl); var encodedShareId = CalculateEncodedShareId(webUrl); - return client.Shares[encodedShareId].Root; + result.SharedDriveItemRequestBuilder = client.Shares[encodedShareId].Items["root"]; + return result; } /*String webUrl = itemLocation.Share.WebUrl; if ("".Equals(itemLocation.LocalPath) == false) @@ -265,11 +460,10 @@ namespace keepass2android.Io pathItem = client.Shares[encodedShareId].Root; */ if (verbose) Kp2aLog.Log("Using driveId=" + itemLocation.DriveId + " and item id=" + itemLocation.LocalPath.Last().Id); - return client.Drives[itemLocation.DriveId].Items[itemLocation.LocalPath.Last().Id]; + result.DriveItemRequestBuilder = client.Drives[itemLocation.DriveId].Items[itemLocation.LocalPath.Last().Id]; } - - return pathItem; + return result; } private static string CalculateEncodedShareId(string webUrl) @@ -280,6 +474,7 @@ namespace keepass2android.Io return encodedShareId; } + public bool hasShare() { return !string.IsNullOrEmpty(itemLocation?.Share?.Id); @@ -289,6 +484,7 @@ namespace keepass2android.Io { return itemLocation.LocalPath.Any(); } + } private string protocolId; @@ -312,24 +508,24 @@ namespace keepass2android.Io class GraphServiceClientWithState { - public IGraphServiceClient Client { get; set; } + public GraphServiceClient Client { get; set; } public DateTime TokenExpiryDate { get; set; } public bool RequiresUserInteraction { get; set; } } - readonly Dictionary mClientByUser = + readonly Dictionary _mClientByUser = new Dictionary(); - private async Task TryGetMsGraphClient(String path, bool tryConnect) + private async Task TryGetMsGraphClient(String path, bool tryConnect) { - String userId = OneDrive2ItemLocation.FromString(path).User.Id; + + string userId = OneDrive2ItemLocation.FromString(path).User.Id; logDebug("TryGetMsGraphClient for " + userId); - if (mClientByUser.ContainsKey(userId)) + if (_mClientByUser.TryGetValue(userId, out var clientWithState)) { logDebug("TryGetMsGraphClient found user " + userId); - GraphServiceClientWithState clientWithState = mClientByUser[userId]; if (!(clientWithState.RequiresUserInteraction || (clientWithState.TokenExpiryDate < DateTime.Now) || (clientWithState.Client == null))) { @@ -348,7 +544,7 @@ namespace keepass2android.Io if (await TryLoginSilent(path) != null) { logDebug("trying to connect ok"); - return mClientByUser[userId].Client; + return _mClientByUser.GetValueOrDefault(userId, null).Client; } logDebug("trying to connect failed"); } @@ -356,47 +552,63 @@ namespace keepass2android.Io return null; } + public class TokenFromAuthResultProvider : IAccessTokenProvider + { + public AuthenticationResult AuthenticationResult + { + get; + set; + } + public async Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return AuthenticationResult.AccessToken; + } - private IGraphServiceClient BuildClient(AuthenticationResult authenticationResult) + public AllowedHostsValidator AllowedHostsValidator { get; } + } + + private GraphServiceClient BuildClient(AuthenticationResult authenticationResult) { logDebug("buildClient..."); - //DeviceCodeProvider authenticationProvider = new DeviceCodeProvider(_publicClientApp, Scopes); - var authenticationProvider = new DelegateAuthenticationProvider( - (requestMessage) => - { - var access_token = authenticationResult.AccessToken; - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token); - return Task.FromResult(0); - }); + var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new TokenFromAuthResultProvider() { AuthenticationResult = authenticationResult }); + /* + (requestMessage) => + + { + var access_token = authenticationResult.AccessToken; + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token); + return Task.FromResult(0); + });*/ GraphServiceClientWithState clientWithState = new GraphServiceClientWithState() { - Client = new GraphServiceClient(authenticationProvider), + Client = new GraphServiceClient(new HttpClient(), authenticationProvider), RequiresUserInteraction = false, TokenExpiryDate = authenticationResult.ExpiresOn.LocalDateTime }; - + if (authenticationResult.Account == null) throw new Exception("authenticationResult.Account == null!"); - mClientByUser[authenticationResult.Account.HomeAccountId.Identifier] = clientWithState; + _mClientByUser[authenticationResult.Account.HomeAccountId.Identifier] = clientWithState; logDebug("buildClient ok."); return clientWithState.Client; } - + private void logDebug(string str) { #if DEBUG Log.Debug("KP2A", "OneDrive2: " + str); #endif } - + protected abstract string SpecialFolder { get; } @@ -404,25 +616,25 @@ namespace keepass2android.Io { PathItemBuilder result = new PathItemBuilder(SpecialFolder); - + result.itemLocation = OneDrive2ItemLocation.FromString(path); if (string.IsNullOrEmpty(result.itemLocation.User?.Name)) { throw new Exception("path does not contain user"); } - - result.client = await TryGetMsGraphClient(path, true); + result.client = await TryGetMsGraphClient(path, true); + if (result.client == null) throw new Exception("Failed to connect or authenticate to OneDrive!"); - - + + return result; } - private Exception convertException(ClientException e) + private Exception convertException(ServiceException e) { if (e.IsMatch(GraphErrorCode.ItemNotFound.ToString())) @@ -436,8 +648,8 @@ namespace keepass2android.Io private Exception convertException(Exception e) { - if (e is ClientException) - return convertException((ClientException)e); + if (e is ServiceException) + return convertException((ServiceException)e); if (e is AggregateException aggregateException) { foreach (var inner in aggregateException.InnerExceptions) @@ -460,13 +672,15 @@ namespace keepass2android.Io { try { - + Task.Run(async () => { PathItemBuilder pathItemBuilder = await GetPathItemBuilder(ioc.Path); - await pathItemBuilder.getPathItem() - .Request() - .DeleteAsync(); + PathItemBuilder.DriveItemRequests pathItem = await pathItemBuilder.BuildPathItemAsync(); + await pathItem.ToAsyncTask() + .ForDriveItemRequests(builder => builder.DeleteAsync()) + .ForCustomDriveItemRequests(b => b.DeleteAsync()) + .Task; }).Wait(); } catch (Exception e) @@ -485,22 +699,59 @@ namespace keepass2android.Io return null; } + public Stream OpenFileForReadx(IOConnectionInfo ioc) + { + try + { + string filename = ioc.Path; + + Stream? result = Task.Run(async () => + { + + logDebug("openFileForRead. Path=" + filename); + //var client = await TryGetMsGraphClient(filename, true); + + PathItemBuilder clientAndPath = await GetPathItemBuilder(filename); + var client = clientAndPath.client; + var itemLocation = clientAndPath.itemLocation; + + return await client.Drives[itemLocation.DriveId].Root.ItemWithPath(itemLocation.LocalPathString).Content + .GetAsync(); + + }).Result; + logDebug("ok"); + return result; + + } + catch (Exception e) + { + throw e; + } + } public Stream OpenFileForRead(IOConnectionInfo ioc) { try { string path = ioc.Path; - + logDebug("openFileForRead. Path=" + path); - Stream result = Task.Run(async () => + + Stream? result = Task.Run(async () => { - PathItemBuilder clientAndpath = await GetPathItemBuilder(path); - return await clientAndpath - .getPathItem() - .Content - .Request() - .GetAsync(); + logDebug("openFileForRead. Path=" + path); + + PathItemBuilder clientAndPath = await GetPathItemBuilder(path); + return await (await clientAndPath.BuildPathItemAsync()) + .ToAsyncResult() + .ForDriveItemRequests((b) => b.Content.GetAsync()) + .ForCustomDriveItemRequests(b => b.Content.GetAsync()) + .ForSpecialDriveItemRequests((b) => b.Content.GetAsync()) + .ForSharedDriveItemRequests((b) => b.Content.GetAsync()) + .Result; + }).Result; + if (result == null) + throw new Exception("failed to open stream"); logDebug("ok"); return result; @@ -551,34 +802,40 @@ namespace keepass2android.Io { PathItemBuilder pathItemBuilder = await GetPathItemBuilder(path); //for small files <2MB use the direct upload: - if (stream.Length < 2* 1024 * 1024) + if (stream.Length < 2 * 1024 * 1024) { - return await - pathItemBuilder - .getPathItem() - .Content - .Request() - .PutAsync(stream); + await + (await pathItemBuilder + .BuildPathItemAsync()) + .ToAsyncTask() + .ForDriveItemRequests((b) => b.Content.PutAsync(stream)) + .ForCustomDriveItemRequests(b => b.Content.PutAsync(stream)) + .ForSpecialDriveItemRequests(b => b.Content.PutAsync(stream)) + .ForSharedDriveItemRequests(b => b.Content.PutAsync(stream)) + .Task; + return; } //for larger files use an upload session. This is required for 4MB and beyond, but as the docs are not very clear about this //limit, let's use it a bit more often to be safe. - var uploadProps = new DriveItemUploadableProperties + var uploadProps = new CreateUploadSessionPostRequestBody { - ODataType = null, AdditionalData = new Dictionary { { "@microsoft.graph.conflictBehavior", "replace" } } }; - - var uploadSession = await pathItemBuilder - .getPathItem() - .CreateUploadSession(uploadProps) - .Request() - .PostAsync(); + + var uploadSession = await (await pathItemBuilder + .BuildPathItemAsync()) + .ToAsyncResult() + .ForSharedDriveItemRequests(b => throw new Exception("Graph SDK doesn't seem to support large file uploads on shares.")) //TODO verify + .ForDriveItemRequests(b => b.CreateUploadSession.PostAsync(uploadProps)) + .ForCustomDriveItemRequests(b => b.CreateUploadSession.PostAsync(uploadProps)) + + .Result; // Max slice size must be a multiple of 320 KiB int maxSliceSize = 320 * 1024; @@ -590,21 +847,12 @@ namespace keepass2android.Io throw new Exception("Failed to upload data!"); } - return uploadResult.ItemResponse; - - }).Wait(); } catch (Exception e) { - Task.Run(async () => - { - PathItemBuilder pathItemBuilder = await GetPathItemBuilder(path); - pathItemBuilder.verbose = true; - pathItemBuilder.getPathItem(); - }).Wait(); throw convertException(e); } } @@ -627,7 +875,7 @@ namespace keepass2android.Io private string GetFilename(string path) { - string localPath = "/"+OneDrive2ItemLocation.FromString(path).LocalPathString; + string localPath = "/" + OneDrive2ItemLocation.FromString(path).LocalPathString; return localPath.Substring(localPath.LastIndexOf("/", StringComparison.Ordinal) + 1); } @@ -648,11 +896,12 @@ namespace keepass2android.Io { PathItemBuilder pathItemBuilder = await GetPathItemBuilder(parentIoc.Path); - - return await pathItemBuilder.getPathItem() - .Children - .Request() - .AddAsync(driveItem); + + return await (await pathItemBuilder.BuildPathItemAsync()) + .ToAsyncResult() + .ForDriveItemRequests(b => b.Children.PostAsync(driveItem)) + .ForCustomDriveItemRequests(b => b.Children.PostAsync(driveItem)) + .Result; }).Result; @@ -681,6 +930,8 @@ namespace keepass2android.Io PathItemBuilder pathItemBuilder = await GetPathItemBuilder(ioc.Path); logDebug("listing files for " + ioc.Path); + OneDrive2ItemLocation itemLocation = OneDrive2ItemLocation.FromString(ioc.Path); + var client = await TryGetMsGraphClient(ioc.Path, true); if (!pathItemBuilder.hasShare() && !pathItemBuilder.hasOneDrivePath()) { logDebug("listing shares."); @@ -693,31 +944,42 @@ namespace keepass2android.Io parentPath = parentPath.substring(getProtocolPrefix().length()); logDebug("parent after: " + parentPath);*/ - IDriveItemChildrenCollectionPage itemsPage = await pathItemBuilder.getPathItem() - .Children - .Request() - .GetAsync(); - while (true) - { - IList items = itemsPage.CurrentPage; - if (!items.Any()) - return result; + var driveItems = await GetDriveItems(pathItemBuilder); - foreach (DriveItem i in items) + if (driveItems != null) + foreach (DriveItem? i in driveItems) { - var e = GetFileDescription(pathItemBuilder.itemLocation.BuildLocalChildLocation(i.Name, i.Id, i.ParentReference?.DriveId), i); + var e = GetFileDescription(itemLocation.BuildLocalChildLocation(i.Name, i.Id, i.ParentReference?.DriveId), i); result.Add(e); } - var nextPageReqBuilder = itemsPage.NextPageRequest; - if (nextPageReqBuilder == null) - return result; - itemsPage = Task.Run(async () => await nextPageReqBuilder.GetAsync()).Result; + return result; + + } + + private async Task> GetDriveItems( + PathItemBuilder pathItemBuilder) + { + var pathItem = await pathItemBuilder.BuildPathItemAsync(); + if (pathItem.SpecialDriveItemRequestBuilder != null) + { + return pathItem.SpecialDriveItemRequestBuilder + .GetAsync(configuration => configuration.QueryParameters.Expand = new[] { "children" }).Result + .Children; } + var response = await pathItem + .ToAsyncResult() + .ForDriveItemRequests(b => b.Children.GetAsync()) + .ForCustomDriveItemRequests(b => b.Children.GetAsync()) + .ForSharedDriveItemRequests(b => + throw new Java.Lang.Exception("TODO listing share children not implemented")) + + .Result; + return response.Value; } - private FileDescription GetFileDescription(OneDrive2ItemLocation path, DriveItem i) + private FileDescription GetFileDescription(OneDrive2ItemLocation path, DriveItem? i) { FileDescription e = new FileDescription(); if (i.Size != null) @@ -740,7 +1002,7 @@ namespace keepass2android.Io { try { - return Task.Run(async() => await GetFileDescriptionAsync(ioc)).Result; + return Task.Run(async () => await GetFileDescriptionAsync(ioc)).Result; } catch (Exception e) { @@ -764,9 +1026,13 @@ namespace keepass2android.Io return rootEntry; } - IDriveItemRequestBuilder pathItem = pathItemBuilder.getPathItem(); - - DriveItem item = await pathItem.Request().GetAsync(); + var driveReq = await pathItemBuilder.BuildPathItemAsync(); + DriveItem? item = await driveReq.ToAsyncResult() + .ForDriveItemRequests(b => b.GetAsync()) + .ForCustomDriveItemRequests(b => b.GetAsync()) + .ForSharedDriveItemRequests(b => b.GetAsync()) + .ForSpecialDriveItemRequests(b => b.GetAsync()) + .Result; return GetFileDescription(pathItemBuilder.itemLocation, item); } @@ -783,7 +1049,7 @@ namespace keepass2android.Io public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) { - String path = ProtocolId+ "://"; + String path = ProtocolId + "://"; activity.StartSelectFileProcess(IOConnectionInfo.FromPath(path), isForSave, requestCode); } @@ -827,7 +1093,7 @@ namespace keepass2android.Io public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) { - if (!Task.Run(async() => await IsConnectedAsync(ioc.Path, true)).Result) + if (!Task.Run(async () => await IsConnectedAsync(ioc.Path, true)).Result) { throw new Exception("MsGraph login required"); } @@ -919,7 +1185,7 @@ namespace keepass2android.Io IAccount account = null; try { - + if (IsConnected(iocPath)) { logDebug("Login Silent ok, connected"); @@ -966,7 +1232,7 @@ namespace keepass2android.Io }; - mClientByUser[account.HomeAccountId.Identifier] = clientWithState; + _mClientByUser[account.HomeAccountId.Identifier] = clientWithState; logDebug("ui required"); return null; } @@ -984,7 +1250,7 @@ namespace keepass2android.Io return OneDrive2ItemLocation.RootForUser(res.Account.Username, res.Account.HomeAccountId.Identifier).ToString(); } - + private void FinishActivityWithSuccess(IFileStorageSetupActivity activity, string rootPathForUser) { activity.State.PutString(FileStorageSetupDefs.ExtraPath, rootPathForUser); @@ -1002,7 +1268,7 @@ namespace keepass2android.Io try { var itemLocation = OneDrive2ItemLocation.FromString(ioc.Path); - string result = ProtocolId+ "://"; + string result = ProtocolId + "://"; if (!string.IsNullOrEmpty(itemLocation.User?.Id)) { result += itemLocation.User?.Name; @@ -1025,75 +1291,72 @@ namespace keepass2android.Io ". Note that SprEnging expressions like {DB_PATH} are not supported with OneDrive!"); return ProtocolId + "://(invalid)"; } - + } - private async Task> ListShares(OneDrive2ItemLocation parentPath, IGraphServiceClient client) + private async Task> ListShares(OneDrive2ItemLocation parentPath, GraphServiceClient client) { - List result = new List(); + List result = []; + var drives = (await client.Me.Drives.GetAsync()).Value; + if (drives != null) + { + drives.ForEach(drive => + { + var e = new FileDescription() + { + DisplayName = drive.Name ?? drive.DriveType ?? "(unnamed drive)", + IsDirectory = true, + CanRead = true, + CanWrite = true, + Path = parentPath.BuildShare("me","me","me", drive.Id).ToString() + }; + result.Add(e); + }); + } - DriveItem root = await client.Me.Drive.Root.Request().GetAsync(); - FileDescription myEntry = GetFileDescription(parentPath.BuildShare("me","me","me", root.ParentReference?.DriveId), root); - myEntry.DisplayName = MyOneDriveDisplayName; - - result.Add(myEntry); - if (!CanListShares) return result; + /* + Not working. See https://stackoverflow.com/questions/79374963/list-shared-folders-with-graph-sdk. + var sharedWithMeResponse = await client.Drives[parentPath.DriveId].SharedWithMe.GetAsSharedWithMeGetResponseAsync(); - - IDriveSharedWithMeCollectionPage sharedWithMeCollectionPage = await client.Me.Drive.SharedWithMe().Request().GetAsync(); - - while (true) + foreach (DriveItem i in sharedWithMeResponse?.Value ?? []) { - IList sharedWithMeItems = sharedWithMeCollectionPage.CurrentPage; - if (!sharedWithMeItems.Any()) - break; - - foreach (DriveItem i in sharedWithMeItems) + var oneDrive2ItemLocation = parentPath.BuildShare(i.Id, i.Name, i.WebUrl, i.ParentReference?.DriveId); + FileDescription sharedFileEntry = new FileDescription() { - FileDescription sharedFileEntry = GetFileDescription(parentPath.BuildShare(i.Id, i.Name, i.WebUrl, i.ParentReference?.DriveId), i); - result.Add(sharedFileEntry); + CanWrite = true, CanRead = true, DisplayName = i.Name, + IsDirectory = true, + Path = oneDrive2ItemLocation.ToString() + }; + result.Add(sharedFileEntry); + + }*/ - } - var b = sharedWithMeCollectionPage.NextPageRequest; - if (b == null) break; - sharedWithMeCollectionPage = await b.GetAsync(); - } return result; } protected virtual string MyOneDriveDisplayName { get { return "My OneDrive"; } } - public abstract bool CanListShares { get; } + public abstract bool CanListShares { get; } - DriveItem TryFindFile(PathItemBuilder parent, string filename) + async Task TryFindFileAsync(PathItemBuilder parent, string filename) { - IDriveItemChildrenCollectionPage itemsPage = Task.Run(async () => await parent.getPathItem() - .Children - .Request() - .GetAsync()).Result; - while (true) - { - IList items = itemsPage.CurrentPage; - if (!items.Any()) - return null; - foreach (DriveItem i in items) + var driveItems = await GetDriveItems(parent); + + if (driveItems != null) + foreach (DriveItem? i in driveItems) { if (i.Name == filename) return i; } - var nextPageReqBuilder = itemsPage.NextPageRequest; - if (nextPageReqBuilder == null) - return null; - itemsPage = Task.Run(async () => await nextPageReqBuilder.GetAsync()).Result; - } + return null; } @@ -1102,13 +1365,13 @@ namespace keepass2android.Io { try { - return Task.Run(async() => await CreateFilePathAsync(parent, newFilename)).Result; + return Task.Run(async () => await CreateFilePathAsync(parent, newFilename)).Result; } catch (Exception e) { throw convertException(e); } - + } private async Task CreateFilePathAsync(string parent, string newFilename) @@ -1116,7 +1379,7 @@ namespace keepass2android.Io PathItemBuilder pathItemBuilder = await GetPathItemBuilder(parent); //see if such a file exists already: - var item = TryFindFile(pathItemBuilder, newFilename); + var item = await TryFindFileAsync(pathItemBuilder, newFilename); if (item != null) { return pathItemBuilder.itemLocation.BuildLocalChildLocation(item.Name, item.Id, item.ParentReference?.DriveId) @@ -1125,12 +1388,14 @@ namespace keepass2android.Io //doesn't exist. Create: logDebug("building request for " + pathItemBuilder.itemLocation); - DriveItem res = await pathItemBuilder.getPathItem() - - .ItemWithPath(newFilename) - .Content - .Request() - .PutAsync(new MemoryStream()); + var emptyStream = new MemoryStream(); + var driveItemReq = await pathItemBuilder.BuildPathItemAsync(); + DriveItem? res = await driveItemReq + .ToAsyncResult() + .ForDriveItemRequests(b => b.Content.PutAsync(emptyStream)) + .ForCustomDriveItemRequests(b => b.Content.PutAsync(emptyStream)) + .ForSharedDriveItemRequests(b => b.Content.PutAsync(emptyStream)) + .Result; return pathItemBuilder.itemLocation.BuildLocalChildLocation(res.Name, res.Id, res.ParentReference?.DriveId) .ToString(); @@ -1157,7 +1422,7 @@ namespace keepass2android.Io } } - public class OneDrive2FullFileStorage: OneDrive2FileStorage + public class OneDrive2FullFileStorage : OneDrive2FileStorage { public override IEnumerable Scopes { @@ -1168,7 +1433,7 @@ namespace keepass2android.Io "https://graph.microsoft.com/Files.ReadWrite", "https://graph.microsoft.com/Files.ReadWrite.All" }; - + } } diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index 15604f0e..86fa1711 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -11,9 +11,10 @@ - + - + +