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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Auditor.sln
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D09148E2-D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "src\test\Tests\Tests.csproj", "{896C7174-7249-4CD8-A7F1-58F27D637BF1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "src\Storage\Storage.csproj", "{61852A44-BC38-4CF4-8BA9-AF5335784ACB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand DownExpand Up@@ -48,6 +50,18 @@ Global
{896C7174-7249-4CD8-A7F1-58F27D637BF1}.Release|x64.Build.0 = Release|Any CPU
{896C7174-7249-4CD8-A7F1-58F27D637BF1}.Release|x86.ActiveCfg = Release|Any CPU
{896C7174-7249-4CD8-A7F1-58F27D637BF1}.Release|x86.Build.0 = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|x64.ActiveCfg = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|x64.Build.0 = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|x86.ActiveCfg = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Debug|x86.Build.0 = Debug|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|Any CPU.Build.0 = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|x64.ActiveCfg = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|x64.Build.0 = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|x86.ActiveCfg = Release|Any CPU
{61852A44-BC38-4CF4-8BA9-AF5335784ACB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1D5C0BDD-9D0A-4849-86E0-FE904F9A1F3E} = {2D90C96D-0481-42E2-9526-6342B17FB37C}
Expand Down
25 changes: 25 additions & 0 deletions src/Storage/AsyncEnumerable.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace DotNetRu.Auditor.Storage
{
internal static class AsyncEnumerable
{
public static IAsyncEnumerable<T> Empty<T>()
{
return EmptyAsyncEnumerable<T>.Instance;
}

private class EmptyAsyncEnumerable<T> : IAsyncEnumerable<T>
{
public static readonly IAsyncEnumerable<T> Instance = new EmptyAsyncEnumerable<T>();

public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = new())
{
await Task.Yield();
yield break;
}
}
}
}
34 changes: 34 additions & 0 deletions src/Storage/FileSystem/FileSystemEntry.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
using System;
using System.IO;

