root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ d78d765c
History | View | Annotate | Download (16 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 |
|
43 |
using System.Diagnostics; |
44 |
using System.Diagnostics.Contracts; |
45 |
using System.IO; |
46 |
using System.Reflection; |
47 |
using System.Threading.Tasks; |
48 |
using Pithos.Interfaces; |
49 |
using log4net; |
50 |
|
51 |
namespace Pithos.Core.Agents |
52 |
{ |
53 |
using System; |
54 |
|
55 |
/// <summary> |
56 |
/// Wraps a FileSystemWatcher and raises Move and child object events |
57 |
/// </summary> |
58 |
public class FileSystemWatcherAdapter |
59 |
{ |
60 |
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
61 |
|
62 |
|
63 |
public event FileSystemEventHandler Changed; |
64 |
public event FileSystemEventHandler Created; |
65 |
public event FileSystemEventHandler Deleted; |
66 |
//public event RenamedEventHandler Renamed; |
67 |
public event MovedEventHandler Moved; |
68 |
|
69 |
public FileSystemWatcherAdapter(FileSystemWatcher watcher) |
70 |
{ |
71 |
if (watcher==null) |
72 |
throw new ArgumentNullException("watcher"); |
73 |
Contract.EndContractBlock(); |
74 |
|
75 |
watcher.Changed += OnChangeOrCreate; |
76 |
watcher.Created += OnChangeOrCreate; |
77 |
watcher.Deleted += OnDeleted; |
78 |
watcher.Renamed += OnRename; |
79 |
watcher.Error += OnError; |
80 |
} |
81 |
|
82 |
private void OnError(object sender, ErrorEventArgs e) |
83 |
{ |
84 |
var error = e.GetException(); |
85 |
Log.Error("FSW error",error); |
86 |
} |
87 |
|
88 |
private string _cachedDeletedFullPath; |
89 |
private string CachedDeletedFullPath |
90 |
{ |
91 |
get { return _cachedDeletedFullPath; } |
92 |
set |
93 |
{ |
94 |
Debug.Assert(Path.IsPathRooted(value)); |
95 |
if (!Path.IsPathRooted(value)) |
96 |
Log.WarnFormat("Storing a relative CachedDeletedFullPath: {0}",value); |
97 |
_cachedDeletedFullPath = value; |
98 |
} |
99 |
} |
100 |
|
101 |
/// <summary> |
102 |
/// Clears any cached deleted file path |
103 |
/// </summary> |
104 |
/// <remarks> |
105 |
/// This method was added to bypass the null checking in the property's setter |
106 |
/// </remarks> |
107 |
private void ClearCachedDeletedPath() |
108 |
{ |
109 |
_cachedDeletedFullPath = null; |
110 |
} |
111 |
|
112 |
|
113 |
private const int PropagateDelay = 10; |
114 |
|
115 |
private void OnDeleted(object sender, FileSystemEventArgs e) |
116 |
{ |
117 |
if (sender == null) |
118 |
throw new ArgumentNullException("sender"); |
119 |
if (e.ChangeType != WatcherChangeTypes.Deleted) |
120 |
throw new ArgumentException("e"); |
121 |
if (string.IsNullOrWhiteSpace(e.FullPath)) |
122 |
throw new ArgumentException("e"); |
123 |
Contract.EndContractBlock(); |
124 |
|
125 |
TaskEx.Run(() => InnerOnDeleted(sender, e)); |
126 |
} |
127 |
|
128 |
private void InnerOnDeleted(object sender, FileSystemEventArgs e) |
129 |
{ |
130 |
//Handle any previously deleted event |
131 |
if (Log.IsDebugEnabled) |
132 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
133 |
PropagateCachedDeleted(sender); |
134 |
|
135 |
//A delete event may be an actual delete event or the first event in a move action. |
136 |
//To decide which action occured, we need to wait for the next action, so |
137 |
//we store the file path and return . |
138 |
//A delete action will not be followed by any other event, so we need to add a watchdog |
139 |
//that will declare a Delete action after a short amount of time |
140 |
|
141 |
//TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents |
142 |
// as this is actually a MOVE operation |
143 |
//Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself |
144 |
CachedDeletedFullPath = e.FullPath; |
145 |
|
146 |
//TODO: This requires synchronization of the CachedDeletedFullPath field |
147 |
//TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives |
148 |
//Maybe, use a timer instead of a task |
149 |
|
150 |
TaskEx.Delay(PropagateDelay).ContinueWith(t => |
151 |
{ |
152 |
var myPath = e.FullPath; |
153 |
if (CachedDeletedFullPath == myPath) |
154 |
PropagateCachedDeleted(sender); |
155 |
}); |
156 |
} |
157 |
|
158 |
private void OnRename(object sender, RenamedEventArgs e) |
159 |
{ |
160 |
if (sender == null) |
161 |
throw new ArgumentNullException("sender"); |
162 |
Contract.EndContractBlock(); |
163 |
|
164 |
TaskEx.Run(() => InnerRename(sender, e)); |
165 |
} |
166 |
|
167 |
private void InnerRename(object sender, RenamedEventArgs e) |
168 |
{ |
169 |
try |
170 |
{ |
171 |
if (Log.IsDebugEnabled) |
172 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
173 |
//Propagate any previous cached delete event |
174 |
PropagateCachedDeleted(sender); |
175 |
|
176 |
if (Moved!= null) |
177 |
{ |
178 |
try |
179 |
{ |
180 |
|
181 |
Moved(sender, new MovedEventArgs(Path.GetDirectoryName(e.FullPath),Path.GetFileName(e.Name),Path.GetDirectoryName(e.OldFullPath),Path.GetFileName(e.OldName))); |
182 |
} |
183 |
catch (Exception exc) |
184 |
{ |
185 |
Log.Error("Rename event error", exc); |
186 |
throw; |
187 |
} |
188 |
|
189 |
var directory = new DirectoryInfo(e.FullPath); |
190 |
if (directory.Exists) |
191 |
{ |
192 |
var newDirectory = e.FullPath; |
193 |
var oldDirectory = e.OldFullPath; |
194 |
|
195 |
foreach ( |
196 |
var child in |
197 |
directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
198 |
{ |
199 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
200 |
|
201 |
var relativePath = child.AsRelativeTo(newDirectory); |
202 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
203 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
204 |
Moved(sender, |
205 |
new MovedEventArgs(newChildDirectory, child.Name, oldChildDirectory, |
206 |
child.Name)); |
207 |
} |
208 |
} |
209 |
} |
210 |
} |
211 |
finally |
212 |
{ |
213 |
ClearCachedDeletedPath(); |
214 |
} |
215 |
} |
216 |
|
217 |
private void OnChangeOrCreate(object sender, FileSystemEventArgs e) |
218 |
{ |
219 |
if (sender == null) |
220 |
throw new ArgumentNullException("sender"); |
221 |
if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed)) |
222 |
throw new ArgumentException("e"); |
223 |
Contract.EndContractBlock(); |
224 |
TaskEx.Run(() => InnerChangeOrCreated(sender, e)); |
225 |
|
226 |
} |
227 |
|
228 |
private void InnerChangeOrCreated(object sender, FileSystemEventArgs e) |
229 |
{ |
230 |
try |
231 |
{ |
232 |
if (Log.IsDebugEnabled) |
233 |
Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath); |
234 |
//A Move action results in a sequence of a Delete and a Create or Change event |
235 |
//If the actual action is a Move, raise a Move event instead of the actual event |
236 |
if (HandleMoved(sender, e)) |
237 |
return; |
238 |
|
239 |
//Otherwise, propagate the Delete event if it exists |
240 |
PropagateCachedDeleted(sender); |
241 |
//and propagate the actual event |
242 |
var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed; |
243 |
|
244 |
if (actualEvent != null) |
245 |
{ |
246 |
actualEvent(sender, e); |
247 |
//For Folders, raise Created events for all children |
248 |
RaiseCreatedForChildren(sender, e); |
249 |
} |
250 |
} |
251 |
finally |
252 |
{ |
253 |
//Finally, make sure the cached path is cleared |
254 |
ClearCachedDeletedPath(); |
255 |
} |
256 |
} |
257 |
|
258 |
|
259 |
private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e) |
260 |
{ |
261 |
if(sender==null) |
262 |
throw new ArgumentNullException("sender"); |
263 |
if (e==null) |
264 |
throw new ArgumentNullException("e"); |
265 |
Contract.EndContractBlock(); |
266 |
|
267 |
if (e.ChangeType != WatcherChangeTypes.Created) |
268 |
return; |
269 |
var dir= new DirectoryInfo(e.FullPath); |
270 |
//Skip if this is not a folder |
271 |
if (!dir.Exists) |
272 |
return; |
273 |
try |
274 |
{ |
275 |
foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories)) |
276 |
{ |
277 |
var path = Path.GetDirectoryName(info.FullName); |
278 |
Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name)); |
279 |
} |
280 |
} |
281 |
catch (IOException) |
282 |
{ |
283 |
TaskEx.Delay(1000) |
284 |
.ContinueWith(_=>RaiseCreatedForChildren(sender,e)); |
285 |
|
286 |
} |
287 |
} |
288 |
|
289 |
private bool HandleMoved(object sender, FileSystemEventArgs e) |
290 |
{ |
291 |
if (sender == null) |
292 |
throw new ArgumentNullException("sender"); |
293 |
if (!(e.ChangeType == WatcherChangeTypes.Created || |
294 |
e.ChangeType == WatcherChangeTypes.Changed)) |
295 |
throw new ArgumentException("e"); |
296 |
Contract.EndContractBlock(); |
297 |
|
298 |
//TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE |
299 |
//instead of a sequence of independent actions |
300 |
//One way to detect this would be to request that the full paths are NOT the same |
301 |
|
302 |
var oldName = Path.GetFileName(CachedDeletedFullPath); |
303 |
//NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path |
304 |
var newName = Path.GetFileName(e.Name); |
305 |
//If the last deleted filename is equal to the current and the action is create, we have a MOVE operation |
306 |
var hasMoved = (CachedDeletedFullPath != e.FullPath && oldName == newName); |
307 |
|
308 |
if (!hasMoved) |
309 |
return false; |
310 |
|
311 |
try |
312 |
{ |
313 |
if (Log.IsDebugEnabled) |
314 |
Log.DebugFormat("Moved for [{0}]", e.FullPath); |
315 |
|
316 |
//If the actual action is a Move, raise a Move event instead of the actual event |
317 |
var newDirectory = Path.GetDirectoryName(e.FullPath); |
318 |
var oldDirectory = Path.GetDirectoryName(CachedDeletedFullPath); |
319 |
|
320 |
if (Moved != null) |
321 |
{ |
322 |
Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName)); |
323 |
//If the moved item is a dictionary, we need to raise a change event for each child item |
324 |
//When a directory is moved within the same volume, Windows raises events only for the directory object, |
325 |
//not its children. This happens because the move actually changes a single directory entry. It doesn't |
326 |
//affect the entries of the children. |
327 |
var directory = new DirectoryInfo(e.FullPath); |
328 |
if (directory.Exists) |
329 |
{ |
330 |
foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
331 |
{ |
332 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
333 |
|
334 |
var relativePath=child.AsRelativeTo(newDirectory); |
335 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
336 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
337 |
Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name)); |
338 |
} |
339 |
} |
340 |
|
341 |
} |
342 |
|
343 |
} |
344 |
finally |
345 |
{ |
346 |
ClearCachedDeletedPath(); |
347 |
} |
348 |
return true; |
349 |
} |
350 |
|
351 |
private void PropagateCachedDeleted(object sender) |
352 |
{ |
353 |
if (sender == null) |
354 |
throw new ArgumentNullException("sender"); |
355 |
Contract.Ensures(CachedDeletedFullPath == null); |
356 |
Contract.EndContractBlock(); |
357 |
|
358 |
//Nothing to handle if there is no cached deleted file |
359 |
if (String.IsNullOrWhiteSpace(CachedDeletedFullPath)) |
360 |
return; |
361 |
|
362 |
var deletedFileName = Path.GetFileName(CachedDeletedFullPath); |
363 |
var deletedFileDirectory = Path.GetDirectoryName(CachedDeletedFullPath); |
364 |
|
365 |
if (Log.IsDebugEnabled) |
366 |
Log.DebugFormat("Propagating delete for [{0}]", CachedDeletedFullPath); |
367 |
|
368 |
//Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation |
369 |
//In this case we need to raise the proper events for all child objects of the deleted directory. |
370 |
//UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted |
371 |
//This should be done at a higher level, eg by checking the stored state |
372 |
if (Deleted != null) |
373 |
Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName)); |
374 |
|
375 |
ClearCachedDeletedPath(); |
376 |
} |
377 |
} |
378 |
} |