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