root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ dccd340f
History | View | Annotate | Download (15 kB)
1 |
#region |
---|---|
2 |
/* ----------------------------------------------------------------------- |
3 |
* <copyright file="FileSystemWatcherAdapter.cs" company="GRNet"> |
4 |
* |
5 |
* Copyright 2011-2012 GRNET S.A. All rights reserved. |
6 |
* |
7 |
* Redistribution and use in source and binary forms, with or |
8 |
* without modification, are permitted provided that the following |
9 |
* conditions are met: |
10 |
* |
11 |
* 1. Redistributions of source code must retain the above |
12 |
* copyright notice, this list of conditions and the following |
13 |
* disclaimer. |
14 |
* |
15 |
* 2. Redistributions in binary form must reproduce the above |
16 |
* copyright notice, this list of conditions and the following |
17 |
* disclaimer in the documentation and/or other materials |
18 |
* provided with the distribution. |
19 |
* |
20 |
* |
21 |
* THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
22 |
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
23 |
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
24 |
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
25 |
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
26 |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
27 |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
28 |
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
29 |
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
30 |
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
31 |
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
32 |
* POSSIBILITY OF SUCH DAMAGE. |
33 |
* |
34 |
* The views and conclusions contained in the software and |
35 |
* documentation are those of the authors and should not be |
36 |
* interpreted as representing official policies, either expressed |
37 |
* or implied, of GRNET S.A. |
38 |
* </copyright> |
39 |
* ----------------------------------------------------------------------- |
40 |
*/ |
41 |
#endregion |
42 |
using System.Diagnostics.Contracts; |
43 |
using System.IO; |
44 |
using System.Reflection; |
45 |
using System.Threading.Tasks; |
46 |
using Pithos.Interfaces; |
47 |
using log4net; |
48 |
|
49 |
namespace Pithos.Core.Agents |
50 |
{ |
51 |
using System; |
52 |
|
53 |
/// <summary> |
54 |
/// Wraps a FileSystemWatcher and raises Move and child object events |
55 |
/// </summary> |
56 |
public class FileSystemWatcherAdapter |
57 |
{ |
58 |
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
59 |
|
60 |
|
61 |
public event FileSystemEventHandler Changed; |
62 |
public event FileSystemEventHandler Created; |
63 |
public event FileSystemEventHandler Deleted; |
64 |
//public event RenamedEventHandler Renamed; |
65 |
public event MovedEventHandler Moved; |
66 |
|
67 |
public FileSystemWatcherAdapter(FileSystemWatcher watcher) |
68 |
{ |
69 |
if (watcher==null) |
70 |
throw new ArgumentNullException("watcher"); |
71 |
Contract.EndContractBlock(); |
72 |
|
73 |
watcher.Changed += OnChangeOrCreate; |
74 |
watcher.Created += OnChangeOrCreate; |
75 |
watcher.Deleted += OnDeleted; |
76 |
watcher.Renamed += OnRename; |
77 |
watcher.Error += OnError; |
78 |
} |
79 |
|
80 |
private void OnError(object sender, ErrorEventArgs e) |
81 |
{ |
82 |
var error = e.GetException(); |
83 |
Log.Error("FSW error",error); |
84 |
} |
85 |
|
86 |
private string _cachedDeletedFullPath; |
87 |
private const int PropagateDelay = 10; |
88 |
|
89 |
private void OnDeleted(object sender, FileSystemEventArgs e) |
90 |
{ |
91 |
if (sender == null) |
92 |
throw new ArgumentNullException("sender"); |
93 |
if (e.ChangeType != WatcherChangeTypes.Deleted) |
94 |
throw new ArgumentException("e"); |
95 |
if (string.IsNullOrWhiteSpace(e.FullPath)) |
96 |
throw new ArgumentException("e"); |
97 |
Contract.EndContractBlock(); |
98 |
|
99 |
TaskEx.Run(() => InnerOnDeleted(sender, e)); |
100 |
} |
101 |
|
102 |
private void InnerOnDeleted(object sender, FileSystemEventArgs e) |
103 |
{ |
104 |
//Handle any previously deleted event |
105 |
if (Log.IsDebugEnabled) |
106 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
107 |
PropagateCachedDeleted(sender); |
108 |
|
109 |
//A delete event may be an actual delete event or the first event in a move action. |
110 |
//To decide which action occured, we need to wait for the next action, so |
111 |
//we store the file path and return . |
112 |
//A delete action will not be followed by any other event, so we need to add a watchdog |
113 |
//that will declare a Delete action after a short amount of time |
114 |
|
115 |
//TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents |
116 |
// as this is actually a MOVE operation |
117 |
//Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself |
118 |
_cachedDeletedFullPath = e.FullPath; |
119 |
|
120 |
//TODO: This requires synchronization of the _cachedDeletedFullPath field |
121 |
//TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives |
122 |
//Maybe, use a timer instead of a task |
123 |
|
124 |
TaskEx.Delay(PropagateDelay).ContinueWith(t => |
125 |
{ |
126 |
var myPath = e.FullPath; |
127 |
if (_cachedDeletedFullPath == myPath) |
128 |
PropagateCachedDeleted(sender); |
129 |
}); |
130 |
} |
131 |
|
132 |
private void OnRename(object sender, RenamedEventArgs e) |
133 |
{ |
134 |
if (sender == null) |
135 |
throw new ArgumentNullException("sender"); |
136 |
Contract.EndContractBlock(); |
137 |
|
138 |
TaskEx.Run(() => InnerRename(sender, e)); |
139 |
} |
140 |
|
141 |
private void InnerRename(object sender, RenamedEventArgs e) |
142 |
{ |
143 |
try |
144 |
{ |
145 |
if (Log.IsDebugEnabled) |
146 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
147 |
//Propagate any previous cached delete event |
148 |
PropagateCachedDeleted(sender); |
149 |
|
150 |
if (Moved!= null) |
151 |
{ |
152 |
try |
153 |
{ |
154 |
|
155 |
Moved(sender, new MovedEventArgs(e.FullPath,e.Name,e.OldFullPath,e.OldName)); |
156 |
} |
157 |
catch (Exception exc) |
158 |
{ |
159 |
Log.Error("Rename event error", exc); |
160 |
throw; |
161 |
} |
162 |
|
163 |
var directory = new DirectoryInfo(e.FullPath); |
164 |
if (directory.Exists) |
165 |
{ |
166 |
var newDirectory = e.FullPath; |
167 |
var oldDirectory = e.OldFullPath; |
168 |
|
169 |
foreach ( |
170 |
var child in |
171 |
directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
172 |
{ |
173 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
174 |
|
175 |
var relativePath = child.AsRelativeTo(newDirectory); |
176 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
177 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
178 |
Moved(sender, |
179 |
new MovedEventArgs(newChildDirectory, child.Name, oldChildDirectory, |
180 |
child.Name)); |
181 |
} |
182 |
} |
183 |
} |
184 |
} |
185 |
finally |
186 |
{ |
187 |
_cachedDeletedFullPath = null; |
188 |
} |
189 |
} |
190 |
|
191 |
private void OnChangeOrCreate(object sender, FileSystemEventArgs e) |
192 |
{ |
193 |
if (sender == null) |
194 |
throw new ArgumentNullException("sender"); |
195 |
if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed)) |
196 |
throw new ArgumentException("e"); |
197 |
Contract.EndContractBlock(); |
198 |
TaskEx.Run(() => InnerChangeOrCreated(sender, e)); |
199 |
|
200 |
} |
201 |
|
202 |
private void InnerChangeOrCreated(object sender, FileSystemEventArgs e) |
203 |
{ |
204 |
try |
205 |
{ |
206 |
if (Log.IsDebugEnabled) |
207 |
Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath); |
208 |
//A Move action results in a sequence of a Delete and a Create or Change event |
209 |
//If the actual action is a Move, raise a Move event instead of the actual event |
210 |
if (HandleMoved(sender, e)) |
211 |
return; |
212 |
|
213 |
//Otherwise, propagate the Delete event if it exists |
214 |
PropagateCachedDeleted(sender); |
215 |
//and propagate the actual event |
216 |
var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed; |
217 |
|
218 |
if (actualEvent != null) |
219 |
{ |
220 |
actualEvent(sender, e); |
221 |
//For Folders, raise Created events for all children |
222 |
RaiseCreatedForChildren(sender, e); |
223 |
} |
224 |
} |
225 |
finally |
226 |
{ |
227 |
//Finally, make sure the cached path is cleared |
228 |
_cachedDeletedFullPath = null; |
229 |
} |
230 |
} |
231 |
|
232 |
private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e) |
233 |
{ |
234 |
Contract.Requires(sender!=null); |
235 |
Contract.Requires(e!=null); |
236 |
|
237 |
if (e.ChangeType != WatcherChangeTypes.Created) |
238 |
return; |
239 |
var dir= new DirectoryInfo(e.FullPath); |
240 |
//Skip if this is not a folder |
241 |
if (!dir.Exists) |
242 |
return; |
243 |
try |
244 |
{ |
245 |
foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories)) |
246 |
{ |
247 |
var path = Path.GetDirectoryName(info.FullName); |
248 |
Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name)); |
249 |
} |
250 |
} |
251 |
catch (IOException exc) |
252 |
{ |
253 |
TaskEx.Delay(1000) |
254 |
.ContinueWith(_=>RaiseCreatedForChildren(sender,e)); |
255 |
|
256 |
} |
257 |
} |
258 |
|
259 |
private bool HandleMoved(object sender, FileSystemEventArgs e) |
260 |
{ |
261 |
if (sender == null) |
262 |
throw new ArgumentNullException("sender"); |
263 |
if (!(e.ChangeType == WatcherChangeTypes.Created || |
264 |
e.ChangeType == WatcherChangeTypes.Changed)) |
265 |
throw new ArgumentException("e"); |
266 |
Contract.EndContractBlock(); |
267 |
|
268 |
//TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE |
269 |
//instead of a sequence of independent actions |
270 |
//One way to detect this would be to request that the full paths are NOT the same |
271 |
|
272 |
var oldName = Path.GetFileName(_cachedDeletedFullPath); |
273 |
//NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path |
274 |
var newName = Path.GetFileName(e.Name); |
275 |
//If the last deleted filename is equal to the current and the action is create, we have a MOVE operation |
276 |
var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName); |
277 |
|
278 |
if (!hasMoved) |
279 |
return false; |
280 |
|
281 |
try |
282 |
{ |
283 |
if (Log.IsDebugEnabled) |
284 |
Log.DebugFormat("Moved for [{0}]", e.FullPath); |
285 |
|
286 |
//If the actual action is a Move, raise a Move event instead of the actual event |
287 |
var newDirectory = Path.GetDirectoryName(e.FullPath); |
288 |
var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
289 |
|
290 |
if (Moved != null) |
291 |
{ |
292 |
Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName)); |
293 |
//If the moved item is a dictionary, we need to raise a change event for each child item |
294 |
//When a directory is moved within the same volume, Windows raises events only for the directory object, |
295 |
//not its children. This happens because the move actually changes a single directory entry. It doesn't |
296 |
//affect the entries of the children. |
297 |
var directory = new DirectoryInfo(e.FullPath); |
298 |
if (directory.Exists) |
299 |
{ |
300 |
foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
301 |
{ |
302 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
303 |
|
304 |
var relativePath=child.AsRelativeTo(newDirectory); |
305 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
306 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
307 |
Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name)); |
308 |
} |
309 |
} |
310 |
|
311 |
} |
312 |
|
313 |
} |
314 |
finally |
315 |
{ |
316 |
_cachedDeletedFullPath = null; |
317 |
} |
318 |
return true; |
319 |
} |
320 |
|
321 |
private void PropagateCachedDeleted(object sender) |
322 |
{ |
323 |
if (sender == null) |
324 |
throw new ArgumentNullException("sender"); |
325 |
Contract.Ensures(_cachedDeletedFullPath == null); |
326 |
Contract.EndContractBlock(); |
327 |
|
328 |
//Nothing to handle if there is no cached deleted file |
329 |
if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath)) |
330 |
return; |
331 |
|
332 |
var deletedFileName = Path.GetFileName(_cachedDeletedFullPath); |
333 |
var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
334 |
|
335 |
if (Log.IsDebugEnabled) |
336 |
Log.DebugFormat("Propagating delete for [{0}]", _cachedDeletedFullPath); |
337 |
|
338 |
//Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation |
339 |
//In this case we need to raise the proper events for all child objects of the deleted directory. |
340 |
//UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted |
341 |
//This should be done at a higher level, eg by checking the stored state |
342 |
if (Deleted != null) |
343 |
Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName)); |
344 |
|
345 |
_cachedDeletedFullPath = null; |
346 |
} |
347 |
} |
348 |
} |