FileSystemHelper.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using MediaBrowser.Model.IO;
  6. using Microsoft.Extensions.Logging;
  7. namespace MediaBrowser.Controller.IO;
  8. /// <summary>
  9. /// Helper methods for file system management.
  10. /// </summary>
  11. public static class FileSystemHelper
  12. {
  13. /// <summary>
  14. /// Deletes the file.
  15. /// </summary>
  16. /// <param name="fileSystem">The fileSystem.</param>
  17. /// <param name="path">The path.</param>
  18. /// <param name="logger">The logger.</param>
  19. public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
  20. {
  21. try
  22. {
  23. fileSystem.DeleteFile(path);
  24. }
  25. catch (UnauthorizedAccessException ex)
  26. {
  27. logger.LogError(ex, "Error deleting file {Path}", path);
  28. }
  29. catch (IOException ex)
  30. {
  31. logger.LogError(ex, "Error deleting file {Path}", path);
  32. }
  33. }
  34. /// <summary>
  35. /// Recursively delete empty folders.
  36. /// </summary>
  37. /// <param name="fileSystem">The fileSystem.</param>
  38. /// <param name="path">The path.</param>
  39. /// <param name="logger">The logger.</param>
  40. public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
  41. {
  42. foreach (var directory in fileSystem.GetDirectoryPaths(path))
  43. {
  44. DeleteEmptyFolders(fileSystem, directory, logger);
  45. if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
  46. {
  47. try
  48. {
  49. Directory.Delete(directory, false);
  50. }
  51. catch (UnauthorizedAccessException ex)
  52. {
  53. logger.LogError(ex, "Error deleting directory {Path}", directory);
  54. }
  55. catch (IOException ex)
  56. {
  57. logger.LogError(ex, "Error deleting directory {Path}", directory);
  58. }
  59. }
  60. }
  61. }
  62. /// <summary>
  63. /// Resolves a single link hop for the specified path.
  64. /// </summary>
  65. /// <remarks>
  66. /// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
  67. /// </remarks>
  68. /// <param name="path">The file path to resolve.</param>
  69. /// <returns>
  70. /// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
  71. /// </returns>
  72. private static FileInfo? Resolve(string path)
  73. {
  74. try
  75. {
  76. return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
  77. }
  78. catch (IOException)
  79. {
  80. // Filesystem doesn't support links (e.g., exFAT).
  81. return null;
  82. }
  83. }
  84. /// <summary>
  85. /// Gets the target of the specified file link.
  86. /// </summary>
  87. /// <remarks>
  88. /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
  89. /// </remarks>
  90. /// <param name="linkPath">The path of the file link.</param>
  91. /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
  92. /// <returns>
  93. /// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
  94. /// </returns>
  95. public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
  96. {
  97. // Check if the file exists so the native resolve handler won't throw at us.
  98. if (!File.Exists(linkPath))
  99. {
  100. return null;
  101. }
  102. if (!returnFinalTarget)
  103. {
  104. return Resolve(linkPath);
  105. }
  106. var targetInfo = Resolve(linkPath);
  107. if (targetInfo is null || !targetInfo.Exists)
  108. {
  109. return targetInfo;
  110. }
  111. var currentPath = targetInfo.FullName;
  112. var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
  113. while (true)
  114. {
  115. var linkInfo = Resolve(currentPath);
  116. if (linkInfo is null)
  117. {
  118. break;
  119. }
  120. var targetPath = linkInfo.FullName;
  121. // If an infinite loop is detected, return the file info for the
  122. // first link in the loop we encountered.
  123. if (!visited.Add(targetPath))
  124. {
  125. return new FileInfo(targetPath);
  126. }
  127. targetInfo = linkInfo;
  128. currentPath = targetPath;
  129. // Exit if the target doesn't exist, so the native resolve handler won't throw at us.
  130. if (!targetInfo.Exists)
  131. {
  132. break;
  133. }
  134. }
  135. return targetInfo;
  136. }
  137. /// <summary>
  138. /// Gets the target of the specified file link.
  139. /// </summary>
  140. /// <remarks>
  141. /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
  142. /// </remarks>
  143. /// <param name="fileInfo">The file info of the file link.</param>
  144. /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
  145. /// <returns>
  146. /// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
  147. /// </returns>
  148. public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
  149. {
  150. ArgumentNullException.ThrowIfNull(fileInfo);
  151. return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
  152. }
  153. }