root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ 89472316
History | View | Annotate | Download (11.4 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.Threading.Tasks; |
45 |
using Pithos.Interfaces; |
46 |
|
47 |
namespace Pithos.Core.Agents |
48 |
{ |
49 |
using System; |
50 |
using System.Collections.Generic; |
51 |
using System.Linq; |
52 |
using System.Text; |
53 |
|
54 |
/// <summary> |
55 |
/// TODO: Update summary. |
56 |
/// </summary> |
57 |
public class FileSystemWatcherAdapter |
58 |
{ |
59 |
public event FileSystemEventHandler Changed; |
60 |
public event FileSystemEventHandler Created; |
61 |
public event FileSystemEventHandler Deleted; |
62 |
public event RenamedEventHandler Renamed; |
63 |
public event MovedEventHandler Moved; |
64 |
|
65 |
public FileSystemWatcherAdapter(FileSystemWatcher watcher) |
66 |
{ |
67 |
if (watcher==null) |
68 |
throw new ArgumentNullException("watcher"); |
69 |
Contract.EndContractBlock(); |
70 |
|
71 |
watcher.Changed += OnChangeOrCreate; |
72 |
watcher.Created += OnChangeOrCreate; |
73 |
watcher.Deleted += OnDeleted; |
74 |
watcher.Renamed += OnRename; |
75 |
|
76 |
} |
77 |
|
78 |
private string _cachedDeletedFullPath; |
79 |
private const int PropagateDelay = 10; |
80 |
|
81 |
private static void OnTimeout(object state) |
82 |
{ |
83 |
throw new NotImplementedException(); |
84 |
} |
85 |
|
86 |
private void OnDeleted(object sender, FileSystemEventArgs e) |
87 |
{ |
88 |
if (sender == null) |
89 |
throw new ArgumentNullException("sender"); |
90 |
if (e.ChangeType != WatcherChangeTypes.Deleted) |
91 |
throw new ArgumentException("e"); |
92 |
if (string.IsNullOrWhiteSpace(e.FullPath)) |
93 |
throw new ArgumentException("e"); |
94 |
Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath)); |
95 |
Contract.EndContractBlock(); |
96 |
|
97 |
//Handle any previously deleted event |
98 |
PropagateCachedDeleted(sender); |
99 |
|
100 |
//A delete event may be an actual delete event or the first event in a move action. |
101 |
//To decide which action occured, we need to wait for the next action, so |
102 |
//we store the file path and return . |
103 |
//A delete action will not be followed by any other event, so we need to add a watchdog |
104 |
//that will declare a Delete action after a short amount of time |
105 |
|
106 |
//TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents |
107 |
// as this is actually a MOVE operation |
108 |
//Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself |
109 |
_cachedDeletedFullPath = e.FullPath; |
110 |
|
111 |
//TODO: This requires synchronization of the _cachedDeletedFullPath field |
112 |
//TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives |
113 |
//Maybe, use a timer instead of a task |
114 |
|
115 |
TaskEx.Delay(PropagateDelay).ContinueWith(t => |
116 |
{ |
117 |
var myPath = e.FullPath; |
118 |
if (this._cachedDeletedFullPath==myPath) |
119 |
PropagateCachedDeleted(sender); |
120 |
}); |
121 |
} |
122 |
|
123 |
private void OnRename(object sender, RenamedEventArgs e) |
124 |
{ |
125 |
if (sender == null) |
126 |
throw new ArgumentNullException("sender"); |
127 |
Contract.Ensures(_cachedDeletedFullPath == null); |
128 |
Contract.EndContractBlock(); |
129 |
|
130 |
try |
131 |
{ |
132 |
//Propagate any previous cached delete event |
133 |
PropagateCachedDeleted(sender); |
134 |
|
135 |
if (Renamed!=null) |
136 |
Renamed(sender, e); |
137 |
|
138 |
} |
139 |
finally |
140 |
{ |
141 |
_cachedDeletedFullPath = null; |
142 |
} |
143 |
} |
144 |
|
145 |
private void OnChangeOrCreate(object sender, FileSystemEventArgs e) |
146 |
{ |
147 |
if (sender == null) |
148 |
throw new ArgumentNullException("sender"); |
149 |
if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed)) |
150 |
throw new ArgumentException("e"); |
151 |
Contract.Ensures(_cachedDeletedFullPath == null); |
152 |
Contract.EndContractBlock(); |
153 |
|
154 |
try |
155 |
{ |
156 |
//A Move action results in a sequence of a Delete and a Create or Change event |
157 |
//If the actual action is a Move, raise a Move event instead of the actual event |
158 |
if (HandleMoved(sender, e)) |
159 |
return; |
160 |
|
161 |
//Otherwise, propagate the Delete event if it exists |
162 |
PropagateCachedDeleted(sender); |
163 |
//and propagate the actual event |
164 |
var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed; |
165 |
if (actualEvent != null) |
166 |
actualEvent(sender, e); |
167 |
} |
168 |
finally |
169 |
{ |
170 |
//Finally, make sure the cached path is cleared |
171 |
_cachedDeletedFullPath = null; |
172 |
} |
173 |
|
174 |
} |
175 |
|
176 |
private bool HandleMoved(object sender, FileSystemEventArgs e) |
177 |
{ |
178 |
if (sender == null) |
179 |
throw new ArgumentNullException("sender"); |
180 |
if (!(e.ChangeType == WatcherChangeTypes.Created || |
181 |
e.ChangeType == WatcherChangeTypes.Changed)) |
182 |
throw new ArgumentException("e"); |
183 |
Contract.EndContractBlock(); |
184 |
|
185 |
//TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE |
186 |
//instead of a sequence of independent actions |
187 |
//One way to detect this would be to request that the full paths are NOT the same |
188 |
|
189 |
var oldName = Path.GetFileName(_cachedDeletedFullPath); |
190 |
//NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path |
191 |
var newName = Path.GetFileName(e.Name); |
192 |
//If the last deleted filename is equal to the current and the action is create, we have a MOVE operation |
193 |
var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName); |
194 |
|
195 |
if (!hasMoved) |
196 |
return false; |
197 |
|
198 |
try |
199 |
{ |
200 |
//If the actual action is a Move, raise a Move event instead of the actual event |
201 |
var newDirectory = Path.GetDirectoryName(e.FullPath); |
202 |
var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
203 |
|
204 |
if (Moved != null) |
205 |
{ |
206 |
Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName)); |
207 |
//If the moved item is a dictionary, we need to raise a change event for each child item |
208 |
//When a directory is moved within the same volume, Windows raises events only for the directory object, |
209 |
//not its children. This happens because the move actually changes a single directory entry. It doesn't |
210 |
//affect the entries of the children. |
211 |
var directory = new DirectoryInfo(e.FullPath); |
212 |
if (directory.Exists) |
213 |
{ |
214 |
foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
215 |
{ |
216 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
217 |
|
218 |
var relativePath=child.AsRelativeTo(newDirectory); |
219 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
220 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
221 |
Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name)); |
222 |
} |
223 |
} |
224 |
|
225 |
} |
226 |
|
227 |
} |
228 |
finally |
229 |
{ |
230 |
_cachedDeletedFullPath = null; |
231 |
} |
232 |
return true; |
233 |
} |
234 |
|
235 |
private void PropagateCachedDeleted(object sender) |
236 |
{ |
237 |
if (sender == null) |
238 |
throw new ArgumentNullException("sender"); |
239 |
Contract.Ensures(_cachedDeletedFullPath == null); |
240 |
Contract.EndContractBlock(); |
241 |
|
242 |
//Nothing to handle if there is no cached deleted file |
243 |
if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath)) |
244 |
return; |
245 |
|
246 |
var deletedFileName = Path.GetFileName(_cachedDeletedFullPath); |
247 |
var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
248 |
|
249 |
//Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation |
250 |
//In this case we need to raise the proper events for all child objects of the deleted directory. |
251 |
//UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted |
252 |
//This should be done at a higher level, eg by checking the stored state |
253 |
if (Deleted != null) |
254 |
Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName)); |
255 |
|
256 |
_cachedDeletedFullPath = null; |
257 |
} |
258 |
} |
259 |
} |