├── .nuke ├── src ├── key.snk ├── Files │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PathKind.cs │ ├── DeletionOption.cs │ ├── NameCollisionOption.cs │ ├── Files.csproj │ ├── IFileSystemElement.cs │ ├── CreationCollisionOption.cs │ ├── StorageFolderProperties.cs │ ├── StorageFileProperties.cs │ ├── Diagram.cd │ ├── KnownFolder.cs │ └── StorageElementProperties.cs ├── global.json ├── Files.Tests │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Mocks │ │ ├── StorageFileMocks.cs │ │ ├── StorageFolderMocks.cs │ │ ├── StoragePathMocks.cs │ │ └── FileSystemMocks.cs │ ├── Files.Tests.csproj │ ├── StorageFileTests.cs │ ├── StorageFolderTests.cs │ └── PathInformationTests.cs ├── Files.FileSystems.InMemory │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Files.FileSystems.InMemory.csproj │ ├── FsTree │ │ ├── FileNode.cs │ │ ├── FileContent.cs │ │ ├── FolderNode.cs │ │ ├── FileContentReadWriteTracker.cs │ │ ├── ElementNode.cs │ │ └── FsDataStorage.cs │ ├── DefaultKnownFolderProvider.cs │ ├── IKnownFolderProvider.cs │ ├── IInMemoryStoragePathProvider.cs │ ├── InMemoryFileSystemOptions.cs │ └── InMemoryFileSystem.cs ├── Files.FileSystems.Physical │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Utilities │ │ ├── ConversionExtensions.cs │ │ ├── FsHelper.cs │ │ └── FilePolyfills.cs │ ├── Files.FileSystems.Physical.csproj │ └── PhysicalFileSystem.cs ├── Files.Specification.Tests │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── FileSystemTestContext.cs │ ├── PathProvider.cs │ ├── Assertions │ │ ├── StorageElementAssertions.cs │ │ ├── StorageFileAssertions.cs │ │ └── StoragePathAssertions.cs │ ├── Stubs │ │ ├── StoragePathStub.cs │ │ └── FileSystemStub.cs │ ├── Files.Specification.Tests.csproj │ ├── Attributes │ │ └── DynamicInstanceDataAttribute.cs │ ├── Setup │ │ └── Default.cs │ └── FileSystemTestBase.cs ├── Files.FileSystems.InMemory.Tests │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Files.FileSystems.InMemory.Tests.csproj │ ├── DefaultInMemoryStoragePathTests.cs │ ├── InMemoryFileSystemTests.cs │ ├── InMemoryFileSystemTestContext.cs │ ├── InMemoryStorageFolderTests.cs │ └── InMemoryStorageFileTests.cs ├── Files.FileSystems.Physical.Tests │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PhysicalStorageFileTests.cs │ ├── PhysicalStorageFolderTests.cs │ ├── PhysicalFileSystemTestContext.cs │ ├── Files.FileSystems.Physical.Tests.csproj │ ├── PhysicalStoragePathTests.cs │ └── PhysicalFileSystemTests.cs ├── Files.FileSystems.WindowsStorage │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Utilities │ │ ├── WinStorageItemExtensions.cs │ │ ├── AsyncExtensions.cs │ │ ├── ConversionExtensions.cs │ │ ├── ExceptionConverter.cs │ │ └── FsHelper.cs │ ├── Files.FileSystems.WindowsStorage.csproj │ └── WindowsStorageFileSystem.cs ├── Files.FileSystems.WindowsStorage.Tests │ ├── Assets │ │ ├── StoreLogo.png │ │ ├── SplashScreen.scale-200.png │ │ ├── LockScreenLogo.scale-200.png │ │ ├── Square44x44Logo.scale-200.png │ │ ├── Wide310x150Logo.scale-200.png │ │ ├── Square150x150Logo.scale-200.png │ │ └── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── UnitTestApp.xaml │ ├── WindowsStorageStorageFileTests.cs │ ├── WindowsStorageStorageFolderTests.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── Default.rd.xml │ ├── WindowsStorageFileSystemTestContext.cs │ ├── UnitTestApp.xaml.cs │ ├── PhysicalStoragePathTests.cs │ ├── Package.appxmanifest │ ├── Files.FileSystems.WindowsStorage.Tests.GeneratedMSBuildEditorConfig.editorconfig │ └── WindowsStorageFileSystemTests.cs ├── nuget.config ├── Files.Shared │ ├── StringExtensions.cs │ ├── Files.Shared.projitems │ ├── Files.Shared.shproj │ └── EnumInfo.cs ├── Default.ruleset ├── Files.Shared.PhysicalStoragePath │ ├── Files.Shared.PhysicalStoragePath.projitems │ ├── Files.Shared.PhysicalStoragePath.shproj │ ├── Utilities │ │ ├── Platform.cs │ │ ├── PhysicalPathHelper.cs │ │ └── PhysicalPathHelper.CaseSensitivity.cs │ └── PhysicalStoragePath.cs ├── Tests.ruleset └── Directory.Build.props ├── assets ├── Icon.design ├── Icon128x128.png ├── Icon256x256.png └── Icon64x64.png ├── doc └── assets │ └── core-api-overview-class-diagram.png ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── documentation_issue.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── pull_request_template.md ├── .gitignore ├── LICENSE ├── CHANGELOG.md └── azure-pipelines.yml /.nuke: -------------------------------------------------------------------------------- 1 | src/Files.sln 2 | -------------------------------------------------------------------------------- /src/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/key.snk -------------------------------------------------------------------------------- /assets/Icon.design: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/assets/Icon.design -------------------------------------------------------------------------------- /assets/Icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/assets/Icon128x128.png -------------------------------------------------------------------------------- /assets/Icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/assets/Icon256x256.png -------------------------------------------------------------------------------- /assets/Icon64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/assets/Icon64x64.png -------------------------------------------------------------------------------- /src/Files/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSBuild.Sdk.Extras": "2.1.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Files.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(false)] 4 | -------------------------------------------------------------------------------- /doc/assets/core-api-overview-class-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/doc/assets/core-api-overview-class-diagram.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/StoreLogo.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelroemer/Files/HEAD/src/Files.FileSystems.WindowsStorage.Tests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask any kind of question. 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | # Question 10 | ## Your Question 11 | 12 | 13 | TODO. 14 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/UnitTestApp.xaml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/FileSystemTestContext.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public abstract class FileSystemTestContext 6 | { 7 | public abstract FileSystem FileSystem { get; } 8 | 9 | public abstract Task GetTestFolderAsync(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/PathProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests 2 | { 3 | /// 4 | /// Creates a new path which is built on the given base path. 5 | /// 6 | /// 7 | /// Another path which should be used as the parent path of the new path which will be returned. 8 | /// 9 | public delegate StoragePath PathProvider(StoragePath basePath); 10 | } 11 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/PhysicalStorageFileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Tests 2 | { 3 | using Files.Specification.Tests; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | [TestClass] 7 | public sealed class PhysicalStorageFileTests : StorageFileSpecificationTests 8 | { 9 | public PhysicalStorageFileTests() 10 | : base(PhysicalFileSystemTestContext.Instance) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Files.Shared/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Shared 2 | { 3 | internal static class StringExtensions 4 | { 5 | internal static string? ToNullIfEmpty(this string str) 6 | { 7 | return string.IsNullOrEmpty(str) ? null : str; 8 | } 9 | 10 | internal static bool Contains(this string str, char[] characters) 11 | { 12 | return str.IndexOfAny(characters) >= 0; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/PhysicalStorageFolderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Tests 2 | { 3 | using Files.Specification.Tests; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | [TestClass] 7 | public sealed class PhysicalStorageFolderTests : StorageFolderSpecificationTests 8 | { 9 | public PhysicalStorageFolderTests() 10 | : base(PhysicalFileSystemTestContext.Instance) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Files/PathKind.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | /// 4 | /// Defines whether a path is relative or absolute. 5 | /// 6 | public enum PathKind 7 | { 8 | /// 9 | /// The path is an absolute path. 10 | /// 11 | Absolute, 12 | 13 | /// 14 | /// The path is a relative path. 15 | /// 16 | Relative, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Issue 3 | about: A suggestion regarding the documentation, both problems and suggestions. 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | # Documentation Issue 10 | ## Description 11 | 12 | 13 | TODO. 14 | 15 | 16 | ## Discussion 17 | 18 | 19 | *None.* 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | # New Feature Proposal 10 | ## Description 11 | 12 | 13 | TODO. 14 | 15 | 16 | ## Discussion 17 | 18 | 19 | *None.* 20 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/WindowsStorageStorageFileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using Files.Specification.Tests; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | [TestClass] 7 | public sealed class WindowsStorageStorageFileTests : StorageFileSpecificationTests 8 | { 9 | public WindowsStorageStorageFileTests() 10 | : base(WindowsStorageFileSystemTestContext.Instance) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/WindowsStorageStorageFolderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using Files.Specification.Tests; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | [TestClass] 7 | public sealed class WindowsStorageStorageFolderTests : StorageFolderSpecificationTests 8 | { 9 | public WindowsStorageStorageFolderTests() 10 | : base(WindowsStorageFileSystemTestContext.Instance) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: Microsoft.NET.Test.Sdk 10 | versions: 11 | - "> 16.5.0" 12 | - dependency-name: Nuke.Common 13 | versions: 14 | - 5.1.0 15 | - dependency-name: MSTest.TestFramework 16 | versions: 17 | - 2.2.1 18 | - 2.2.2 19 | - dependency-name: MSTest.TestAdapter 20 | versions: 21 | - 2.2.1 22 | - 2.2.2 23 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Utilities/WinStorageItemExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Utilities 2 | { 3 | using System.IO; 4 | using Files.Shared; 5 | using Windows.Storage; 6 | 7 | internal static class WinStorageItemExtensions 8 | { 9 | internal static string GetPathOrThrow(this IStorageItem storageItem) 10 | { 11 | return storageItem.Path ?? throw new IOException( 12 | ExceptionStrings.WindowsStorageCompatibility.WindowsStorageElementHasNoPath() 13 | ); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Files.Tests/Mocks/StorageFileMocks.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests.Mocks 2 | { 3 | using Moq; 4 | using static Files.Tests.Mocks.FileSystemMocks; 5 | using static Files.Tests.Mocks.StoragePathMocks; 6 | 7 | public static class StorageFileMocks 8 | { 9 | public static Mock CreateOrdinalFileMock() => 10 | Create(CreateOrdinalFsMock().Object, CreateOrdinalPathMock().Object); 11 | 12 | public static Mock Create(FileSystem fileSystem, StoragePath path) 13 | { 14 | return new Mock(fileSystem, path) { CallBase = true }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Files.Tests/Mocks/StorageFolderMocks.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests.Mocks 2 | { 3 | using Moq; 4 | using static Files.Tests.Mocks.FileSystemMocks; 5 | using static Files.Tests.Mocks.StoragePathMocks; 6 | 7 | public static class StorageFolderMocks 8 | { 9 | public static Mock CreateOrdinalFolderMock() => 10 | Create(CreateOrdinalFsMock().Object, CreateOrdinalPathMock().Object); 11 | 12 | public static Mock Create(FileSystem fileSystem, StoragePath path) 13 | { 14 | return new Mock(fileSystem, path) { CallBase = true }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # New Pull Request 2 | ## Prerequisites 3 | 4 | 5 | - [ ] All new/updated public APIs are documented via XML comments. 6 | - [ ] `CHANGELOG.md` has been updated. 7 | 8 | 9 | ## Change Type 10 | 11 | - [ ] New Feature(s) 12 | - [ ] Bug Fix(es) 13 | - [ ] Documentation Update(s) 14 | 15 | 16 | ## Description 17 | 18 | 19 | TODO. 20 | 21 | 22 | ## Discussion 23 | 24 | 25 | *None.* 26 | -------------------------------------------------------------------------------- /src/Files.Tests/Files.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Files/DeletionOption.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | /// 4 | /// Defines different ways to react to a missing element during a deletion attempt. 5 | /// 6 | public enum DeletionOption 7 | { 8 | /// 9 | /// If the element does not exist (for example because it has already been deleted) 10 | /// the operation should throw an exception. 11 | /// 12 | Fail, 13 | 14 | /// 15 | /// If the element or one of its parent folders does not exist (for example because 16 | /// it has already been deleted) the operation should not fail, but exit without any errors. 17 | /// 18 | IgnoreMissing, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Files.Shared/Files.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | f03ebc81-80be-4b62-90a4-9368a4624d9f 7 | 8 | 9 | Files.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Assertions/StorageElementAssertions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Assertions 2 | { 3 | using System.Threading.Tasks; 4 | using Shouldly; 5 | 6 | public static class StorageElementAssertions 7 | { 8 | public static async Task ShouldExistAsync(this StorageElement element) 9 | { 10 | var exists = await element.ExistsAsync(); 11 | exists.ShouldBeTrue($"The element at {element.Path} should exist, but doesn't."); 12 | } 13 | 14 | public static async Task ShouldNotExistAsync(this StorageElement element) 15 | { 16 | var exists = await element.ExistsAsync(); 17 | exists.ShouldBeFalse($"The element at {element.Path} should not exist, but does."); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("Files.FileSystems.WindowsStorage.Tests")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("Files.FileSystems.WindowsStorage.Tests")] 10 | [assembly: AssemblyCopyright("Copyright © 2020")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | [assembly: AssemblyMetadata("TargetPlatform", "UAP")] 14 | 15 | // [assembly: AssemblyVersion("1.0.*")] 16 | [assembly: AssemblyVersion("1.0.0.0")] 17 | [assembly: AssemblyFileVersion("1.0.0.0")] 18 | [assembly: ComVisible(false)] 19 | [assembly: CLSCompliant(false)] 20 | -------------------------------------------------------------------------------- /src/Default.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Utilities/AsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Utilities 2 | { 3 | using System; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using Windows.Foundation; 7 | 8 | internal static class AsyncExtensions 9 | { 10 | internal static ConfiguredTaskAwaitable AsAwaitable( 11 | this IAsyncAction asyncAction, 12 | CancellationToken cancellationToken 13 | ) 14 | { 15 | return asyncAction.AsTask(cancellationToken).ConfigureAwait(false); 16 | } 17 | 18 | internal static ConfiguredTaskAwaitable AsAwaitable( 19 | this IAsyncOperation asyncOperation, 20 | CancellationToken cancellationToken 21 | ) 22 | { 23 | return asyncOperation.AsTask(cancellationToken).ConfigureAwait(false); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/Files.FileSystems.InMemory.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Files/NameCollisionOption.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | /// 4 | /// Defines different ways to react to name collisions during file and folder operations. 5 | /// 6 | public enum NameCollisionOption 7 | { 8 | /// 9 | /// The method should throw an exception if a file or folder already exists at the destination. 10 | /// 11 | Fail = CreationCollisionOption.Fail, 12 | 13 | /// 14 | /// Any existing file or folder at the destination should be replaced. 15 | /// 16 | /// Specifying this flag only replaces elements of the same type. 17 | /// If a folder already exists in the location where a file is supposed to be copied or moved, 18 | /// the operation will fail (and vice versa). 19 | /// 20 | ReplaceExisting = CreationCollisionOption.ReplaceExisting, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Visual Studio 7 | .vs/ 8 | Generated\ Files/ 9 | [Tt]est[Rr]esult*/ 10 | [Bb]uild[Ll]og.* 11 | 12 | # Visual Studio Code 13 | .vscode/* 14 | !.vscode/settings.json 15 | !.vscode/tasks.json 16 | !.vscode/launch.json 17 | !.vscode/extensions.json 18 | 19 | # JetBrains Rider and ReSharper 20 | .idea/ 21 | *.sln.iml 22 | *.DotSettings.user 23 | _ReSharper*/ 24 | *.[Rr]e[Ss]harper 25 | 26 | # .NET Core 27 | project.lock.json 28 | project.fragment.lock.json 29 | artifacts/ 30 | 31 | # NuGet 32 | *.nupkg 33 | **/[Pp]ackages/* 34 | !**/[Pp]ackages/build/ 35 | *.nuget.props 36 | *.nuget.targets 37 | 38 | # User-specific files 39 | *.rsuser 40 | *.suo 41 | *.user 42 | *.userosscache 43 | *.sln.docstates 44 | *.userprefs 45 | 46 | # Build results 47 | [Dd]ebug/ 48 | [Dd]ebugPublic/ 49 | [Rr]elease/ 50 | [Rr]eleases/ 51 | x64/ 52 | x86/ 53 | [Aa][Rr][Mm]/ 54 | [Aa][Rr][Mm]64/ 55 | bld/ 56 | [Bb]in/ 57 | [Oo]bj/ 58 | [Ll]og/ 59 | AppPackages 60 | BundleArtifacts 61 | 62 | # Temporary 63 | .tmp 64 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/Utilities/ConversionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Utilities 2 | { 3 | using System; 4 | using System.IO; 5 | using Files.Shared; 6 | 7 | internal static class ConversionExtensions 8 | { 9 | public static bool ToOverwriteBool(this NameCollisionOption options) => options switch 10 | { 11 | NameCollisionOption.Fail => false, 12 | NameCollisionOption.ReplaceExisting => true, 13 | _ => throw new NotSupportedException(ExceptionStrings.Enum.UnsupportedValue(options)), 14 | }; 15 | 16 | public static FileMode ToFileMode(this CreationCollisionOption options) => options switch 17 | { 18 | CreationCollisionOption.Fail => FileMode.CreateNew, 19 | CreationCollisionOption.ReplaceExisting => FileMode.Create, 20 | CreationCollisionOption.UseExisting => FileMode.OpenOrCreate, 21 | _ => throw new NotSupportedException(ExceptionStrings.Enum.UnsupportedValue(options)), 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/Files.Shared.PhysicalStoragePath.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | fd470c52-aebe-4967-8252-ebc1b586d6b2 7 | 8 | 9 | Files.Shared.PhysicalStoragePath 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Files/Files.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;netstandard2.1;net5.0 4 | 5 | A modern, immutable, async-first abstraction of hierarchical file systems with a consistent and developer friendly API that allows seamless switching between multiple underlying file system manifestations, while also fixing and hiding the flaws and inconsistencies of the wrapped APIs. 6 | 7 | This package provides the Core API of the Files libraries which defines the API contract and abstractions. In addition, it provides several extensions and conveniences which simplify working with the Core API. 8 | You should use the abstractions provided by this package for your implementations. Nontheless, you must also install a package which provides a concrete implementation of these file system abstractions. 9 | 10 | Learn more about Files at https://github.com/manuelroemer/Files. 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Files.Shared/Files.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f03ebc81-80be-4b62-90a4-9368a4624d9f 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Manuel Römer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/Files.FileSystems.InMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;netstandard2.1;net5.0 4 | 5 | A modern, immutable, async-first abstraction of hierarchical file systems with a consistent and developer friendly API that allows seamless switching between multiple underlying file system manifestations, while also fixing and hiding the flaws and inconsistencies of the wrapped APIs. 6 | 7 | This package provides an in-memory FileSystem implementation which is specifically designed for testing. The InMemoryFileSystem is completely independent of any real-world file system and can thus run in any testing configuration. In addition, each InMemoryFileSystem is isolated, leading to predictable testing states. 8 | 9 | Learn more about Files at https://github.com/manuelroemer/Files. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/WindowsStorageFileSystemTestContext.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using System.Threading.Tasks; 4 | using Files.Specification.Tests; 5 | using Files.Specification.Tests.Setup; 6 | 7 | public sealed class WindowsStorageFileSystemTestContext : FileSystemTestContext 8 | { 9 | public static WindowsStorageFileSystemTestContext Instance { get; } = 10 | new WindowsStorageFileSystemTestContext(); 11 | 12 | public override FileSystem FileSystem { get; } = new WindowsStorageFileSystem(); 13 | 14 | private WindowsStorageFileSystemTestContext() 15 | { 16 | Default.Setup(FileSystem); 17 | } 18 | 19 | public override async Task GetTestFolderAsync() 20 | { 21 | var folder = FileSystem 22 | .GetFolder(KnownFolder.TemporaryData) 23 | .GetFolder("TestFolder"); 24 | await folder.CreateAsync(recursive: true, CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); 25 | return folder; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/Files.Shared.PhysicalStoragePath.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fd470c52-aebe-4967-8252-ebc1b586d6b2 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/UnitTestApp.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using Microsoft.VisualStudio.TestPlatform.TestExecutor; 6 | using Windows.ApplicationModel.Activation; 7 | using Windows.UI.Xaml; 8 | using Windows.UI.Xaml.Controls; 9 | 10 | public sealed partial class App 11 | { 12 | public App() 13 | { 14 | InitializeComponent(); 15 | } 16 | 17 | protected override void OnLaunched(LaunchActivatedEventArgs e) 18 | { 19 | if (!(Window.Current.Content is Frame rootFrame)) 20 | { 21 | rootFrame = new Frame(); 22 | rootFrame.NavigationFailed += (sender, e) => 23 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName); 24 | Window.Current.Content = rootFrame; 25 | } 26 | 27 | DebugSettings.EnableFrameRateCounter = Debugger.IsAttached; 28 | UnitTestClient.CreateDefaultUI(); 29 | Window.Current.Activate(); 30 | UnitTestClient.Run(e.Arguments); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/PhysicalFileSystemTestContext.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Tests 2 | { 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Files; 6 | using Files.Specification.Tests; 7 | using Files.Specification.Tests.Setup; 8 | 9 | public sealed class PhysicalFileSystemTestContext : FileSystemTestContext 10 | { 11 | public static PhysicalFileSystemTestContext Instance { get; } = new PhysicalFileSystemTestContext(); 12 | 13 | public override FileSystem FileSystem => new PhysicalFileSystem(); 14 | 15 | private PhysicalFileSystemTestContext() 16 | { 17 | Default.Setup(FileSystem); 18 | } 19 | 20 | public override async Task GetTestFolderAsync() 21 | { 22 | var folder = FileSystem 23 | .GetFolder(KnownFolder.TemporaryData) 24 | .GetFolder(Assembly.GetExecutingAssembly().GetName().Name ?? "") 25 | .GetFolder("TestFolder"); 26 | await folder.CreateAsync(recursive: true, CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); 27 | return folder; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Stubs/StoragePathStub.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Stubs 2 | { 3 | using Files; 4 | 5 | /// 6 | /// A providing incredibly simplified, canned results. 7 | /// This is mainly introduced for testing foreign file system support. 8 | /// 9 | public sealed class StoragePathStub : StoragePath 10 | { 11 | // This class is mainly introduced for having a foreign file system member. 12 | // The properties/methods (apart from ToString()) should ideally never be used and do not 13 | // have to make any sense, as long as they return a valid object. 14 | 15 | public override PathKind Kind => PathKind.Absolute; 16 | public override StoragePath? Root => this; 17 | public override StoragePath? Parent => null; 18 | public override StoragePath FullPath => this; 19 | public override string Name => ToString(); 20 | public override string NameWithoutExtension => ToString(); 21 | public override string? Extension => ""; 22 | 23 | public StoragePathStub(FileSystem fileSystem, string path) 24 | : base(fileSystem, path) { } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Files.FileSystems.WindowsStorage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | uap10.0.16299 4 | $(DefineConstants);UAP 5 | 6 | A modern, immutable, async-first abstraction of hierarchical file systems with a consistent and developer friendly API that allows seamless switching between multiple underlying file system manifestations, while also fixing and hiding the flaws and inconsistencies of the wrapped APIs. 7 | 8 | This package provides a FileSystem implementation which interacts with the local machine's file system via UWP's Windows.Storage APIs. This implementation is designed for applications targeting the UWP which, due to sandbox restrictions, cannot use .NET's System.IO APIs. 9 | 10 | Learn more about Files at https://github.com/manuelroemer/Files. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/Files.FileSystems.Physical.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0; 5 | netcoreapp2.1; 6 | netcoreapp2.2; 7 | netcoreapp3.0; 8 | netcoreapp3.1; 9 | net5.0 10 | 11 | 12 | $(TargetFrameworks); 13 | net48 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Files/IFileSystemElement.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | /// 4 | /// Represents an element which is associated with an arbitrary file system. 5 | /// See remarks for details. 6 | /// 7 | /// 8 | /// The interface is implemented by every member 9 | /// which can be created by the class. 10 | /// The interface gives you access to the file system with which the implementing element 11 | /// is associated. This enables you to easily create other elements of the same file system 12 | /// implementation without direct access to the instance: 13 | /// 14 | /// 15 | /// StorageFile GetFileFromPath(StoragePath path) 16 | /// { 17 | /// FileSystem fs = path.FileSystem; 18 | /// return fs.GetFile(path); 19 | /// } 20 | /// 21 | /// 22 | public interface IFileSystemElement 23 | { 24 | /// 25 | /// Gets the file system with which this element is associated. 26 | /// 27 | FileSystem FileSystem { get; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/DefaultInMemoryStoragePathTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.Tests 2 | { 3 | using System.Collections.Generic; 4 | using Files.Specification.Tests; 5 | using Files.Specification.Tests.Setup; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | [TestClass] 9 | public sealed class DefaultInMemoryStoragePathTests : StoragePathSpecificationTests 10 | { 11 | public override IEnumerable AppendPathsWithInvalidPartsData => new[] 12 | { 13 | new[] { Default.PathName, "\0" }, 14 | }; 15 | 16 | public override IEnumerable CombinePathsWithInvalidOthersData => new[] 17 | { 18 | new[] { Default.PathName, "\0" }, 19 | }; 20 | 21 | public override IEnumerable JoinPathsWithInvalidOthersData => new[] 22 | { 23 | new[] { Default.PathName, "\0" }, 24 | }; 25 | 26 | public override IEnumerable LinkPathsWithInvalidOthersData => new[] 27 | { 28 | new[] { Default.PathName, "\0" }, 29 | }; 30 | 31 | public DefaultInMemoryStoragePathTests() 32 | : base(InMemoryFileSystemTestContext.Instance) { } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/PhysicalStoragePathTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using System.Collections.Generic; 4 | using Files.Specification.Tests; 5 | using Files.Specification.Tests.Setup; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | [TestClass] 9 | public sealed class PhysicalStoragePathTests : StoragePathSpecificationTests 10 | { 11 | public override IEnumerable AppendPathsWithInvalidPartsData => new[] 12 | { 13 | new[] { Default.PathName, "\0" }, 14 | }; 15 | 16 | public override IEnumerable CombinePathsWithInvalidOthersData => new[] 17 | { 18 | new[] { Default.PathName, "\0" }, 19 | }; 20 | 21 | public override IEnumerable JoinPathsWithInvalidOthersData => new[] 22 | { 23 | new[] { Default.PathName, "\0" }, 24 | }; 25 | 26 | public override IEnumerable LinkPathsWithInvalidOthersData => new[] 27 | { 28 | new[] { Default.PathName, "\0" }, 29 | }; 30 | 31 | public PhysicalStoragePathTests() 32 | : base(WindowsStorageFileSystemTestContext.Instance) { } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/Files.FileSystems.Physical.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0; 5 | netstandard2.1; 6 | netcoreapp2.0; 7 | netcoreapp2.1; 8 | netcoreapp2.2; 9 | netcoreapp3.0; 10 | net5.0 11 | 12 | 13 | A modern, immutable, async-first abstraction of hierarchical file systems with a consistent and developer friendly API that allows seamless switching between multiple underlying file system manifestations, while also fixing and hiding the flaws and inconsistencies of the wrapped APIs. 14 | 15 | This package provides a FileSystem implementation which interacts with the local machine's file system via the System.IO APIs. As such, it is the prefered way for interacting with a real-world file system. 16 | 17 | Learn more about Files at https://github.com/manuelroemer/Files. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Notify us about a problem. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | # Bug Report 10 | ## Environment / Version Details 11 | 12 | 13 | **Operating System (OS)**: 14 | - [ ] Windows 15 | - [ ] Win32 16 | - [ ] UWP 17 | - [ ] Linux 18 | - [ ] macOS 19 | 20 | **.NET Runtime**: 21 | - [ ] .NET Framework 22 | - [ ] .NET Core 23 | - [ ] .NET 24 | - [ ] Other (Please specify the details in the description) 25 | 26 | **Files Version**: 27 | - [ ] Newest Version 28 | - [ ] Older Version (Please specify the details in the description) 29 | 30 | 31 | ## Description 32 | 33 | 34 | TODO. 35 | 36 | 37 | ## Expected behavior 38 | 39 | 40 | TODO. 41 | 42 | 43 | ## Actual behavior 44 | 45 | 46 | TODO. 47 | 48 | 49 | ## Steps to Reproduce 50 | 51 | 52 | *None.* 53 | 54 | 55 | ## Discussion 56 | 57 | 58 | *None.* 59 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Files.Specification.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;netstandard2.1;net5.0 4 | false 5 | true 6 | 7 | A modern, immutable, async-first abstraction of hierarchical file systems with a consistent and developer friendly API that allows seamless switching between multiple underlying file system manifestations, while also fixing and hiding the flaws and inconsistencies of the wrapped APIs. 8 | 9 | This package is specifically targeted at developers who wish to implement their own FileSystem implementation. It provides a suite of MS Test test cases which assert that you correctly implemented the abstractions of the "Files" package. 10 | 11 | Learn more about Files at https://github.com/manuelroemer/Files. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Properties/Default.rd.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Stubs/FileSystemStub.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Stubs 2 | { 3 | using System; 4 | using System.IO; 5 | using Files; 6 | 7 | /// 8 | /// A providing incredibly simplified, canned results. 9 | /// This is mainly introduced for testing foreign file system support. 10 | /// 11 | public sealed class FileSystemStub : FileSystem 12 | { 13 | private static readonly PathInformation StubPathInformation = new PathInformation( 14 | Path.GetInvalidPathChars(), 15 | Path.GetInvalidFileNameChars(), 16 | '/', 17 | '/', 18 | '.', 19 | ':', 20 | ".", 21 | "..", 22 | StringComparison.Ordinal 23 | ); 24 | 25 | public FileSystemStub() 26 | : base(StubPathInformation) { } 27 | 28 | public override StorageFile GetFile(StoragePath path) => 29 | throw new NotImplementedException(); 30 | 31 | public override StorageFolder GetFolder(StoragePath path) => 32 | throw new NotImplementedException(); 33 | 34 | public override StoragePath GetPath(string path) => 35 | new StoragePathStub(this, path); 36 | 37 | public override StoragePath GetPath(KnownFolder knownFolder) => 38 | GetPath(knownFolder.ToString()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Tests.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Files/CreationCollisionOption.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | /// 4 | /// Defines different ways to react to name collisions during the creation 5 | /// of a new file or folder. 6 | /// 7 | public enum CreationCollisionOption 8 | { 9 | /// 10 | /// The method should throw an exception if a file or folder already exists in the same location. 11 | /// 12 | Fail, 13 | 14 | /// 15 | /// An existing file or folder should be replaced. 16 | /// 17 | /// Specifying this flag only replaces elements of the same type. 18 | /// If a folder already exists in the location where a file is supposed to be created 19 | /// the creation will fail (and vice versa). 20 | /// 21 | ReplaceExisting, 22 | 23 | /// 24 | /// An existing file or folder should be preserved and used instead of creating a new one. 25 | /// The operation will ignore the creation attempt and finish without modifying the 26 | /// file system. 27 | /// 28 | /// Specifying this flag only ignores elements of the same type. 29 | /// If a folder already exists in the location where a file is supposed to be created 30 | /// the creation will fail (and vice versa). 31 | /// 32 | UseExisting, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/InMemoryFileSystemTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Files.Specification.Tests; 7 | using Files.Specification.Tests.Setup; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | [TestClass] 11 | public sealed class InMemoryFileSystemTests : FileSystemSpecificationTests 12 | { 13 | public override IEnumerable KnownFolderData => 14 | Enum.GetValues(typeof(KnownFolder)) 15 | .Cast() 16 | .Select(knownFolder => new object[] 17 | { 18 | knownFolder, 19 | DefaultKnownFolderProvider.Default.GetPath( 20 | (InMemoryFileSystem)InMemoryFileSystemTestContext.Instance.FileSystem, 21 | knownFolder 22 | ) 23 | .ToString() 24 | }); 25 | 26 | public override IEnumerable ValidPathStringData => new[] 27 | { 28 | new object[] { Default.PathName }, 29 | }; 30 | 31 | public override IEnumerable InvalidPathStringData => new[] 32 | { 33 | new object[] { "" }, 34 | new object[] { "\0" }, 35 | new object[] { Default.PathName + "\0" }, 36 | }; 37 | 38 | public InMemoryFileSystemTests() 39 | : base(InMemoryFileSystemTestContext.Instance) { } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Files.Tests/Mocks/StoragePathMocks.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests.Mocks 2 | { 3 | using Moq; 4 | using static Files.Tests.Mocks.FileSystemMocks; 5 | 6 | public static class StoragePathMocks 7 | { 8 | public const string MockedPathString = "MockPathString"; 9 | public const string UpperMockedPathString = "MOCKPATHSTRING"; 10 | public const string LowerMockedPathString = "mockpathstring"; 11 | 12 | public static Mock CreateOrdinalPathMock() => 13 | Create(CreateOrdinalFsMock().Object, MockedPathString); 14 | 15 | public static Mock CreateOrdinalIgnoreCasePathMock() => 16 | Create(CreateOrdinalIgnoreCaseFsMock().Object, MockedPathString); 17 | 18 | public static Mock CreateOrdinalUpperPathMock() => 19 | Create(CreateOrdinalFsMock().Object, UpperMockedPathString); 20 | 21 | public static Mock CreateOrdinalLowerPathMock() => 22 | Create(CreateOrdinalFsMock().Object, LowerMockedPathString); 23 | 24 | public static Mock Create(FileSystem fileSystem, string path) 25 | { 26 | var pathMock = new Mock(fileSystem, path) { CallBase = true }; 27 | 28 | pathMock 29 | .SetupGet(x => x.FullPath) 30 | .Returns(() => pathMock.Object); 31 | 32 | pathMock 33 | .SetupGet(x => x.Parent) 34 | .Returns(() => pathMock.Object); 35 | 36 | return pathMock; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Files.Tests/StorageFileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests 2 | { 3 | using Files.Tests.Mocks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Shouldly; 6 | using static Files.Tests.Mocks.StorageFileMocks; 7 | using static Files.Tests.Mocks.StoragePathMocks; 8 | 9 | [TestClass] 10 | public class StorageFileTests 11 | { 12 | #region AsFile Tests 13 | 14 | [TestMethod] 15 | public void AsFile_ReturnsSameInstance() 16 | { 17 | var initialFile = CreateOrdinalFileMock().Object; 18 | var retrievedFile = initialFile.AsFile(); 19 | retrievedFile.ShouldBeSameAs(initialFile); 20 | } 21 | 22 | #endregion 23 | 24 | #region AsFolder Tests 25 | 26 | [TestMethod] 27 | public void AsFolder_ReturnsStorageFolderWithSamePathString() 28 | { 29 | var initialFile = CreateOrdinalFileMock().Object; 30 | var retrievedFolder = initialFile.AsFolder(); 31 | retrievedFolder.Path.ToString().ShouldBe(initialFile.Path.ToString()); 32 | } 33 | 34 | #endregion 35 | 36 | #region ToString Tests 37 | 38 | [TestMethod] 39 | public void ToString_ReturnsFullPathString() 40 | { 41 | var pathMock = CreateOrdinalPathMock(); 42 | var fileMock = StorageFileMocks.Create(pathMock.Object.FileSystem, pathMock.Object); 43 | var result = fileMock.Object.ToString(); 44 | result.ShouldBe(pathMock.Object.FullPath); 45 | } 46 | 47 | #endregion 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Files.Tests/StorageFolderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests 2 | { 3 | using Files.Tests.Mocks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Shouldly; 6 | using static Files.Tests.Mocks.StorageFolderMocks; 7 | using static Files.Tests.Mocks.StoragePathMocks; 8 | 9 | [TestClass] 10 | public class StorageFolderTests 11 | { 12 | #region AsFile Tests 13 | 14 | [TestMethod] 15 | public void AsFile_ReturnsStorageFileWithSamePathString() 16 | { 17 | var initialFolder = CreateOrdinalFolderMock().Object; 18 | var retrievedFile = initialFolder.AsFile(); 19 | retrievedFile.Path.ToString().ShouldBe(initialFolder.Path.ToString()); 20 | } 21 | 22 | #endregion 23 | 24 | #region AsFolder Tests 25 | 26 | [TestMethod] 27 | public void AsFolder_ReturnsSameInstance() 28 | { 29 | var initialFolder = CreateOrdinalFolderMock().Object; 30 | var retrievedFolder = initialFolder.AsFolder(); 31 | retrievedFolder.ShouldBeSameAs(initialFolder); 32 | } 33 | 34 | #endregion 35 | 36 | #region ToString Tests 37 | 38 | [TestMethod] 39 | public void ToString_ReturnsFullPathString() 40 | { 41 | var pathMock = CreateOrdinalPathMock(); 42 | var folderMock = StorageFolderMocks.Create(pathMock.Object.FileSystem, pathMock.Object); 43 | var result = folderMock.Object.ToString(); 44 | result.ShouldBe(pathMock.Object.FullPath); 45 | } 46 | 47 | #endregion 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Files/StorageFolderProperties.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | using System; 4 | 5 | /// 6 | /// Provides basic properties of a folder in a file system. 7 | /// 8 | public sealed class StorageFolderProperties : StorageElementProperties 9 | { 10 | // Keep this class even though there are currently no properties here. 11 | // This ensures that extension in the future is possible without having users of the library 12 | // replace 'FileSystemElementProperties' passages in their code. 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The real name of the element. 18 | /// The real name of the element without an extension. 19 | /// The real extension of the element. 20 | /// The point in time when the element has been created. 21 | /// The point in time when the element has been modified the last time. 22 | /// 23 | /// or is . 24 | /// 25 | public StorageFolderProperties( 26 | string name, 27 | string nameWithoutExtension, 28 | string? extension, 29 | DateTimeOffset createdOn, 30 | DateTimeOffset? modifiedOn 31 | ) : base(name, nameWithoutExtension, extension, createdOn, modifiedOn) { } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Files/StorageFileProperties.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | using System; 4 | 5 | /// 6 | /// Provides basic properties of a file in a file system. 7 | /// 8 | public sealed class StorageFileProperties : StorageElementProperties 9 | { 10 | /// 11 | /// Gets the file's size in bytes. 12 | /// 13 | public ulong Size { get; } 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The real name of the element. 19 | /// The real name of the element without an extension. 20 | /// The real extension of the element. 21 | /// The point in time when the element has been created. 22 | /// The point in time when the element has been modified the last time. 23 | /// The file's size in bytes. 24 | /// 25 | /// or is . 26 | /// 27 | public StorageFileProperties( 28 | string name, 29 | string nameWithoutExtension, 30 | string? extension, 31 | DateTimeOffset createdOn, 32 | DateTimeOffset? modifiedOn, 33 | ulong size 34 | ) : base(name, nameWithoutExtension, extension, createdOn, modifiedOn) 35 | { 36 | Size = size; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.0 4 | 5 | This release adds targets for .NET 5.0. Internally, C# 9.0 specific features have been added to the 6 | code base. 7 | The `StorageFile` has been extended with `CreateAndOpenAsync` overloads. 8 | 9 | ### Files 10 | 11 | * Added `net5.0` TFM. 12 | * Added `StorageFile.CreateAndOpenAsync` overloads which allow to (pseudo-)atomically create and 13 | open a file (atomicity depends on the corresponding `FileSystem` implementation). 14 | * Fixed `cancellationToken`s not being passed everywhere. 15 | * Added a package icon. 16 | 17 | ### Files.FileSystems.Physical 18 | 19 | * Added `net5.0` TFM. 20 | * Fixed `cancellationToken`s not being passed everywhere. 21 | * Added a package icon. 22 | 23 | ### Files.FileSystems.WindowsStorage 24 | 25 | * Fixed scenarios where a wrong exception type could be thrown (throw `IOException` by default when the 26 | Windows API returns a generic exception). 27 | * Added a package icon. 28 | 29 | ### Files.FileSystems.InMemory 30 | 31 | * Added `net5.0` TFM. 32 | * Added a package icon. 33 | 34 | ### Files.Specification.Tests 35 | 36 | * Added `net5.0` TFM. 37 | * Added a package icon. 38 | 39 | 40 | 41 | ## v0.1.0 42 | 43 | This is the initial release of the Files library, featuring the core abstractions, three 44 | implementations and a public test suite which can be used to test your Files implementation 45 | against the specification. 46 | 47 | ### Files 48 | 49 | ✨ Initial release. 50 | 51 | ### Files.FileSystems.Physical 52 | 53 | ✨ Initial release. 54 | 55 | ### Files.FileSystems.WindowsStorage 56 | 57 | ✨ Initial release. 58 | 59 | ### Files.FileSystems.InMemory 60 | 61 | ✨ Initial release. 62 | 63 | ### Files.Specification.Tests 64 | 65 | ✨ Initial release. 66 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/FileNode.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.FsTree 2 | { 3 | using System.IO; 4 | using Files; 5 | 6 | /// 7 | internal sealed class FileNode : ElementNode 8 | { 9 | public new FolderNode Parent => base.Parent!; 10 | 11 | public FileContent Content { get; private set; } 12 | 13 | private FileNode(FsDataStorage storage, StoragePath path, FolderNode parent) 14 | : base(storage, path, parent) 15 | { 16 | Content = new FileContent(ownerFileNode: this); 17 | Attributes = FileAttributes.Normal; 18 | } 19 | 20 | public static FileNode Create(FsDataStorage storage, StoragePath path) 21 | { 22 | var parentNode = storage.GetParentNodeAndRequirePathToHaveParent(path); 23 | var node = new FileNode(storage, path, parentNode); 24 | storage.RegisterNode(node); 25 | parentNode.RegisterChildNode(node); 26 | return node; 27 | } 28 | 29 | protected override void CopyImpl(StoragePath destinationPath) 30 | { 31 | var newNode = Create(Storage, destinationPath); 32 | newNode.Attributes = Attributes; 33 | newNode.CreatedOn = CreatedOn; 34 | newNode.ModifiedOn = ModifiedOn; 35 | newNode.Content = Content.Copy(newOwnerFileNode: newNode); 36 | } 37 | 38 | public override void EnsureNotLocked() 39 | { 40 | Content.ReadWriteTracker.EnsureCanReadWrite(); 41 | } 42 | 43 | protected override void DeleteSameNodeTypeAtPathIfExisting(StoragePath destinationPath) 44 | { 45 | Storage.TryGetFileNodeAndThrowOnConflictingFolder(destinationPath)?.Delete(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Assertions/StorageFileAssertions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Assertions 2 | { 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Shouldly; 6 | 7 | public static class StorageFileAssertions 8 | { 9 | public static async Task ShouldHaveContentAsync(this StorageFile file, string expectedContent, Encoding? encoding = null) 10 | { 11 | var content = await file.ReadTextAsync(encoding).ConfigureAwait(false); 12 | content.ShouldBe(expectedContent); 13 | } 14 | 15 | public static async Task ShouldNotHaveContentAsync(this StorageFile file, string unexpectedContent, Encoding? encoding = null) 16 | { 17 | var content = await file.ReadTextAsync(encoding).ConfigureAwait(false); 18 | content.ShouldNotBe(unexpectedContent); 19 | } 20 | 21 | public static async Task ShouldHaveContentAsync(this StorageFile file, byte[] expectedContent) 22 | { 23 | var content = await file.ReadBytesAsync(); 24 | content.ShouldBe(expectedContent); 25 | } 26 | 27 | public static async Task ShouldNotHaveContentAsync(this StorageFile file, byte[] unexpectedContent) 28 | { 29 | var content = await file.ReadBytesAsync(); 30 | content.ShouldNotBe(unexpectedContent); 31 | } 32 | 33 | public static async Task ShouldHaveEmptyContentAsync(this StorageFile file) 34 | { 35 | var content = await file.ReadBytesAsync(); 36 | content.ShouldBeEmpty(); 37 | } 38 | 39 | public static async Task ShouldNotHaveEmptyContentAsync(this StorageFile file) 40 | { 41 | var content = await file.ReadBytesAsync(); 42 | content.ShouldNotBeEmpty(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/InMemoryFileSystemTestContext.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.Tests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Files; 6 | using Files.Specification.Tests; 7 | using Files.Specification.Tests.Setup; 8 | 9 | public sealed class InMemoryFileSystemTestContext : FileSystemTestContext 10 | { 11 | public static InMemoryFileSystemTestContext Instance { get; } = new InMemoryFileSystemTestContext(); 12 | 13 | public override FileSystem FileSystem { get; } 14 | 15 | private InMemoryFileSystemTestContext() 16 | { 17 | // Since we want to test certain behaviors, e.g. that the DefaultInMemoryStoragePath validates 18 | // invalid characters, we require custom PathInformation. 19 | var testPathInformation = new PathInformation( 20 | invalidPathChars: new[] { '\0' }, 21 | invalidFileNameChars: new[] { '\0' }, 22 | '/', 23 | '\\', 24 | '.', 25 | '/', 26 | ".", 27 | "..", 28 | StringComparison.OrdinalIgnoreCase 29 | ); 30 | 31 | var options = new InMemoryFileSystemOptions() 32 | { 33 | PathProvider = new DefaultInMemoryStoragePathProvider(testPathInformation), 34 | }; 35 | 36 | FileSystem = new InMemoryFileSystem(options); 37 | 38 | Default.Setup(FileSystem); 39 | } 40 | 41 | public override async Task GetTestFolderAsync() 42 | { 43 | var folder = FileSystem.GetFolder("TestFolder"); 44 | await folder.CreateAsync(recursive: true, CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); 45 | return folder; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/InMemoryStorageFolderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.Tests 2 | { 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Files.Specification.Tests; 6 | using Files.Specification.Tests.Assertions; 7 | using Files.Specification.Tests.Setup; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using Shouldly; 10 | 11 | [TestClass] 12 | public sealed class InMemoryStorageFolderTests : StorageFolderSpecificationTests 13 | { 14 | public InMemoryStorageFolderTests() 15 | : base(InMemoryFileSystemTestContext.Instance) { } 16 | 17 | #region Locked Folder Tests 18 | 19 | [TestMethod] 20 | [DataRow(FileAccess.Read)] 21 | [DataRow(FileAccess.Write)] 22 | [DataRow(FileAccess.ReadWrite)] 23 | public async Task MethodsMutatingFolderLocationOrContent_LockedNestedFile_ShouldThrowIOException(FileAccess fileAccess) 24 | { 25 | var folder = await TestFolder.SetupFolderAsync(Default.FolderName); 26 | var nestedFile = await folder.SetupFileAsync(Default.FileName); 27 | var dstParentFolder = await TestFolder.SetupFolderAsync(Default.DstParentFolderName); 28 | var dstFilePath = TestFolder.GetPath(Default.DstFileSegments); 29 | 30 | using (await nestedFile.OpenAsync(fileAccess)) 31 | { 32 | await Should.ThrowAsync(async () => await folder.CreateAsync(CreationCollisionOption.ReplaceExisting)); 33 | await Should.ThrowAsync(async () => await folder.DeleteAsync()); 34 | await Should.ThrowAsync(async () => await folder.CopyAsync(dstFilePath)); 35 | await Should.ThrowAsync(async () => await folder.MoveAsync(dstFilePath)); 36 | await folder.ShouldExistAsync(); 37 | } 38 | } 39 | 40 | #endregion 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | Files.FileSystems.WindowsStorage.Tests 16 | Files 17 | Assets\StoreLogo.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Files/Diagram.cd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ACAAAAIAAAAAAQAAAAAAwAAERAAAgACIAAAAAAAAAEA= 10 | StorageFile.cs 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | AAAAAISAAAAAAAIEAAAAAAAMAAAAgAABAAAQAYAAAIA= 20 | StorageElement.cs 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ACAAAAIAAAAAAAAAAAAAwSAEUAAAgAAAAAAEAAAAAAA= 31 | StorageFolder.cs 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | CCAAUACQAABAACwEwAQAQFRQQgAgAYAAAAABQAAAAiA= 41 | StoragePath.cs 42 | 43 | 44 | 45 | 46 | 47 | 48 | AAAAAAAAAAAAAAAAAAAAACAQAAACAAAQACAEAAAAAAA= 49 | FileSystem.cs 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/Files.FileSystems.WindowsStorage.Tests.GeneratedMSBuildEditorConfig.editorconfig: -------------------------------------------------------------------------------- 1 | is_global = true 2 | build_property.TargetFramework = 3 | build_property.TargetFramework = 4 | build_property.TargetFramework = 5 | build_property.TargetFramework = 6 | build_property.TargetFramework = 7 | build_property.TargetPlatformMinVersion = 10.0.16299.0 8 | build_property.TargetPlatformMinVersion = 10.0.16299.0 9 | build_property.TargetPlatformMinVersion = 10.0.16299.0 10 | build_property.TargetPlatformMinVersion = 10.0.16299.0 11 | build_property.TargetPlatformMinVersion = 10.0.16299.0 12 | build_property.UsingMicrosoftNETSdkWeb = 13 | build_property.UsingMicrosoftNETSdkWeb = 14 | build_property.UsingMicrosoftNETSdkWeb = 15 | build_property.UsingMicrosoftNETSdkWeb = 16 | build_property.UsingMicrosoftNETSdkWeb = 17 | build_property.ProjectTypeGuids = {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 18 | build_property.ProjectTypeGuids = {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | build_property.ProjectTypeGuids = {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 20 | build_property.ProjectTypeGuids = {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 21 | build_property.ProjectTypeGuids = {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 22 | build_property.PublishSingleFile = 23 | build_property.PublishSingleFile = 24 | build_property.PublishSingleFile = 25 | build_property.PublishSingleFile = 26 | build_property.PublishSingleFile = 27 | build_property.IncludeAllContentForSelfExtract = 28 | build_property.IncludeAllContentForSelfExtract = 29 | build_property.IncludeAllContentForSelfExtract = 30 | build_property.IncludeAllContentForSelfExtract = 31 | build_property.IncludeAllContentForSelfExtract = 32 | build_property._SupportedPlatformList = 33 | build_property._SupportedPlatformList = 34 | build_property._SupportedPlatformList = 35 | build_property._SupportedPlatformList = 36 | build_property._SupportedPlatformList = 37 | -------------------------------------------------------------------------------- /src/Files.Shared/EnumInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Shared 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | /// 10 | /// A rather hacky class which provides enum information to the guard functions. 11 | /// This class only works with the "default" enums, i.e. enums which are based on 12 | /// and, if flagged, have the and follow 13 | /// the typical flags number/value order. 14 | /// 15 | /// At the time of writing this, this is good enough for all cases in this library. 16 | /// If a need for a more sophisticated solution arises, we should most likely just 17 | /// use the Enums.NET library instead of this. 18 | /// 19 | internal static class EnumInfo 20 | { 21 | internal static bool IsDefined(T value) where T : struct, Enum, IConvertible 22 | { 23 | var intValue = value.ToInt32(CultureInfo.InvariantCulture); 24 | return EnumCache.IsFlagged 25 | ? (EnumCache.CompositeFlaggedValue & intValue) == intValue 26 | : Enum.IsDefined(typeof(T), value); 27 | } 28 | 29 | private static class EnumCache where T : struct, Enum, IConvertible 30 | { 31 | public static bool IsFlagged { get; } 32 | public static int CompositeFlaggedValue { get; } 33 | 34 | static EnumCache() 35 | { 36 | Debug.Assert( 37 | typeof(T).GetEnumUnderlyingType() == typeof(int), 38 | "Only use enum functionalities with enumerations based on Int32." 39 | ); 40 | 41 | IsFlagged = typeof(T).GetCustomAttribute() is not null; 42 | 43 | if (IsFlagged) 44 | { 45 | CompositeFlaggedValue = ((T[])Enum.GetValues(typeof(T))) 46 | .Select(x => x.ToInt32(CultureInfo.InvariantCulture)) 47 | .Aggregate((l, r) => l | r); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Utilities/ConversionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Utilities 2 | { 3 | using System; 4 | using WinCreationCollisionOption = Windows.Storage.CreationCollisionOption; 5 | using WinNameCollisionOption = Windows.Storage.NameCollisionOption; 6 | using WinFileAttributes = Windows.Storage.FileAttributes; 7 | using IOFileAttributes = System.IO.FileAttributes; 8 | using Files.Shared; 9 | 10 | internal static class ConversionExtensions 11 | { 12 | internal static WinCreationCollisionOption ToWinOptions(this CreationCollisionOption options) => options switch 13 | { 14 | CreationCollisionOption.Fail => WinCreationCollisionOption.FailIfExists, 15 | CreationCollisionOption.ReplaceExisting => WinCreationCollisionOption.ReplaceExisting, 16 | CreationCollisionOption.UseExisting => WinCreationCollisionOption.OpenIfExists, 17 | _ => throw new NotSupportedException(ExceptionStrings.Enum.UnsupportedValue(options)), 18 | }; 19 | 20 | internal static WinNameCollisionOption ToWinOptions(this NameCollisionOption options) => options switch 21 | { 22 | NameCollisionOption.Fail => WinNameCollisionOption.FailIfExists, 23 | NameCollisionOption.ReplaceExisting => WinNameCollisionOption.ReplaceExisting, 24 | _ => throw new NotSupportedException(ExceptionStrings.Enum.UnsupportedValue(options)), 25 | }; 26 | 27 | internal static IOFileAttributes ToIOFileAttributes(this WinFileAttributes fileAttributes) => fileAttributes switch 28 | { 29 | // Normal is the only attribute with a different value (0 vs. 128). 30 | WinFileAttributes.Normal => IOFileAttributes.Normal, 31 | _ => (IOFileAttributes)fileAttributes, 32 | }; 33 | 34 | internal static WinFileAttributes ToWinFileAttributes(this IOFileAttributes fileAttributes) => fileAttributes switch 35 | { 36 | // Normal is the only attribute with a different value (0 vs. 128). 37 | IOFileAttributes.Normal => WinFileAttributes.Normal, 38 | _ => (WinFileAttributes)fileAttributes, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/PhysicalStoragePathTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Tests 2 | { 3 | using System.Collections.Generic; 4 | using Files.Specification.Tests; 5 | using Files.Specification.Tests.Setup; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | [TestClass] 9 | public sealed class PhysicalStoragePathTests : StoragePathSpecificationTests 10 | { 11 | // Note on the test data: 12 | // 13 | // Ever since .NET Core 2.1, the BCL doesn't manually validate paths anymore. 14 | // Therefore, instead of ArgumentExceptions in the Path class, the BCL throws IOExceptions 15 | // when using an invalid path for file I/O. 16 | // Since manual path handling is incredibly error prone, we are not doing it in this library. 17 | // Instead, there's going to be a difference depending on the runtime. PhysicalStoragePath 18 | // will throw ArgumentExceptions on .NET FW/.NET Core <= 2.0, while any newer version will 19 | // throw delayed IOExceptions for invalid paths. 20 | // The specification is tuned to respect this. 21 | // 22 | // Since we must still provide data for invalid paths, e.g. for Append tests, we can abuse 23 | // the fact that the '\0' character continues to be illegal. 24 | // We can easily use it to enforce ArgumentExceptions in the PhysicalStoragePath constructor. 25 | 26 | public override IEnumerable AppendPathsWithInvalidPartsData => new[] 27 | { 28 | new[] { Default.PathName, "\0" }, 29 | }; 30 | 31 | public override IEnumerable CombinePathsWithInvalidOthersData => new[] 32 | { 33 | new[] { Default.PathName, "\0" }, 34 | }; 35 | 36 | public override IEnumerable JoinPathsWithInvalidOthersData => new[] 37 | { 38 | new[] { Default.PathName, "\0" }, 39 | }; 40 | 41 | public override IEnumerable LinkPathsWithInvalidOthersData => new[] 42 | { 43 | new[] { Default.PathName, "\0" }, 44 | }; 45 | 46 | public PhysicalStoragePathTests() 47 | : base(PhysicalFileSystemTestContext.Instance) { } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Files.Tests/Mocks/FileSystemMocks.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests.Mocks 2 | { 3 | using System; 4 | using Moq; 5 | 6 | public static class FileSystemMocks 7 | { 8 | public static readonly PathInformation PathInfoOrdinal = new PathInformation( 9 | invalidPathChars: Array.Empty(), 10 | invalidFileNameChars: Array.Empty(), 11 | directorySeparatorChar: '/', 12 | altDirectorySeparatorChar: '\\', 13 | extensionSeparatorChar: '.', 14 | volumeSeparatorChar: ':', 15 | currentDirectorySegment: ".", 16 | parentDirectorySegment: "..", 17 | defaultStringComparison: StringComparison.Ordinal 18 | ); 19 | 20 | public static readonly PathInformation PathInfoOrdinalIgnoreCase = new PathInformation( 21 | invalidPathChars: Array.Empty(), 22 | invalidFileNameChars: Array.Empty(), 23 | directorySeparatorChar: '/', 24 | altDirectorySeparatorChar: '\\', 25 | extensionSeparatorChar: '.', 26 | volumeSeparatorChar: ':', 27 | currentDirectorySegment: ".", 28 | parentDirectorySegment: "..", 29 | defaultStringComparison: StringComparison.OrdinalIgnoreCase 30 | ); 31 | 32 | public static Mock CreateOrdinalFsMock() => 33 | Create(PathInfoOrdinal); 34 | 35 | public static Mock CreateOrdinalIgnoreCaseFsMock() => 36 | Create(PathInfoOrdinalIgnoreCase); 37 | 38 | public static Mock Create(PathInformation pathInformation) 39 | { 40 | var fsMock = new Mock(pathInformation) { CallBase = true }; 41 | 42 | fsMock 43 | .Setup(x => x.GetPath(It.IsAny())) 44 | .Returns((string path) => StoragePathMocks.Create(fsMock.Object, path).Object); 45 | 46 | fsMock 47 | .Setup(x => x.GetFile(It.IsAny())) 48 | .Returns((StoragePath path) => StorageFileMocks.Create(fsMock.Object, path).Object); 49 | 50 | fsMock 51 | .Setup(x => x.GetFolder(It.IsAny())) 52 | .Returns((StoragePath path) => StorageFolderMocks.Create(fsMock.Object, path).Object); 53 | 54 | return fsMock; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9.0 4 | enable 5 | true 6 | AllEnabledByDefault 7 | 8 | Files 9 | 0.2.0 10 | Manuel Römer 11 | en 12 | git 13 | https://github.com/manuelroemer/Files 14 | https://github.com/manuelroemer/Files 15 | https://github.com/manuelroemer/Files/blob/master/CHANGELOG.md 16 | MIT 17 | true 18 | true 19 | true 20 | snupkg 21 | true 22 | Icon256x256.png 23 | 24 | true 25 | $(NoWarn);CS1591 26 | 27 | true 28 | ../key.snk 29 | 30 | ../Default.ruleset 31 | 32 | $(MSBuildProjectName.Contains('Test')) 33 | 34 | 35 | 36 | true 37 | 38 | 39 | 40 | true 41 | 42 | 43 | 44 | false 45 | ../Tests.ruleset 46 | $(NoWarn);CS1573 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage.Tests/WindowsStorageFileSystemTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Tests 2 | { 3 | using System.Collections.Generic; 4 | using Files.Specification.Tests; 5 | using Files.Specification.Tests.Setup; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using Windows.Storage; 8 | using static System.Environment; 9 | 10 | [TestClass] 11 | public sealed class WindowsStorageFileSystemTests : FileSystemSpecificationTests 12 | { 13 | public override IEnumerable KnownFolderData => new[] 14 | { 15 | new object[] { KnownFolder.TemporaryData, ApplicationData.Current.TemporaryFolder.Path }, 16 | new object[] { KnownFolder.RoamingApplicationData, ApplicationData.Current.RoamingFolder.Path }, 17 | new object[] { KnownFolder.LocalApplicationData, ApplicationData.Current.LocalFolder.Path }, 18 | new object[] { KnownFolder.ProgramData, GetFolderPath(SpecialFolder.CommonApplicationData) }, 19 | new object[] { KnownFolder.UserProfile, GetFolderPath(SpecialFolder.UserProfile) }, 20 | new object[] { KnownFolder.Desktop, GetFolderPath(SpecialFolder.Desktop) }, 21 | new object[] { KnownFolder.DocumentsLibrary, GetFolderPath(SpecialFolder.MyDocuments) }, 22 | new object[] { KnownFolder.PicturesLibrary, GetFolderPath(SpecialFolder.MyPictures) }, 23 | new object[] { KnownFolder.VideosLibrary, GetFolderPath(SpecialFolder.MyVideos) }, 24 | new object[] { KnownFolder.MusicLibrary, GetFolderPath(SpecialFolder.MyMusic) }, 25 | }; 26 | 27 | public override IEnumerable ValidPathStringData => new[] 28 | { 29 | new object[] { Default.PathName }, 30 | new object[] { PathInformation.CurrentDirectorySegment }, 31 | new object[] { PathInformation.ParentDirectorySegment }, 32 | new object[] { ApplicationData.Current.TemporaryFolder.Path }, // Any absolute path is fine. 33 | }; 34 | 35 | public override IEnumerable InvalidPathStringData => new[] 36 | { 37 | new object[] { "" }, 38 | new object[] { "\0" }, 39 | new object[] { Default.PathName + "\0" }, 40 | }; 41 | 42 | public WindowsStorageFileSystemTests() 43 | : base(WindowsStorageFileSystemTestContext.Instance) { } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Files/KnownFolder.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | using static System.Environment; 4 | 5 | /// 6 | /// Defines specific folders which are typically present in a file system. 7 | /// 8 | public enum KnownFolder 9 | { 10 | /// 11 | /// A folder in which temporary data can be stored. 12 | /// Temporary data may be erased at any point in time. 13 | /// 14 | TemporaryData = ushort.MaxValue, // "Random" value which is most likely not going to end up in SpecialFolder. 15 | 16 | /// 17 | /// A folder in which user-specific application data can be stored. 18 | /// This folder is intended for roaming application data which might be synchronized 19 | /// between multiple devices. 20 | /// 21 | RoamingApplicationData = SpecialFolder.ApplicationData, 22 | 23 | /// 24 | /// A folder in which user-specific application data can be stored. 25 | /// This folder is intended for application data which is only required on the local 26 | /// device. 27 | /// 28 | LocalApplicationData = SpecialFolder.LocalApplicationData, 29 | 30 | /// 31 | /// A folder in which global, i.e. non user-specific, application data can be stored. 32 | /// 33 | ProgramData = SpecialFolder.CommonApplicationData, 34 | 35 | /// 36 | /// The current user's profile folder. 37 | /// 38 | UserProfile = SpecialFolder.UserProfile, 39 | 40 | /// 41 | /// The folder in which the user's desktop data is located. 42 | /// 43 | Desktop = SpecialFolder.Desktop, 44 | 45 | /// 46 | /// The folder in which the user's documents are stored. 47 | /// 48 | DocumentsLibrary = SpecialFolder.MyDocuments, 49 | 50 | /// 51 | /// The folder in which the user's pictures are stored. 52 | /// 53 | PicturesLibrary = SpecialFolder.MyPictures, 54 | 55 | /// 56 | /// The folder in which the user's videos are stored. 57 | /// 58 | VideosLibrary = SpecialFolder.MyVideos, 59 | 60 | /// 61 | /// The folder in which the user's music is stored. 62 | /// 63 | MusicLibrary = SpecialFolder.MyMusic, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Assertions/StoragePathAssertions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Assertions 2 | { 3 | using Shouldly; 4 | 5 | public static class StoragePathAssertions 6 | { 7 | public static void ShouldBeWithNormalizedPathSeparators(this StoragePath? path, string? other) 8 | { 9 | var first = NormalizePathSeparators(path?.ToString(), path?.FileSystem.PathInformation); 10 | var second = NormalizePathSeparators(other, path?.FileSystem.PathInformation); 11 | first.ShouldBe(second); 12 | } 13 | 14 | public static void ShouldNotBeWithNormalizedPathSeparators(this StoragePath? path, string? other) 15 | { 16 | var first = NormalizePathSeparators(path?.ToString(), path?.FileSystem.PathInformation); 17 | var second = NormalizePathSeparators(other, path?.FileSystem.PathInformation); 18 | first.ShouldNotBe(second); 19 | } 20 | 21 | private static string? NormalizePathSeparators(string? str, PathInformation? pathInformation) 22 | { 23 | if (str is null || pathInformation is null) 24 | { 25 | return null; 26 | } 27 | return str.Replace(pathInformation.AltDirectorySeparatorChar, pathInformation.DirectorySeparatorChar); 28 | } 29 | 30 | public static void ShouldBeEffectivelyEqualTo(this StoragePath? path, StoragePath? other) 31 | { 32 | NormalizeFull(path).ShouldBe(NormalizeFull(other)); 33 | } 34 | 35 | public static void ShouldNotBeEffectivelyEqualTo(this StoragePath? path, StoragePath? other) 36 | { 37 | NormalizeFull(path).ShouldNotBe(NormalizeFull(other)); 38 | } 39 | 40 | private static StoragePath? NormalizeFull(StoragePath? path) 41 | { 42 | // We cannot really normalize the paths since there is no corresponding method. 43 | // The best we can do is to grab a full path and then remove any trailing separators. 44 | // This way, we should arrive at two equal paths in a lot of cases, at least. 45 | if (path is null) 46 | { 47 | return null; 48 | } 49 | 50 | var fullPath = path.FullPath; 51 | if (fullPath.TryTrimEndingDirectorySeparator(out var trimmedPath)) 52 | { 53 | return trimmedPath; 54 | } 55 | else 56 | { 57 | return fullPath; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/DefaultKnownFolderProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory 2 | { 3 | using System; 4 | using Files.Shared; 5 | 6 | /// 7 | /// The default implementation of the interface. 8 | /// 9 | /// This implementation stringifies the value and then returns 10 | /// a full path based on the string. 11 | /// 12 | public sealed class DefaultKnownFolderProvider : IKnownFolderProvider 13 | { 14 | /// 15 | /// Gets a default instance of the class. 16 | /// 17 | public static DefaultKnownFolderProvider Default { get; } = new DefaultKnownFolderProvider(); 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | public DefaultKnownFolderProvider() { } 23 | 24 | /// 25 | /// Converts the to a string and then calls the 26 | /// method with it. 27 | /// Then returns the full path of the result. 28 | /// 29 | /// 30 | /// An instance for which the 31 | /// should be located. 32 | /// 33 | /// 34 | /// The value to be located for the specified . 35 | /// 36 | /// 37 | /// A which locates the specified 38 | /// within the provided . 39 | /// 40 | /// 41 | /// is an invalid enumeration value. 42 | /// 43 | public StoragePath GetPath(InMemoryFileSystem fileSystem, KnownFolder knownFolder) 44 | { 45 | _ = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); 46 | 47 | if (!EnumInfo.IsDefined(knownFolder)) 48 | { 49 | throw new ArgumentException(ExceptionStrings.Enum.UndefinedValue(knownFolder), nameof(knownFolder)); 50 | } 51 | 52 | return fileSystem.GetPath(knownFolder.ToString()).FullPath; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical.Tests/PhysicalFileSystemTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Tests 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using Files.Specification.Tests; 6 | using Files.Specification.Tests.Setup; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using static System.Environment; 9 | using static System.Environment.SpecialFolder; 10 | 11 | [TestClass] 12 | public sealed class PhysicalFileSystemTests : FileSystemSpecificationTests 13 | { 14 | public override IEnumerable KnownFolderData => new[] 15 | { 16 | new object[] { KnownFolder.TemporaryData, Path.GetTempPath() }, 17 | new object[] { KnownFolder.RoamingApplicationData, GetFolderPath(ApplicationData, SpecialFolderOption.DoNotVerify) }, 18 | new object[] { KnownFolder.LocalApplicationData, GetFolderPath(LocalApplicationData, SpecialFolderOption.DoNotVerify) }, 19 | new object[] { KnownFolder.ProgramData, GetFolderPath(CommonApplicationData, SpecialFolderOption.DoNotVerify) }, 20 | new object[] { KnownFolder.UserProfile, GetFolderPath(UserProfile, SpecialFolderOption.DoNotVerify) }, 21 | new object[] { KnownFolder.Desktop, GetFolderPath(Desktop, SpecialFolderOption.DoNotVerify) }, 22 | new object[] { KnownFolder.DocumentsLibrary, GetFolderPath(MyDocuments, SpecialFolderOption.DoNotVerify) }, 23 | new object[] { KnownFolder.PicturesLibrary, GetFolderPath(MyPictures, SpecialFolderOption.DoNotVerify) }, 24 | new object[] { KnownFolder.VideosLibrary, GetFolderPath(MyVideos, SpecialFolderOption.DoNotVerify) }, 25 | new object[] { KnownFolder.MusicLibrary, GetFolderPath(MyMusic, SpecialFolderOption.DoNotVerify) }, 26 | }; 27 | 28 | public override IEnumerable ValidPathStringData => new[] 29 | { 30 | new object[] { Default.PathName }, 31 | new object[] { PathInformation.CurrentDirectorySegment }, 32 | new object[] { PathInformation.ParentDirectorySegment }, 33 | new object[] { Path.GetTempPath() }, // Any absolute path is fine. 34 | }; 35 | 36 | public override IEnumerable InvalidPathStringData => new[] 37 | { 38 | // See comment in PhysicalStoragePathTests for details on why '\0' is used specifically. 39 | new object[] { "" }, 40 | new object[] { "\0" }, 41 | new object[] { Default.PathName + "\0" }, 42 | }; 43 | 44 | public PhysicalFileSystemTests() 45 | : base(PhysicalFileSystemTestContext.Instance) { } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/Utilities/Platform.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Shared.PhysicalStoragePath.Utilities 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | /// 7 | /// Provides values indicating on which platform the application is currently running. 8 | /// 9 | /// To ensure that the information is as accurate as possible, this class uses multiple 10 | /// approaches for determining the platform. 11 | /// 12 | internal static class Platform 13 | { 14 | public static PlatformID? Current { get; } = GetCurrentPlatformID(); 15 | 16 | private static PlatformID? GetCurrentPlatformID() 17 | { 18 | return GetViaRuntimeInformation() 19 | ?? GetViaEnvironment() 20 | ?? null; 21 | 22 | static PlatformID? GetViaRuntimeInformation() 23 | { 24 | try 25 | { 26 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 27 | { 28 | return PlatformID.Win32NT; 29 | } 30 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 31 | { 32 | return PlatformID.Unix; 33 | } 34 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 35 | { 36 | return PlatformID.Unix; 37 | } 38 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("FREEBSD"))) 39 | { 40 | return PlatformID.Unix; 41 | } 42 | else 43 | { 44 | return null; 45 | } 46 | } 47 | catch 48 | { 49 | return null; 50 | } 51 | } 52 | 53 | static PlatformID? GetViaEnvironment() 54 | { 55 | try 56 | { 57 | // The switch might seem irrelevant, but we want to cover different cases not in .NET Standard. 58 | return Environment.OSVersion.Platform switch 59 | { 60 | PlatformID.Win32NT => PlatformID.Win32NT, 61 | PlatformID.Unix => PlatformID.Unix, 62 | _ => null, 63 | }; 64 | } 65 | catch 66 | { 67 | return null; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Attributes/DynamicInstanceDataAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Attributes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | 9 | /// 10 | /// Provides test data rows from an instance property or method of the test class, 11 | /// initialized via . 12 | /// 13 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 14 | public sealed class DynamicInstanceDataAttribute : Attribute, ITestDataSource 15 | { 16 | public string DynamicDataSourceName { get; } 17 | 18 | public DynamicDataSourceType DynamicDataSourceType { get; } 19 | 20 | public DynamicInstanceDataAttribute( 21 | string dynamicDataSourceName, 22 | DynamicDataSourceType dynamicDataSourceType = DynamicDataSourceType.Property 23 | ) 24 | { 25 | DynamicDataSourceName = dynamicDataSourceName; 26 | DynamicDataSourceType = dynamicDataSourceType; 27 | } 28 | 29 | public IEnumerable GetData(MethodInfo testMethodInfo) 30 | { 31 | var testClass = GetTestClassInstance(testMethodInfo); 32 | IEnumerable? result; 33 | 34 | if (DynamicDataSourceType == DynamicDataSourceType.Property) 35 | { 36 | var property = testClass.GetType().GetProperty(DynamicDataSourceName); 37 | result = (IEnumerable?)property?.GetValue(testClass); 38 | } 39 | else 40 | { 41 | var method = testClass.GetType().GetMethod(DynamicDataSourceName); 42 | result = (IEnumerable?)method?.Invoke(testClass, null); 43 | } 44 | 45 | return result ?? Enumerable.Empty(); 46 | } 47 | 48 | private static object GetTestClassInstance(MethodInfo testMethodInfo) 49 | { 50 | if (testMethodInfo.ReflectedType is null || 51 | Activator.CreateInstance(testMethodInfo.ReflectedType) is not object result) 52 | { 53 | throw new InvalidOperationException($"Creating an instance of type ${testMethodInfo.ReflectedType} failed."); 54 | } 55 | return result; 56 | } 57 | 58 | public string? GetDisplayName(MethodInfo methodInfo, object[] data) 59 | { 60 | if (data is not null) 61 | { 62 | return $"{methodInfo.Name}({string.Join(", ", data)})"; 63 | } 64 | return null; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/FileContent.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.FsTree 2 | { 3 | using System; 4 | using System.IO; 5 | using Files.Shared; 6 | 7 | internal sealed class FileContent 8 | { 9 | private readonly FileNode _ownerFileNode; 10 | private byte[] _content = Array.Empty(); 11 | 12 | public ulong Size => (ulong)_content.LongLength; 13 | 14 | public FileContentReadWriteTracker ReadWriteTracker { get; } = new FileContentReadWriteTracker(); 15 | 16 | public FileContent(FileNode ownerFileNode) 17 | { 18 | _ownerFileNode = ownerFileNode; 19 | } 20 | 21 | public FileContentStream Open(FileAccess fileAccess, bool replaceExistingContent) 22 | { 23 | switch (fileAccess) 24 | { 25 | case FileAccess.Read: 26 | ReadWriteTracker.AddReader(); 27 | break; 28 | case FileAccess.Write: 29 | ReadWriteTracker.AddWriter(); 30 | break; 31 | case FileAccess.ReadWrite: 32 | ReadWriteTracker.AddReaderWriter(); 33 | break; 34 | default: 35 | throw new NotSupportedException(ExceptionStrings.Enum.UnsupportedValue(fileAccess)); 36 | } 37 | 38 | var content = replaceExistingContent ? Array.Empty() : _content; 39 | var stream = new FileContentStream(content, fileAccess); 40 | stream.Disposed += Stream_Disposed; 41 | return stream; 42 | 43 | void Stream_Disposed(object? sender, EventArgs e) 44 | { 45 | stream.Disposed -= Stream_Disposed; 46 | 47 | switch (fileAccess) 48 | { 49 | case FileAccess.Read: 50 | ReadWriteTracker.CloseReader(); 51 | break; 52 | case FileAccess.Write: 53 | ReadWriteTracker.CloseWriter(); 54 | _content = stream.ToArray(); 55 | _ownerFileNode.SetModifiedToNow(); 56 | break; 57 | case FileAccess.ReadWrite: 58 | ReadWriteTracker.CloseReaderWriter(); 59 | _content = stream.ToArray(); 60 | _ownerFileNode.SetModifiedToNow(); 61 | break; 62 | } 63 | } 64 | } 65 | 66 | public FileContent Copy(FileNode newOwnerFileNode) 67 | { 68 | ReadWriteTracker.EnsureCanRead(); 69 | 70 | return new FileContent(newOwnerFileNode) 71 | { 72 | _content = (byte[])_content.Clone(), 73 | }; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/IKnownFolderProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory 2 | { 3 | using System; 4 | 5 | /// 6 | /// Represents a member which is able to return a that locates 7 | /// a specific value. 8 | /// 9 | /// Implementing this interface allows you to provide the to 10 | /// mapping which is used by an . 11 | /// 12 | public interface IKnownFolderProvider 13 | { 14 | /// 15 | /// Returns a which locates the specified 16 | /// within the provided . 17 | /// 18 | /// This method is called by the method. 19 | /// 20 | /// 21 | /// An instance for which the 22 | /// should be located. 23 | /// 24 | /// 25 | /// The value to be located for the specified . 26 | /// 27 | /// 28 | /// A which locates the specified 29 | /// within the provided . 30 | /// 31 | /// 32 | /// When implementing this method, make sure that you don't call the 33 | /// method as doing so will lead 34 | /// to a . 35 | /// You can, however, call the method. 36 | /// 37 | /// You must also ensure that you always use the specified 38 | /// to create the , because each 39 | /// is only compatible with those paths that have been created by itself. 40 | /// 41 | /// If you cannot provide a value for the parameter, you 42 | /// can throw one of the listed exceptions. 43 | /// 44 | /// 45 | /// is an invalid enumeration value. 46 | /// 47 | /// 48 | /// The requested folder is not supported by this file system implementation. 49 | /// 50 | StoragePath GetPath(InMemoryFileSystem fileSystem, KnownFolder knownFolder); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Files/StorageElementProperties.cs: -------------------------------------------------------------------------------- 1 | namespace Files 2 | { 3 | using System; 4 | 5 | /// 6 | /// Provides basic properties of a file or folder in a file system. 7 | /// 8 | /// 9 | /// 10 | public abstract class StorageElementProperties 11 | { 12 | /// 13 | /// Gets the real name of the element. 14 | /// 15 | public string Name { get; } 16 | 17 | /// 18 | /// Gets the real name of the element without an extension. 19 | /// 20 | public string NameWithoutExtension { get; } 21 | 22 | /// 23 | /// Gets the real extension of the element without the file system's extension separator. 24 | /// If the element doesn't have an extension this returns . 25 | /// 26 | public string? Extension { get; } 27 | 28 | /// 29 | /// Gets the point in time when the element has been created. 30 | /// 31 | public DateTimeOffset CreatedOn { get; } 32 | 33 | /// 34 | /// Gets the point in time when the element has been modified the last time. 35 | /// This may be if the element has not been modified. 36 | /// 37 | public DateTimeOffset? ModifiedOn { get; } 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// The real name of the element. 43 | /// The real name of the element without an extension. 44 | /// The real extension of the element. 45 | /// The point in time when the element has been created. 46 | /// The point in time when the element has been modified the last time. 47 | /// 48 | /// or is . 49 | /// 50 | private protected StorageElementProperties( 51 | string name, 52 | string nameWithoutExtension, 53 | string? extension, 54 | DateTimeOffset createdOn, 55 | DateTimeOffset? modifiedOn 56 | ) 57 | { 58 | Name = name ?? throw new ArgumentNullException(nameof(name)); 59 | NameWithoutExtension = nameWithoutExtension ?? throw new ArgumentNullException(nameof(nameWithoutExtension)); 60 | Extension = extension; 61 | CreatedOn = createdOn; 62 | ModifiedOn = modifiedOn; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/Utilities/PhysicalPathHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Shared.PhysicalStoragePath.Utilities 2 | { 3 | using System; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | /// 9 | /// Provides static utility members for interacting with physical path strings. 10 | /// 11 | internal static partial class PhysicalPathHelper 12 | { 13 | internal const char ExtensionSeparatorChar = '.'; 14 | internal const string CurrentDirectorySegment = "."; 15 | internal const string ParentDirectorySegment = ".."; 16 | internal static readonly char[] DirectorySeparatorChars = new[] 17 | { 18 | Path.DirectorySeparatorChar, 19 | Path.AltDirectorySeparatorChar, 20 | } 21 | .Distinct() 22 | .ToArray(); 23 | 24 | internal static readonly char[] InvalidNewNameCharacters = 25 | new[] 26 | { 27 | // Used for the newName parameter in methods like RenameAsync. 28 | // In essence, we want to avoid names like "foo/bar", i.e. relative paths. 29 | // We simply forbid directory separator chars in the name to achieve that. 30 | // Also forbid the volume separator, because it might also be a /. 31 | Path.DirectorySeparatorChar, 32 | Path.AltDirectorySeparatorChar, 33 | Path.VolumeSeparatorChar, 34 | } 35 | .Distinct() 36 | .ToArray(); 37 | 38 | internal static readonly PathInformation PhysicalPathInformation = new PathInformation( 39 | Path.GetInvalidPathChars(), 40 | Path.GetInvalidFileNameChars(), 41 | Path.DirectorySeparatorChar, 42 | Path.AltDirectorySeparatorChar, 43 | extensionSeparatorChar: ExtensionSeparatorChar, 44 | Path.VolumeSeparatorChar, 45 | currentDirectorySegment: CurrentDirectorySegment, 46 | parentDirectorySegment: ParentDirectorySegment, 47 | defaultStringComparison: StringComparison 48 | ); 49 | 50 | internal static string? GetExtensionWithoutTrailingExtensionSeparator(string? path) 51 | { 52 | return TrimTrailingExtensionSeparator(Path.GetExtension(path)); 53 | 54 | static string? TrimTrailingExtensionSeparator(string? extension) 55 | { 56 | if (string.IsNullOrEmpty(extension) || extension![0] != ExtensionSeparatorChar) 57 | { 58 | return extension; 59 | } 60 | return extension.Substring(1); 61 | } 62 | } 63 | 64 | internal static string GetTemporaryElementName() 65 | { 66 | var guidPart = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); 67 | return $"{guidPart}~tmp"; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/Utilities/FsHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Utilities 2 | { 3 | using System.IO; 4 | using System.Threading; 5 | using Files.Shared.PhysicalStoragePath.Utilities; 6 | 7 | internal static class FsHelper 8 | { 9 | public static void CopyDirectory(string source, string destination, CancellationToken cancellationToken = default) 10 | { 11 | cancellationToken.ThrowIfCancellationRequested(); 12 | 13 | var directories = Directory.GetDirectories(source); 14 | var files = Directory.GetFiles(source); 15 | 16 | Directory.CreateDirectory(destination); 17 | 18 | foreach (var dirSrc in directories) 19 | { 20 | cancellationToken.ThrowIfCancellationRequested(); 21 | var dirDest = PathPolyfills.Join(destination, Path.GetFileName(dirSrc)); 22 | CopyDirectory(dirSrc, dirDest, cancellationToken); 23 | } 24 | 25 | foreach (var fileSrc in files) 26 | { 27 | cancellationToken.ThrowIfCancellationRequested(); 28 | var fileDest = PathPolyfills.Join(destination, Path.GetFileName(fileSrc)); 29 | File.Copy(fileSrc, fileDest); 30 | } 31 | } 32 | 33 | public static string? GetRealFileName(string fullPath) 34 | { 35 | var parentPath = Path.GetDirectoryName(fullPath); 36 | if (parentPath is null) 37 | { 38 | // fullPath is a root. We cannot get a parent directory here from which we retrieve its children. 39 | return null; 40 | } 41 | 42 | var fileName = Path.GetFileName(fullPath); 43 | var realChildren = Directory.GetFiles(parentPath, searchPattern: fileName); 44 | 45 | if (realChildren.Length != 1) 46 | { 47 | // There is either no child with this name or too many to make a pick. 48 | return null; 49 | } 50 | else 51 | { 52 | return Path.GetFileName(realChildren[0]); 53 | } 54 | } 55 | 56 | public static string? GetRealDirectoryName(string fullPath) 57 | { 58 | var parentPath = Path.GetDirectoryName(fullPath); 59 | if (parentPath is null) 60 | { 61 | // fullPath is a root. We cannot get a parent directory here from which we retrieve its children. 62 | return null; 63 | } 64 | 65 | var directoryName = Path.GetFileName(fullPath); 66 | var realChildren = Directory.GetDirectories(parentPath, searchPattern: directoryName); 67 | 68 | if (realChildren.Length != 1) 69 | { 70 | // There is either no child with this name or too many to make a pick. 71 | return null; 72 | } 73 | else 74 | { 75 | return Path.GetFileName(realChildren[0]); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory.Tests/InMemoryStorageFileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.Tests 2 | { 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Files.Specification.Tests; 6 | using Files.Specification.Tests.Assertions; 7 | using Files.Specification.Tests.Setup; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using Shouldly; 10 | 11 | [TestClass] 12 | public sealed class InMemoryStorageFileTests : StorageFileSpecificationTests 13 | { 14 | public InMemoryStorageFileTests() 15 | : base(InMemoryFileSystemTestContext.Instance) { } 16 | 17 | #region OpenAsync Tests 18 | 19 | [TestMethod] 20 | [DataRow(FileAccess.Read, FileAccess.Read)] 21 | [DataRow(FileAccess.Read, FileAccess.Write)] 22 | [DataRow(FileAccess.Read, FileAccess.ReadWrite)] 23 | [DataRow(FileAccess.Write, FileAccess.Read)] 24 | [DataRow(FileAccess.Write, FileAccess.Write)] 25 | [DataRow(FileAccess.Write, FileAccess.ReadWrite)] 26 | [DataRow(FileAccess.ReadWrite, FileAccess.Read)] 27 | [DataRow(FileAccess.ReadWrite, FileAccess.Write)] 28 | [DataRow(FileAccess.ReadWrite, FileAccess.ReadWrite)] 29 | public async Task OpenAsync_ConflictingOpenRequests_ThrowsIOException(FileAccess firstFileAccess, FileAccess secondFileAccess) 30 | { 31 | var file = await TestFolder.SetupFileAsync(Default.FileName); 32 | using var stream1 = await file.OpenAsync(firstFileAccess); 33 | await Should.ThrowAsync(async () => await file.OpenAsync(secondFileAccess)); 34 | } 35 | 36 | #endregion 37 | 38 | #region Locked File Tests 39 | 40 | [TestMethod] 41 | [DataRow(FileAccess.Read)] 42 | [DataRow(FileAccess.Write)] 43 | [DataRow(FileAccess.ReadWrite)] 44 | public async Task MethodsMutatingFileLocationOrContent_LockedFile_ShouldThrowIOException(FileAccess fileAccess) 45 | { 46 | var file = await TestFolder.SetupFileAsync(Default.FileName); 47 | var dstParentFolder = await TestFolder.SetupFolderAsync(Default.DstParentFolderName); 48 | var dstFilePath = TestFolder.GetPath(Default.DstFileSegments); 49 | 50 | using (await file.OpenAsync(fileAccess)) 51 | { 52 | await Should.ThrowAsync(async () => await file.CreateAsync(CreationCollisionOption.ReplaceExisting)); 53 | await Should.ThrowAsync(async () => await file.DeleteAsync()); 54 | await Should.ThrowAsync(async () => await file.CopyAsync(dstFilePath)); 55 | await Should.ThrowAsync(async () => await file.MoveAsync(dstFilePath)); 56 | await Should.ThrowAsync(async () => await file.OpenAsync(FileAccess.Write)); 57 | await Should.ThrowAsync(async () => await file.WriteBytesAsync(Default.ByteContent)); 58 | await Should.ThrowAsync(async () => await file.WriteTextAsync(Default.TextContent)); 59 | await file.ShouldExistAsync(); 60 | } 61 | } 62 | 63 | #endregion 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | variables: 5 | buildConfiguration: 'Release' 6 | releaseBranch: 'refs/heads/master' 7 | nuGetArtifactName: 'NuGet Packages' 8 | CI: true 9 | 10 | jobs: 11 | - job: Build 12 | strategy: 13 | matrix: 14 | Windows: 15 | imageName: 'windows-2019' 16 | Linux: 17 | imageName: 'ubuntu-latest' 18 | macOS: 19 | imageName: 'macOS-latest' 20 | pool: 21 | vmImage: $(imageName) 22 | steps: 23 | - task: UseDotNet@2 24 | displayName: Install .NET Core 2.0 SDK 25 | inputs: 26 | version: '2.0.x' 27 | 28 | - task: UseDotNet@2 29 | displayName: Install .NET Core 2.1 SDK 30 | inputs: 31 | version: '2.1.x' 32 | 33 | - task: UseDotNet@2 34 | displayName: Install .NET Core 2.2 SDK 35 | inputs: 36 | version: '2.2.x' 37 | 38 | - task: UseDotNet@2 39 | displayName: Install .NET Core 3.0 SDK 40 | inputs: 41 | version: '3.0.x' 42 | 43 | - task: UseDotNet@2 44 | displayName: Install .NET Core 3.1 SDK 45 | inputs: 46 | version: '3.1.x' 47 | 48 | - task: UseDotNet@2 49 | displayName: Install .NET 5.0 SDK 50 | inputs: 51 | version: '5.0.x' 52 | 53 | - task: PowerShell@2 54 | displayName: Build / Test / Pack 55 | inputs: 56 | filePath: 'build/build.ps1' 57 | arguments: '--configuration $(buildConfiguration) --ArtifactsDirectory $(Build.ArtifactStagingDirectory) --no-logo' 58 | 59 | - task: PublishTestResults@2 60 | displayName: Publish Test Results 61 | inputs: 62 | testResultsFormat: 'VSTest' 63 | testResultsFiles: '**/*.trx' 64 | condition: always() 65 | 66 | # Only publish Windows artifacts since only Windows supports a full build. 67 | # The Publish job will not run if any OS fails building. 68 | - task: PublishBuildArtifacts@1 69 | displayName: Publish NuGet Package Artifacts (Windows) 70 | inputs: 71 | PathtoPublish: '$(Build.ArtifactStagingDirectory)' 72 | ArtifactName: '$(nuGetArtifactName)' 73 | condition: | 74 | and 75 | ( 76 | succeeded(), 77 | eq(variables['build.sourceBranch'], variables['releaseBranch']), 78 | eq(variables['system.pullrequest.isfork'], false), 79 | eq(variables['Agent.OS'], 'Windows_NT') 80 | ) 81 | 82 | - job: Publish 83 | dependsOn: Build 84 | condition: | 85 | and 86 | ( 87 | succeeded(), 88 | eq(variables['build.sourceBranch'], variables['releaseBranch']), 89 | eq(variables['system.pullrequest.isfork'], false) 90 | ) 91 | pool: 92 | vmImage: 'ubuntu-latest' 93 | steps: 94 | - task: DownloadBuildArtifacts@0 95 | displayName: Download NuGet Package Artifacts 96 | inputs: 97 | buildType: 'current' 98 | downloadType: 'single' 99 | artifactName: '$(nuGetArtifactName)' 100 | downloadPath: '$(System.ArtifactsDirectory)' 101 | - task: NuGetCommand@2 102 | displayName: Push to NuGet 103 | inputs: 104 | command: 'push' 105 | packagesToPush: '$(System.ArtifactsDirectory)/**/*.nupkg' 106 | nuGetFeedType: 'external' 107 | publishFeedCredentials: 'NuGet' 108 | allowPackageConflicts: true 109 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/IInMemoryStoragePathProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory 2 | { 3 | using System; 4 | 5 | /// 6 | /// Represents a member which is able to create instances 7 | /// of a single type on behalf of the . 8 | /// 9 | /// Implementing this interface allows you to provide the 10 | /// instances which are used by an , thereby enabling you 11 | /// to provide customized path behavior. 12 | /// 13 | public interface IInMemoryStoragePathProvider 14 | { 15 | /// 16 | /// Gets a instance which provides information about 17 | /// special path characteristics of the instances created by 18 | /// this provider. 19 | /// 20 | /// This property is returned by the 's 21 | /// property. 22 | /// 23 | PathInformation PathInformation { get; } 24 | 25 | /// 26 | /// Creates and returns a instance for the specified 27 | /// from a string. 28 | /// 29 | /// This method is called by the method. 30 | /// 31 | /// 32 | /// An instance for which the 33 | /// should be located. 34 | /// 35 | /// 36 | /// The string from which a new instance should be created. 37 | /// 38 | /// 39 | /// A new instance created from the specified string. 40 | /// 41 | /// 42 | /// When implementing this method, make sure that you don't call the 43 | /// or 44 | /// method as doing so will usually result in a . 45 | /// 46 | /// You must also ensure that you always use the specified 47 | /// to create the , because each 48 | /// is only compatible with those paths that have been created by itself. 49 | /// 50 | /// If you cannot create a from the specified , you 51 | /// can throw one of the listed exceptions. 52 | /// 53 | /// 54 | /// or is . 55 | /// 56 | /// 57 | /// is an empty string or has an otherwise invalid format. 58 | /// 59 | StoragePath GetPath(InMemoryFileSystem fileSystem, string path); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/FolderNode.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.FsTree 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using Files; 6 | 7 | /// 8 | internal sealed class FolderNode : ElementNode 9 | { 10 | // Keeps track of the children of this folder. This list is unordered. 11 | private readonly List _mutableChildren = new List(); 12 | 13 | public IReadOnlyCollection Children => _mutableChildren; 14 | 15 | private FolderNode(FsDataStorage storage, StoragePath path, FolderNode? parent) 16 | : base(storage, path, parent) 17 | { 18 | Attributes = FileAttributes.Directory | FileAttributes.Normal; 19 | } 20 | 21 | public static FolderNode Create(FsDataStorage storage, StoragePath path) 22 | { 23 | var parentNode = storage.GetParentNode(path); 24 | var node = new FolderNode(storage, path, parentNode); 25 | storage.RegisterNode(node); 26 | parentNode?.RegisterChildNode(node); 27 | return node; 28 | } 29 | 30 | public void RegisterChildNode(ElementNode node) 31 | { 32 | _mutableChildren.Add(node); 33 | } 34 | 35 | public void UnregisterChildNode(ElementNode node) 36 | { 37 | _mutableChildren.Remove(node); 38 | } 39 | 40 | public override void Move(StoragePath destinationPath, bool replaceExisting) 41 | { 42 | base.Move(destinationPath, replaceExisting); 43 | 44 | // Cannot use foreach because Move(..) mutates the list and thus invalidates the enumerator. 45 | // Because of this, we must also iterate from behind, because the order in the list changes here. 46 | for (var i = _mutableChildren.Count - 1; i >= 0; i--) 47 | { 48 | var child = _mutableChildren[i]; 49 | var newLocation = Path.FullPath.Join(child.Path.FullPath.Name); 50 | child.Move(newLocation, replaceExisting: false); 51 | } 52 | } 53 | 54 | protected override void CopyImpl(StoragePath destinationPath) 55 | { 56 | var newNode = Create(Storage, destinationPath); 57 | newNode.Attributes = Attributes; 58 | newNode.CreatedOn = CreatedOn; 59 | newNode.ModifiedOn = ModifiedOn; 60 | 61 | foreach (var child in _mutableChildren) 62 | { 63 | var childDestinationPath = destinationPath.FullPath.Join(child.Path.FullPath.Name); 64 | child.Copy(childDestinationPath, replaceExisting: false); 65 | } 66 | } 67 | 68 | public override void Delete() 69 | { 70 | // Because deleting a child can fail, it is essential that the children are deleted before 71 | // this folder node. 72 | // This ensures that a user can retry deleting if something goes wrong. 73 | 74 | while (_mutableChildren.Count > 0) 75 | { 76 | // Delete() automatically removes the element from the list, hence no foreach/additional logic. 77 | _mutableChildren[_mutableChildren.Count - 1].Delete(); 78 | } 79 | 80 | base.Delete(); 81 | } 82 | 83 | public override void EnsureNotLocked() 84 | { 85 | foreach (var child in _mutableChildren) 86 | { 87 | child.EnsureNotLocked(); 88 | } 89 | } 90 | 91 | protected override void DeleteSameNodeTypeAtPathIfExisting(StoragePath destinationPath) 92 | { 93 | Storage.TryGetFolderNodeAndThrowOnConflictingFile(destinationPath)?.Delete(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/Utilities/PhysicalPathHelper.CaseSensitivity.cs: -------------------------------------------------------------------------------- 1 | // Parts of this file are copied from the source files of the dotnet/runtime repository and adapted/enhanced. 2 | // The relevant file can be found at: 3 | // https://github.com/dotnet/runtime/blob/8cc28d7ee63ae6f07dd9a37b1b8a935be565076e/src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs 4 | // -or- 5 | // https://source.dot.net/#System.IO.FileSystem/Common/System/IO/PathInternal.CaseSensitivity.cs 6 | // 7 | // The code is licensed by the .NET Foundation with the following license header: 8 | // > Licensed to the .NET Foundation under one or more agreements. 9 | // > The .NET Foundation licenses this file to you under the MIT license. 10 | // > See the LICENSE file in the project root for more information. 11 | 12 | namespace Files.Shared.PhysicalStoragePath.Utilities 13 | { 14 | using System; 15 | using System.IO; 16 | 17 | // The .NET BCL doesn't provide us with a public API to determine whether the current FS is 18 | // case-sensitive or not. 19 | // Since we need that information in several parts of the library, we are using the same code 20 | // as the .NET Core team (see the top of this file, both for the source and for explanations). 21 | // 22 | // The code in this file should be updated if a better way/API ever becomes available. 23 | 24 | internal static partial class PhysicalPathHelper 25 | { 26 | internal static StringComparison StringComparison => IsCaseSensitive 27 | ? StringComparison.Ordinal 28 | : StringComparison.OrdinalIgnoreCase; 29 | 30 | #if UAP 31 | // For the WindowsStorage project. 32 | // Windows is always assumed to be case-insensitive. 33 | // Not doing the full check as below, since writing files is harder to do in a UWP sandbox. 34 | internal static bool IsCaseSensitive { get; } 35 | #else 36 | internal static bool IsCaseSensitive { get; } = GetIsCaseSensitive(); 37 | 38 | private static bool GetIsCaseSensitive() 39 | { 40 | // The original .NET code creates a file with upper-case letters and tries to access it 41 | // with a lower-case filename. If this fails with an exception, they simply assume false. 42 | // This seems hacky, but it works. Furthermore, since we are exactly mimicing the .NET 43 | // behavior, we shouldn't run into too many troubles. 44 | // We enhance that logic with a look at the current platform, since we can usually assume 45 | // that Windows is case-insensitive, while Unix platforms are not. 46 | return GetViaTemporaryFile() 47 | ?? GetViaPlatform() 48 | ?? false; 49 | 50 | static bool? GetViaTemporaryFile() 51 | { 52 | #pragma warning disable CA1305, CA1308 53 | try 54 | { 55 | var pathWithUpperCase = Path.Combine(Path.GetTempPath(), "CASESENSITIVETEST" + Guid.NewGuid().ToString("N")); 56 | using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose)) 57 | { 58 | var lowerCased = pathWithUpperCase.ToLowerInvariant(); 59 | return !File.Exists(lowerCased); 60 | } 61 | } 62 | catch 63 | { 64 | return null; 65 | } 66 | #pragma warning restore 67 | } 68 | 69 | static bool? GetViaPlatform() 70 | { 71 | return Platform.Current switch 72 | { 73 | PlatformID.Win32NT => false, 74 | PlatformID.Unix => true, 75 | _ => null, 76 | }; 77 | } 78 | } 79 | #endif 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/InMemoryFileSystemOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | /// 8 | /// Allows configuring specific parts and behaviors of an instance. 9 | /// 10 | public sealed class InMemoryFileSystemOptions 11 | { 12 | private IEqualityComparer _storagePathComparer = DefaultStoragePathEqualityComparer.Default; 13 | private IInMemoryStoragePathProvider _pathProvider = DefaultInMemoryStoragePathProvider.DefaultOrdinal; 14 | private IKnownFolderProvider _knownFolderProvider = DefaultKnownFolderProvider.Default; 15 | private Encoding _defaultEncoding = Encoding.UTF8; 16 | 17 | /// 18 | /// Initializes a new instance of the with a 19 | /// default set of values. 20 | /// 21 | public InMemoryFileSystemOptions() { } 22 | 23 | /// 24 | /// Gets or sets an equality comparer which is used by the 25 | /// to determine whether two instances locate the same file or folder. 26 | /// 27 | /// The default value is a instance. 28 | /// 29 | public IEqualityComparer StoragePathComparer 30 | { 31 | get => _storagePathComparer; 32 | set => _storagePathComparer = value ?? throw new ArgumentNullException(nameof(value)); 33 | } 34 | 35 | /// 36 | /// Gets or sets the which is used by the 37 | /// to create instances. 38 | /// 39 | /// The default value is a instance. 40 | /// 41 | public IInMemoryStoragePathProvider PathProvider 42 | { 43 | get => _pathProvider; 44 | set => _pathProvider = value ?? throw new ArgumentNullException(nameof(value)); 45 | } 46 | 47 | /// 48 | /// Gets or sets the which is used by the 49 | /// to create instances 50 | /// which locate values. 51 | /// 52 | /// The default value is a instance. 53 | /// 54 | /// 55 | public IKnownFolderProvider KnownFolderProvider 56 | { 57 | get => _knownFolderProvider; 58 | set => _knownFolderProvider = value ?? throw new ArgumentNullException(nameof(value)); 59 | } 60 | 61 | /// 62 | /// Gets or sets the default encoding which is used by the 63 | /// when reading/writing from/to files. 64 | /// 65 | /// The default value is . 66 | /// 67 | /// 68 | public Encoding DefaultEncoding 69 | { 70 | get => _defaultEncoding; 71 | set => _defaultEncoding = value ?? throw new ArgumentNullException(nameof(value)); 72 | } 73 | 74 | /// 75 | /// Clones the options. 76 | /// Introduced in order to prevent retroactive modification once the options have been 77 | /// used to initialize an instance. 78 | /// 79 | internal InMemoryFileSystemOptions Clone() => 80 | (InMemoryFileSystemOptions)MemberwiseClone(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/FileContentReadWriteTracker.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.FsTree 2 | { 3 | using System.IO; 4 | using Files.Shared; 5 | 6 | /// 7 | /// A very simple implementation of a reader/writer tracker for the 8 | /// which ensures that concurrent reading/writing is not possible. 9 | /// 10 | /// This class is specifically introduced in favor of thread-based solutions like 11 | /// ReaderWriterLockSlim because the InMemoryFileSystem is currently single-threaded anyway. 12 | /// We must only ensure here that the reader/writer tracking is appropriately handled on 13 | /// the Dispose() call of the FileContentStreams (which might be done on another thread). 14 | /// Apart from that, it's just simple tracking of whether the content is currently open or not. 15 | /// 16 | internal sealed class FileContentReadWriteTracker 17 | { 18 | // While this class has a public API surface which differentiates between readers/writers, 19 | // the internal implementation only allows a single reader/writer right now. 20 | // This is done to match the FileShare.None behavior in the PhysicalFileSystem. 21 | // To make changes easier in the future, methods like AddReader/AddWriter have already been 22 | // added now though, even though they internally do the same. 23 | // Future extensions might change the behavior based on a FileShare value. 24 | 25 | private readonly object _lock = new object(); 26 | private bool _isLocked; 27 | 28 | public void AddReader() 29 | { 30 | lock (_lock) 31 | { 32 | EnsureCanReadInternal(); 33 | _isLocked = true; 34 | } 35 | } 36 | 37 | public void AddWriter() 38 | { 39 | lock (_lock) 40 | { 41 | EnsureCanWriteInternal(); 42 | _isLocked = true; 43 | } 44 | } 45 | 46 | public void AddReaderWriter() 47 | { 48 | lock (_lock) 49 | { 50 | EnsureCanReadWriteInternal(); 51 | _isLocked = true; 52 | } 53 | } 54 | 55 | public void CloseReader() 56 | { 57 | lock (_lock) 58 | { 59 | _isLocked = false; 60 | } 61 | } 62 | 63 | public void CloseWriter() 64 | { 65 | lock (_lock) 66 | { 67 | _isLocked = false; 68 | } 69 | } 70 | 71 | public void CloseReaderWriter() 72 | { 73 | lock (_lock) 74 | { 75 | _isLocked = false; 76 | } 77 | } 78 | 79 | public void EnsureCanRead() 80 | { 81 | lock (_lock) 82 | { 83 | EnsureCanReadInternal(); 84 | } 85 | } 86 | 87 | public void EnsureCanWrite() 88 | { 89 | lock (_lock) 90 | { 91 | EnsureCanWriteInternal(); 92 | } 93 | } 94 | 95 | public void EnsureCanReadWrite() 96 | { 97 | lock (_lock) 98 | { 99 | EnsureCanReadWriteInternal(); 100 | } 101 | } 102 | 103 | private void EnsureCanReadInternal() 104 | { 105 | if (_isLocked) 106 | { 107 | throw new IOException(ExceptionStrings.StorageFile.FileIsLocked()); 108 | } 109 | } 110 | 111 | private void EnsureCanWriteInternal() 112 | { 113 | if (_isLocked) 114 | { 115 | throw new IOException(ExceptionStrings.StorageFile.FileIsLocked()); 116 | } 117 | } 118 | 119 | private void EnsureCanReadWriteInternal() 120 | { 121 | EnsureCanReadInternal(); 122 | EnsureCanWriteInternal(); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Utilities/ExceptionConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Utilities 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | 8 | /// 9 | /// The Windows Storage API is incredibly inconsistent with the exceptions being thrown 10 | /// (from a .NET perspective), i.e. it frequently happens that 11 | /// is thrown instead of or that is 12 | /// thrown. 13 | /// 14 | /// For fulfilling the specification, we obviously have to handle such cases. 15 | /// Sometimes this can be done by checking the HResult of an exception and reacting 16 | /// according to the table here: 17 | /// https://docs.microsoft.com/en-us/windows/uwp/files/best-practices-for-writing-to-files 18 | /// 19 | /// This class is responsible for that, i.e. it's the "make-exceptions-sane" converter. 20 | /// 21 | internal static class ExceptionConverter 22 | { 23 | // Possible HResults. See this link for details: https://docs.microsoft.com/en-us/windows/uwp/files/best-practices-for-writing-to-files#common-error-codes-for-write-methods-of-the-fileio-and-pathio-classes 24 | private const int ErrorElementAlreadyExists = unchecked((int)0x800700B7); 25 | private const int ErrorAccessDenied = unchecked((int)0x80070005); 26 | private const int ErrorSharingViolation = unchecked((int)0x80070020); 27 | private const int ErrorUnableToRemoveReplaced = unchecked((int)0x80070497); 28 | private const int ErrorDiskFull = unchecked((int)0x80070070); 29 | private const int ErrorOutOfMemory = unchecked((int)0x8007000E); 30 | private const int ErrorFail = unchecked((int)0x80004005); 31 | 32 | internal static async Task WithConvertedException(this Task task) 33 | { 34 | try 35 | { 36 | return await task.ConfigureAwait(false); 37 | } 38 | catch (Exception ex) 39 | { 40 | throw Convert(ex); 41 | } 42 | } 43 | 44 | internal static async Task WithConvertedException(this Task task) 45 | { 46 | try 47 | { 48 | await task.ConfigureAwait(false); 49 | } 50 | catch (Exception ex) 51 | { 52 | throw Convert(ex); 53 | } 54 | } 55 | 56 | internal static Exception Convert(Exception ex) => ex.HResult switch 57 | { 58 | ErrorAccessDenied => new UnauthorizedAccessException(ex.Message, ex), 59 | ErrorElementAlreadyExists => new IOException(ex.Message, ex), 60 | ErrorOutOfMemory => new IOException(ex.Message, ex), 61 | ErrorDiskFull => new IOException(ex.Message, ex), 62 | ErrorSharingViolation => new IOException(ex.Message, ex), 63 | ErrorUnableToRemoveReplaced => new IOException(ex.Message, ex), 64 | ErrorFail => new IOException(ex.Message, ex), 65 | 66 | // The following general fallback to an IOException might cause problems in the future, 67 | // e.g. when we run into an exception with an HResult signifying an argument error 68 | // or something alike. Such errors potentially shouldn't be converted to IOExceptions. 69 | // 70 | // With that being said, why do we have this fallback? 71 | // While working on the 0.2.0 release, some tests started to fail because Windows 72 | // returned weird HResults after an update that don't have anything to do with 73 | // file I/O. Whatever is going on under the hood - in most cases, an exception 74 | // should be translated into an IOException. Should other cases (e.g. argument exceptions) 75 | // ever occur, they can be added as special cases above. 76 | // I'm also not removing the (redundant) HResult switches returning 77 | // an IOException above - these are HResults where we *definitely* know that we want 78 | // an IOException. If the default fallback ever changes for some reason, there's no need to 79 | // look these error codes up again. 80 | _ => new IOException(ex.Message, ex), 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/PhysicalFileSystem.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical 2 | { 3 | using System; 4 | using System.IO; 5 | using Files; 6 | using Files.Shared; 7 | using Files.Shared.PhysicalStoragePath; 8 | using Files.Shared.PhysicalStoragePath.Utilities; 9 | using static System.Environment; 10 | using static System.Environment.SpecialFolder; 11 | 12 | /// 13 | /// A implementation which uses the API 14 | /// for interacting with the local physical file system. 15 | /// See remarks for details. 16 | /// 17 | /// 18 | /// The uses the API 19 | /// for interacting with the local file system. 20 | /// It is therefore compatible with any other file system implementation using the path format 21 | /// of the underlying operating system. 22 | /// 23 | /// While it is possible without errors to create multiple instances of the 24 | /// , you should ideally create and reuse a single 25 | /// instance of this class. 26 | /// 27 | public sealed class PhysicalFileSystem : FileSystem 28 | { 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | public PhysicalFileSystem() 33 | : base(PhysicalPathHelper.PhysicalPathInformation) { } 34 | 35 | /// 36 | public override StorageFile GetFile(StoragePath path) 37 | { 38 | _ = path ?? throw new ArgumentNullException(nameof(path)); 39 | 40 | if (path is not PhysicalStoragePath physicalStoragePath) 41 | { 42 | throw new ArgumentException( 43 | ExceptionStrings.FsCompatibility.StoragePathTypeNotSupported(), 44 | nameof(path) 45 | ); 46 | } 47 | 48 | return new PhysicalStorageFile(this, physicalStoragePath); 49 | } 50 | 51 | /// 52 | public override StorageFolder GetFolder(StoragePath path) 53 | { 54 | _ = path ?? throw new ArgumentNullException(nameof(path)); 55 | 56 | if (path is not PhysicalStoragePath physicalStoragePath) 57 | { 58 | throw new ArgumentException( 59 | ExceptionStrings.FsCompatibility.StoragePathTypeNotSupported(), 60 | nameof(path) 61 | ); 62 | } 63 | 64 | return new PhysicalStorageFolder(this, physicalStoragePath); 65 | } 66 | 67 | /// 68 | public override StoragePath GetPath(string path) 69 | { 70 | _ = path ?? throw new ArgumentNullException(nameof(path)); 71 | return new PhysicalStoragePath(path, this); 72 | } 73 | 74 | /// 75 | public override StoragePath GetPath(KnownFolder knownFolder) 76 | { 77 | if (!EnumInfo.IsDefined(knownFolder)) 78 | { 79 | throw new ArgumentException(ExceptionStrings.Enum.UndefinedValue(knownFolder), nameof(knownFolder)); 80 | } 81 | 82 | var path = knownFolder switch 83 | { 84 | KnownFolder.TemporaryData => Path.GetTempPath(), 85 | KnownFolder.RoamingApplicationData => GetSpecialFolder(ApplicationData), 86 | KnownFolder.LocalApplicationData => GetSpecialFolder(LocalApplicationData), 87 | KnownFolder.ProgramData => GetSpecialFolder(CommonApplicationData), 88 | KnownFolder.UserProfile => GetSpecialFolder(UserProfile), 89 | KnownFolder.Desktop => GetSpecialFolder(Desktop), 90 | KnownFolder.DocumentsLibrary => GetSpecialFolder(MyDocuments), 91 | KnownFolder.PicturesLibrary => GetSpecialFolder(MyPictures), 92 | KnownFolder.VideosLibrary => GetSpecialFolder(MyVideos), 93 | KnownFolder.MusicLibrary => GetSpecialFolder(MyMusic), 94 | _ => throw new NotSupportedException(ExceptionStrings.FileSystem.KnownFolderNotSupported(knownFolder)), 95 | }; 96 | return GetPath(path); 97 | 98 | static string GetSpecialFolder(SpecialFolder specialFolder) => 99 | GetFolderPath(specialFolder, SpecialFolderOption.DoNotVerify); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/WindowsStorageFileSystem.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage 2 | { 3 | using System; 4 | using Files; 5 | using Files.Shared.PhysicalStoragePath; 6 | using Files.Shared.PhysicalStoragePath.Utilities; 7 | using Files.FileSystems.WindowsStorage.Utilities; 8 | using Files.Shared; 9 | using Windows.Storage; 10 | using static System.Environment; 11 | using StorageFile = StorageFile; 12 | using StorageFolder = StorageFolder; 13 | 14 | /// 15 | /// A implementation which uses the 16 | /// API of the Windows SDK for interacting with the local physical file system. 17 | /// See remarks for details. 18 | /// 19 | /// 20 | /// The uses the API 21 | /// for interacting with the local file system. 22 | /// Therefore, all restrictions which apply to a sandboxed application also apply to this 23 | /// file system. This means that, by default, most operations on locations which are not 24 | /// accessible out of the box will throw an . 25 | /// To access additional locations, you must be granted access by the application's user, 26 | /// for example via the . 27 | /// 28 | /// Due to these restrictions, this file system implementation returns application specific 29 | /// paths for certain values, e.g. the application's own temporary 30 | /// data folder. 31 | /// 32 | /// While it is possible without errors to create multiple instances of the 33 | /// , you should ideally create and reuse a single 34 | /// instance of this class. 35 | /// 36 | public sealed class WindowsStorageFileSystem : FileSystem 37 | { 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | public WindowsStorageFileSystem() 42 | : base(PhysicalPathHelper.PhysicalPathInformation) { } 43 | 44 | /// 45 | public override StorageFile GetFile(StoragePath path) 46 | { 47 | _ = path ?? throw new ArgumentNullException(nameof(path)); 48 | 49 | if (!(path is PhysicalStoragePath physicalStoragePath)) 50 | { 51 | throw new ArgumentException( 52 | ExceptionStrings.FsCompatibility.StoragePathTypeNotSupported(), 53 | nameof(path) 54 | ); 55 | } 56 | 57 | return new WindowsStorageStorageFile(this, physicalStoragePath); 58 | } 59 | 60 | /// 61 | public override StorageFolder GetFolder(StoragePath path) 62 | { 63 | _ = path ?? throw new ArgumentNullException(nameof(path)); 64 | 65 | if (!(path is PhysicalStoragePath physicalStoragePath)) 66 | { 67 | throw new ArgumentException( 68 | ExceptionStrings.FsCompatibility.StoragePathTypeNotSupported(), 69 | nameof(path) 70 | ); 71 | } 72 | 73 | return new WindowsStorageStorageFolder(this, physicalStoragePath); 74 | } 75 | 76 | /// 77 | public override StoragePath GetPath(string path) 78 | { 79 | _ = path ?? throw new ArgumentNullException(nameof(path)); 80 | return new PhysicalStoragePath(path, this); 81 | } 82 | 83 | /// 84 | public override StoragePath GetPath(KnownFolder knownFolder) 85 | { 86 | if (!Enum.IsDefined(typeof(KnownFolder), knownFolder)) 87 | { 88 | throw new ArgumentException(ExceptionStrings.Enum.UndefinedValue(knownFolder), nameof(knownFolder)); 89 | } 90 | 91 | var path = knownFolder switch 92 | { 93 | KnownFolder.TemporaryData => ApplicationData.Current.TemporaryFolder.GetPathOrThrow(), 94 | KnownFolder.RoamingApplicationData => ApplicationData.Current.RoamingFolder.GetPathOrThrow(), 95 | KnownFolder.LocalApplicationData => ApplicationData.Current.LocalFolder.GetPathOrThrow(), 96 | KnownFolder.ProgramData => GetSpecialFolder(SpecialFolder.CommonApplicationData), 97 | KnownFolder.UserProfile => GetSpecialFolder(SpecialFolder.UserProfile), 98 | KnownFolder.Desktop => GetSpecialFolder(SpecialFolder.Desktop), 99 | KnownFolder.DocumentsLibrary => GetSpecialFolder(SpecialFolder.MyDocuments), 100 | KnownFolder.PicturesLibrary => GetSpecialFolder(SpecialFolder.MyPictures), 101 | KnownFolder.VideosLibrary => GetSpecialFolder(SpecialFolder.MyVideos), 102 | KnownFolder.MusicLibrary => GetSpecialFolder(SpecialFolder.MyMusic), 103 | _ => throw new NotSupportedException(ExceptionStrings.FileSystem.KnownFolderNotSupported(knownFolder)), 104 | }; 105 | return GetPath(path); 106 | 107 | static string GetSpecialFolder(SpecialFolder specialFolder) => 108 | GetFolderPath(specialFolder, SpecialFolderOption.DoNotVerify); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Files.Shared.PhysicalStoragePath/PhysicalStoragePath.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Shared.PhysicalStoragePath 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using Files; 7 | using Files.Shared; 8 | using Files.Shared.PhysicalStoragePath.Utilities; 9 | 10 | internal sealed class PhysicalStoragePath : StoragePath 11 | { 12 | // All properties which return a StoragePath are wrapped in a Lazy in order to not fully 13 | // expand/walk a path tree whenever a path is initialized. 14 | // Consider for example the path foo/bar/baz. 15 | // Without Lazy, three path instances would be created immediately through the Parent property. 16 | private readonly Lazy _rootLazy; 17 | private readonly Lazy _parentLazy; 18 | private readonly Lazy _fullPathLazy; 19 | 20 | public override PathKind Kind { get; } 21 | 22 | public override StoragePath? Root => _rootLazy.Value; 23 | 24 | public override StoragePath? Parent => _parentLazy.Value; 25 | 26 | public override StoragePath FullPath => _fullPathLazy.Value; 27 | 28 | public override string Name { get; } 29 | 30 | public override string NameWithoutExtension { get; } 31 | 32 | public override string? Extension { get; } 33 | 34 | internal PhysicalStoragePath(string path, FileSystem fileSystem) 35 | : base(fileSystem, path) 36 | { 37 | Debug.Assert( 38 | ReferenceEquals(fileSystem.PathInformation, PhysicalPathHelper.PhysicalPathInformation), 39 | $"When using the PhysicalStoragePath, your file system should be using the corresponding " + 40 | $"{nameof(PhysicalPathHelper.PhysicalPathInformation)}." 41 | ); 42 | 43 | var fullPath = GetFullPathOrThrow(path); 44 | var rootPath = Path.GetPathRoot(path); 45 | var pathWithoutTrailingSeparator = PathPolyfills.TrimEndingDirectorySeparator(path); 46 | var directoryPath = Path.GetDirectoryName(pathWithoutTrailingSeparator); 47 | var name = Path.GetFileName(pathWithoutTrailingSeparator); 48 | var nameWithoutExtension = GetNameWithoutExtension(name); 49 | var extension = PhysicalPathHelper.GetExtensionWithoutTrailingExtensionSeparator(pathWithoutTrailingSeparator); 50 | var isPathFullyQualified = PathPolyfills.IsPathFullyQualified(path); 51 | 52 | Kind = isPathFullyQualified ? PathKind.Absolute : PathKind.Relative; 53 | Name = name; 54 | NameWithoutExtension = nameWithoutExtension; 55 | Extension = string.IsNullOrEmpty(extension) ? null : extension; 56 | 57 | _rootLazy = new Lazy( 58 | () => string.IsNullOrEmpty(rootPath) ? null : fileSystem.GetPath(rootPath) 59 | ); 60 | 61 | _parentLazy = new Lazy( 62 | () => string.IsNullOrEmpty(directoryPath) ? null : fileSystem.GetPath(directoryPath) 63 | ); 64 | 65 | _fullPathLazy = new Lazy( 66 | () => fileSystem.GetPath(fullPath) 67 | ); 68 | 69 | static string GetFullPathOrThrow(string path) 70 | { 71 | // This method, apart from returning the full path, has another goal: 72 | // Validate that the path has the correct format. 73 | // Path rules are incredibly complex depending on the current OS. It's best to not try 74 | // and emulate these rules here, but to actually use the OS/FS APIs, i.e. GetFullPath, directly. 75 | // One thing to note is that there are differences between the different .NET runtimes here. 76 | // Older versions have stricter path validations and throw ArgumentExceptions here while 77 | // newer implementations delay that and throw IOExceptions for invalid paths in the various 78 | // File/Directory APIs. 79 | // There is not much we can do about this. Again, emulating and artifically supporting this is 80 | // incredibly complex. Therefore we simply allow this and document this fact. 81 | 82 | try 83 | { 84 | return Path.GetFullPath(path); 85 | } 86 | catch (Exception ex) when ( 87 | ex is ArgumentException 88 | || ex is NotSupportedException 89 | || ex is PathTooLongException 90 | ) 91 | { 92 | throw new ArgumentException( 93 | ExceptionStrings.StoragePath.InvalidFormat(), 94 | nameof(path), 95 | ex 96 | ); 97 | } 98 | } 99 | 100 | static string GetNameWithoutExtension(string name) 101 | { 102 | // Specification requires special handling for these two directories. 103 | // Without this code, we'd return "" and ".", because Path.GetFileNameWithoutExtension 104 | // trims one dot. 105 | if (name == PhysicalPathHelper.CurrentDirectorySegment || 106 | name == PhysicalPathHelper.ParentDirectorySegment) 107 | { 108 | return name; 109 | } 110 | return Path.GetFileNameWithoutExtension(name); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Files.Tests/PathInformationTests.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Tests 2 | { 3 | using System; 4 | using System.Collections.ObjectModel; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Shouldly; 7 | 8 | [TestClass] 9 | public class PathInformationTests 10 | { 11 | private static readonly char[] InvalidPathChars = new[] { 'a', 'b', 'c' }; 12 | private static readonly char[] InvalidFileNameChars = new[] { 'd', 'e', 'f' }; 13 | private const char DirectorySeparator = '/'; 14 | private const char AltDirectorySeparator = '/'; 15 | private const char ExtensionSeparator = '.'; 16 | private const char VolumeSeparator = ':'; 17 | private const string CurrentDirectorySegment = "."; 18 | private const string ParentDirectorySegment = ".."; 19 | private const StringComparison DefaultStringComparison = StringComparison.OrdinalIgnoreCase; 20 | 21 | #region Constructor Tests 22 | 23 | [TestMethod] 24 | public void Constructor_NullParameters_ThrowsArgumentNullException() 25 | { 26 | Should.Throw(() => new PathInformation( 27 | null!, 28 | InvalidFileNameChars, 29 | DirectorySeparator, 30 | AltDirectorySeparator, 31 | ExtensionSeparator, 32 | VolumeSeparator, 33 | CurrentDirectorySegment, 34 | ParentDirectorySegment, 35 | DefaultStringComparison 36 | )); 37 | 38 | Should.Throw(() => new PathInformation( 39 | InvalidPathChars, 40 | null!, 41 | DirectorySeparator, 42 | AltDirectorySeparator, 43 | ExtensionSeparator, 44 | VolumeSeparator, 45 | CurrentDirectorySegment, 46 | ParentDirectorySegment, 47 | DefaultStringComparison 48 | )); 49 | 50 | Should.Throw(() => new PathInformation( 51 | InvalidFileNameChars, 52 | InvalidFileNameChars, 53 | DirectorySeparator, 54 | AltDirectorySeparator, 55 | ExtensionSeparator, 56 | VolumeSeparator, 57 | null!, 58 | ParentDirectorySegment, 59 | DefaultStringComparison 60 | )); 61 | 62 | Should.Throw(() => new PathInformation( 63 | InvalidFileNameChars, 64 | InvalidFileNameChars, 65 | DirectorySeparator, 66 | AltDirectorySeparator, 67 | ExtensionSeparator, 68 | VolumeSeparator, 69 | CurrentDirectorySegment, 70 | null!, 71 | DefaultStringComparison 72 | )); 73 | } 74 | 75 | [TestMethod] 76 | public void Constructor_EmptyStrings_ThrowsArgumentException() 77 | { 78 | Should.Throw(() => new PathInformation( 79 | InvalidPathChars, 80 | InvalidFileNameChars, 81 | DirectorySeparator, 82 | AltDirectorySeparator, 83 | ExtensionSeparator, 84 | VolumeSeparator, 85 | "", 86 | ParentDirectorySegment, 87 | DefaultStringComparison 88 | )); 89 | 90 | Should.Throw(() => new PathInformation( 91 | InvalidPathChars, 92 | InvalidFileNameChars, 93 | DirectorySeparator, 94 | AltDirectorySeparator, 95 | ExtensionSeparator, 96 | VolumeSeparator, 97 | CurrentDirectorySegment, 98 | "", 99 | DefaultStringComparison 100 | )); 101 | } 102 | 103 | [TestMethod] 104 | public void Constructor_StandardParameters_SetsProperties() 105 | { 106 | var info = new PathInformation( 107 | InvalidPathChars, 108 | InvalidFileNameChars, 109 | DirectorySeparator, 110 | AltDirectorySeparator, 111 | ExtensionSeparator, 112 | VolumeSeparator, 113 | CurrentDirectorySegment, 114 | ParentDirectorySegment, 115 | DefaultStringComparison 116 | ); 117 | 118 | info.InvalidPathChars.ShouldBe(InvalidPathChars); 119 | info.InvalidFileNameChars.ShouldBe(InvalidFileNameChars); 120 | info.DirectorySeparatorChar.ShouldBe(DirectorySeparator); 121 | info.AltDirectorySeparatorChar.ShouldBe(AltDirectorySeparator); 122 | info.ExtensionSeparatorChar.ShouldBe(ExtensionSeparator); 123 | info.VolumeSeparatorChar.ShouldBe(VolumeSeparator); 124 | info.CurrentDirectorySegment.ShouldBe(CurrentDirectorySegment); 125 | info.ParentDirectorySegment.ShouldBe(ParentDirectorySegment); 126 | info.DefaultStringComparison.ShouldBe(DefaultStringComparison); 127 | 128 | info.InvalidPathChars.ShouldBeOfType>(); 129 | info.InvalidFileNameChars.ShouldBeOfType>(); 130 | info.DirectorySeparatorChars.ShouldBeOfType>(); 131 | info.DirectorySeparatorChars.ShouldBe(new[] { '/' }); 132 | } 133 | 134 | #endregion 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/Setup/Default.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests.Setup 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Text; 6 | 7 | /// 8 | /// Defines default values for file system structures which are used by the tests. 9 | /// 10 | /// Because this classes requires certain values from a file system's 11 | /// property, it must be setup before any test cases 12 | /// can be run. 13 | /// 14 | /// You can easily do this by calling before running any tests. 15 | /// If you are only testing one file system in your project, you can easily do this during 16 | /// the creation of your custom . 17 | /// 18 | public static class Default 19 | { 20 | private static char? _extensionSeparator; 21 | private static char? _directorySeparator; 22 | 23 | private static char ExtensionSeparator 24 | { 25 | get => _extensionSeparator ?? throw MissingPropertyException(); 26 | set => _extensionSeparator = value; 27 | } 28 | 29 | private static char DirectorySeparator 30 | { 31 | get => _directorySeparator ?? throw MissingPropertyException(); 32 | set => _directorySeparator = value; 33 | } 34 | 35 | private static Exception MissingPropertyException() => 36 | new InvalidOperationException( 37 | $"The {nameof(Default)} class has not been setup yet. " + 38 | $"Please ensure that the property is set to the appropriate value of your file system under test. " + 39 | $"You can do this with the {nameof(Default)}.{nameof(Setup)} function." 40 | ); 41 | 42 | /// 43 | /// Automatically sets up the class with values taken from the specified file system. 44 | /// 45 | public static void Setup(FileSystem fileSystem) 46 | { 47 | ExtensionSeparator = fileSystem.PathInformation.ExtensionSeparatorChar; 48 | DirectorySeparator = fileSystem.PathInformation.DirectorySeparatorChar; 49 | } 50 | 51 | private static string FormatExtension(string format) => 52 | string.Format(format, ExtensionSeparator); 53 | 54 | private static string FormatSeparators(string format) => 55 | string.Format(format, DirectorySeparator); 56 | 57 | 58 | 59 | public static FileAttributes InvalidFileAttributes => (FileAttributes)(-1); 60 | public static FileAccess InvalidFileAccess => (FileAccess)(-1); 61 | public static CreationCollisionOption InvalidCreationCollisionOption => (CreationCollisionOption)(-1); 62 | public static NameCollisionOption InvalidNameCollisionOption => (NameCollisionOption)(-1); 63 | public static DeletionOption InvalidDeletionOption => (DeletionOption)(-1); 64 | 65 | public static string TextContent => "Hello World! \n\n This is the default file content used during testing."; 66 | public static byte[] ByteContent => Encoding.UTF8.GetBytes(TextContent); 67 | 68 | public static string FileName => FormatExtension("defaultFile{0}ext"); 69 | public static string FolderName => "defaultFolder"; 70 | public static string SharedFileFolderName => "maybeFileMaybeFolder"; 71 | 72 | public static string ConflictingFileName => FormatExtension("conflictingFile{0}ext"); 73 | public static string ConflictingFolderName => "conflictingFolder"; 74 | 75 | public static string RenamedFileName => FormatExtension("renamedFile{0}ext"); 76 | public static string RenamedFolderName => "renamedFolder"; 77 | 78 | public static string NonExistingParentFolderName => "nonExistingParentFolder"; 79 | public static string FileWithNonExistingParentName => FormatExtension("fileWithoutParent{0}ext"); 80 | public static string FolderWithNonExistingParentName => "folderWithoutParent"; 81 | 82 | public static string[] FileWithNonExistingParentSegments => new[] 83 | { 84 | NonExistingParentFolderName, 85 | FileWithNonExistingParentName 86 | }; 87 | 88 | public static string[] FolderWithNonExistingParentSegments => new[] 89 | { 90 | NonExistingParentFolderName, 91 | FolderWithNonExistingParentName 92 | }; 93 | 94 | public static string SrcParentFolderName => "src"; 95 | public static string DstParentFolderName => "dst"; 96 | public static string SrcFileName => "srcFile.ext"; 97 | public static string DstFileName => FormatExtension("dstFile{0}ext"); 98 | public static string SrcFolderName => "srcFolder"; 99 | public static string DstFolderName => "dstFolder"; 100 | 101 | public static string[] SrcFileSegments => new[] { SrcParentFolderName, SrcFileName }; 102 | public static string[] DstFileSegments => new[] { DstParentFolderName, DstFileName }; 103 | public static string[] SrcFolderSegments => new[] { SrcParentFolderName, SrcFolderName }; 104 | public static string[] DstFolderSegments => new[] { DstParentFolderName, DstFolderName }; 105 | public static string[] SharedFileFolderInSrcSegments => new[] { SrcParentFolderName, SharedFileFolderName }; 106 | public static string[] SharedFileFolderInDstSegments => new[] { DstParentFolderName, SharedFileFolderName }; 107 | 108 | 109 | 110 | public static string PathName => $"{PathNameWithoutExtension}{ExtensionSeparator}{PathNameExtension}"; 111 | public static string PathNameWithoutExtension => "name"; 112 | public static string PathNameExtension => "ext"; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Files.FileSystems.WindowsStorage/Utilities/FsHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.WindowsStorage.Utilities 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using WinStorageFile = Windows.Storage.StorageFile; 6 | using WinStorageFolder = Windows.Storage.StorageFolder; 7 | using WinCreationCollisionOption = Windows.Storage.CreationCollisionOption; 8 | using System.IO; 9 | using System.Threading; 10 | using Files; 11 | using System.Runtime.ExceptionServices; 12 | using Files.Shared; 13 | 14 | internal static class FsHelper 15 | { 16 | internal static async Task GetFileAsync( 17 | StoragePath fullPath, 18 | CancellationToken cancellationToken 19 | ) 20 | { 21 | try 22 | { 23 | return await WinStorageFile.GetFileFromPathAsync(fullPath).AsAwaitable(cancellationToken); 24 | } 25 | catch (FileNotFoundException ex) 26 | { 27 | // FileNotFoundException might be thrown if the parent directory does not exist. 28 | // Specification requires DirectoryNotFoundException in such cases. 29 | 30 | // Some folder operations might call this method though. In such cases, we might not 31 | // have a parent path. In that case, just go with the FileNotFoundException. 32 | if (fullPath.Parent is null) 33 | { 34 | throw; 35 | } 36 | 37 | try 38 | { 39 | await WinStorageFolder.GetFolderFromPathAsync(fullPath.Parent).AsAwaitable(cancellationToken); 40 | } 41 | catch 42 | { 43 | // Getting the parent folder failed. 44 | throw new DirectoryNotFoundException(ExceptionStrings.StorageFile.ParentFolderDoesNotExist(), ex); 45 | } 46 | 47 | // At this point we know that the parent folder exists, but the file doesn't. 48 | // We can go with the FileNotFoundException. 49 | throw; 50 | } 51 | catch (ArgumentException ex) 52 | { 53 | // An ArgumentException might indicate that a conflicting folder exists at this location. 54 | // Try to get a folder. If that succeedes, we can be certain and throw the IOException 55 | // which is required by specification. 56 | try 57 | { 58 | await WinStorageFolder.GetFolderFromPathAsync(fullPath).AsAwaitable(cancellationToken); 59 | } 60 | catch 61 | { 62 | // Getting the folder failed too. Rethrow the ArgumentException from before. 63 | ExceptionDispatchInfo.Throw(ex); 64 | } 65 | 66 | // At this point, getting the folder succeeded. We must manually throw to fulfill the specification. 67 | throw new IOException(ExceptionStrings.StorageFile.ConflictingFolderExistsAtFileLocation(), ex); 68 | } 69 | } 70 | 71 | internal static async Task GetOrCreateFolderAsync( 72 | StoragePath fullPath, 73 | CancellationToken cancellationToken 74 | ) 75 | { 76 | try 77 | { 78 | // If this succeedes, the folder exists. Nothing to do. 79 | return await GetFolderAsync(fullPath, cancellationToken).ConfigureAwait(false); 80 | } 81 | catch (DirectoryNotFoundException) 82 | { 83 | var parentPath = fullPath.Parent; 84 | if (parentPath is null) 85 | { 86 | // Root folder. 87 | throw new UnauthorizedAccessException(); 88 | } 89 | 90 | var parentFolder = await GetOrCreateFolderAsync(parentPath, cancellationToken).ConfigureAwait(false); 91 | return await parentFolder 92 | .CreateFolderAsync(fullPath.Name, WinCreationCollisionOption.OpenIfExists) 93 | .AsAwaitable(cancellationToken); 94 | } 95 | } 96 | 97 | internal static async Task GetFolderAsync( 98 | StoragePath fullPath, 99 | CancellationToken cancellationToken 100 | ) 101 | { 102 | try 103 | { 104 | return await WinStorageFolder.GetFolderFromPathAsync(fullPath).AsAwaitable(cancellationToken); 105 | } 106 | catch (FileNotFoundException ex) 107 | { 108 | throw new DirectoryNotFoundException(message: null, innerException: ex); 109 | } 110 | catch (ArgumentException ex) 111 | { 112 | // An ArgumentException might indicate that a conflicting file exists at this location. 113 | // Try to get a file. If that succeedes, we can be certain and throw the IOException 114 | // which is required by specification. 115 | try 116 | { 117 | await WinStorageFile.GetFileFromPathAsync(fullPath).AsAwaitable(cancellationToken); 118 | } 119 | catch 120 | { 121 | // Getting the file failed too. Rethrow the ArgumentException from before. 122 | ExceptionDispatchInfo.Throw(ex); 123 | } 124 | 125 | // At this point, getting the file succeeded. We must manually throw to fulfill the specification. 126 | throw new IOException(ExceptionStrings.StorageFolder.ConflictingFileExistsAtFolderLocation(), ex); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/ElementNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using Files.Shared; 5 | 6 | namespace Files.FileSystems.InMemory.FsTree 7 | { 8 | /// 9 | /// This class is the base class for the and 10 | /// and as such encapsulates the shared properties/logic which is used for maintaining 11 | /// the in-memory file system tree. 12 | /// 13 | /// These node classes only provide operations which modify either the node's content or 14 | /// the FS tree itself. They do not implement special behavior which is defined by the 15 | /// Files specification. This is the role of the and 16 | /// . These classes implement the specification based 17 | /// on the provided methods in these node classes. 18 | /// Of course, the nodes are designed with the specifications in mind, i.e. they throw 19 | /// the expected exceptions when applicable. 20 | /// 21 | /// The node classes are not doing any thread synchronization. 22 | /// Instead, the user must ensure that the FS tree is only accessed synchronously. 23 | /// 24 | [DebuggerDisplay("{Path}")] 25 | internal abstract class ElementNode 26 | { 27 | public FsDataStorage Storage { get; } 28 | 29 | public StoragePath Path { get; private set; } 30 | 31 | public FolderNode? Parent { get; private set; } 32 | 33 | public FileAttributes Attributes { get; set; } 34 | 35 | public DateTimeOffset CreatedOn { get; protected set; } 36 | 37 | public DateTimeOffset? ModifiedOn { get; protected set; } 38 | 39 | protected ElementNode(FsDataStorage storage, StoragePath path, FolderNode? parent) 40 | { 41 | Storage = storage; 42 | Parent = parent; 43 | Path = path.FullPath; 44 | ModifiedOn = CreatedOn = DateTimeOffset.Now; 45 | } 46 | 47 | public virtual void Copy(StoragePath destinationPath, bool replaceExisting) 48 | { 49 | EnsureNotLocked(); 50 | 51 | if (Storage.IsSameElement(Path, destinationPath)) 52 | { 53 | throw new IOException(ExceptionStrings.StorageElement.CannotCopyToSameLocation()); 54 | } 55 | 56 | var newParentNode = Storage.GetParentNode(destinationPath); 57 | if (newParentNode is null) 58 | { 59 | throw new IOException(ExceptionStrings.StorageElement.CannotCopyToRootLocation()); 60 | } 61 | 62 | if (replaceExisting) 63 | { 64 | DeleteSameNodeTypeAtPathIfExisting(destinationPath); 65 | } 66 | 67 | Storage.EnsureNoConflictingNodeExists(destinationPath); 68 | 69 | CopyImpl(destinationPath); 70 | } 71 | 72 | protected abstract void CopyImpl(StoragePath destinationPath); 73 | 74 | public virtual void Move(StoragePath destinationPath, bool replaceExisting) 75 | { 76 | EnsureNotLocked(); 77 | 78 | if (Storage.IsSameElement(Path, destinationPath)) 79 | { 80 | throw new IOException(ExceptionStrings.StorageElement.CannotMoveToSameLocation()); 81 | } 82 | 83 | if (Parent is null) 84 | { 85 | throw new IOException(ExceptionStrings.StorageElement.CannotMoveFromRootLocation()); 86 | } 87 | 88 | var newParentNode = Storage.GetParentNode(destinationPath); 89 | if (newParentNode is null) 90 | { 91 | throw new IOException(ExceptionStrings.StorageElement.CannotMoveToRootLocation()); 92 | } 93 | 94 | var currentParent = newParentNode; 95 | do 96 | { 97 | if (ReferenceEquals(currentParent, this)) 98 | { 99 | throw new IOException(ExceptionStrings.StorageFolder.CannotMoveParentFolderIntoChildFolder()); 100 | } 101 | } while ((currentParent = currentParent!.Parent) is not null); 102 | 103 | // Moving can be done by re-registering the node associations. There is no need 104 | // to create/clone new nodes. 105 | // Before updating the registrations, we must preemptively ensure that no conflicting 106 | // node exists at the move location. Otherwise `Storage.RegisterNode(this)` might throw 107 | // when the associations have already been mutated. 108 | // Care must also be taken with the properties to be updated. Anything that depends 109 | // on the path must be updated. 110 | if (replaceExisting) 111 | { 112 | DeleteSameNodeTypeAtPathIfExisting(destinationPath); 113 | } 114 | 115 | Storage.EnsureNoConflictingNodeExists(destinationPath); 116 | 117 | Parent.UnregisterChildNode(this); 118 | Storage.UnregisterNode(this); 119 | 120 | Parent = newParentNode; 121 | Path = destinationPath.FullPath; 122 | 123 | Storage.RegisterNode(this); 124 | newParentNode.RegisterChildNode(this); 125 | 126 | SetModifiedToNow(); 127 | } 128 | 129 | protected abstract void DeleteSameNodeTypeAtPathIfExisting(StoragePath destinationPath); 130 | 131 | public virtual void Delete() 132 | { 133 | EnsureNotLocked(); 134 | Parent?.UnregisterChildNode(this); 135 | Storage.UnregisterNode(this); 136 | } 137 | 138 | public abstract void EnsureNotLocked(); 139 | 140 | public void SetModifiedToNow() 141 | { 142 | ModifiedOn = DateTimeOffset.Now; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/InMemoryFileSystem.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using Files; 7 | using Files.FileSystems.InMemory.FsTree; 8 | using Files.Shared; 9 | 10 | /// 11 | /// A implementation specifically designed for testing which only 12 | /// exists in-memory and therefore has no dependency or influence on a real file system. 13 | /// See remarks for details. 14 | /// 15 | /// 16 | /// The only exists in-memory. Not having a dependency on 17 | /// a real file system brings many advantages: You are able to run/test components which 18 | /// require a in any situation and on any machine. You don't have 19 | /// to worry about cleaning up test data and you can guarantee that the tests are deterministic, 20 | /// since each instance will always start in the same state. 21 | /// Last but not least, the can, in comparison to a real-world 22 | /// file system, be configured to some extent. Via the , 23 | /// you are, for example, able to define exactly which paths are mapped to which file system 24 | /// element or which characters are used as the directory separator(s). 25 | /// 26 | /// An important difference between the and a real-world 27 | /// file system implementation is that each instance is 28 | /// isolated. Two different instances of the PhysicalFileSystem, for example, still 29 | /// operate on the same file system of the local machine. This is not true for two different 30 | /// instances. Each "contains" different data and might 31 | /// even use different implementations and rules. 32 | /// For this reason, the is more strict on which parameters 33 | /// it allows. The PhysicalFileSystem will, for example, allow a 34 | /// instance created by another PhysicalFileSystem instance in the 35 | /// method. The 36 | /// will throw an exception here. 37 | /// Therefore it is essential that you correctly implement your code and, ideally, don't 38 | /// interchange different instances. 39 | /// 40 | /// You must also be aware that the is entirely empty 41 | /// in the beginning, i.e. there are no files or folders present. You can easily create 42 | /// these during a setup phase though, for example by utilizing the 43 | /// 44 | /// method with the recursive: true parameter. 45 | /// 46 | public sealed class InMemoryFileSystem : FileSystem 47 | { 48 | internal InMemoryFileSystemOptions Options { get; } 49 | 50 | /// Care: This property is used for thread synchronization and is locked upon. 51 | internal FsDataStorage Storage { get; } 52 | 53 | internal char[] InvalidNewNameCharacters { get; } 54 | 55 | public InMemoryFileSystem() 56 | : this(new InMemoryFileSystemOptions()) { } 57 | 58 | public InMemoryFileSystem(InMemoryFileSystemOptions options) 59 | : base((options ?? throw new ArgumentNullException(nameof(options))).PathProvider.PathInformation) 60 | { 61 | Options = options.Clone(); 62 | Storage = new FsDataStorage(options.StoragePathComparer); 63 | 64 | InvalidNewNameCharacters = new[] 65 | { 66 | PathInformation.DirectorySeparatorChar, 67 | PathInformation.AltDirectorySeparatorChar, 68 | PathInformation.VolumeSeparatorChar, 69 | } 70 | .Distinct() 71 | .ToArray(); 72 | } 73 | 74 | /// 75 | public override StorageFile GetFile(StoragePath path) 76 | { 77 | _ = path ?? throw new ArgumentNullException(nameof(path)); 78 | if (!ReferenceEquals(path.FileSystem, this)) 79 | { 80 | throw new ArgumentException( 81 | ExceptionStrings.InMemoryFileSystem.MemberIncompatibleWithInstance(), 82 | nameof(path) 83 | ); 84 | } 85 | 86 | return new InMemoryStorageFile(this, path); 87 | } 88 | 89 | /// 90 | public override StorageFolder GetFolder(StoragePath path) 91 | { 92 | _ = path ?? throw new ArgumentNullException(nameof(path)); 93 | if (!ReferenceEquals(path.FileSystem, this)) 94 | { 95 | throw new ArgumentException( 96 | ExceptionStrings.InMemoryFileSystem.MemberIncompatibleWithInstance(), 97 | nameof(path) 98 | ); 99 | } 100 | 101 | return new InMemoryStorageFolder(this, path); 102 | } 103 | 104 | /// 105 | public override StoragePath GetPath(string path) 106 | { 107 | _ = path ?? throw new ArgumentNullException(nameof(path)); 108 | return Options.PathProvider.GetPath(this, path); 109 | } 110 | 111 | /// 112 | public override StoragePath GetPath(KnownFolder knownFolder) 113 | { 114 | if (!EnumInfo.IsDefined(knownFolder)) 115 | { 116 | throw new ArgumentException(ExceptionStrings.Enum.UndefinedValue(knownFolder), nameof(knownFolder)); 117 | } 118 | return Options.KnownFolderProvider.GetPath(this, knownFolder); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Files.Specification.Tests/FileSystemTestBase.cs: -------------------------------------------------------------------------------- 1 | namespace Files.Specification.Tests 2 | { 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Files; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; 10 | 11 | /// 12 | /// A base for test classes which need to interact or test a file system element 13 | /// of some kind. 14 | /// Using this base class provides access to a and 15 | /// thus locations where a specific can be tested. 16 | /// 17 | [TestClass] 18 | public abstract class FileSystemTestBase 19 | { 20 | private StorageFolder? _testFolder; 21 | 22 | /// 23 | /// Gets the with which the test class has been initialized. 24 | /// 25 | protected FileSystemTestContext Context { get; } 26 | 27 | /// 28 | /// Gets the value of the property of the 29 | /// . This simply exists for quickly accessing the file system. 30 | /// 31 | protected FileSystem FileSystem => Context.FileSystem; 32 | 33 | /// 34 | /// Gets the value of the property. 35 | /// This simply exists for quickly accessing the file system. 36 | /// 37 | protected PathInformation PathInformation => Context.FileSystem.PathInformation; 38 | 39 | /// 40 | /// Gets a folder in which all file system tests should operate. 41 | /// By only interacting with items in this folder, the tests are deterministic. 42 | /// 43 | [AllowNull] 44 | protected StorageFolder TestFolder 45 | { 46 | get => _testFolder ?? throw new InvalidOperationException("The test folder has not been initialized yet."); 47 | set => _testFolder = value; 48 | } 49 | 50 | /// 51 | /// Gets the root folder which contains the . 52 | /// No I/O operations should be performed in this folder. Restrict tests on the root folder 53 | /// on read-only tests. 54 | /// 55 | protected StorageFolder RootFolder => 56 | FileSystem.GetFolder( 57 | TestFolder.Path.FullPath.Root ?? 58 | throw new InvalidOperationException("Could not retrieve a root folder from the test folder path.") 59 | ); 60 | 61 | /// 62 | /// Initializes a new instance using the specified 63 | /// for testing a file system. 64 | /// 65 | /// The context to be used for testing a file system. 66 | protected FileSystemTestBase(FileSystemTestContext context) 67 | { 68 | Context = context ?? throw new ArgumentNullException(nameof(context)); 69 | TestFolder = null!; // That's good enough for the tests. 70 | } 71 | 72 | /// 73 | /// Initializes the isolated , resulting in a clean and empty state. 74 | /// 75 | [TestInitialize] 76 | public virtual async Task Initialize() 77 | { 78 | // If the tests got aborted before, a cleanup may be necessary. 79 | await Cleanup().ConfigureAwait(false); 80 | 81 | TestFolder = await Context.GetTestFolderAsync().ConfigureAwait(false); 82 | await TestFolder.CreateAsync(CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); 83 | } 84 | 85 | /// 86 | /// Deletes the isolated . 87 | /// 88 | [TestCleanup] 89 | public virtual async Task Cleanup() 90 | { 91 | try 92 | { 93 | if (_testFolder is not null) 94 | { 95 | await LogFinalTestFolderStateAsync(); 96 | await _testFolder.DeleteAsync(DeletionOption.IgnoreMissing); 97 | } 98 | } 99 | catch (DirectoryNotFoundException) 100 | { 101 | // Should not be thrown, but it MAY happen if the underlying implementation is wrong. 102 | } 103 | } 104 | 105 | private async Task LogFinalTestFolderStateAsync() 106 | { 107 | try 108 | { 109 | Logger.LogMessage("Final test folder state:"); 110 | if (!await TestFolder.ExistsAsync()) 111 | { 112 | Logger.LogMessage("The test folder does not exist anymore."); 113 | return; 114 | } 115 | 116 | await LogRecursively(TestFolder); 117 | } 118 | catch (Exception ex) 119 | { 120 | Logger.LogMessage($"Logging the state of the test folder failed with an exception: {ex.ToString()}"); 121 | } 122 | 123 | static async Task LogRecursively(StorageFolder folder, int level = 0) 124 | { 125 | LogFsMember(folder, level); 126 | 127 | foreach (var file in await folder.GetAllFilesAsync()) 128 | { 129 | LogFsMember(file, level + 2); 130 | } 131 | 132 | foreach (var subFolder in await folder.GetAllFoldersAsync()) 133 | { 134 | await LogRecursively(subFolder, level + 2); 135 | } 136 | } 137 | 138 | static void LogFsMember(StorageElement element, int level) 139 | { 140 | var emojiPrefix = element is StorageFile ? "📄" : "📁"; 141 | var levelWhitespace = new string(' ', level); 142 | var msg = $"{levelWhitespace}|_ {emojiPrefix} {element.Path.Name}"; 143 | Logger.LogMessage($"{msg}"); 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Files.FileSystems.Physical/Utilities/FilePolyfills.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.Physical.Utilities 2 | { 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | internal static class FilePolyfills 9 | { 10 | #if NETSTANDARD2_0 11 | // Implementing the following methods to really be async is harder one might think. 12 | // See for example the implementation of ReadAllBytesAsync: 13 | // https://github.com/dotnet/runtime/blob/f30675618fc379e112376acc6f1efa53733ee881/src/libraries/System.IO.FileSystem/src/System/IO/File.cs#L762 14 | // 15 | // Since all of these methods are going to run on a Task anyway due to how 16 | // PhysicalStorageFile/PhysicalStorageFolder are implemented, we can get away with 17 | // simply running these methods synchronously. This prevents a second thread from being 18 | // blocked for no reason. 19 | // If we are running on a TFM which supports async, go with that, of course. 20 | 21 | internal static Task ReadAllBytesMaybeAsync(string path, CancellationToken cancellationToken) 22 | { 23 | cancellationToken.ThrowIfCancellationRequested(); 24 | return Task.FromResult(File.ReadAllBytes(path)); 25 | } 26 | 27 | internal static Task WriteAllBytesMaybeAsync(string path, byte[] bytes, CancellationToken cancellationToken) 28 | { 29 | cancellationToken.ThrowIfCancellationRequested(); 30 | File.WriteAllBytes(path, bytes); 31 | return Task.CompletedTask; 32 | } 33 | 34 | internal static Task ReadAllTextMaybeAsync(string path, CancellationToken cancellationToken) 35 | { 36 | cancellationToken.ThrowIfCancellationRequested(); 37 | return Task.FromResult(File.ReadAllText(path)); 38 | } 39 | 40 | internal static Task ReadAllTextMaybeAsync(string path, Encoding encoding, CancellationToken cancellationToken) 41 | { 42 | cancellationToken.ThrowIfCancellationRequested(); 43 | return Task.FromResult(File.ReadAllText(path, encoding)); 44 | } 45 | 46 | internal static Task WriteAllTextMaybeAsync(string path, string contents, CancellationToken cancellationToken) 47 | { 48 | cancellationToken.ThrowIfCancellationRequested(); 49 | File.WriteAllText(path, contents); 50 | return Task.CompletedTask; 51 | } 52 | 53 | internal static Task WriteAllTextMaybeAsync(string path, string contents, Encoding encoding, CancellationToken cancellationToken) 54 | { 55 | cancellationToken.ThrowIfCancellationRequested(); 56 | File.WriteAllText(path, contents, encoding); 57 | return Task.CompletedTask; 58 | } 59 | #else 60 | internal static Task ReadAllBytesMaybeAsync(string path, CancellationToken cancellationToken) 61 | { 62 | return File.ReadAllBytesAsync(path, cancellationToken); 63 | } 64 | 65 | internal static Task WriteAllBytesMaybeAsync(string path, byte[] bytes, CancellationToken cancellationToken) 66 | { 67 | return File.WriteAllBytesAsync(path, bytes, cancellationToken); 68 | } 69 | 70 | internal static Task ReadAllTextMaybeAsync(string path, CancellationToken cancellationToken) 71 | { 72 | return File.ReadAllTextAsync(path, cancellationToken); 73 | } 74 | 75 | internal static Task ReadAllTextMaybeAsync(string path, Encoding encoding, CancellationToken cancellationToken) 76 | { 77 | return File.ReadAllTextAsync(path, encoding, cancellationToken); 78 | } 79 | 80 | internal static Task WriteAllTextMaybeAsync(string path, string contents, CancellationToken cancellationToken) 81 | { 82 | return File.WriteAllTextAsync(path, contents, cancellationToken); 83 | } 84 | 85 | internal static Task WriteAllTextMaybeAsync(string path, string contents, Encoding encoding, CancellationToken cancellationToken) 86 | { 87 | return File.WriteAllTextAsync(path, contents, encoding, cancellationToken); 88 | } 89 | #endif 90 | 91 | #if NETSTANDARD2_1 || NETCOREAPP2_2 || NETCOREAPP2_1 || NETCOREAPP2_0 || NETSTANDARD2_0 92 | internal static void Move(string sourceFileName, string destFileName, bool overwrite) 93 | { 94 | if (overwrite) 95 | { 96 | try 97 | { 98 | // We are mocking overwrite support by simply deleting any existing destination file. 99 | // But caution: 100 | // If sourceFileName and destFileName point to the same file, we would, by default, 101 | // delete the source file here before failing with a FileNotFoundException at the Move call later. 102 | // The file would then be unrecoverable. 103 | // This must obviously be avoided. 104 | // We are doing this by locking the file with a stream before deleting. 105 | // If source == dest, deleting the file will fail because of the open stream 106 | // and the Move call below will throw an appropriate IOException. 107 | using (var lockSrcStream = File.Open(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.None)) 108 | { 109 | File.Delete(destFileName); 110 | } 111 | } 112 | catch 113 | { 114 | // We are okay with Delete failing for two reasons: 115 | // 1) Depending on the error, Move might still succeed. 116 | // 2) We prefer exceptions (like for invalid paths) to be thrown by 117 | // Move, since the messages might be better (e.g. for paramNames in ArgumentExceptions). 118 | // Furthermore, there might be functional differences between Delete and Move 119 | // regarding error handling/error severity. 120 | } 121 | } 122 | 123 | File.Move(sourceFileName, destFileName); 124 | } 125 | #else 126 | internal static void Move(string sourceFileName, string destFileName, bool overwrite) 127 | { 128 | File.Move(sourceFileName, destFileName, overwrite); 129 | } 130 | #endif 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Files.FileSystems.InMemory/FsTree/FsDataStorage.cs: -------------------------------------------------------------------------------- 1 | namespace Files.FileSystems.InMemory.FsTree 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using Files.Shared; 6 | 7 | /// 8 | /// The InMemoryFileSystem is, like a real file system, based on a file/folder tree structure. 9 | /// This class is the entry point to these trees, i.e. it stores and manages the root nodes. 10 | /// In addition, it provides methods for quickly finding and interacting with specific nodes. 11 | /// 12 | /// This class (aswell as the associated node classes) is not doing any thread synchronization. 13 | /// Instead, the user must ensure that the FS tree is only accessed synchronously. 14 | /// 15 | internal sealed class FsDataStorage 16 | { 17 | private readonly Dictionary _allNodes; 18 | private readonly List _rootFolderNodes; 19 | 20 | /// 21 | /// Gets a dictionary which tracks all available file/folder nodes. 22 | /// These nodes are uniquely identified via their path. For efficient access, that path 23 | /// is used as the dictionary's key. This enables not having to search the entire 24 | /// tree when looking for a node at a specific path. 25 | /// 26 | public IReadOnlyDictionary AllNodes => _allNodes; 27 | 28 | /// 29 | /// Gets a list of all tracked root folder nodes, i.e. those folder nodes which don't 30 | /// have a parent node. 31 | /// 32 | public IReadOnlyList RootFolderNodes => _rootFolderNodes; 33 | 34 | public FsDataStorage(IEqualityComparer pathComparer) 35 | { 36 | _allNodes = new Dictionary(pathComparer); 37 | _rootFolderNodes = new List(); 38 | } 39 | 40 | public void RegisterNode(ElementNode node) 41 | { 42 | EnsureNoConflictingNodeExists(node.Path); 43 | _allNodes.Add(node.Path.FullPath, node); 44 | 45 | if (node is FolderNode folderNode && folderNode.Parent is null) 46 | { 47 | _rootFolderNodes.Add(folderNode); 48 | } 49 | } 50 | 51 | public void UnregisterNode(ElementNode node) 52 | { 53 | _allNodes.Remove(node.Path.FullPath); 54 | 55 | if (node is FolderNode folderNode) 56 | { 57 | _rootFolderNodes.Remove(folderNode); 58 | } 59 | } 60 | 61 | public bool IsSameElement(StoragePath path1, StoragePath path2) => 62 | ReferenceEquals(TryGetElementNode(path1), TryGetElementNode(path2)); 63 | 64 | public bool HasFileNode(StoragePath path) => 65 | TryGetFileNode(path) is not null; 66 | 67 | public bool HasFolderNode(StoragePath path) => 68 | TryGetFolderNode(path) is not null; 69 | 70 | public bool HasElementNode(StoragePath path) => 71 | TryGetElementNode(path) is not null; 72 | 73 | public FolderNode GetParentNodeAndRequirePathToHaveParent(StoragePath path) => 74 | GetParentNode(path) ?? throw new IOException(ExceptionStrings.StoragePath.PathHasNoParent()); 75 | 76 | public FolderNode? GetParentNode(StoragePath path) => 77 | path.FullPath.Parent is null ? null : GetFolderNode(path.FullPath.Parent); 78 | 79 | public FileNode GetFileNode(StoragePath path) 80 | { 81 | var fileNode = TryGetFileNodeAndThrowOnConflictingFolder(path); 82 | 83 | if (fileNode is null) 84 | { 85 | if (path.FullPath.Parent is not null && HasFolderNode(path.FullPath.Parent)) 86 | { 87 | throw new FileNotFoundException(ExceptionStrings.StorageFile.NotFound(path)); 88 | } 89 | else 90 | { 91 | throw new DirectoryNotFoundException(ExceptionStrings.StorageFile.ParentNotFound(path)); 92 | } 93 | } 94 | 95 | return fileNode; 96 | } 97 | 98 | public FolderNode GetFolderNode(StoragePath path) 99 | { 100 | var folderNode = TryGetFolderNodeAndThrowOnConflictingFile(path); 101 | 102 | if (folderNode is null) 103 | { 104 | if (path.FullPath.Parent is not null && HasFolderNode(path.FullPath.Parent)) 105 | { 106 | throw new DirectoryNotFoundException(ExceptionStrings.StorageFolder.NotFound(path)); 107 | } 108 | else 109 | { 110 | throw new DirectoryNotFoundException(ExceptionStrings.StorageFolder.ParentNotFound(path)); 111 | } 112 | } 113 | 114 | return folderNode; 115 | } 116 | 117 | public FileNode? TryGetFileNodeAndThrowOnConflictingFolder(StoragePath path) 118 | { 119 | EnsureNoConflictingFolderNodeExistsAt(path); 120 | return TryGetFileNode(path); 121 | } 122 | 123 | public FolderNode? TryGetFolderNodeAndThrowOnConflictingFile(StoragePath path) 124 | { 125 | EnsureNoConflictingFileNodeExistsAt(path); 126 | return TryGetFolderNode(path); 127 | } 128 | 129 | public FileNode? TryGetFileNode(StoragePath path) => 130 | TryGetElementNode(path) as FileNode; 131 | 132 | public FolderNode? TryGetFolderNode(StoragePath path) => 133 | TryGetElementNode(path) as FolderNode; 134 | 135 | public ElementNode? TryGetElementNode(StoragePath path) => 136 | _allNodes.TryGetValue(path.FullPath, out var node) ? node : null; 137 | 138 | public void EnsureNoConflictingNodeExists(StoragePath path) 139 | { 140 | EnsureNoConflictingFileNodeExistsAt(path); 141 | EnsureNoConflictingFolderNodeExistsAt(path); 142 | } 143 | 144 | private void EnsureNoConflictingFileNodeExistsAt(StoragePath path) 145 | { 146 | if (HasFileNode(path)) 147 | { 148 | throw new IOException(ExceptionStrings.StorageFolder.ConflictingFileExistsAtFolderLocation()); 149 | } 150 | } 151 | 152 | private void EnsureNoConflictingFolderNodeExistsAt(StoragePath path) 153 | { 154 | if (HasFolderNode(path)) 155 | { 156 | throw new IOException(ExceptionStrings.StorageFile.ConflictingFolderExistsAtFileLocation()); 157 | } 158 | } 159 | } 160 | } 161 | --------------------------------------------------------------------------------