FileRefresher.cs 6.3 KB

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