namespace DotNetRu.Auditor.Storage.FileSystem
{
public abstract class FileSystemEntry : IFileSystemEntry
{
protected FileSystemEntry(string fullName, bool exists)
{
FullName = Path.GetFullPath(fullName);
Name = Path.GetFileName(FullName);
Exists = exists;
}

public string Name { get; }

public string FullName { get; }

public bool Exists { get; }

protected string GetFullPath(string subPath)
{
var childrenPath = Path.Combine(FullName, subPath);
var childrenFullPath = Path.GetFullPath(childrenPath);

if (!childrenFullPath.StartsWith(FullName, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException($"Children path «{childrenFullPath}» should be under the root «{FullName}»");
}

return childrenFullPath;
}
}
}
25 changes: 25 additions & 0 deletions src/Storage/FileSystem/IDirectory.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DotNetRu.Auditor.Storage.FileSystem
{
/// <remarks>
/// In fact, most operating systems do not support asynchronous enumeration of file system contents.
/// This is why there is no asynchronous API in the official BCL. But we have a Blazor consumer that
/// throws an exception when using synchronization operations, and a GutHub HTTP API implementation
/// that can perform real asynchronous operations.
///
/// To respect I/O operations, we decided to use asynchronous abstractions. But we assume that real
/// asynchronous operations will be rare, so we decided to use ValueTask.
/// </remarks>>
public interface IDirectory : IFileSystemEntry
{
ValueTask<IDirectory> GetDirectoryInfoAsync(string subPath);

ValueTask<IFile> GetFileInfoAsync(string subPath);

IAsyncEnumerable<IDirectory> EnumerateDirectoriesAsync();

IAsyncEnumerable<IFile> EnumerateFilesAsync();
}
}
12 changes: 12 additions & 0 deletions src/Storage/FileSystem/IFile.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
using System.IO;
using System.Threading.Tasks;

Comment thread
kulakovt marked this conversation as resolved.
namespace DotNetRu.Auditor.Storage.FileSystem
{
public interface IFile : IFileSystemEntry
{
ValueTask<Stream> OpenForReadAsync();

ValueTask<Stream> OpenForWriteAsync();
}
}
6 changes: 6 additions & 0 deletions src/Storage/FileSystem/IFileSystem.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
namespace DotNetRu.Auditor.Storage.FileSystem
{
public interface IFileSystem : IDirectory
{
}
}
11 changes: 11 additions & 0 deletions src/Storage/FileSystem/IFileSystemEntry.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
namespace DotNetRu.Auditor.Storage.FileSystem
{
public interface IFileSystemEntry
{
string Name { get; }

string FullName { get; }

bool Exists { get; }
}
}
23 changes: 23 additions & 0 deletions src/Storage/FileSystem/NotFoundDirectory.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DotNetRu.Auditor.Storage.FileSystem
{
public class NotFoundDirectory : FileSystemEntry, IDirectory
{
public NotFoundDirectory(string fullName)
: base(fullName, false)
{
}

public static ValueTask<IDirectory> ToTask(string fullName) => ValueTask.FromResult<IDirectory>(new NotFoundDirectory(fullName));

public ValueTask<IDirectory> GetDirectoryInfoAsync(string subPath) => ToTask(subPath);

public ValueTask<IFile> GetFileInfoAsync(string subPath) => NotFoundFile.ToTask(subPath);

public IAsyncEnumerable<IDirectory> EnumerateDirectoriesAsync() => AsyncEnumerable.Empty<IDirectory>();

public IAsyncEnumerable<IFile> EnumerateFilesAsync() => AsyncEnumerable.Empty<IFile>();
}
}
22 changes: 22 additions & 0 deletions src/Storage/FileSystem/NotFoundFile.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
using System;
using System.IO;
using System.Threading.Tasks;

namespace DotNetRu.Auditor.Storage.FileSystem
{
public class NotFoundFile : FileSystemEntry, IFile
{
public NotFoundFile(string fullName)
: base(fullName, false)
{
}

public static ValueTask<IFile> ToTask(string fullName) => ValueTask.FromResult<IFile>(new NotFoundFile(fullName));

public static Exception ToException(string fullName) => new FileNotFoundException($"Could not find file: {fullName}", fullName);

public ValueTask<Stream> OpenForReadAsync() => throw ToException(FullName);

public ValueTask<Stream> OpenForWriteAsync() => throw ToException(FullName);
}
}
14 changes: 14 additions & 0 deletions src/Storage/FileSystem/NullFileSystem.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
using System;

namespace DotNetRu.Auditor.Storage.FileSystem
{
public class NullFileSystem : NotFoundDirectory, IFileSystem
{
public static readonly NullFileSystem Instance = new NullFileSystem();

public NullFileSystem()
: base(String.Empty)
{
}
}
}
68 changes: 68 additions & 0 deletions src/Storage/FileSystem/Physical/PhysicalDirectory.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace DotNetRu.Auditor.Storage.FileSystem.Physical
{
public class PhysicalDirectory : FileSystemEntry, IDirectory
{
public PhysicalDirectory(string fullName)
: base(fullName, Directory.Exists(fullName))
{
}

public ValueTask<IDirectory> GetDirectoryInfoAsync(string subPath)
{
if (!Exists)
{
return NotFoundDirectory.ToTask(subPath);
}

var fullDirectoryName = GetFullPath(subPath);
var directory = new PhysicalDirectory(fullDirectoryName);
return ValueTask.FromResult<IDirectory>(directory);
}

public ValueTask<IFile> GetFileInfoAsync(string subPath)
{
if (!Exists)
{
return NotFoundFile.ToTask(subPath);
}

var fullFileName = GetFullPath(subPath);
var file = new PhysicalFile(fullFileName);
return ValueTask.FromResult<IFile>(file);
}

public async IAsyncEnumerable<IDirectory> EnumerateDirectoriesAsync()
{
if (!Exists)
{
yield break;
}

await Task.Yield();

foreach (var directoryFullName in Directory.EnumerateDirectories(FullName))
{
yield return new PhysicalDirectory(directoryFullName);
}
}

public async IAsyncEnumerable<IFile> EnumerateFilesAsync()
{
if (!Exists)
{
yield break;
}

await Task.Yield();

foreach (var fileFullName in Directory.EnumerateFiles(FullName))
{
yield return new PhysicalFile(fileFullName);
}
}
}
}
35 changes: 35 additions & 0 deletions src/Storage/FileSystem/Physical/PhysicalFile.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
using System.IO;
using System.Threading.Tasks;

Comment thread
kulakovt marked this conversation as resolved.
namespace DotNetRu.Auditor.Storage.FileSystem.Physical
{
public sealed class PhysicalFile : FileSystemEntry, IFile
{
public PhysicalFile(string fullName)
: base(fullName, File.Exists(fullName))
{
}

public ValueTask<Stream> OpenForReadAsync()
{
if (!Exists)
{
throw NotFoundFile.ToException(FullName);
}

var inputStream = File.OpenRead(FullName);
return ValueTask.FromResult<Stream>(inputStream);
}

public ValueTask<Stream> OpenForWriteAsync()
{
if (!Exists)
{
throw NotFoundFile.ToException(FullName);
}

var outputStream = File.OpenWrite(FullName);
return ValueTask.FromResult<Stream>(outputStream);
}
}
}
17 changes: 17 additions & 0 deletions src/Storage/FileSystem/Physical/PhysicalFileSystem.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
using System;
using System.IO;

namespace DotNetRu.Auditor.Storage.FileSystem.Physical
{
public sealed class PhysicalFileSystem : PhysicalDirectory, IFileSystem
{
public PhysicalFileSystem(string root)
: base(root)
{
if (!Path.IsPathRooted(root))
{
throw new ArgumentException($"The path «{root}» must be absolute", nameof(root));
}
}
}
}
13 changes: 13 additions & 0 deletions src/Storage/Storage.csproj
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AssemblyName>DotNetRu.Auditor.Storage</AssemblyName>
<RootNamespace>DotNetRu.Auditor.Storage</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>

</Project>
Loading