From ac0758e33f98e43683c9b0650c82538f0cff5793 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Mon, 23 Oct 2023 21:34:35 -0400 Subject: [PATCH 1/5] add initial google drive adapter --- .github/workflows/Build.yaml | 8 +- .github/workflows/Release.yaml | 2 +- Directory.Build.props | 2 +- .../src/AmazonS3Adapter.cs | 32 +- .../src/AmazonS3AdapterConfiguration.cs | 8 + .../src/AzureBlobStorageAdapter.cs | 5 +- .../AzureBlobStorageAdapterConfiguration.cs | 8 + .../src/AzureFileStorageAdapter.cs | 4 +- .../AzureFileStorageAdapterConfiguration.cs | 8 + .../src/DropboxAdapter.cs | 4 +- .../src/DropboxAdapterConfiguration.cs | 8 + .../FileSystem.Adapters.GoogleDrive.csproj | 27 + .../src/GoogleDriveAdapter.cs | 470 ++++++++++++++++++ .../src/GoogleDriveAdapterConfiguration.cs | 8 + .../src/ModelFactory.cs | 33 ++ .../src/MicrosoftOneDriveAdapter.cs | 5 +- .../MicrosoftOneDriveAdapterConfiguration.cs | 8 + FileSystem.Adapters.Sftp/src/SftpAdapter.cs | 4 +- .../src/SftpAdapterConfiguration.cs | 8 + FileSystem.sln | 6 + FileSystem/FileSystem.csproj | 3 + FileSystem/src/Adapters/Adapter.cs | 63 ++- FileSystem/src/Adapters/IAdapter.cs | 3 + FileSystem/src/Adapters/LocalAdapter.cs | 4 +- .../src/Adapters/LocalAdapterConfiguration.cs | 8 + FileSystem/src/Cache/CacheEntry.cs | 14 + FileSystem/src/Cache/ICacheEntry.cs | 6 + .../src/Configuration/AdapterConfiguration.cs | 7 + .../Configuration/IAdapterConfiguration.cs | 7 + FileSystem/src/FileSystem.cs | 33 ++ FileSystem/src/IFileSystem.cs | 4 +- .../src/Utilities/ContentTypeProvider.cs | 12 + FileSystem/src/Utilities/PathUtilities.cs | 79 ++- Tests/Tests.csproj | 6 +- .../AmazonS3AdapterTest.cs | 2 +- .../AzureBlobStorageAdapterTest.cs | 2 +- .../AzureFileStorageAdapterTest.cs | 2 +- .../DropboxAdapterTest.cs | 2 +- .../GoogleDriveAdapterTest.cs | 116 +++++ .../TestHttpMessageHandler.cs | 17 + .../MicrosoftOneDriveAdapterTest.cs | 2 +- .../SftpAdapterTest.cs | 2 +- .../{ => Adapters}/LocalAdapterTest.cs | 2 +- .../Extensions/StringExtensionsTest.cs | 35 ++ Tests/src/FileSystem/FileSystemTest.cs | 2 +- .../src/FileSystem/Models/ModelFactoryTest.cs | 53 ++ .../Utilities/ContentTypeProviderTest.cs | 18 + .../FileSystem/Utilities/PathUtilitiesTest.cs | 69 +++ .../Utilities/StreamUtilitiesTest.cs | 22 + Tests/src/IAdapterTests.cs | 2 +- Tests/var/data/test.txt | 0 scripts/publish.ps1 | 5 - 52 files changed, 1180 insertions(+), 80 deletions(-) create mode 100644 FileSystem.Adapters.AmazonS3/src/AmazonS3AdapterConfiguration.cs create mode 100644 FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.Dropbox/src/DropboxAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.GoogleDrive/FileSystem.Adapters.GoogleDrive.csproj create mode 100644 FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs create mode 100644 FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.GoogleDrive/src/ModelFactory.cs create mode 100644 FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.Sftp/src/SftpAdapterConfiguration.cs create mode 100644 FileSystem/src/Adapters/LocalAdapterConfiguration.cs create mode 100644 FileSystem/src/Cache/CacheEntry.cs create mode 100644 FileSystem/src/Cache/ICacheEntry.cs create mode 100644 FileSystem/src/Configuration/AdapterConfiguration.cs create mode 100644 FileSystem/src/Configuration/IAdapterConfiguration.cs create mode 100644 FileSystem/src/Utilities/ContentTypeProvider.cs create mode 100644 Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs create mode 100644 Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs rename Tests/src/FileSystem/{ => Adapters}/LocalAdapterTest.cs (98%) create mode 100644 Tests/src/FileSystem/Extensions/StringExtensionsTest.cs create mode 100644 Tests/src/FileSystem/Models/ModelFactoryTest.cs create mode 100644 Tests/src/FileSystem/Utilities/ContentTypeProviderTest.cs create mode 100644 Tests/src/FileSystem/Utilities/PathUtilitiesTest.cs create mode 100644 Tests/src/FileSystem/Utilities/StreamUtilitiesTest.cs create mode 100644 Tests/var/data/test.txt delete mode 100644 scripts/publish.ps1 diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 13db036..f1e5bce 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -3,7 +3,11 @@ name: FileSystem [Build] env: JAVA_VERSION: 17 JAVA_DISTRIBUTION: microsoft - DOTNET_VERSION: 7.0.x + DOTNET_VERSION: | + 3.1.x + 6.0.x + 7.0.x + 8.0.x DOTNET_BUILD_CONFIGURATION: Release SONAR_PATH: .\.sonar\scanner SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -34,7 +38,7 @@ jobs: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - - name: Setup .NET + - name: Set up .NET uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index a055080..7d50678 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -22,7 +22,7 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET + - name: Set up .NET uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/Directory.Build.props b/Directory.Build.props index ce2046d..1afc86d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 enable 8.0 NU1701 diff --git a/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs b/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs index 04f8bb3..fdf4501 100644 --- a/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs +++ b/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs @@ -14,12 +14,12 @@ namespace SharpGrip.FileSystem.Adapters.AmazonS3 { - public class AmazonS3Adapter : Adapter + public class AmazonS3Adapter : Adapter { private readonly IAmazonS3 client; private readonly string bucketName; - public AmazonS3Adapter(string prefix, string rootPath, IAmazonS3 client, string bucketName) : base(prefix, rootPath) + public AmazonS3Adapter(string prefix, string rootPath, IAmazonS3 client, string bucketName, Action? configuration = null) : base(prefix, rootPath, configuration) { this.client = client; this.bucketName = bucketName; @@ -61,12 +61,7 @@ public override async Task GetFileAsync(string virtualPath, CancellationT public override async Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) { - var path = GetPath(virtualPath); - - if (!path.EndsWith("/")) - { - path += "/"; - } + var path = GetPath(virtualPath).RemoveLeadingForwardSlash().EnsureTrailingForwardSlash(); try { @@ -109,12 +104,7 @@ public override async Task GetDirectoryAsync(string virtualPath, Can public override async Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default) { await GetDirectoryAsync(virtualPath, cancellationToken); - var path = GetPath(virtualPath); - - if (!path.EndsWith("/")) - { - path += "/"; - } + var path = GetPath(virtualPath).EnsureTrailingForwardSlash(); if (path == "/") { @@ -134,7 +124,6 @@ public override async Task> GetFilesAsync(string virtualPath foreach (var item in response.S3Objects) { - // var itemName = item.Key.Substring(0, item.Key.Length - path.Length); var itemName = item.Key.Substring(path.Length).RemoveLeadingForwardSlash(); if (!item.Key.EndsWith("/") && !itemName.Contains('/')) @@ -157,12 +146,7 @@ public override async Task> GetFilesAsync(string virtualPath public override async Task> GetDirectoriesAsync(string virtualPath = "", CancellationToken cancellationToken = default) { await GetDirectoryAsync(virtualPath, cancellationToken); - var path = GetPath(virtualPath); - - if (!path.EndsWith("/")) - { - path += "/"; - } + var path = GetPath(virtualPath).EnsureTrailingForwardSlash(); if (path == "/") { @@ -208,8 +192,7 @@ public override async Task CreateDirectoryAsync(string virtualPath, Cancellation throw new DirectoryExistsException(GetPath(virtualPath), Prefix); } - var path = GetPath(virtualPath); - path = path.EndsWith("/") ? path : path + "/"; + var path = GetPath(virtualPath).EnsureTrailingForwardSlash(); try { @@ -226,8 +209,7 @@ public override async Task DeleteDirectoryAsync(string virtualPath, Cancellation { await GetDirectoryAsync(virtualPath, cancellationToken); - var path = GetPath(virtualPath); - path = path.EndsWith("/") ? path : path + "/"; + var path = GetPath(virtualPath).EnsureTrailingForwardSlash(); try { diff --git a/FileSystem.Adapters.AmazonS3/src/AmazonS3AdapterConfiguration.cs b/FileSystem.Adapters.AmazonS3/src/AmazonS3AdapterConfiguration.cs new file mode 100644 index 0000000..b669788 --- /dev/null +++ b/FileSystem.Adapters.AmazonS3/src/AmazonS3AdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.AmazonS3 +{ + public class AmazonS3AdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs index 0d74a80..ef97030 100644 --- a/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs +++ b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs @@ -14,11 +14,12 @@ namespace SharpGrip.FileSystem.Adapters.AzureBlobStorage { - public class AzureBlobStorageAdapter : Adapter + public class AzureBlobStorageAdapter : Adapter { private readonly BlobContainerClient client; - public AzureBlobStorageAdapter(string prefix, string rootPath, BlobContainerClient client) : base(prefix, rootPath) + public AzureBlobStorageAdapter(string prefix, string rootPath, BlobContainerClient client, Action? configuration = null) + : base(prefix, rootPath, configuration) { this.client = client; } diff --git a/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapterConfiguration.cs b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapterConfiguration.cs new file mode 100644 index 0000000..603709a --- /dev/null +++ b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.AzureBlobStorage +{ + public class AzureBlobStorageAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs index a5068f8..906047f 100644 --- a/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs +++ b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs @@ -12,11 +12,11 @@ namespace SharpGrip.FileSystem.Adapters.AzureFileStorage { - public class AzureFileStorageAdapter : Adapter + public class AzureFileStorageAdapter : Adapter { private readonly ShareClient client; - public AzureFileStorageAdapter(string prefix, string rootPath, ShareClient client) : base(prefix, rootPath) + public AzureFileStorageAdapter(string prefix, string rootPath, ShareClient client, Action? configuration = null) : base(prefix, rootPath, configuration) { this.client = client; } diff --git a/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapterConfiguration.cs b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapterConfiguration.cs new file mode 100644 index 0000000..c8c8f26 --- /dev/null +++ b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.AzureFileStorage +{ + public class AzureFileStorageAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs b/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs index 3e1dc26..3ed37c8 100644 --- a/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs +++ b/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs @@ -13,11 +13,11 @@ namespace SharpGrip.FileSystem.Adapters.Dropbox { - public class DropboxAdapter : Adapter + public class DropboxAdapter : Adapter { private readonly DropboxClient client; - public DropboxAdapter(string prefix, string rootPath, DropboxClient client) : base(prefix, rootPath) + public DropboxAdapter(string prefix, string rootPath, DropboxClient client, Action? configuration = null) : base(prefix, rootPath, configuration) { this.client = client; } diff --git a/FileSystem.Adapters.Dropbox/src/DropboxAdapterConfiguration.cs b/FileSystem.Adapters.Dropbox/src/DropboxAdapterConfiguration.cs new file mode 100644 index 0000000..7feb351 --- /dev/null +++ b/FileSystem.Adapters.Dropbox/src/DropboxAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.Dropbox +{ + public class DropboxAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.GoogleDrive/FileSystem.Adapters.GoogleDrive.csproj b/FileSystem.Adapters.GoogleDrive/FileSystem.Adapters.GoogleDrive.csproj new file mode 100644 index 0000000..72aea8b --- /dev/null +++ b/FileSystem.Adapters.GoogleDrive/FileSystem.Adapters.GoogleDrive.csproj @@ -0,0 +1,27 @@ + + + + SharpGrip.FileSystem.Adapters.GoogleDrive + + + + SharpGrip.FileSystem.Adapters.GoogleDrive + SharpGrip.FileSystem.Adapters.GoogleDrive + SharpGrip FileSystem Google Drive adapter + The SharpGrip FileSystem Google Drive adapter. + sharpgrip;file-system;google-drive + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs new file mode 100644 index 0000000..87f0a9c --- /dev/null +++ b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Drive.v3; +using Google.Apis.Drive.v3.Data; +using SharpGrip.FileSystem.Cache; +using SharpGrip.FileSystem.Exceptions; +using SharpGrip.FileSystem.Extensions; +using SharpGrip.FileSystem.Models; +using SharpGrip.FileSystem.Utilities; +using DirectoryNotFoundException = SharpGrip.FileSystem.Exceptions.DirectoryNotFoundException; +using File = Google.Apis.Drive.v3.Data.File; +using FileNotFoundException = SharpGrip.FileSystem.Exceptions.FileNotFoundException; + +namespace SharpGrip.FileSystem.Adapters.GoogleDrive +{ + public class GoogleDriveAdapter : Adapter + { + private readonly DriveService client; + + private const string DirectoryMimeType = "application/vnd.google-apps.folder"; + private const string SingleRequestFields = "id, name, size, modifiedTime, createdTime, parents"; + private static readonly string MultipleRequestFields = $"nextPageToken, files({SingleRequestFields})"; + private static readonly string FileRequestQuery = $"mimeType != '{DirectoryMimeType}' and trashed = false"; + private static readonly string DirectoryRequestQuery = $"mimeType = '{DirectoryMimeType}' and trashed = false"; + + public GoogleDriveAdapter(string prefix, string rootPath, DriveService client, Action? configuration = null) : base(prefix, rootPath, configuration) + { + this.client = client; + } + + public override void Dispose() + { + client.Dispose(); + } + + public override void Connect() + { + } + + public override async Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default) + { + var path = GetPath(virtualPath).RemoveLeadingForwardSlash(); + + try + { + var file = await RequestFileByPath(path, cancellationToken); + + if (file == null) + { + throw new FileNotFoundException(path, Prefix); + } + + return ModelFactory.CreateFile(file, path, virtualPath); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + var path = GetPath(virtualPath); + path = path.EnsureTrailingForwardSlash(); + + try + { + if (path == "/") + { + return ModelFactory.CreateDirectory(new File {Name = "/"}, path, virtualPath); + } + + path = path.RemoveLeadingForwardSlash(); + + var directory = await RequestDirectoryByPath(path, cancellationToken); + + if (directory == null) + { + throw new DirectoryNotFoundException(path, Prefix); + } + + return ModelFactory.CreateDirectory(directory, path, virtualPath); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + + var path = GetPath(virtualPath).EnsureTrailingForwardSlash().RemoveLeadingForwardSlash(); + + if (path == "") + { + path = "/"; + } + + try + { + var request = client.Files.List(); + FileList fileList; + + request.Q = FileRequestQuery; + request.Fields = MultipleRequestFields; + + var files = new List(); + + do + { + fileList = await request.ExecuteAsync(cancellationToken); + + foreach (var file in fileList.Files) + { + TryAddCacheEntry(new CacheEntry(file.Id, file)); + + var filePath = await GetAbsolutePath(file); + var directoryPath = GetParentPathPart(filePath).EnsureTrailingForwardSlash(); + + if (directoryPath == path) + { + files.Add(ModelFactory.CreateFile(file, filePath, GetVirtualPath(filePath))); + } + } + + request.PageToken = fileList.NextPageToken; + } while (fileList.NextPageToken != null); + + return files; + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task> GetDirectoriesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + + var path = GetPath(virtualPath).EnsureTrailingForwardSlash().RemoveLeadingForwardSlash(); + + if (path == "") + { + path = "/"; + } + + try + { + var request = client.Files.List(); + FileList directoryList; + + request.Q = DirectoryRequestQuery; + request.Fields = MultipleRequestFields; + + var directories = new List(); + + do + { + directoryList = await request.ExecuteAsync(cancellationToken); + + foreach (var directory in directoryList.Files) + { + TryAddCacheEntry(new CacheEntry(directory.Id, directory)); + + var directoryPath = (await GetAbsolutePath(directory)).EnsureTrailingForwardSlash(); + var directoryParentPathPart = GetParentPathPart(directoryPath).EnsureTrailingForwardSlash(); + + if (directoryParentPathPart == path) + { + directories.Add(ModelFactory.CreateDirectory(directory, directoryPath, GetVirtualPath(directoryPath))); + } + } + + request.PageToken = directoryList.NextPageToken; + } while (directoryList.NextPageToken != null); + + return directories; + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task CreateDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + if (await DirectoryExistsAsync(virtualPath, cancellationToken)) + { + throw new DirectoryExistsException(GetPath(virtualPath), Prefix); + } + + var path = GetPath(virtualPath).RemoveTrailingForwardSlash().RemoveLeadingForwardSlash(); + + if (path == "") + { + path = "/"; + } + + try + { + var parent = await RequestParentDirectory(path, cancellationToken); + + var directory = new File + { + Name = GetLastPathPart(path), + Parents = new List {parent.Id}, + MimeType = DirectoryMimeType + }; + + var request = client.Files.Create(directory); + request.Fields = SingleRequestFields; + + var response = await request.ExecuteAsync(cancellationToken); + + TryAddCacheEntry(new CacheEntry(response.Id, response)); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task DeleteDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + + var path = GetPath(virtualPath).RemoveLeadingForwardSlash().EnsureTrailingForwardSlash(); + + try + { + var directory = await RequestDirectoryByPath(path, cancellationToken); + + if (directory == null) + { + throw new DirectoryNotFoundException(GetPath(virtualPath), Prefix); + } + + var deleteRequest = client.Files.Delete(directory.Id); + await deleteRequest.ExecuteAsync(cancellationToken); + + TryRemoveCacheEntry(directory.Id); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task DeleteFileAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetFileAsync(virtualPath, cancellationToken); + + var path = GetPath(virtualPath).RemoveLeadingForwardSlash().EnsureTrailingForwardSlash(); + + try + { + var file = await RequestFileByPath(path, cancellationToken); + + if (file == null) + { + throw new FileNotFoundException(GetPath(virtualPath), Prefix); + } + + var deleteRequest = client.Files.Delete(file.Id); + await deleteRequest.ExecuteAsync(cancellationToken); + + TryRemoveCacheEntry(file.Id); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task ReadFileStreamAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetFileAsync(virtualPath, cancellationToken); + + var path = GetPath(virtualPath).RemoveLeadingForwardSlash(); + + try + { + var file = await RequestFileByPath(path, cancellationToken); + + var memoryStream = new MemoryStream(); + var request = client.Files.Get(file!.Id); + + await request.DownloadAsync(memoryStream, cancellationToken); + + memoryStream.Position = 0; + + return memoryStream; + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task WriteFileAsync(string virtualPath, Stream contents, bool overwrite = false, CancellationToken cancellationToken = default) + { + if (!overwrite && await FileExistsAsync(virtualPath, cancellationToken)) + { + throw new FileExistsException(GetPath(virtualPath), Prefix); + } + + var path = GetPath(virtualPath); + + try + { + var parent = await RequestParentDirectory(path, cancellationToken); + var contentType = ContentTypeProvider.GetContentType(path); + + var file = new File + { + Name = GetLastPathPart(path), + Parents = new List {parent.Id}, + MimeType = contentType + }; + + var request = client.Files.Create(file, contents, contentType); + request.Fields = SingleRequestFields; + + await request.UploadAsync(cancellationToken); + + var response = request.ResponseBody; + + TryAddCacheEntry(new CacheEntry(response.Id, response)); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + protected override Exception Exception(Exception exception) + { + if (exception is FileSystemException) + { + return exception; + } + + return new AdapterRuntimeException(exception); + } + + private async Task GetAbsolutePath(File file) + { + if (file.Parents == null || !file.Parents.Any()) + { + return file.Name; + } + + var pathParts = new List {file.Name}; + + while (file.Parents != null && file.Parents.Any()) + { + var parentId = file.Parents[0]; + var cacheEntry = await GetOrCreateCacheEntryAsync(parentId, async () => new CacheEntry(parentId, await RequestFileById(parentId))); + var parent = cacheEntry.Value; + + if (parent.Parents == null || !parent.Parents.Any()) + { + break; + } + + pathParts.Insert(0, parent.Name); + file = parent; + } + + return string.Join("/", pathParts); + } + + private async Task RequestParentDirectory(string path, CancellationToken cancellationToken = default) + { + var parentPathPart = GetParentPathPart(path); + + var parentDirectory = await RequestDirectoryByPath(parentPathPart, cancellationToken); + + if (parentDirectory != null) + { + return parentDirectory; + } + + return await RequestRootDrive(cancellationToken); + } + + private async Task RequestRootDrive(CancellationToken cancellationToken = default) + { + return await RequestFileById("root", cancellationToken); + } + + private async Task RequestFileById(string id, CancellationToken cancellationToken = default) + { + var request = client.Files.Get(id); + request.Fields = SingleRequestFields; + + var file = await request.ExecuteAsync(cancellationToken); + + TryAddCacheEntry(new CacheEntry(file.Id, file)); + + return file; + } + + private async Task RequestFileByPath(string path, CancellationToken cancellationToken = default) + { + var request = client.Files.List(); + + request.Q = $"mimeType != '{DirectoryMimeType}' and name = '{GetLastPathPart(path)}' and trashed = false"; + request.Fields = MultipleRequestFields; + + do + { + var fileList = await request.ExecuteAsync(cancellationToken); + + foreach (var file in fileList.Files) + { + TryAddCacheEntry(new CacheEntry(file.Id, file)); + + var filePath = await GetAbsolutePath(file); + + if (filePath == path) + { + return file; + } + } + + request.PageToken = fileList.NextPageToken; + } while (request.PageToken != null); + + return null; + } + + private async Task RequestDirectoryByPath(string path, CancellationToken cancellationToken = default) + { + var request = client.Files.List(); + FileList directoryList; + + request.Q = $"mimeType = '{DirectoryMimeType}' and name = '{GetLastPathPart(path)}' and trashed = false"; + request.Fields = MultipleRequestFields; + + do + { + directoryList = await request.ExecuteAsync(cancellationToken); + + foreach (var directory in directoryList.Files) + { + TryAddCacheEntry(new CacheEntry(directory.Id, directory)); + + var directoryPath = (await GetAbsolutePath(directory)).EnsureTrailingForwardSlash(); + + if (directoryPath == path) + { + return directory; + } + } + + request.PageToken = directoryList.NextPageToken; + } while (directoryList.NextPageToken != null); + + return null; + } + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapterConfiguration.cs b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapterConfiguration.cs new file mode 100644 index 0000000..e258ddb --- /dev/null +++ b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.GoogleDrive +{ + public class GoogleDriveAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.GoogleDrive/src/ModelFactory.cs b/FileSystem.Adapters.GoogleDrive/src/ModelFactory.cs new file mode 100644 index 0000000..4a079e2 --- /dev/null +++ b/FileSystem.Adapters.GoogleDrive/src/ModelFactory.cs @@ -0,0 +1,33 @@ +using Google.Apis.Drive.v3.Data; +using SharpGrip.FileSystem.Models; + +namespace SharpGrip.FileSystem.Adapters.GoogleDrive +{ + public static class ModelFactory + { + public static FileModel CreateFile(File file, string path, string virtualPath) + { + return new FileModel + { + Name = file.Name, + Path = path, + VirtualPath = virtualPath, + Length = file.Size, + LastModifiedDateTime = file.ModifiedByMeTimeDateTimeOffset?.DateTime, + CreatedDateTime = file.CreatedTimeDateTimeOffset?.DateTime + }; + } + + public static DirectoryModel CreateDirectory(File directory, string path, string virtualPath) + { + return new DirectoryModel + { + Name = directory.Name, + Path = path, + VirtualPath = virtualPath, + LastModifiedDateTime = directory.ModifiedByMeTimeDateTimeOffset?.DateTime, + CreatedDateTime = directory.CreatedTimeDateTimeOffset?.DateTime + }; + } + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs index 5340d29..69f3082 100644 --- a/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs +++ b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs @@ -12,12 +12,13 @@ namespace SharpGrip.FileSystem.Adapters.MicrosoftOneDrive { - public class MicrosoftOneDriveAdapter : Adapter + public class MicrosoftOneDriveAdapter : Adapter { private readonly GraphServiceClient client; private readonly string driveId; - public MicrosoftOneDriveAdapter(string prefix, string rootPath, GraphServiceClient client, string driveId) : base(prefix, rootPath) + public MicrosoftOneDriveAdapter(string prefix, string rootPath, GraphServiceClient client, string driveId, Action? configuration = null) + : base(prefix, rootPath, configuration) { this.client = client; this.driveId = driveId; diff --git a/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapterConfiguration.cs b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapterConfiguration.cs new file mode 100644 index 0000000..9472e76 --- /dev/null +++ b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.MicrosoftOneDrive +{ + public class MicrosoftOneDriveAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.Sftp/src/SftpAdapter.cs b/FileSystem.Adapters.Sftp/src/SftpAdapter.cs index bfe2356..5143e6e 100644 --- a/FileSystem.Adapters.Sftp/src/SftpAdapter.cs +++ b/FileSystem.Adapters.Sftp/src/SftpAdapter.cs @@ -16,11 +16,11 @@ namespace SharpGrip.FileSystem.Adapters.Sftp { - public class SftpAdapter : Adapter + public class SftpAdapter : Adapter { private readonly SftpClient client; - public SftpAdapter(string prefix, string rootPath, SftpClient client) : base(prefix, rootPath) + public SftpAdapter(string prefix, string rootPath, SftpClient client, Action? configuration = null) : base(prefix, rootPath, configuration) { this.client = client; } diff --git a/FileSystem.Adapters.Sftp/src/SftpAdapterConfiguration.cs b/FileSystem.Adapters.Sftp/src/SftpAdapterConfiguration.cs new file mode 100644 index 0000000..c3a20fe --- /dev/null +++ b/FileSystem.Adapters.Sftp/src/SftpAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.Sftp +{ + public class SftpAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.sln b/FileSystem.sln index 9750f15..39ed7fe 100644 --- a/FileSystem.sln +++ b/FileSystem.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.AzureFi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.Dropbox", "FileSystem.Adapters.Dropbox\FileSystem.Adapters.Dropbox.csproj", "{FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.GoogleDrive", "FileSystem.Adapters.GoogleDrive\FileSystem.Adapters.GoogleDrive.csproj", "{CB1F411C-3FB5-4441-9541-423951C17BAF}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.MicrosoftOneDrive", "FileSystem.Adapters.MicrosoftOneDrive\FileSystem.Adapters.MicrosoftOneDrive.csproj", "{9955422E-4A50-43CD-BC2B-91E6820F206C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.Sftp", "FileSystem.Adapters.Sftp\FileSystem.Adapters.Sftp.csproj", "{DFDEAACC-CCFA-4A78-8E7D-AC61FB1A002D}" @@ -42,6 +44,10 @@ Global {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Release|Any CPU.Build.0 = Release|Any CPU + {CB1F411C-3FB5-4441-9541-423951C17BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB1F411C-3FB5-4441-9541-423951C17BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB1F411C-3FB5-4441-9541-423951C17BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB1F411C-3FB5-4441-9541-423951C17BAF}.Release|Any CPU.Build.0 = Release|Any CPU {9955422E-4A50-43CD-BC2B-91E6820F206C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9955422E-4A50-43CD-BC2B-91E6820F206C}.Debug|Any CPU.Build.0 = Debug|Any CPU {9955422E-4A50-43CD-BC2B-91E6820F206C}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/FileSystem/FileSystem.csproj b/FileSystem/FileSystem.csproj index ab39496..756ce46 100644 --- a/FileSystem/FileSystem.csproj +++ b/FileSystem/FileSystem.csproj @@ -16,5 +16,8 @@ + + + diff --git a/FileSystem/src/Adapters/Adapter.cs b/FileSystem/src/Adapters/Adapter.cs index a77f32f..837e137 100644 --- a/FileSystem/src/Adapters/Adapter.cs +++ b/FileSystem/src/Adapters/Adapter.cs @@ -5,8 +5,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using SharpGrip.FileSystem.Cache; +using SharpGrip.FileSystem.Configuration; using SharpGrip.FileSystem.Constants; -using SharpGrip.FileSystem.Extensions; using SharpGrip.FileSystem.Models; using SharpGrip.FileSystem.Utilities; using DirectoryNotFoundException = SharpGrip.FileSystem.Exceptions.DirectoryNotFoundException; @@ -14,15 +15,26 @@ namespace SharpGrip.FileSystem.Adapters { - public abstract class Adapter : IAdapter + public abstract class Adapter : IAdapter + where TAdapterConfiguration : IAdapterConfiguration, new() + where TCacheKey : class { public string Prefix { get; } public string RootPath { get; } + public TAdapterConfiguration Configuration { get; } + public IAdapterConfiguration AdapterConfiguration => Configuration; - protected Adapter(string prefix, string rootPath) + private IDictionary> Cache { get; set; } = new Dictionary>(); + + protected Adapter(string prefix, string rootPath, Action? configuration) { Prefix = prefix; - RootPath = rootPath; + RootPath = PathUtilities.NormalizeRootPath(rootPath); + + var adapterConfiguration = new TAdapterConfiguration(); + configuration?.Invoke(adapterConfiguration); + + Configuration = adapterConfiguration; } public IFile GetFile(string virtualPath) @@ -230,29 +242,54 @@ protected string GetVirtualPath(string path) protected string[] GetPathParts(string path) { - return path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + return PathUtilities.GetPathParts(path); } protected string GetLastPathPart(string path) { - if (path.IsNullOrEmpty()) + return PathUtilities.GetLastPathPart(path); + } + + protected string GetParentPathPart(string path) + { + return PathUtilities.GetParentPathPart(path); + } + + public void ClearCache() + { + Cache = new Dictionary>(); + } + + protected async Task> GetOrCreateCacheEntryAsync(TCacheKey cacheKey, Func>> factory) + { + var cacheEntry = GetCacheEntry(cacheKey); + + if (cacheEntry == null) { - return ""; + cacheEntry = await factory(); + + TryAddCacheEntry(cacheEntry); } - return GetPathParts(path).Last(); + return cacheEntry; } - protected string GetParentPathPart(string path) + protected void TryAddCacheEntry(CacheEntry cacheEntry) { - if (path.IsNullOrEmpty()) + if (!Cache.ContainsKey(cacheEntry.Key)) { - path = "/"; + Cache.Add(cacheEntry.Key, cacheEntry); } + } - var pathParts = GetPathParts(path); + protected void TryRemoveCacheEntry(TCacheKey cacheKey) + { + Cache.Remove(cacheKey); + } - return string.Join("/", pathParts.Take(pathParts.Length - 1)); + private CacheEntry? GetCacheEntry(TCacheKey cacheKey) + { + return Cache.TryGetValue(cacheKey, out var cacheEntry) ? cacheEntry : null; } } } \ No newline at end of file diff --git a/FileSystem/src/Adapters/IAdapter.cs b/FileSystem/src/Adapters/IAdapter.cs index 4546220..1303719 100644 --- a/FileSystem/src/Adapters/IAdapter.cs +++ b/FileSystem/src/Adapters/IAdapter.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpGrip.FileSystem.Configuration; using SharpGrip.FileSystem.Models; namespace SharpGrip.FileSystem.Adapters @@ -11,7 +12,9 @@ public interface IAdapter : IDisposable { string Prefix { get; } string RootPath { get; } + public IAdapterConfiguration AdapterConfiguration { get; } public void Connect(); + public void ClearCache(); IFile GetFile(string virtualPath); Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default); IDirectory GetDirectory(string virtualPath); diff --git a/FileSystem/src/Adapters/LocalAdapter.cs b/FileSystem/src/Adapters/LocalAdapter.cs index fe7c555..a29ce51 100644 --- a/FileSystem/src/Adapters/LocalAdapter.cs +++ b/FileSystem/src/Adapters/LocalAdapter.cs @@ -11,9 +11,9 @@ namespace SharpGrip.FileSystem.Adapters { - public class LocalAdapter : Adapter + public class LocalAdapter : Adapter { - public LocalAdapter(string prefix, string rootPath) : base(prefix, rootPath) + public LocalAdapter(string prefix, string rootPath, Action? configuration = null) : base(prefix, rootPath, configuration) { } diff --git a/FileSystem/src/Adapters/LocalAdapterConfiguration.cs b/FileSystem/src/Adapters/LocalAdapterConfiguration.cs new file mode 100644 index 0000000..c1f1132 --- /dev/null +++ b/FileSystem/src/Adapters/LocalAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters +{ + public class LocalAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem/src/Cache/CacheEntry.cs b/FileSystem/src/Cache/CacheEntry.cs new file mode 100644 index 0000000..09b4353 --- /dev/null +++ b/FileSystem/src/Cache/CacheEntry.cs @@ -0,0 +1,14 @@ +namespace SharpGrip.FileSystem.Cache +{ + public class CacheEntry : ICacheEntry + { + public TKey Key { get; set; } + public TValue Value { get; set; } + + public CacheEntry(TKey key, TValue value) + { + Key = key; + Value = value; + } + } +} \ No newline at end of file diff --git a/FileSystem/src/Cache/ICacheEntry.cs b/FileSystem/src/Cache/ICacheEntry.cs new file mode 100644 index 0000000..e750156 --- /dev/null +++ b/FileSystem/src/Cache/ICacheEntry.cs @@ -0,0 +1,6 @@ +namespace SharpGrip.FileSystem.Cache +{ + public interface ICacheEntry + { + } +} \ No newline at end of file diff --git a/FileSystem/src/Configuration/AdapterConfiguration.cs b/FileSystem/src/Configuration/AdapterConfiguration.cs new file mode 100644 index 0000000..0a8fb39 --- /dev/null +++ b/FileSystem/src/Configuration/AdapterConfiguration.cs @@ -0,0 +1,7 @@ +namespace SharpGrip.FileSystem.Configuration +{ + public abstract class AdapterConfiguration : IAdapterConfiguration + { + public bool EnableCache { get; set; } + } +} \ No newline at end of file diff --git a/FileSystem/src/Configuration/IAdapterConfiguration.cs b/FileSystem/src/Configuration/IAdapterConfiguration.cs new file mode 100644 index 0000000..a348fa2 --- /dev/null +++ b/FileSystem/src/Configuration/IAdapterConfiguration.cs @@ -0,0 +1,7 @@ +namespace SharpGrip.FileSystem.Configuration +{ + public interface IAdapterConfiguration + { + public bool EnableCache { get; set; } + } +} \ No newline at end of file diff --git a/FileSystem/src/FileSystem.cs b/FileSystem/src/FileSystem.cs index 211d339..d4f3b68 100644 --- a/FileSystem/src/FileSystem.cs +++ b/FileSystem/src/FileSystem.cs @@ -113,6 +113,12 @@ public Task GetFileAsync(string virtualPath, CancellationToken cancellati adapter.Connect(); + if (adapter.AdapterConfiguration.EnableCache) + { + } + + adapter.ClearCache(); + return adapter.GetFileAsync(virtualPath, cancellationToken); } @@ -152,6 +158,7 @@ public Task GetDirectoryAsync(string virtualPath, CancellationToken var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return adapter.GetDirectoryAsync(virtualPath, cancellationToken); } @@ -193,6 +200,11 @@ public Task> GetFilesAsync(string virtualPath = "", Cancellat adapter.Connect(); + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } + return adapter.GetFilesAsync(virtualPath, cancellationToken); } @@ -232,6 +244,7 @@ public Task> GetDirectoriesAsync(string virtualPath = "" var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return adapter.GetDirectoriesAsync(virtualPath, cancellationToken); } @@ -270,6 +283,7 @@ public async Task FileExistsAsync(string virtualPath, CancellationToken ca var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return await adapter.FileExistsAsync(virtualPath, cancellationToken); } @@ -308,6 +322,7 @@ public async Task DirectoryExistsAsync(string virtualPath, CancellationTok var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return await adapter.DirectoryExistsAsync(virtualPath, cancellationToken); } @@ -346,6 +361,7 @@ public async Task CreateDirectoryAsync(string virtualPath, CancellationToken can var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.CreateDirectoryAsync(virtualPath, cancellationToken); } @@ -384,6 +400,7 @@ public async Task DeleteFileAsync(string virtualPath, CancellationToken cancella var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.DeleteFileAsync(virtualPath, cancellationToken); } @@ -422,6 +439,7 @@ public async Task DeleteDirectoryAsync(string virtualPath, CancellationToken can var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.DeleteDirectoryAsync(virtualPath, cancellationToken); } @@ -445,6 +463,7 @@ public async Task ReadFileStreamAsync(string virtualPath, CancellationTo var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return await adapter.ReadFileStreamAsync(virtualPath, cancellationToken); } @@ -485,6 +504,7 @@ public async Task ReadFileAsync(string virtualPath, CancellationToken ca var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return await adapter.ReadFileAsync(virtualPath, cancellationToken); } @@ -525,6 +545,7 @@ public async Task ReadTextFileAsync(string virtualPath, CancellationToke var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); return await adapter.ReadTextFileAsync(virtualPath, cancellationToken); } @@ -572,7 +593,10 @@ public async Task CopyFileAsync(string virtualSourcePath, string virtualDestinat var destinationAdapter = GetAdapter(destinationPrefix); sourceAdapter.Connect(); + sourceAdapter.ClearCache(); + destinationAdapter.Connect(); + sourceAdapter.ClearCache(); var contents = await sourceAdapter.ReadFileAsync(virtualSourcePath, cancellationToken); await destinationAdapter.WriteFileAsync(virtualDestinationPath, contents, overwrite, cancellationToken); @@ -621,7 +645,10 @@ public async Task MoveFileAsync(string virtualSourcePath, string virtualDestinat var destinationAdapter = GetAdapter(destinationPrefix); sourceAdapter.Connect(); + sourceAdapter.ClearCache(); + destinationAdapter.Connect(); + sourceAdapter.ClearCache(); var contents = await sourceAdapter.ReadFileAsync(virtualSourcePath, cancellationToken); await destinationAdapter.WriteFileAsync(virtualDestinationPath, contents, overwrite, cancellationToken); @@ -648,6 +675,7 @@ public async Task WriteFileAsync(string virtualPath, Stream contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -690,6 +718,7 @@ public async Task WriteFileAsync(string virtualPath, byte[] contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -732,6 +761,7 @@ public async Task WriteFileAsync(string virtualPath, string contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -755,6 +785,7 @@ public async Task AppendFileAsync(string virtualPath, Stream contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } @@ -795,6 +826,7 @@ public async Task AppendFileAsync(string virtualPath, byte[] contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } @@ -835,6 +867,7 @@ public async Task AppendFileAsync(string virtualPath, string contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); + adapter.ClearCache(); await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } diff --git a/FileSystem/src/IFileSystem.cs b/FileSystem/src/IFileSystem.cs index a875272..15a57d9 100644 --- a/FileSystem/src/IFileSystem.cs +++ b/FileSystem/src/IFileSystem.cs @@ -28,7 +28,7 @@ public interface IFileSystem public IAdapter GetAdapter(string prefix); /// - /// Return a file. + /// Returns a file. /// /// The virtual path (including prefix) to the file. /// The file. @@ -43,7 +43,7 @@ public interface IFileSystem public IFile GetFile(string virtualPath); /// - /// Return a file. + /// Returns a file. /// /// The virtual path (including prefix) to the file. /// Optional to propagate notifications that the operation should be cancelled. diff --git a/FileSystem/src/Utilities/ContentTypeProvider.cs b/FileSystem/src/Utilities/ContentTypeProvider.cs new file mode 100644 index 0000000..e94cc58 --- /dev/null +++ b/FileSystem/src/Utilities/ContentTypeProvider.cs @@ -0,0 +1,12 @@ +using MimeTypes; + +namespace SharpGrip.FileSystem.Utilities +{ + public static class ContentTypeProvider + { + public static string GetContentType(string path) + { + return MimeTypeMap.GetMimeType(path); + } + } +} \ No newline at end of file diff --git a/FileSystem/src/Utilities/PathUtilities.cs b/FileSystem/src/Utilities/PathUtilities.cs index b6cf6f3..e85673d 100644 --- a/FileSystem/src/Utilities/PathUtilities.cs +++ b/FileSystem/src/Utilities/PathUtilities.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using SharpGrip.FileSystem.Exceptions; using SharpGrip.FileSystem.Extensions; @@ -8,6 +9,7 @@ public static class PathUtilities { private const string AdapterPrefixSeparator = "://"; private const string PathSeparator = "/"; + private const string InvalidPathSeparator = "\\"; /// /// Returns the prefix from a prefixed path. @@ -19,6 +21,21 @@ public static string GetPrefix(string virtualPath) return ResolvePrefixAndPath(virtualPath)[0]; } + /// + /// Normalizes the adapter's root path. + /// + /// The adapter's root path. + /// The path. + public static string NormalizeRootPath(string rootPath) + { + if (string.IsNullOrWhiteSpace(rootPath) || rootPath == PathSeparator) + { + return PathSeparator; + } + + return rootPath.Replace(InvalidPathSeparator, PathSeparator).RemoveTrailingForwardSlash(); + } + /// /// Returns the path from a prefixed path. /// @@ -27,12 +44,19 @@ public static string GetPrefix(string virtualPath) /// The path. public static string GetPath(string virtualPath, string rootPath) { - if (string.IsNullOrWhiteSpace(rootPath)) + if (rootPath == PathSeparator) { - return string.Join(PathSeparator, ResolvePrefixAndPath(virtualPath)[1]); + rootPath = ""; } - return string.Join(PathSeparator, rootPath, ResolvePrefixAndPath(virtualPath)[1]); + var prefixAndPath = ResolvePrefixAndPath(virtualPath); + + if (prefixAndPath.Length == 1) + { + return string.Join(PathSeparator, rootPath); + } + + return string.Join(PathSeparator, rootPath, prefixAndPath[1]); } /// @@ -49,19 +73,58 @@ public static string GetVirtualPath(string path, string prefix, string rootPath) throw new InvalidPathException(path); } - path = path.Replace('\\', '/'); + path = path.Replace(InvalidPathSeparator, PathSeparator); if (!rootPath.IsNullOrEmpty() && rootPath != "/") { path = path.Replace(rootPath, ""); } - if (path.StartsWith("/")) + path = path.RemoveLeadingForwardSlash(); + + return string.Join(AdapterPrefixSeparator, prefix, path); + } + + /// + /// Returns the parent path part from a path. + /// + /// The prefixed path. + /// The parent path part. + public static string GetParentPathPart(string path) + { + if (path.IsNullOrEmpty()) { - path = path.Substring(1); + path = "/"; } - return string.Join(AdapterPrefixSeparator, prefix, path); + var pathParts = GetPathParts(path); + + return string.Join("/", pathParts.Take(pathParts.Length - 1)); + } + + /// + /// Returns the last path part from a path. + /// + /// The path. + /// The last path part. + public static string GetLastPathPart(string path) + { + if (path.IsNullOrEmpty()) + { + return ""; + } + + return GetPathParts(path).Last(); + } + + /// + /// Returns the path parts from a path. + /// + /// The path. + /// The path parts. + public static string[] GetPathParts(string path) + { + return path.Split(new[] {PathSeparator}, StringSplitOptions.RemoveEmptyEntries); } /// @@ -76,7 +139,7 @@ private static string[] ResolvePrefixAndPath(string path) throw new PrefixNotFoundInPathException(path); } - return path.Split(new[] {AdapterPrefixSeparator}, StringSplitOptions.None); + return path.Split(new[] {AdapterPrefixSeparator}, StringSplitOptions.RemoveEmptyEntries); } } } \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 7264d21..e386134 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,9 +1,10 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 latest false + SharpGrip.FileSystem.Tests @@ -38,8 +39,9 @@ + - + \ No newline at end of file diff --git a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs index 4a37370..c4687dd 100644 --- a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs @@ -16,7 +16,7 @@ using DirectoryNotFoundException = SharpGrip.FileSystem.Exceptions.DirectoryNotFoundException; using FileNotFoundException = SharpGrip.FileSystem.Exceptions.FileNotFoundException; -namespace Tests.FileSystem.Adapters.AmazonS3 +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.AmazonS3 { public class AmazonS3AdapterTest : IAdapterTests { diff --git a/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs b/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs index 0e57584..a09c3a0 100644 --- a/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs @@ -4,7 +4,7 @@ using SharpGrip.FileSystem.Adapters.AzureBlobStorage; using Xunit; -namespace Tests.FileSystem.Adapters.AzureBlobStorage +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.AzureBlobStorage { public class AzureBlobStorageAdapterTest { diff --git a/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs b/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs index c8ed7c4..43701b0 100644 --- a/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs @@ -4,7 +4,7 @@ using SharpGrip.FileSystem.Adapters.AzureFileStorage; using Xunit; -namespace Tests.FileSystem.Adapters.AzureFileStorage +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.AzureFileStorage { public class AzureFileStorageAdapterTest { diff --git a/Tests/src/FileSystem.Adapters.Dropbox/DropboxAdapterTest.cs b/Tests/src/FileSystem.Adapters.Dropbox/DropboxAdapterTest.cs index 1bdb4b7..fbe51e5 100644 --- a/Tests/src/FileSystem.Adapters.Dropbox/DropboxAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.Dropbox/DropboxAdapterTest.cs @@ -1,4 +1,4 @@ -namespace Tests.FileSystem.Adapters.Dropbox +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.Dropbox { public class DropboxAdapterTest { diff --git a/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs b/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs new file mode 100644 index 0000000..2601477 --- /dev/null +++ b/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs @@ -0,0 +1,116 @@ +using System.Threading.Tasks; +using Google.Apis.Drive.v3; +using NSubstitute; +using SharpGrip.FileSystem.Adapters.GoogleDrive; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.GoogleDrive +{ + public class GoogleDriveAdapterTest : IAdapterTests + { + [Fact] + public void Test_Instantiation() + { + var googleDriveClient = Substitute.For(); + var googleDriveAdapter = new GoogleDriveAdapter("prefix", "/root-path", googleDriveClient); + + Assert.Equal("prefix", googleDriveAdapter.Prefix); + Assert.Equal("/root-path", googleDriveAdapter.RootPath); + } + + [Fact] + public Task Test_Connect() + { + var googleDriveClient = Substitute.For(); + var googleDriveAdapter = new GoogleDriveAdapter("prefix", "/root-path", googleDriveClient); + + googleDriveAdapter.Connect(); + + return Task.CompletedTask; + } + + [Fact] + public Task Test_Get_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Get_Directory_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Get_Files_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Get_Directories_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_File_Exists_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Directory_Exists_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Create_Directory_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Delete_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Delete_Directory_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Read_File_Stream_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Read_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Read_Text_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Write_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + + [Fact] + public Task Test_Append_File_Async() + { + return Task.FromResult(Task.CompletedTask); + } + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs b/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs new file mode 100644 index 0000000..ab57ce6 --- /dev/null +++ b/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs @@ -0,0 +1,17 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Http; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.GoogleDrive; + +public class TestHttpExecuteInterceptor : IHttpExecuteInterceptor +{ + public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Console.WriteLine(request.RequestUri); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs b/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs index 451b67d..bc8dcc1 100644 --- a/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs @@ -5,7 +5,7 @@ using SharpGrip.FileSystem.Adapters.MicrosoftOneDrive; using Xunit; -namespace Tests.FileSystem.Adapters.MicrosoftOneDrive +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.MicrosoftOneDrive { public class MicrosoftOneDriveAdapterTest { diff --git a/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs b/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs index ae30675..493386e 100644 --- a/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs @@ -4,7 +4,7 @@ using SharpGrip.FileSystem.Exceptions; using Xunit; -namespace Tests.FileSystem.Adapters.Sftp +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.Sftp { public class SftpAdapterTest { diff --git a/Tests/src/FileSystem/LocalAdapterTest.cs b/Tests/src/FileSystem/Adapters/LocalAdapterTest.cs similarity index 98% rename from Tests/src/FileSystem/LocalAdapterTest.cs rename to Tests/src/FileSystem/Adapters/LocalAdapterTest.cs index 8b3dd23..e344bea 100644 --- a/Tests/src/FileSystem/LocalAdapterTest.cs +++ b/Tests/src/FileSystem/Adapters/LocalAdapterTest.cs @@ -3,7 +3,7 @@ using SharpGrip.FileSystem.Exceptions; using Xunit; -namespace Tests.FileSystem +namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters { public class LocalAdapterTest : IAdapterTests { diff --git a/Tests/src/FileSystem/Extensions/StringExtensionsTest.cs b/Tests/src/FileSystem/Extensions/StringExtensionsTest.cs new file mode 100644 index 0000000..cd6df73 --- /dev/null +++ b/Tests/src/FileSystem/Extensions/StringExtensionsTest.cs @@ -0,0 +1,35 @@ +using SharpGrip.FileSystem.Extensions; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Extensions +{ + public class StringExtensionsTest + { + [Fact] + public void Test_IsNullOrEmpty() + { + Assert.True("".IsNullOrEmpty()); + } + + [Fact] + public void Test_RemoveLeadingForwardSlash() + { + Assert.Equal("test/", "/test/".RemoveLeadingForwardSlash()); + Assert.Equal("test/", "test/".RemoveLeadingForwardSlash()); + } + + [Fact] + public void Test_RemoveTrailingForwardSlash() + { + Assert.Equal("/test", "/test/".RemoveTrailingForwardSlash()); + Assert.Equal("/test", "/test".RemoveTrailingForwardSlash()); + } + + [Fact] + public void Test_EnsureTrailingForwardSlash() + { + Assert.Equal("/test/", "/test/".EnsureTrailingForwardSlash()); + Assert.Equal("/test/", "/test".EnsureTrailingForwardSlash()); + } + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem/FileSystemTest.cs b/Tests/src/FileSystem/FileSystemTest.cs index 6e7fb12..4f14438 100644 --- a/Tests/src/FileSystem/FileSystemTest.cs +++ b/Tests/src/FileSystem/FileSystemTest.cs @@ -3,7 +3,7 @@ using SharpGrip.FileSystem.Exceptions; using Xunit; -namespace Tests.FileSystem +namespace SharpGrip.FileSystem.Tests.FileSystem { public class FileSystemTest { diff --git a/Tests/src/FileSystem/Models/ModelFactoryTest.cs b/Tests/src/FileSystem/Models/ModelFactoryTest.cs new file mode 100644 index 0000000..3b94f53 --- /dev/null +++ b/Tests/src/FileSystem/Models/ModelFactoryTest.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using SharpGrip.FileSystem.Models; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Models +{ + public class ModelFactoryTest + { + [Fact] + public void Test_CreateFile() + { + var lastModifiedDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + var createdDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + + var file = new FileInfo("../../../var/data/test.txt") + { + LastWriteTime = lastModifiedDateTime, + CreationTime = createdDateTime + }; + + var fileModel = ModelFactory.CreateFile(file, "test://test.txt"); + + Assert.Equal("test.txt", fileModel.Name); + Assert.Contains(@"\var\data\test.txt", fileModel.Path); + Assert.Equal("test://test.txt", fileModel.VirtualPath); + Assert.Equal(lastModifiedDateTime, fileModel.LastModifiedDateTime); + Assert.Equal(createdDateTime, fileModel.CreatedDateTime); + Assert.Equal(0, fileModel.Length); + } + + [Fact] + public void Test_CreateDirectory() + { + var lastModifiedDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + var createdDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + + var directory = new DirectoryInfo("../../../var/data") + { + LastWriteTime = lastModifiedDateTime, + CreationTime = createdDateTime + }; + + var directoryModel = ModelFactory.CreateDirectory(directory, "test://data"); + + Assert.Equal("data", directoryModel.Name); + Assert.Contains(@"\var\data", directoryModel.Path); + Assert.Equal("test://data", directoryModel.VirtualPath); + Assert.Equal(lastModifiedDateTime, directoryModel.LastModifiedDateTime); + Assert.Equal(createdDateTime, directoryModel.CreatedDateTime); + } + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem/Utilities/ContentTypeProviderTest.cs b/Tests/src/FileSystem/Utilities/ContentTypeProviderTest.cs new file mode 100644 index 0000000..a125789 --- /dev/null +++ b/Tests/src/FileSystem/Utilities/ContentTypeProviderTest.cs @@ -0,0 +1,18 @@ +using SharpGrip.FileSystem.Utilities; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Utilities +{ + public class ContentTypeProviderTest + { + [Fact] + public void Test_GetContentType() + { + Assert.Equal("text/plain", ContentTypeProvider.GetContentType("text.txt")); + Assert.Equal("application/pdf", ContentTypeProvider.GetContentType("text.pdf")); + Assert.Equal("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ContentTypeProvider.GetContentType("text.docx")); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ContentTypeProvider.GetContentType("text.xlsx")); + Assert.Equal("application/vnd.openxmlformats-officedocument.presentationml.presentation", ContentTypeProvider.GetContentType("text.pptx")); + } + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem/Utilities/PathUtilitiesTest.cs b/Tests/src/FileSystem/Utilities/PathUtilitiesTest.cs new file mode 100644 index 0000000..18f5756 --- /dev/null +++ b/Tests/src/FileSystem/Utilities/PathUtilitiesTest.cs @@ -0,0 +1,69 @@ +using SharpGrip.FileSystem.Exceptions; +using SharpGrip.FileSystem.Utilities; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Utilities +{ + public class PathUtilitiesTest + { + [Fact] + public void Test_GetPrefix() + { + Assert.Equal("prefix", PathUtilities.GetPrefix("prefix://test.txt")); + Assert.Throws(() => PathUtilities.GetPrefix("test.txt")); + } + + [Fact] + public void Test_NormalizeRootPath() + { + Assert.Equal("/", PathUtilities.NormalizeRootPath("")); + Assert.Equal("/", PathUtilities.NormalizeRootPath("/")); + Assert.Equal("/test1/test2/test3", PathUtilities.NormalizeRootPath(@"\test1\test2\test3\")); + Assert.Equal("/test1/test2/test3", PathUtilities.NormalizeRootPath(@"\test1\test2\test3")); + } + + [Fact] + public void Test_GetPath() + { + Assert.Equal("/root-path/virtual-path", PathUtilities.GetPath("prefix://virtual-path", "/root-path")); + Assert.Equal("/root-path", PathUtilities.GetPath("prefix://", "/root-path")); + Assert.Equal("", PathUtilities.GetPath("prefix://", "")); + } + + [Fact] + public void Test_GetVirtualPath() + { + Assert.Equal("prefix://test.txt", PathUtilities.GetVirtualPath("root-path/test.txt", "prefix", "root-path")); + Assert.Equal("prefix://root-path/test.txt", PathUtilities.GetVirtualPath("root-path/test.txt", "prefix", "/")); + Assert.Equal("prefix://root-path/test.txt", PathUtilities.GetVirtualPath("root-path/test.txt", "prefix", "")); + Assert.Throws(() => PathUtilities.GetVirtualPath("test://test.txt", "prefix", "root-path")); + } + + [Fact] + public void Test_GetParentPathPart() + { + Assert.Equal("", PathUtilities.GetParentPathPart("")); + Assert.Equal("", PathUtilities.GetParentPathPart("/test-1")); + Assert.Equal("test-1", PathUtilities.GetParentPathPart("/test-1/test-2")); + Assert.Equal("test-1/test-2", PathUtilities.GetParentPathPart("/test-1/test-2/test-3")); + } + + [Fact] + public void Test_GetLastPathPart() + { + Assert.Equal("", PathUtilities.GetLastPathPart("")); + Assert.Equal("test-1", PathUtilities.GetLastPathPart("/test-1")); + Assert.Equal("test-2", PathUtilities.GetLastPathPart("/test-1/test-2")); + Assert.Equal("test-3", PathUtilities.GetLastPathPart("/test-1/test-2/test-3")); + } + + [Fact] + public void Test_GetPathParts() + { + Assert.Equal(new string[] { }, PathUtilities.GetPathParts("")); + Assert.Equal(new[] {"test-1"}, PathUtilities.GetPathParts("/test-1")); + Assert.Equal(new[] {"test-1", "test-2"}, PathUtilities.GetPathParts("/test-1/test-2")); + Assert.Equal(new[] {"test-1", "test-2", "test-3"}, PathUtilities.GetPathParts("/test-1/test-2/test-3")); + } + } +} \ No newline at end of file diff --git a/Tests/src/FileSystem/Utilities/StreamUtilitiesTest.cs b/Tests/src/FileSystem/Utilities/StreamUtilitiesTest.cs new file mode 100644 index 0000000..f88ef4f --- /dev/null +++ b/Tests/src/FileSystem/Utilities/StreamUtilitiesTest.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading.Tasks; +using SharpGrip.FileSystem.Utilities; +using Xunit; + +namespace SharpGrip.FileSystem.Tests.FileSystem.Utilities +{ + public class StreamUtilitiesTest + { + [Fact] + public async Task Test_CopyContentsToMemoryStreamAsync() + { + var memoryStream1 = new MemoryStream("test"u8.ToArray()); + var memoryStream2 = await StreamUtilities.CopyContentsToMemoryStreamAsync(memoryStream1); + + var memoryStreamContents1 = await new StreamReader(memoryStream1).ReadToEndAsync(); + var memoryStreamContents2 = await new StreamReader(memoryStream2).ReadToEndAsync(); + + Assert.Equal(memoryStreamContents1, memoryStreamContents2); + } + } +} \ No newline at end of file diff --git a/Tests/src/IAdapterTests.cs b/Tests/src/IAdapterTests.cs index 3704d4e..7e19179 100644 --- a/Tests/src/IAdapterTests.cs +++ b/Tests/src/IAdapterTests.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Tests; +namespace SharpGrip.FileSystem.Tests; public interface IAdapterTests { diff --git a/Tests/var/data/test.txt b/Tests/var/data/test.txt new file mode 100644 index 0000000..e69de29 diff --git a/scripts/publish.ps1 b/scripts/publish.ps1 deleted file mode 100644 index bcf7989..0000000 --- a/scripts/publish.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -$outputPath = ".nuget" - -New-Item -ItemType Directory -Force $outputPath - -dotnet pack -o $outputPath \ No newline at end of file From 4cd4086812ee630fb95d869614077a01a50a0c71 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Mon, 23 Oct 2023 22:00:17 -0400 Subject: [PATCH 2/5] make clear cache calls configurable --- FileSystem/src/FileSystem.cs | 123 ++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 22 deletions(-) diff --git a/FileSystem/src/FileSystem.cs b/FileSystem/src/FileSystem.cs index d4f3b68..19ee188 100644 --- a/FileSystem/src/FileSystem.cs +++ b/FileSystem/src/FileSystem.cs @@ -115,10 +115,9 @@ public Task GetFileAsync(string virtualPath, CancellationToken cancellati if (adapter.AdapterConfiguration.EnableCache) { + adapter.ClearCache(); } - adapter.ClearCache(); - return adapter.GetFileAsync(virtualPath, cancellationToken); } @@ -158,7 +157,11 @@ public Task GetDirectoryAsync(string virtualPath, CancellationToken var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return adapter.GetDirectoryAsync(virtualPath, cancellationToken); } @@ -244,7 +247,11 @@ public Task> GetDirectoriesAsync(string virtualPath = "" var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return adapter.GetDirectoriesAsync(virtualPath, cancellationToken); } @@ -283,7 +290,11 @@ public async Task FileExistsAsync(string virtualPath, CancellationToken ca var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return await adapter.FileExistsAsync(virtualPath, cancellationToken); } @@ -322,7 +333,11 @@ public async Task DirectoryExistsAsync(string virtualPath, CancellationTok var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return await adapter.DirectoryExistsAsync(virtualPath, cancellationToken); } @@ -361,7 +376,11 @@ public async Task CreateDirectoryAsync(string virtualPath, CancellationToken can var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.CreateDirectoryAsync(virtualPath, cancellationToken); } @@ -400,7 +419,11 @@ public async Task DeleteFileAsync(string virtualPath, CancellationToken cancella var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.DeleteFileAsync(virtualPath, cancellationToken); } @@ -439,7 +462,11 @@ public async Task DeleteDirectoryAsync(string virtualPath, CancellationToken can var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.DeleteDirectoryAsync(virtualPath, cancellationToken); } @@ -463,7 +490,11 @@ public async Task ReadFileStreamAsync(string virtualPath, CancellationTo var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return await adapter.ReadFileStreamAsync(virtualPath, cancellationToken); } @@ -504,7 +535,11 @@ public async Task ReadFileAsync(string virtualPath, CancellationToken ca var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return await adapter.ReadFileAsync(virtualPath, cancellationToken); } @@ -545,7 +580,11 @@ public async Task ReadTextFileAsync(string virtualPath, CancellationToke var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } return await adapter.ReadTextFileAsync(virtualPath, cancellationToken); } @@ -593,10 +632,18 @@ public async Task CopyFileAsync(string virtualSourcePath, string virtualDestinat var destinationAdapter = GetAdapter(destinationPrefix); sourceAdapter.Connect(); - sourceAdapter.ClearCache(); + + if (!sourceAdapter.AdapterConfiguration.EnableCache) + { + sourceAdapter.ClearCache(); + } destinationAdapter.Connect(); - sourceAdapter.ClearCache(); + + if (!destinationAdapter.AdapterConfiguration.EnableCache) + { + destinationAdapter.ClearCache(); + } var contents = await sourceAdapter.ReadFileAsync(virtualSourcePath, cancellationToken); await destinationAdapter.WriteFileAsync(virtualDestinationPath, contents, overwrite, cancellationToken); @@ -645,10 +692,18 @@ public async Task MoveFileAsync(string virtualSourcePath, string virtualDestinat var destinationAdapter = GetAdapter(destinationPrefix); sourceAdapter.Connect(); - sourceAdapter.ClearCache(); + + if (!sourceAdapter.AdapterConfiguration.EnableCache) + { + sourceAdapter.ClearCache(); + } destinationAdapter.Connect(); - sourceAdapter.ClearCache(); + + if (!destinationAdapter.AdapterConfiguration.EnableCache) + { + destinationAdapter.ClearCache(); + } var contents = await sourceAdapter.ReadFileAsync(virtualSourcePath, cancellationToken); await destinationAdapter.WriteFileAsync(virtualDestinationPath, contents, overwrite, cancellationToken); @@ -675,7 +730,11 @@ public async Task WriteFileAsync(string virtualPath, Stream contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -718,7 +777,11 @@ public async Task WriteFileAsync(string virtualPath, byte[] contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -761,7 +824,11 @@ public async Task WriteFileAsync(string virtualPath, string contents, bool overw var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.WriteFileAsync(virtualPath, contents, overwrite, cancellationToken); } @@ -785,7 +852,11 @@ public async Task AppendFileAsync(string virtualPath, Stream contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } @@ -826,7 +897,11 @@ public async Task AppendFileAsync(string virtualPath, byte[] contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } @@ -867,7 +942,11 @@ public async Task AppendFileAsync(string virtualPath, string contents, Cancellat var adapter = GetAdapter(prefix); adapter.Connect(); - adapter.ClearCache(); + + if (!adapter.AdapterConfiguration.EnableCache) + { + adapter.ClearCache(); + } await adapter.AppendFileAsync(virtualPath, contents, cancellationToken); } From a19fd2f626d6081ea19b870555c0ba604355719a Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Mon, 23 Oct 2023 22:25:32 -0400 Subject: [PATCH 3/5] code cleanup --- .../src/Exceptions/AdapterNotFoundException.cs | 2 +- FileSystem/src/Models/Model.cs | 4 ++-- .../AmazonS3AdapterTest.cs | 3 ++- .../TestHttpMessageHandler.cs | 17 ----------------- 4 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs diff --git a/FileSystem/src/Exceptions/AdapterNotFoundException.cs b/FileSystem/src/Exceptions/AdapterNotFoundException.cs index eca5aa2..191b035 100644 --- a/FileSystem/src/Exceptions/AdapterNotFoundException.cs +++ b/FileSystem/src/Exceptions/AdapterNotFoundException.cs @@ -18,7 +18,7 @@ public AdapterNotFoundException(string prefix, IList adapters) : base( private static string GetMessage(string prefix, IEnumerable adapters) { var adaptersString = string.Join("', '", adapters.Select(adapter => adapter.Prefix).ToArray()); - + return $"No adapter found with prefix '{prefix}'. Registered adapters are: '{adaptersString}'."; } } diff --git a/FileSystem/src/Models/Model.cs b/FileSystem/src/Models/Model.cs index e32e261..be74c8f 100644 --- a/FileSystem/src/Models/Model.cs +++ b/FileSystem/src/Models/Model.cs @@ -7,7 +7,7 @@ public abstract class Model public string Name { get; set; } = ""; public string Path { get; set; } = ""; public string VirtualPath { get; set; } = ""; - public DateTime? LastModifiedDateTime {get; set; } - public DateTime? CreatedDateTime {get; set; } + public DateTime? LastModifiedDateTime { get; set; } + public DateTime? CreatedDateTime { get; set; } } } \ No newline at end of file diff --git a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs index c4687dd..12fdf4d 100644 --- a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Text; using System.Threading.Tasks; using Amazon.Runtime; using Amazon.S3; @@ -428,7 +429,7 @@ public async Task Test_Read_File_Async() var fileContents = await fileSystem.ReadFileAsync("prefix-1://test1.txt"); - Assert.Equal("test1", System.Text.Encoding.UTF8.GetString(fileContents)); + Assert.Equal("test1", Encoding.UTF8.GetString(fileContents)); await Assert.ThrowsAsync(() => fileSystem.ReadFileAsync("prefix-1://test2.txt")); await Assert.ThrowsAsync(() => fileSystem.ReadFileAsync("prefix-1://test3.txt")); await Assert.ThrowsAsync(() => fileSystem.ReadFileAsync("prefix-1://test4.txt")); diff --git a/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs b/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs deleted file mode 100644 index ab57ce6..0000000 --- a/Tests/src/FileSystem.Adapters.GoogleDrive/TestHttpMessageHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Google.Apis.Http; - -namespace SharpGrip.FileSystem.Tests.FileSystem.Adapters.GoogleDrive; - -public class TestHttpExecuteInterceptor : IHttpExecuteInterceptor -{ - public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - Console.WriteLine(request.RequestUri); - - return Task.CompletedTask; - } -} \ No newline at end of file From 88b9c87dc355d2745b0b5fcc37c89c2b3e9637af Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Tue, 24 Oct 2023 09:20:16 -0400 Subject: [PATCH 4/5] change clean step name --- .github/workflows/Build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index f1e5bce..d262036 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -59,7 +59,7 @@ jobs: - name: Test solution run: dotnet test --no-build -c ${{ env.DOTNET_BUILD_CONFIGURATION }} --verbosity normal - - name: Cleanup solution + - name: Clean solution run: dotnet clean - name: Analyze solution From da4a45d75d5085463cb45fdc62264ccd300ff721 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Tue, 24 Oct 2023 17:41:38 -0400 Subject: [PATCH 5/5] inline method calls --- FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs index 87f0a9c..0e38d80 100644 --- a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs +++ b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs @@ -64,8 +64,7 @@ public override async Task GetFileAsync(string virtualPath, CancellationT public override async Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) { - var path = GetPath(virtualPath); - path = path.EnsureTrailingForwardSlash(); + var path = GetPath(virtualPath).EnsureTrailingForwardSlash(); try {