Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ var tenantStorage = app.Services.GetRequiredKeyedService<IStorage>("tenant-a");
builder.Services.AddGoogleDriveStorageAsDefault(options =>
{
options.DriveService = driveService;
options.RootFolderId = "root"; // or a specific folder id you control
options.RootFolderId = "root"; // or a specific folder id you control / shared team drive folder id
options.CreateContainerIfNotExists = true;
options.SupportsAllDrives = true; // To support shared/team drives
});
```

Expand Down Expand Up @@ -723,7 +724,7 @@ Using in default mode:
public class MyService
{
private readonly IStorage _storage;

public MyService(IStorage storage)
{
_storage = storage;
Expand Down Expand Up @@ -797,7 +798,7 @@ Using in default mode:
public class MyService
{
private readonly IStorage _storage;

public MyService(IStorage storage)
{
_storage = storage;
Expand Down Expand Up @@ -858,7 +859,7 @@ Using in default mode:
public class MyService
{
private readonly IStorage _storage;

public MyService(IStorage storage)
{
_storage = storage;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Google.Apis.Drive.v3;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.IO.Pipelines;
using Google.Apis.Drive.v3;
using DriveFile = Google.Apis.Drive.v3.Data.File;

namespace ManagedCode.Storage.GoogleDrive.Clients;
Expand All @@ -27,9 +27,9 @@ public Task EnsureRootAsync(string rootFolderId, bool createIfNotExists, Cancell
return Task.CompletedTask;
}

public async Task<DriveFile> UploadAsync(string rootFolderId, string path, Stream content, string? contentType, CancellationToken cancellationToken)
public async Task<DriveFile> UploadAsync(string rootFolderId, string path, Stream content, string? contentType, bool supportsAllDrives, CancellationToken cancellationToken)
{
var (parentId, fileName) = await EnsureParentFolderAsync(rootFolderId, path, cancellationToken);
var (parentId, fileName) = await EnsureParentFolderAsync(rootFolderId, path, supportsAllDrives, cancellationToken);

var fileMetadata = new DriveFile
{
Expand All @@ -39,20 +39,24 @@ public async Task<DriveFile> UploadAsync(string rootFolderId, string path, Strea

var request = _driveService.Files.Create(fileMetadata, content, contentType ?? "application/octet-stream");
request.Fields = "id,name,parents,createdTime,modifiedTime,md5Checksum,size";
request.SupportsAllDrives = supportsAllDrives;
await request.UploadAsync(cancellationToken);
return request.ResponseBody ?? throw new InvalidOperationException("Google Drive upload returned no metadata.");
}

public async Task<Stream> DownloadAsync(string rootFolderId, string path, CancellationToken cancellationToken)
public async Task<Stream> DownloadAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken)
{
var file = await FindFileByPathAsync(rootFolderId, path, cancellationToken) ?? throw new FileNotFoundException(path);
var file = await FindFileByPathAsync(rootFolderId, path, supportsAllDrives, cancellationToken) ?? throw new FileNotFoundException(path);
var pipe = new Pipe();
var getRequest = _driveService.Files.Get(file.Id);
getRequest.SupportsAllDrives = supportsAllDrives;

_ = Task.Run(async () =>
{
try
{
await using var destination = pipe.Writer.AsStream(leaveOpen: true);
await _driveService.Files.Get(file.Id).DownloadAsync(destination, cancellationToken);
await getRequest.DownloadAsync(destination, cancellationToken);
pipe.Writer.Complete();
}
catch (Exception ex)
Expand All @@ -64,29 +68,29 @@ public async Task<Stream> DownloadAsync(string rootFolderId, string path, Cancel
return pipe.Reader.AsStream();
}

public async Task<bool> DeleteAsync(string rootFolderId, string path, CancellationToken cancellationToken)
public async Task<bool> DeleteAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken)
{
var file = await FindFileByPathAsync(rootFolderId, path, cancellationToken);
var file = await FindFileByPathAsync(rootFolderId, path, supportsAllDrives, cancellationToken);
if (file == null)
{
return false;
}

await DeleteRecursiveAsync(file.Id, file.MimeType, cancellationToken);
await DeleteRecursiveAsync(file.Id, file.MimeType, supportsAllDrives, cancellationToken);
return true;
}

public async Task<bool> ExistsAsync(string rootFolderId, string path, CancellationToken cancellationToken)
public async Task<bool> ExistsAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken)
{
return await FindFileByPathAsync(rootFolderId, path, cancellationToken) != null;
return await FindFileByPathAsync(rootFolderId, path, supportsAllDrives, cancellationToken) != null;
}

public Task<DriveFile?> GetMetadataAsync(string rootFolderId, string path, CancellationToken cancellationToken)
public Task<DriveFile?> GetMetadataAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken)
{
return FindFileByPathAsync(rootFolderId, path, cancellationToken);
return FindFileByPathAsync(rootFolderId, path, supportsAllDrives, cancellationToken);
}

public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string? directory, [EnumeratorCancellation] CancellationToken cancellationToken)
public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string? directory, bool supportsAllDrives, [EnumeratorCancellation] CancellationToken cancellationToken)
{
string parentId;
if (string.IsNullOrWhiteSpace(directory))
Expand All @@ -95,15 +99,19 @@ public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string?
}
else
{
parentId = await EnsureFolderPathAsync(rootFolderId, directory!, false, cancellationToken) ?? string.Empty;
parentId = await EnsureFolderPathAsync(rootFolderId, directory!, false, supportsAllDrives, cancellationToken) ?? string.Empty;
if (string.IsNullOrWhiteSpace(parentId))
{
yield break;
}
}

var request = _driveService.Files.List();
request.SupportsAllDrives = supportsAllDrives;
request.IncludeItemsFromAllDrives = supportsAllDrives;

request.Q = $"'{parentId}' in parents and trashed=false";

request.Fields = "nextPageToken,files(id,name,parents,createdTime,modifiedTime,md5Checksum,size,mimeType)";

do
Expand All @@ -119,7 +127,7 @@ public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string?
} while (!string.IsNullOrEmpty(request.PageToken));
}

private async Task<(string ParentId, string Name)> EnsureParentFolderAsync(string rootFolderId, string fullPath, CancellationToken cancellationToken)
private async Task<(string ParentId, string Name)> EnsureParentFolderAsync(string rootFolderId, string fullPath, bool supportsAllDrives, CancellationToken cancellationToken)
{
var normalizedPath = fullPath.Replace("\\", "/").Trim('/');
var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -129,16 +137,16 @@ public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string?
}

var parentPath = string.Join('/', segments.Take(segments.Length - 1));
var parentId = await EnsureFolderPathAsync(rootFolderId, parentPath, true, cancellationToken) ?? rootFolderId;
var parentId = await EnsureFolderPathAsync(rootFolderId, parentPath, true, supportsAllDrives, cancellationToken) ?? rootFolderId;
return (parentId, segments.Last());
}

private async Task<string?> EnsureFolderPathAsync(string rootFolderId, string path, bool createIfMissing, CancellationToken cancellationToken)
private async Task<string?> EnsureFolderPathAsync(string rootFolderId, string path, bool createIfMissing, bool supportsAllDrives, CancellationToken cancellationToken)
{
var currentId = rootFolderId;
foreach (var segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries))
{
var folder = await FindChildAsync(currentId, segment, cancellationToken);
var folder = await FindChildAsync(currentId, segment, supportsAllDrives, cancellationToken);
if (folder == null)
{
if (!createIfMissing)
Expand All @@ -147,7 +155,9 @@ public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string?
}

var metadata = new DriveFile { Name = segment, MimeType = "application/vnd.google-apps.folder", Parents = new List<string> { currentId } };
folder = await _driveService.Files.Create(metadata).ExecuteAsync(cancellationToken);
var createRequest = _driveService.Files.Create(metadata);
createRequest.SupportsAllDrives = supportsAllDrives;
folder = await createRequest.ExecuteAsync(cancellationToken);
}

currentId = folder.Id;
Expand All @@ -156,32 +166,38 @@ public async IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string?
return currentId;
}

private async Task<DriveFile?> FindChildAsync(string parentId, string name, CancellationToken cancellationToken)
private async Task<DriveFile?> FindChildAsync(string parentId, string name, bool supportsAllDrives, CancellationToken cancellationToken)
{
var request = _driveService.Files.List();
request.Q = $"'{parentId}' in parents and name='{name}' and trashed=false";
request.Fields = "files(id,name,parents,createdTime,modifiedTime,md5Checksum,size,mimeType)";
request.SupportsAllDrives = supportsAllDrives;
request.IncludeItemsFromAllDrives = supportsAllDrives;
var response = await request.ExecuteAsync(cancellationToken);
return response.Files?.FirstOrDefault();
}

private async Task DeleteRecursiveAsync(string fileId, string? mimeType, CancellationToken cancellationToken)
private async Task DeleteRecursiveAsync(string fileId, string? mimeType, bool supportsAllDrives, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (string.Equals(mimeType, FolderMimeType, StringComparison.OrdinalIgnoreCase))
{
await DeleteFolderChildrenAsync(fileId, cancellationToken);
await DeleteFolderChildrenAsync(fileId, supportsAllDrives, cancellationToken);
}

await _driveService.Files.Delete(fileId).ExecuteAsync(cancellationToken);
var trashRequest = _driveService.Files.Update(new DriveFile { Trashed = true }, fileId);
trashRequest.SupportsAllDrives = supportsAllDrives;
await trashRequest.ExecuteAsync(cancellationToken);
}

private async Task DeleteFolderChildrenAsync(string folderId, CancellationToken cancellationToken)
private async Task DeleteFolderChildrenAsync(string folderId, bool supportsAllDrives, CancellationToken cancellationToken)
{
var request = _driveService.Files.List();
request.Q = $"'{folderId}' in parents and trashed=false";
request.Fields = "nextPageToken,files(id,mimeType)";
request.SupportsAllDrives = supportsAllDrives;
request.IncludeItemsFromAllDrives = supportsAllDrives;

do
{
Expand All @@ -194,14 +210,14 @@ private async Task DeleteFolderChildrenAsync(string folderId, CancellationToken
continue;
}

await DeleteRecursiveAsync(entry.Id, entry.MimeType, cancellationToken);
await DeleteRecursiveAsync(entry.Id, entry.MimeType, supportsAllDrives, cancellationToken);
}

request.PageToken = response.NextPageToken;
} while (!string.IsNullOrWhiteSpace(request.PageToken));
}

private async Task<DriveFile?> FindFileByPathAsync(string rootFolderId, string path, CancellationToken cancellationToken)
private async Task<DriveFile?> FindFileByPathAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken)
{
var normalizedPath = path.Replace("\\", "/").Trim('/');
var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -212,12 +228,12 @@ private async Task DeleteFolderChildrenAsync(string folderId, CancellationToken

var parentPath = string.Join('/', segments.Take(segments.Length - 1));
var fileName = segments.Last();
var parentId = await EnsureFolderPathAsync(rootFolderId, parentPath, false, cancellationToken);
var parentId = await EnsureFolderPathAsync(rootFolderId, parentPath, false, supportsAllDrives, cancellationToken);
if (parentId == null)
{
return null;
}

return await FindChildAsync(parentId, fileName, cancellationToken);
return await FindChildAsync(parentId, fileName, supportsAllDrives, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ public interface IGoogleDriveClient
{
Task EnsureRootAsync(string rootFolderId, bool createIfNotExists, CancellationToken cancellationToken);

Task<DriveFile> UploadAsync(string rootFolderId, string path, Stream content, string? contentType, CancellationToken cancellationToken);
Task<DriveFile> UploadAsync(string rootFolderId, string path, Stream content, string? contentType, bool supportsAllDrives, CancellationToken cancellationToken);

Task<Stream> DownloadAsync(string rootFolderId, string path, CancellationToken cancellationToken);
Task<Stream> DownloadAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken);

Task<bool> DeleteAsync(string rootFolderId, string path, CancellationToken cancellationToken);
Task<bool> DeleteAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken);

Task<bool> ExistsAsync(string rootFolderId, string path, CancellationToken cancellationToken);
Task<bool> ExistsAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken);

Task<DriveFile?> GetMetadataAsync(string rootFolderId, string path, CancellationToken cancellationToken);
Task<DriveFile?> GetMetadataAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken);

IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string? directory, CancellationToken cancellationToken);
IAsyncEnumerable<DriveFile> ListAsync(string rootFolderId, string? directory, bool supportsAllDrives, CancellationToken cancellationToken);
}
Loading
Loading