| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="DirectoryService.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Diagnostics.CodeAnalysis; |
| | | 9 | | using System.IO; |
| | | 10 | | using Microsoft.Extensions.Logging; |
| | | 11 | | using Microsoft.Extensions.Logging.Abstractions; |
| | | 12 | | |
| | | 13 | | namespace MyNet.IO; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Manages a directory root and provides helpers to create uniquely named files and sub-directories. |
| | | 17 | | /// The root path is created on demand, supports environment-variable expansion, and falls back to |
| | | 18 | | /// the system temporary directory when no path is provided. |
| | | 19 | | /// </summary> |
| | | 20 | | /// <remarks> |
| | | 21 | | /// Initializes a new instance of the <see cref="DirectoryService"/> class. |
| | | 22 | | /// Initializes a new instance of <see cref="DirectoryService"/>. |
| | | 23 | | /// </remarks> |
| | | 24 | | /// <param name="root"> |
| | | 25 | | /// The root directory path. Environment variables (e.g. <c>%TEMP%</c>) are expanded automatically. |
| | | 26 | | /// Pass an empty string or <c>null</c> to use the system temporary directory. |
| | | 27 | | /// </param> |
| | | 28 | | /// <param name="logger">Optional logger. Defaults to the application-wide logger for this class.</param> |
| | | 29 | | [SuppressMessage("Performance", "CA1823:Avoid unused private fields", Justification = "Used by LoggerMessage source gene |
| | | 30 | | public partial class DirectoryService(string root, ILogger<DirectoryService>? logger = null) : IDirectoryService |
| | | 31 | | { |
| | 24 | 32 | | private readonly ILogger<DirectoryService> _logger = logger ?? NullLogger<DirectoryService>.Instance; |
| | | 33 | | |
| | | 34 | | /// <summary> |
| | | 35 | | /// Gets the absolute root path managed by this instance. The directory is created on demand if it does not exist. |
| | | 36 | | /// </summary> |
| | 24 | 37 | | public string RootDirectory { get; } = ResolveRoot(root); |
| | | 38 | | |
| | | 39 | | /// <summary> |
| | | 40 | | /// Resolves the root directory from the supplied raw path. Falls back to the system temporary directory when <param |
| | | 41 | | /// </summary> |
| | | 42 | | /// <param name="root">The raw root path.</param> |
| | | 43 | | /// <returns>The resolved absolute root path.</returns> |
| | | 44 | | private static string ResolveRoot(string? root) |
| | | 45 | | { |
| | 24 | 46 | | var path = string.IsNullOrWhiteSpace(root) |
| | 24 | 47 | | ? Path.GetTempPath() |
| | 24 | 48 | | : Environment.ExpandEnvironmentVariables(root); |
| | | 49 | | |
| | 24 | 50 | | FileHelper.EnsureDirectoryExists(path); |
| | | 51 | | |
| | 24 | 52 | | return path; |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | /// <inheritdoc/> |
| | | 56 | | public string CreateSubDirectory(string name) |
| | | 57 | | { |
| | 3 | 58 | | if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Subdirectory name cannot be empty.", nameof(na |
| | | 59 | | |
| | 3 | 60 | | var fullPath = Path.Combine(RootDirectory, name); |
| | 3 | 61 | | Directory.CreateDirectory(fullPath); |
| | | 62 | | |
| | 3 | 63 | | return fullPath; |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <inheritdoc/> |
| | | 67 | | public string CreateFile(string? fileExtension = null, string? preferredFileName = null) |
| | | 68 | | { |
| | | 69 | | const int maxAttempts = 10; |
| | | 70 | | |
| | 18 | 71 | | for (var i = 0; i < maxAttempts; i++) |
| | | 72 | | { |
| | 9 | 73 | | var path = GetFileName(fileExtension, preferredFileName); |
| | | 74 | | |
| | 9 | 75 | | if (FileHelper.TryCreateFile(path)) |
| | 6 | 76 | | return path; |
| | | 77 | | |
| | 3 | 78 | | preferredFileName = null; |
| | | 79 | | } |
| | | 80 | | |
| | 0 | 81 | | throw new IOException("Unable to create a unique file after multiple attempts."); |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <inheritdoc/> |
| | | 85 | | public string GetFileName(string? fileExtension = null, string? preferredFileName = null) |
| | | 86 | | { |
| | 15 | 87 | | var extension = NormalizeExtension(fileExtension); |
| | | 88 | | |
| | 15 | 89 | | var baseName = string.IsNullOrWhiteSpace(preferredFileName) ? Path.GetRandomFileName() : Path.GetFileNameWithout |
| | | 90 | | |
| | 15 | 91 | | return Path.Combine(RootDirectory, Path.ChangeExtension(baseName, extension)); |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | /// <summary> |
| | | 95 | | /// Normalizes a raw file-extension token into a canonical form with a leading dot (e.g. <c>.csv</c>). |
| | | 96 | | /// Falls back to <c>.tmp</c> when both parameters are empty. |
| | | 97 | | /// </summary> |
| | | 98 | | private static string NormalizeExtension(string? extension) |
| | | 99 | | { |
| | 15 | 100 | | if (string.IsNullOrWhiteSpace(extension)) |
| | 3 | 101 | | return ".tmp"; |
| | | 102 | | |
| | 12 | 103 | | extension = extension.Trim(); |
| | | 104 | | |
| | 12 | 105 | | if (extension.StartsWith('*')) |
| | 0 | 106 | | extension = extension[1..]; |
| | | 107 | | |
| | 12 | 108 | | if (!extension.StartsWith('.')) |
| | 12 | 109 | | extension = "." + extension; |
| | | 110 | | |
| | 12 | 111 | | return extension; |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | /// <inheritdoc/> |
| | | 115 | | public void Delete() |
| | | 116 | | { |
| | | 117 | | try |
| | | 118 | | { |
| | 3 | 119 | | Directory.Delete(RootDirectory, true); |
| | 3 | 120 | | } |
| | 0 | 121 | | catch (Exception ex) |
| | | 122 | | { |
| | 0 | 123 | | LogFailedToDeleteRoot(ex); |
| | 0 | 124 | | } |
| | 3 | 125 | | } |
| | | 126 | | |
| | | 127 | | /// <inheritdoc/> |
| | | 128 | | public void Clean() |
| | | 129 | | { |
| | | 130 | | try |
| | | 131 | | { |
| | 3 | 132 | | var dir = new DirectoryInfo(RootDirectory); |
| | | 133 | | |
| | 12 | 134 | | foreach (var file in dir.GetFiles()) |
| | 3 | 135 | | TryDelete(file); |
| | | 136 | | |
| | 12 | 137 | | foreach (var subDir in dir.GetDirectories()) |
| | 3 | 138 | | TryDelete(subDir); |
| | 3 | 139 | | } |
| | 0 | 140 | | catch (Exception ex) |
| | | 141 | | { |
| | 0 | 142 | | LogFailedToClean(ex); |
| | 0 | 143 | | } |
| | 3 | 144 | | } |
| | | 145 | | |
| | | 146 | | /// <summary> |
| | | 147 | | /// Attempts to delete a directory recursively, logging a warning on failure. |
| | | 148 | | /// </summary> |
| | | 149 | | /// <param name="info">The directory to delete.</param> |
| | | 150 | | private void TryDelete(FileSystemInfo info) |
| | | 151 | | { |
| | | 152 | | try |
| | | 153 | | { |
| | 3 | 154 | | if (info is DirectoryInfo dir) |
| | 3 | 155 | | dir.Delete(true); |
| | | 156 | | else |
| | 0 | 157 | | info.Delete(); |
| | 3 | 158 | | } |
| | 0 | 159 | | catch (Exception ex) |
| | | 160 | | { |
| | 0 | 161 | | LogFailedToDeleteItem(ex); |
| | 0 | 162 | | } |
| | 3 | 163 | | } |
| | | 164 | | |
| | | 165 | | /// <summary> |
| | | 166 | | /// Attempts to delete a single file, logging a warning on failure. |
| | | 167 | | /// </summary> |
| | | 168 | | private void TryDelete(FileInfo info) |
| | | 169 | | { |
| | | 170 | | try |
| | | 171 | | { |
| | 3 | 172 | | info.Delete(); |
| | 3 | 173 | | } |
| | 0 | 174 | | catch (Exception ex) |
| | | 175 | | { |
| | 0 | 176 | | LogFailedToDeleteAFile(ex); |
| | 0 | 177 | | } |
| | 3 | 178 | | } |
| | | 179 | | |
| | | 180 | | [LoggerMessage(LogLevel.Warning, "Failed to clean directory.")] |
| | | 181 | | private partial void LogFailedToClean(Exception exception); |
| | | 182 | | |
| | | 183 | | [LoggerMessage(LogLevel.Warning, "Failed to delete root directory.")] |
| | | 184 | | private partial void LogFailedToDeleteRoot(Exception exception); |
| | | 185 | | |
| | | 186 | | [LoggerMessage(LogLevel.Warning, "Failed to delete file system item.")] |
| | | 187 | | private partial void LogFailedToDeleteItem(Exception exception); |
| | | 188 | | |
| | | 189 | | [LoggerMessage(LogLevel.Warning, "Failed to delete a file.")] |
| | | 190 | | private partial void LogFailedToDeleteAFile(Exception exception); |
| | | 191 | | } |
| | | 192 | | |