using Microsoft.Extensions.FileProviders.Physical; using System.IO; namespace Finch.FileStorage; public class PhysicalFileSystem : IFileSystem { readonly string _root; public PhysicalFileSystem(string root) { _root = root; } /// public virtual string Map(string path) { return ResolvePath(path); } /// public virtual string MapToPublicPath(string path) { return path.EnsureStartsWith('/'); } /// public virtual Task Exists(string path, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); return Task.FromResult(File.Exists(resolvedPath) || Directory.Exists(resolvedPath)); } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not get file exists info for path '{path}'", ex); } } /// public virtual Task GetFileInfo(string path, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); PhysicalFileInfo fileInfo = new(new FileInfo(resolvedPath)); if (!fileInfo.Exists) { return Task.FromResult(default); } return Task.FromResult(new PhysicalFileMeta(fileInfo, path)); } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not get file info for path '{path}'", ex); } } /// public virtual Task StreamFile(string path, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); if (resolvedPath.IsNullOrEmpty() || !File.Exists(resolvedPath)) { throw new FileSystemException($"The path '{path}' does not exist in the file system"); } return Task.FromResult(File.OpenRead(resolvedPath)); } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not get file stream for path '{path}'", ex); } } /// public virtual async Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); if ((!overwrite && File.Exists(resolvedPath)) || Directory.Exists(resolvedPath)) { throw new FileSystemException($"A file/directory at the path '{path}' already existsin the file system"); } string directoryPath = Path.GetDirectoryName(resolvedPath); Directory.CreateDirectory(directoryPath); FileInfo fileInfo = new(resolvedPath); using var outputStream = fileInfo.Create(); await fileStream.CopyToAsync(outputStream, cancellationToken); } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not get file stream for path '{path}'", ex); } } /// public virtual Task DeleteFile(string path, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); if (!File.Exists(resolvedPath)) { return Task.CompletedTask; } File.Delete(resolvedPath); return Task.CompletedTask; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not delete file for path '{path}'", ex); } } /// public virtual Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default) { try { string resolvedOldPath = ResolvePath(oldPath); string resolvedNewPath = ResolvePath(newPath); if (!File.Exists(resolvedOldPath)) { throw new FileSystemException($"The path '{oldPath}' does not exist in the file system"); } if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath)) { throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system"); } File.Move(resolvedOldPath, resolvedNewPath); return Task.CompletedTask; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not move file from path '{oldPath}' to '{newPath}'", ex); } } /// public virtual Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default) { try { string resolvedOldPath = ResolvePath(oldPath); string resolvedNewPath = ResolvePath(newPath); if (!File.Exists(resolvedOldPath)) { throw new FileSystemException($"The path '{oldPath}' does not exist in the file system"); } if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath)) { throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system"); } File.Copy(resolvedOldPath, resolvedNewPath); return Task.CompletedTask; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not copy file from path '{oldPath}' to '{newPath}'", ex); } } /// public virtual Task GetDirectoryInfo(string path, CancellationToken cancellationToken = default) => GetFileInfo(path, cancellationToken); /// public virtual IEnumerable GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); List results = []; SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; if (!Directory.Exists(resolvedPath)) { return results; } results.AddRange(Directory.GetDirectories(resolvedPath, "*", searchOption).Select(f => { PhysicalDirectoryInfo fileSystemInfo = new(new DirectoryInfo(f)); return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length))); })); results.AddRange(Directory.GetFiles(resolvedPath, "*", searchOption).Select(f => { PhysicalFileInfo fileSystemInfo = new(new FileInfo(f)); return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length))); })); return results; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not get directory content for path '{path}'", ex); } } /// public virtual Task CreateDirectory(string path, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); if (File.Exists(resolvedPath)) { throw new FileSystemException($"A file at the path '{path}' already exists in the file system"); } if (!Directory.Exists(resolvedPath)) { Directory.CreateDirectory(resolvedPath); } return Task.CompletedTask; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not create directory at path '{path}'", ex); } } /// public virtual Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default) { try { string resolvedPath = ResolvePath(path); if (!Directory.Exists(resolvedPath)) { return Task.CompletedTask; } Directory.Delete(resolvedPath, recursive); return Task.CompletedTask; } catch (Exception ex) when (ex is not FileSystemException) { throw new FileSystemException($"Could not delete directory at path '{path}'", ex); } } /// /// Creates a fully-usable absolte path from the given path /// protected virtual string ResolvePath(string path) { try { if (path.IsNullOrWhiteSpace()) { return _root; } // normalize path path = path.Replace('\\', '/').FullTrim().Trim('/'); string physicalPath = Path.Combine(_root, path); string rootPath = Path.GetFullPath(_root); // do not allow paths which are outside this file system // the boundaries are set be the basePath which is assigned in the file system constructor if (!Path.GetFullPath(physicalPath).StartsWith(rootPath, StringComparison.InvariantCultureIgnoreCase)) { throw new FileSystemException($"The path '{path}' is outside the file system"); } return physicalPath; } catch (FileSystemException) { throw; } catch (Exception ex) { throw new FileSystemException($"Could not resolve path '{path}'", ex); } } }