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
42 using System.Diagnostics.Contracts;
\r
44 using System.Threading.Tasks;
\r
46 namespace Pithos.Core.Agents
\r
49 using System.Collections.Generic;
\r
54 /// TODO: Update summary.
\r
56 public class FileSystemWatcherAdapter
\r
58 public event FileSystemEventHandler Changed;
\r
59 public event FileSystemEventHandler Created;
\r
60 public event FileSystemEventHandler Deleted;
\r
61 public event RenamedEventHandler Renamed;
\r
62 public event MovedEventHandler Moved;
\r
64 public FileSystemWatcherAdapter(FileSystemWatcher watcher)
\r
67 throw new ArgumentNullException("watcher");
\r
68 Contract.EndContractBlock();
\r
70 watcher.Changed += OnChangeOrCreate;
\r
71 watcher.Created += OnChangeOrCreate;
\r
72 watcher.Deleted += OnDeleted;
\r
73 watcher.Renamed += OnRename;
\r
77 private string _cachedDeletedFullPath;
\r
78 private const int PropagateDelay = 10;
\r
80 private static void OnTimeout(object state)
\r
82 throw new NotImplementedException();
\r
85 private void OnDeleted(object sender, FileSystemEventArgs e)
\r
88 throw new ArgumentNullException("sender");
\r
89 if (e.ChangeType != WatcherChangeTypes.Deleted)
\r
90 throw new ArgumentException("e");
\r
91 if (string.IsNullOrWhiteSpace(e.FullPath))
\r
92 throw new ArgumentException("e");
\r
93 Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));
\r
94 Contract.EndContractBlock();
\r
96 //Handle any previously deleted event
\r
97 PropagateCachedDeleted(sender);
\r
99 //A delete event may be an actual delete event or the first event in a move action.
\r
100 //To decide which action occured, we need to wait for the next action, so
\r
101 //we store the file path and return .
\r
102 //A delete action will not be followed by any other event, so we need to add a watchdog
\r
103 //that will declare a Delete action after a short amount of time
\r
105 //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents
\r
106 // as this is actually a MOVE operation
\r
107 //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself
\r
108 _cachedDeletedFullPath = e.FullPath;
\r
110 //TODO: This requires synchronization of the _cachedDeletedFullPath field
\r
111 //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
\r
112 //Maybe, use a timer instead of a task
\r
114 TaskEx.Delay(PropagateDelay).ContinueWith(t =>
\r
116 var myPath = e.FullPath;
\r
117 if (this._cachedDeletedFullPath==myPath)
\r
118 PropagateCachedDeleted(sender);
\r
122 private void OnRename(object sender, RenamedEventArgs e)
\r
124 if (sender == null)
\r
125 throw new ArgumentNullException("sender");
\r
126 Contract.Ensures(_cachedDeletedFullPath == null);
\r
127 Contract.EndContractBlock();
\r
131 //Propagate any previous cached delete event
\r
132 PropagateCachedDeleted(sender);
\r
135 Renamed(sender, e);
\r
140 _cachedDeletedFullPath = null;
\r
144 private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
\r
146 if (sender == null)
\r
147 throw new ArgumentNullException("sender");
\r
148 if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
\r
149 throw new ArgumentException("e");
\r
150 Contract.Ensures(_cachedDeletedFullPath == null);
\r
151 Contract.EndContractBlock();
\r
155 //A Move action results in a sequence of a Delete and a Create or Change event
\r
156 //If the actual action is a Move, raise a Move event instead of the actual event
\r
157 if (HandleMoved(sender, e))
\r
160 //Otherwise, propagate the Delete event if it exists
\r
161 PropagateCachedDeleted(sender);
\r
162 //and propagate the actual event
\r
163 var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
\r
164 if (actualEvent != null)
\r
165 actualEvent(sender, e);
\r
169 //Finally, make sure the cached path is cleared
\r
170 _cachedDeletedFullPath = null;
\r
175 private bool HandleMoved(object sender, FileSystemEventArgs e)
\r
177 if (sender == null)
\r
178 throw new ArgumentNullException("sender");
\r
179 if (!(e.ChangeType == WatcherChangeTypes.Created ||
\r
180 e.ChangeType == WatcherChangeTypes.Changed))
\r
181 throw new ArgumentException("e");
\r
182 Contract.EndContractBlock();
\r
184 //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE
\r
185 //instead of a sequence of independent actions
\r
186 //One way to detect this would be to request that the full paths are NOT the same
\r
188 var oldName = Path.GetFileName(_cachedDeletedFullPath);
\r
189 //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
\r
190 var newName = Path.GetFileName(e.Name);
\r
191 //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
\r
192 var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);
\r
199 //If the actual action is a Move, raise a Move event instead of the actual event
\r
200 var newDirectory = Path.GetDirectoryName(e.FullPath);
\r
201 var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
\r
203 Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
\r
207 _cachedDeletedFullPath = null;
\r
212 private void PropagateCachedDeleted(object sender)
\r
214 if (sender == null)
\r
215 throw new ArgumentNullException("sender");
\r
216 Contract.Ensures(_cachedDeletedFullPath == null);
\r
217 Contract.EndContractBlock();
\r
219 //Nothing to handle if there is no cached deleted file
\r
220 if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
\r
223 var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
\r
224 var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
\r
227 Deleted(sender, new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));
\r
229 _cachedDeletedFullPath = null;
\r