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