FileRefresher.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Threading;
  6. using MediaBrowser.Controller.Configuration;
  7. using MediaBrowser.Controller.Entities;
  8. using MediaBrowser.Controller.Library;
  9. using Microsoft.Extensions.Logging;
  10. namespace Emby.Server.Implementations.IO
  11. {
  12. public class FileRefresher : IDisposable
  13. {
  14. private ILogger Logger { get; set; }
  15. private ILibraryManager LibraryManager { get; set; }
  16. private IServerConfigurationManager ConfigurationManager { get; set; }
  17. private readonly List<string> _affectedPaths = new List<string>();
  18. private Timer _timer;
  19. private readonly object _timerLock = new object();
  20. public string Path { get; private set; }
  21. public event EventHandler<EventArgs> Completed;
  22. public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
  23. {
  24. logger.LogDebug("New file refresher created for {0}", path);
  25. Path = path;
  26. ConfigurationManager = configurationManager;
  27. LibraryManager = libraryManager;
  28. Logger = logger;
  29. AddPath(path);
  30. }
  31. private void AddAffectedPath(string path)
  32. {
  33. if (string.IsNullOrEmpty(path))
  34. {
  35. throw new ArgumentNullException(nameof(path));
  36. }
  37. if (!_affectedPaths.Contains(path, StringComparer.Ordinal))
  38. {
  39. _affectedPaths.Add(path);
  40. }
  41. }
  42. public void AddPath(string path)
  43. {
  44. if (string.IsNullOrEmpty(path))
  45. {
  46. throw new ArgumentNullException(nameof(path));
  47. }
  48. lock (_timerLock)
  49. {
  50. AddAffectedPath(path);
  51. }
  52. RestartTimer();
  53. }
  54. public void RestartTimer()
  55. {
  56. if (_disposed)
  57. {
  58. return;
  59. }
  60. lock (_timerLock)
  61. {
  62. if (_disposed)
  63. {
  64. return;
  65. }
  66. if (_timer == null)
  67. {
  68. _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  69. }
  70. else
  71. {
  72. _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  73. }
  74. }
  75. }
  76. public void ResetPath(string path, string affectedFile)
  77. {
  78. lock (_timerLock)
  79. {
  80. Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
  81. Path = path;
  82. AddAffectedPath(path);
  83. if (!string.IsNullOrEmpty(affectedFile))
  84. {
  85. AddAffectedPath(affectedFile);
  86. }
  87. }
  88. RestartTimer();
  89. }
  90. private void OnTimerCallback(object state)
  91. {
  92. List<string> paths;
  93. lock (_timerLock)
  94. {
  95. paths = _affectedPaths.ToList();
  96. }
  97. Logger.LogDebug("Timer stopped.");
  98. DisposeTimer();
  99. Completed?.Invoke(this, EventArgs.Empty);
  100. try
  101. {
  102. ProcessPathChanges(paths.ToList());
  103. }
  104. catch (Exception ex)
  105. {
  106. Logger.LogError(ex, "Error processing directory changes");
  107. }
  108. }
  109. private void ProcessPathChanges(List<string> paths)
  110. {
  111. var itemsToRefresh = paths
  112. .Distinct(StringComparer.OrdinalIgnoreCase)
  113. .Select(GetAffectedBaseItem)
  114. .Where(item => item != null)
  115. .GroupBy(x => x.Id)
  116. .Select(x => x.First());
  117. foreach (var item in itemsToRefresh)
  118. {
  119. if (item is AggregateFolder)
  120. {
  121. continue;
  122. }
  123. Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
  124. try
  125. {
  126. item.ChangedExternally();
  127. }
  128. catch (IOException ex)
  129. {
  130. // For now swallow and log.
  131. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
  132. // Should we remove it from it's parent?
  133. Logger.LogError(ex, "Error refreshing {name}", item.Name);
  134. }
  135. catch (Exception ex)
  136. {
  137. Logger.LogError(ex, "Error refreshing {name}", item.Name);
  138. }
  139. }
  140. }
  141. /// <summary>
  142. /// Gets the affected base item.
  143. /// </summary>
  144. /// <param name="path">The path.</param>
  145. /// <returns>BaseItem.</returns>
  146. private BaseItem GetAffectedBaseItem(string path)
  147. {
  148. BaseItem item = null;
  149. while (item == null && !string.IsNullOrEmpty(path))
  150. {
  151. item = LibraryManager.FindByPath(path, null);
  152. path = System.IO.Path.GetDirectoryName(path);
  153. }
  154. if (item != null)
  155. {
  156. // If the item has been deleted find the first valid parent that still exists
  157. while (!Directory.Exists(item.Path) && !File.Exists(item.Path))
  158. {
  159. item = item.GetOwner() ?? item.GetParent();
  160. if (item == null)
  161. {
  162. break;
  163. }
  164. }
  165. }
  166. return item;
  167. }
  168. private void DisposeTimer()
  169. {
  170. lock (_timerLock)
  171. {
  172. if (_timer != null)
  173. {
  174. _timer.Dispose();
  175. _timer = null;
  176. }
  177. }
  178. }
  179. private bool _disposed;
  180. public void Dispose()
  181. {
  182. _disposed = true;
  183. DisposeTimer();
  184. }
  185. }
  186. }