-
Notifications
You must be signed in to change notification settings - Fork 28
Add OneDrive, Google Drive, and Dropbox storage providers #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.IO; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Linq; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Runtime.CompilerServices; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Dropbox.Api; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Dropbox.Api.Files; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace ManagedCode.Storage.Dropbox.Clients; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class DropboxClientWrapper : IDropboxClientWrapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly DropboxClient _client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public DropboxClientWrapper(DropboxClient client) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _client = client ?? throw new ArgumentNullException(nameof(client)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task EnsureRootAsync(string rootPath, bool createIfNotExists, CancellationToken cancellationToken) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (string.IsNullOrWhiteSpace(rootPath)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var normalized = Normalize(rootPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _client.Files.GetMetadataAsync(normalized); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch (ApiException<GetMetadataError> ex) when (ex.ErrorResponse.IsPath && ex.ErrorResponse.AsPath.Value.IsNotFound) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!createIfNotExists) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _client.Files.CreateFolderV2Async(normalized, autorename: false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<DropboxItemMetadata> UploadAsync(string rootPath, string path, Stream content, string? contentType, CancellationToken cancellationToken) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var fullPath = Combine(rootPath, path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var uploaded = await _client.Files.UploadAsync(fullPath, WriteMode.Overwrite.Instance, body: content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var uploaded = await _client.Files.UploadAsync(fullPath, WriteMode.Overwrite.Instance, body: content); | |
| var uploaded = await _client.Files.UploadAsync(fullPath, WriteMode.Overwrite.Instance, body: content, cancellationToken: cancellationToken); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancellationToken is not used in the DownloadAsync call, preventing cancellation of long-running downloads. The Dropbox API supports cancellation tokens, so they should be passed through.
| var response = await _client.Files.DownloadAsync(fullPath); | |
| return await response.GetContentAsStreamAsync(); | |
| var response = await _client.Files.DownloadAsync(fullPath, cancellationToken: cancellationToken); | |
| return await response.GetContentAsStreamAsync(cancellationToken); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancellationToken parameter is not passed to the DeleteV2Async call, which means delete operations cannot be cancelled. Pass the cancellationToken to the API call.
| await _client.Files.DeleteV2Async(fullPath); | |
| await _client.Files.DeleteV2Async(fullPath, cancellationToken: cancellationToken); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetMetadataAsync and ExistsAsync methods don't pass the cancellationToken to the API calls, preventing cancellation of these operations.
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var item in list.Entries) | |
| { | |
| if (item.IsFile) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| } | |
| while (list.HasMore) | |
| { | |
| list = await _client.Files.ListFolderContinueAsync(list.Cursor); | |
| foreach (var item in list.Entries) | |
| { | |
| if (item.IsFile) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| foreach (var item in list.Entries.Where(item => item.IsFile)) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| while (list.HasMore) | |
| { | |
| list = await _client.Files.ListFolderContinueAsync(list.Cursor); | |
| foreach (var item in list.Entries.Where(item => item.IsFile)) | |
| { | |
| yield return ToItem(item.AsFile); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var item in list.Entries) | |
| { | |
| if (item.IsFile) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| } | |
| while (list.HasMore) | |
| { | |
| list = await _client.Files.ListFolderContinueAsync(list.Cursor); | |
| foreach (var item in list.Entries) | |
| { | |
| if (item.IsFile) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| foreach (var item in list.Entries.Where(item => item.IsFile)) | |
| { | |
| yield return ToItem(item.AsFile); | |
| } | |
| while (list.HasMore) | |
| { | |
| list = await _client.Files.ListFolderContinueAsync(list.Cursor); | |
| foreach (var item in list.Entries.Where(item => item.IsFile)) | |
| { | |
| yield return ToItem(item.AsFile); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ListFolderAsync and ListFolderContinueAsync calls don't pass the cancellationToken, and the while loop doesn't check for cancellation between pages. This means large folder listings cannot be cancelled mid-operation.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using System; | ||
|
|
||
| namespace ManagedCode.Storage.Dropbox.Clients; | ||
|
|
||
| public class DropboxItemMetadata | ||
| { | ||
| public required string Name { get; set; } | ||
| public required string Path { get; set; } | ||
| public ulong Size { get; set; } | ||
| public DateTime ClientModified { get; set; } | ||
| public DateTime ServerModified { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Dropbox.Api.Files; | ||
|
|
||
| namespace ManagedCode.Storage.Dropbox.Clients; | ||
|
|
||
| public interface IDropboxClientWrapper | ||
| { | ||
| Task EnsureRootAsync(string rootPath, bool createIfNotExists, CancellationToken cancellationToken); | ||
|
|
||
| Task<DropboxItemMetadata> UploadAsync(string rootPath, string path, Stream content, string? contentType, CancellationToken cancellationToken); | ||
|
|
||
| Task<Stream> DownloadAsync(string rootPath, string path, CancellationToken cancellationToken); | ||
|
|
||
| Task<bool> DeleteAsync(string rootPath, string path, CancellationToken cancellationToken); | ||
|
|
||
| Task<bool> ExistsAsync(string rootPath, string path, CancellationToken cancellationToken); | ||
|
|
||
| Task<DropboxItemMetadata?> GetMetadataAsync(string rootPath, string path, CancellationToken cancellationToken); | ||
|
|
||
| IAsyncEnumerable<DropboxItemMetadata> ListAsync(string rootPath, string? directory, CancellationToken cancellationToken); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EnsureRootAsync implementation creates a folder but doesn't verify if it already exists before creation, which could lead to exceptions if createIfNotExists is false but the folder doesn't exist. The method should check existence first and only create when both the folder is missing and createIfNotExists is true.