2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="FileSystemWatcherAdapter.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
43 using System.Diagnostics;
\r
44 using System.Diagnostics.Contracts;
\r
46 using System.Reflection;
\r
47 using System.Threading.Tasks;
\r
48 using Pithos.Interfaces;
\r
51 namespace Pithos.Core.Agents
\r
56 /// Wraps a FileSystemWatcher and raises Move and child object events
\r
58 public class FileSystemWatcherAdapter
\r
60 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
63 public event FileSystemEventHandler Changed;
\r
64 public event FileSystemEventHandler Created;
\r
65 public event FileSystemEventHandler Deleted;
\r
66 //public event RenamedEventHandler Renamed;
\r
67 public event MovedEventHandler Moved;
\r
69 public FileSystemWatcherAdapter(FileSystemWatcher watcher)
\r
72 throw new ArgumentNullException("watcher");
\r
73 Contract.EndContractBlock();
\r
75 watcher.Changed += OnChangeOrCreate;
\r
76 watcher.Created += OnChangeOrCreate;
\r
77 watcher.Deleted += OnDeleted;
\r
78 watcher.Renamed += OnRename;
\r
79 watcher.Error += OnError;
\r
82 private void OnError(object sender, ErrorEventArgs e)
\r
84 var error = e.GetException();
\r
85 Log.Error("FSW error",error);
\r
88 private string _cachedDeletedFullPath;
\r
89 private string CachedDeletedFullPath
\r
91 get { return _cachedDeletedFullPath; }
\r
94 Debug.Assert(Path.IsPathRooted(value));
\r
95 if (!Path.IsPathRooted(value))
\r
96 Log.WarnFormat("Storing a relative CachedDeletedFullPath: {0}",value);
\r
97 _cachedDeletedFullPath = value;
\r
102 /// Clears any cached deleted file path
\r
105 /// This method was added to bypass the null checking in the property's setter
\r
107 private void ClearCachedDeletedPath()
\r
109 _cachedDeletedFullPath = null;
\r
113 private const int PropagateDelay = 10;
\r
115 private void OnDeleted(object sender, FileSystemEventArgs e)
\r
117 if (sender == null)
\r
118 throw new ArgumentNullException("sender");
\r
119 if (e.ChangeType != WatcherChangeTypes.Deleted)
\r
120 throw new ArgumentException("e");
\r
121 if (string.IsNullOrWhiteSpace(e.FullPath))
\r
122 throw new ArgumentException("e");
\r
123 Contract.EndContractBlock();
\r
125 TaskEx.Run(() => InnerOnDeleted(sender, e));
\r
128 private void InnerOnDeleted(object sender, FileSystemEventArgs e)
\r
130 //Handle any previously deleted event
\r
131 if (Log.IsDebugEnabled)
\r
132 Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
\r
133 PropagateCachedDeleted(sender);
\r
135 //A delete event may be an actual delete event or the first event in a move action.
\r
136 //To decide which action occured, we need to wait for the next action, so
\r
137 //we store the file path and return .
\r
138 //A delete action will not be followed by any other event, so we need to add a watchdog
\r
139 //that will declare a Delete action after a short amount of time
\r
141 //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents
\r
142 // as this is actually a MOVE operation
\r
143 //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself
\r
144 CachedDeletedFullPath = e.FullPath;
\r
146 //TODO: This requires synchronization of the CachedDeletedFullPath field
\r
147 //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
\r
148 //Maybe, use a timer instead of a task
\r
150 TaskEx.Delay(PropagateDelay).ContinueWith(t =>
\r
152 var myPath = e.FullPath;
\r
153 if (CachedDeletedFullPath == myPath)
\r
154 PropagateCachedDeleted(sender);
\r
158 private void OnRename(object sender, RenamedEventArgs e)
\r
160 if (sender == null)
\r
161 throw new ArgumentNullException("sender");
\r
162 Contract.EndContractBlock();
\r
164 TaskEx.Run(() => InnerRename(sender, e));
\r
167 private void InnerRename(object sender, RenamedEventArgs e)
\r
171 if (Log.IsDebugEnabled)
\r
172 Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
\r
173 //Propagate any previous cached delete event
\r
174 PropagateCachedDeleted(sender);
\r
181 Moved(sender, new MovedEventArgs(Path.GetDirectoryName(e.FullPath),Path.GetFileName(e.Name),Path.GetDirectoryName(e.OldFullPath),Path.GetFileName(e.OldName)));
\r
183 catch (Exception exc)
\r
185 Log.Error("Rename event error", exc);
\r
189 var directory = new DirectoryInfo(e.FullPath);
\r
190 if (directory.Exists)
\r
192 var newDirectory = e.FullPath;
\r
193 var oldDirectory = e.OldFullPath;
\r
197 directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
\r
199 var newChildDirectory = Path.GetDirectoryName(child.FullName);
\r
201 var relativePath = child.AsRelativeTo(newDirectory);
\r
202 var relativeFolder = Path.GetDirectoryName(relativePath);
\r
203 var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
\r
205 new MovedEventArgs(newChildDirectory, child.Name, oldChildDirectory,
\r
213 ClearCachedDeletedPath();
\r
217 private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
\r
219 if (sender == null)
\r
220 throw new ArgumentNullException("sender");
\r
221 if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
\r
222 throw new ArgumentException("e");
\r
223 Contract.EndContractBlock();
\r
224 TaskEx.Run(() => InnerChangeOrCreated(sender, e));
\r
228 private void InnerChangeOrCreated(object sender, FileSystemEventArgs e)
\r
232 if (Log.IsDebugEnabled)
\r
233 Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath);
\r
234 //A Move action results in a sequence of a Delete and a Create or Change event
\r
235 //If the actual action is a Move, raise a Move event instead of the actual event
\r
236 if (HandleMoved(sender, e))
\r
239 //Otherwise, propagate the Delete event if it exists
\r
240 PropagateCachedDeleted(sender);
\r
241 //and propagate the actual event
\r
242 var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
\r
244 if (actualEvent != null)
\r
246 actualEvent(sender, e);
\r
247 //For Folders, raise Created events for all children
\r
248 RaiseCreatedForChildren(sender, e);
\r
253 //Finally, make sure the cached path is cleared
\r
254 ClearCachedDeletedPath();
\r
259 private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e)
\r
262 throw new ArgumentNullException("sender");
\r
264 throw new ArgumentNullException("e");
\r
265 Contract.EndContractBlock();
\r
267 if (e.ChangeType != WatcherChangeTypes.Created)
\r
269 var dir= new DirectoryInfo(e.FullPath);
\r
270 //Skip if this is not a folder
\r
275 foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories))
\r
277 var path = Path.GetDirectoryName(info.FullName);
\r
278 Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name));
\r
281 catch (IOException)
\r
284 .ContinueWith(_=>RaiseCreatedForChildren(sender,e));
\r
289 private bool HandleMoved(object sender, FileSystemEventArgs e)
\r
291 if (sender == null)
\r
292 throw new ArgumentNullException("sender");
\r
293 if (!(e.ChangeType == WatcherChangeTypes.Created ||
\r
294 e.ChangeType == WatcherChangeTypes.Changed))
\r
295 throw new ArgumentException("e");
\r
296 Contract.EndContractBlock();
\r
298 //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE
\r
299 //instead of a sequence of independent actions
\r
300 //One way to detect this would be to request that the full paths are NOT the same
\r
302 var oldName = Path.GetFileName(CachedDeletedFullPath);
\r
303 //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
\r
304 var newName = Path.GetFileName(e.Name);
\r
305 //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
\r
306 var hasMoved = (CachedDeletedFullPath != e.FullPath && oldName == newName);
\r
313 if (Log.IsDebugEnabled)
\r
314 Log.DebugFormat("Moved for [{0}]", e.FullPath);
\r
316 //If the actual action is a Move, raise a Move event instead of the actual event
\r
317 var newDirectory = Path.GetDirectoryName(e.FullPath);
\r
318 var oldDirectory = Path.GetDirectoryName(CachedDeletedFullPath);
\r
322 Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
\r
323 //If the moved item is a dictionary, we need to raise a change event for each child item
\r
324 //When a directory is moved within the same volume, Windows raises events only for the directory object,
\r
325 //not its children. This happens because the move actually changes a single directory entry. It doesn't
\r
326 //affect the entries of the children.
\r
327 var directory = new DirectoryInfo(e.FullPath);
\r
328 if (directory.Exists)
\r
330 foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
\r
332 var newChildDirectory = Path.GetDirectoryName(child.FullName);
\r
334 var relativePath=child.AsRelativeTo(newDirectory);
\r
335 var relativeFolder = Path.GetDirectoryName(relativePath);
\r
336 var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
\r
337 Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name));
\r
346 ClearCachedDeletedPath();
\r
351 private void PropagateCachedDeleted(object sender)
\r
353 if (sender == null)
\r
354 throw new ArgumentNullException("sender");
\r
355 Contract.Ensures(CachedDeletedFullPath == null);
\r
356 Contract.EndContractBlock();
\r
358 //Nothing to handle if there is no cached deleted file
\r
359 if (String.IsNullOrWhiteSpace(CachedDeletedFullPath))
\r
362 var deletedFileName = Path.GetFileName(CachedDeletedFullPath);
\r
363 var deletedFileDirectory = Path.GetDirectoryName(CachedDeletedFullPath);
\r
365 if (Log.IsDebugEnabled)
\r
366 Log.DebugFormat("Propagating delete for [{0}]", CachedDeletedFullPath);
\r
368 //Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation
\r
369 //In this case we need to raise the proper events for all child objects of the deleted directory.
\r
370 //UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted
\r
371 //This should be done at a higher level, eg by checking the stored state
\r
372 if (Deleted != null)
\r
373 Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));
\r
375 ClearCachedDeletedPath();
\r