Using MD5 to quickly check for local modifications before calculating the expensive...
[pithos-ms-client] / trunk / Pithos.Core / Agents / FileAgent.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="FileAgent.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;
43 using System.Collections.Generic;
44 using System.Diagnostics.Contracts;
45 using System.IO;
46 using System.Linq;
47 using System.Reflection;
48 using System.Threading.Tasks;
49 using Pithos.Interfaces;
50 using Pithos.Network;
51 using log4net;
52
53 namespace Pithos.Core.Agents
54 {
55 //    [Export]
56     public class FileAgent
57     {
58         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
59
60         /*
61                 Agent<WorkflowState> _agent;
62         */
63         private FileSystemWatcher _watcher;
64         private FileSystemWatcherAdapter _adapter;
65         private FileEventIdleBatch _eventIdleBatch;
66
67         //[Import]
68         public IStatusKeeper StatusKeeper { get; set; }
69
70         public IStatusNotification StatusNotification { get; set; }
71         //[Import]
72         public IPithosWorkflow Workflow { get; set; }
73         //[Import]
74         //public WorkflowAgent WorkflowAgent { get; set; }
75
76         private AccountInfo AccountInfo { get; set; }
77
78         internal string RootPath { get;  set; }
79         
80         public TimeSpan IdleTimeout { get; set; }
81
82         public PollAgent PollAgent { get; set; }
83
84         private void ProcessBatchedEvents(Dictionary<string, FileSystemEventArgs[]> fileEvents)
85         {
86             PollAgent.SynchNow();
87         }
88
89 /*
90         private void ProcessBatchedEvents(Dictionary<string, FileSystemEventArgs[]> fileEvents)
91         {
92             StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Uploading {0} files",fileEvents.Count));
93             //Start with events that do not originate in one of the ignored folders
94             var initialEvents = from evt in fileEvents
95                               where !IgnorePaths(evt.Key)
96                               select evt;
97
98             IEnumerable<KeyValuePair<string, FileSystemEventArgs[]>> cleanEvents;
99
100             
101             var selectiveEnabled = Selectives.IsSelectiveEnabled(AccountInfo.AccountKey);
102             //When selective sync is enabled,
103             if (selectiveEnabled)
104             {
105                 //Include all selected items
106                 var selectedEvents = from evt in initialEvents
107                                      where Selectives.IsSelected(AccountInfo, evt.Key)
108                                      select evt;                
109                 //And all folder creations in the unselected folders
110                 var folderCreations = from evt in initialEvents
111                                       let folderPath=evt.Key
112                                       //The original folder may not exist due to renames. Just make sure that the path is not a file
113                                       where !File.Exists(folderPath)
114                                             //We only want unselected items
115                                             && !Selectives.IsSelected(AccountInfo, folderPath)
116                                             //Is there any creation event related to the folder?
117                                             && evt.Value.Any(arg => arg.ChangeType == WatcherChangeTypes.Created)
118                                       select evt;
119                 cleanEvents = selectedEvents.Union(folderCreations).ToList();
120             }
121             //If selective is disabled, only exclude the shared folders 
122             else
123             {
124                 cleanEvents = (from evt in initialEvents
125                               where !evt.Key.IsSharedTo(AccountInfo)
126                               select evt).ToList();
127             }
128
129
130             foreach (var fileEvent in cleanEvents)
131             {
132                 //var filePath = fileEvent.Key;
133                 var changes = fileEvent.Value;
134
135                 var isNotFile = !File.Exists(fileEvent.Key);
136                 foreach (var change in changes)
137                 {
138                     if (change.ChangeType == WatcherChangeTypes.Renamed)
139                     {
140                         var rename = (MovedEventArgs) change;
141                         _agent.Post(new WorkflowState(change)
142                                         {
143                                             AccountInfo = AccountInfo,
144                                             OldPath = rename.OldFullPath,
145                                             OldFileName = Path.GetFileName(rename.OldName),
146                                             Path = rename.FullPath,
147                                             FileName = Path.GetFileName(rename.Name),
148                                             TriggeringChange = rename.ChangeType
149                                         });
150                     }
151                     else
152                     {
153                         var isCreation = selectiveEnabled && isNotFile && change.ChangeType == WatcherChangeTypes.Created;
154                         _agent.Post(new WorkflowState(change)
155                                         {
156                                             AccountInfo = AccountInfo,
157                                             Path = change.FullPath,
158                                             FileName = Path.GetFileName(change.Name),
159                                             TriggeringChange = change.ChangeType,
160                                             IsCreation=isCreation
161                                         });
162                     }
163                 }
164             }
165             StatusNotification.SetPithosStatus(PithosStatus.LocalComplete);
166         }
167 */
168
169         public void Start(AccountInfo accountInfo,string rootPath)
170         {
171             if (accountInfo==null)
172                 throw new ArgumentNullException("accountInfo");
173             if (String.IsNullOrWhiteSpace(rootPath))
174                 throw new ArgumentNullException("rootPath");
175             if (!Path.IsPathRooted(rootPath))
176                 throw new ArgumentException("rootPath must be an absolute path","rootPath");
177             if (IdleTimeout == null)
178                 throw new InvalidOperationException("IdleTimeout must have a valid value");
179                 Contract.EndContractBlock();
180
181             AccountInfo = accountInfo;
182             RootPath = rootPath;
183             
184             _eventIdleBatch = new FileEventIdleBatch((int)IdleTimeout.TotalMilliseconds, ProcessBatchedEvents);
185
186             _watcher = new FileSystemWatcher(rootPath) { IncludeSubdirectories = true, InternalBufferSize = 8 * 4096 };
187             _adapter = new FileSystemWatcherAdapter(_watcher);
188
189             _adapter.Changed += OnFileEvent;
190             _adapter.Created += OnFileEvent;
191             _adapter.Deleted += OnFileEvent;
192             //_adapter.Renamed += OnRenameEvent;
193             _adapter.Moved += OnMoveEvent;
194             _watcher.EnableRaisingEvents = true;
195
196 /*            
197
198
199
200             _agent = Agent<WorkflowState>.Start(inbox =>
201             {
202                 Action loop = null;
203                 loop = () =>
204                 {
205                     var message = inbox.Receive();
206                     var process=message.Then(Process,inbox.CancellationToken);                    
207                     inbox.LoopAsync(process,loop,ex=>
208                         Log.ErrorFormat("[ERROR] File Event Processing:\r{0}", ex));
209                 };
210                 loop();
211             });*/
212         }
213
214 /*
215         private Task<object> Process(WorkflowState state)
216         {
217             if (state==null)
218                 throw new ArgumentNullException("state");
219             Contract.EndContractBlock();
220
221             if (Ignore(state.Path))
222                 return CompletedTask<object>.Default;
223
224             var networkState = NetworkGate.GetNetworkState(state.Path);
225             //Skip if the file is already being downloaded or uploaded and 
226             //the change is create or modify
227             if (networkState != NetworkOperation.None &&
228                 (
229                     state.TriggeringChange == WatcherChangeTypes.Created ||
230                     state.TriggeringChange == WatcherChangeTypes.Changed
231                 ))
232                 return CompletedTask<object>.Default;
233
234             try
235             {
236                 //StatusKeeper.EnsureFileState(state.Path);
237                 
238                 UpdateFileStatus(state);
239                 UpdateOverlayStatus(state);
240                 UpdateFileChecksum(state);
241                 WorkflowAgent.Post(state);
242             }
243             catch (IOException exc)
244             {
245                 if (File.Exists(state.Path))
246                 {
247                     Log.WarnFormat("File access error occured, retrying {0}\n{1}", state.Path, exc);
248                     _agent.Post(state);
249                 }
250                 else
251                 {
252                     Log.WarnFormat("File {0} does not exist. Will be ignored\n{1}", state.Path, exc);
253                 }
254             }
255             catch (Exception exc)
256             {
257                 Log.WarnFormat("Error occured while indexing{0}. The file will be skipped\n{1}",
258                                state.Path, exc);
259             }
260             return CompletedTask<object>.Default;
261         }
262
263         public bool Pause
264         {
265             get { return _watcher == null || !_watcher.EnableRaisingEvents; }
266             set
267             {
268                 if (_watcher != null)
269                     _watcher.EnableRaisingEvents = !value;                
270             }
271         }
272 */
273
274         public string CachePath { get; set; }
275
276         /*private List<string> _selectivePaths = new List<string>();
277         public List<string> SelectivePaths
278         {
279             get { return _selectivePaths; }
280             set { _selectivePaths = value; }
281         }
282 */
283         public Selectives Selectives { get; set; }
284
285
286 /*
287         public void Post(WorkflowState workflowState)
288         {
289             if (workflowState == null)
290                 throw new ArgumentNullException("workflowState");
291             Contract.EndContractBlock();
292
293             _agent.Post(workflowState);
294         }
295
296         public void Stop()
297         {
298             if (_watcher != null)
299             {
300                 _watcher.Dispose();
301             }
302             _watcher = null;
303
304             if (_agent!=null)
305                 _agent.Stop();
306         }
307
308 */
309         // Enumerate all files in the Pithos directory except those in the Fragment folder
310         // and files with a .ignore extension
311         public IEnumerable<string> EnumerateFiles(string searchPattern="*")
312         {
313             var monitoredFiles = from filePath in Directory.EnumerateFileSystemEntries(RootPath, searchPattern, SearchOption.AllDirectories)
314                                  where !Ignore(filePath)
315                                  orderby filePath ascending 
316                                  select filePath;
317             return monitoredFiles;
318         }
319
320         public IEnumerable<FileInfo> EnumerateFileInfos(string searchPattern="*")
321         {
322             var rootDir = new DirectoryInfo(RootPath);
323             var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
324                                  where !Ignore(file.FullName)
325                                  orderby file.FullName ascending 
326                                  select file;
327             return monitoredFiles;
328         }                
329
330         public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern="*")
331         {
332             var rootDir = new DirectoryInfo(RootPath);
333             //Ensure folders appear first, to allow folder processing as soon as possilbe
334             var folders = (from file in rootDir.EnumerateDirectories(searchPattern, SearchOption.AllDirectories)
335                                      where !Ignore(file.FullName)
336                                      orderby file.FullName ascending
337                                      select file).ToList();
338             var files = (from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
339                                   where !Ignore(file.FullName)
340                                   orderby file.Length ascending
341                                   select file as FileSystemInfo).ToList();
342             var monitoredFiles = folders
343                                  //Process small files first, leaving expensive large files for last
344                                  .Concat(files);
345             return monitoredFiles;
346         }                
347
348         public IEnumerable<string> EnumerateFilesAsRelativeUrls(string searchPattern="*")
349         {
350             var rootDir = new DirectoryInfo(RootPath);
351             var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
352                                  where !Ignore(file.FullName)
353                                  orderby file.FullName ascending 
354                                  select file.AsRelativeUrlTo(RootPath);
355             return monitoredFiles;
356         }                
357
358         public IEnumerable<string> EnumerateFilesSystemInfosAsRelativeUrls(string searchPattern="*")
359         {
360             var rootDir = new DirectoryInfo(RootPath);
361             var monitoredFiles = from file in rootDir.EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories)
362                                  where !Ignore(file.FullName)
363                                  orderby file.FullName ascending 
364                                  select file.AsRelativeUrlTo(RootPath);
365             return monitoredFiles;
366         }                
367
368
369         
370
371         public bool Ignore(string filePath)
372         {
373             if (IgnorePaths(filePath)) return true;
374
375
376             //If selective sync is enabled, 
377             if (IsUnselectedRootFolder(filePath))
378                     return false;
379             //Ignore if selective synchronization is defined, 
380             //And the target file is not below any of the selective paths
381             var ignore = !Selectives.IsSelected(AccountInfo, filePath);
382             return ignore;
383         }
384
385         public bool IsUnselectedRootFolder(string filePath)
386         {
387             return Selectives.IsSelectiveEnabled(AccountInfo.AccountKey) //propagate folder events 
388                    && Directory.Exists(filePath) //from the container root folder only. Note, in the first level below the account root path are the containers
389                    && FoundBelowRoot(filePath, RootPath, 2);
390         }
391
392         public bool IgnorePaths(string filePath)
393         {
394 //Ignore all first-level directories and files (ie at the container folders level)
395             if (FoundBelowRoot(filePath, RootPath, 1))
396                 return true;
397
398             //Ignore first-level items under the "others" folder (ie at the accounts folders level).
399             var othersPath = Path.Combine(RootPath, FolderConstants.OthersFolder);
400             if (FoundBelowRoot(filePath, othersPath, 1))
401                 return true;
402
403             //Ignore second-level (container) folders under the "others" folder (ie at the container folders level). 
404             if (FoundBelowRoot(filePath, othersPath, 2))
405                 return true;
406
407
408             //Ignore anything happening in the cache path
409             if (filePath.StartsWith(CachePath))
410                 return true;
411             
412             //Finally, ignore events about one of the ignored files
413             return _ignoreFiles.ContainsKey(filePath.ToLower());
414         }
415
416 /*        private static bool FoundInRoot(string filePath, string rootPath)
417         {
418             //var rootDirectory = new DirectoryInfo(rootPath);
419
420             //If the paths are equal, return true
421             if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
422                 return true;
423
424             //If the filepath is below the root path
425             if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
426             {
427                 //Get the relative path
428                 var relativePath = filePath.Substring(rootPath.Length + 1);
429                 //If the relativePath does NOT contains a path separator, we found a match
430                 return (!relativePath.Contains(@"\"));
431             }
432
433             //If the filepath is not under the root path, return false
434             return false;            
435         }*/
436
437
438         private static bool FoundBelowRoot(string filePath, string rootPath,int level)
439         {
440             //var rootDirectory = new DirectoryInfo(rootPath);
441
442             //If the paths are equal, return true
443             if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
444                 return true;
445
446             //If the filepath is below the root path
447             if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
448             {
449                 //Get the relative path
450                 var relativePath = filePath.Substring(rootPath.Length + 1);
451                 //If the relativePath does NOT contains a path separator, we found a match
452                 var levels=relativePath.ToCharArray().Count(c=>c=='\\')+1;                
453                 return levels==level;
454             }
455
456             //If the filepath is not under the root path, return false
457             return false;            
458         }
459
460         /*
461         //Post a Change message for renames containing the old and new names
462         void OnRenameEvent(object sender, RenamedEventArgs e)
463         {
464             var oldFullPath = e.OldFullPath;
465             var fullPath = e.FullPath;
466             if (Ignore(oldFullPath) || Ignore(fullPath))
467                 return;
468
469             _agent.Post(new WorkflowState
470             {
471                 AccountInfo=AccountInfo,
472                 OldPath = oldFullPath,
473                 OldFileName = e.OldName,
474                 Path = fullPath,
475                 FileName = e.Name,
476                 TriggeringChange = e.ChangeType
477             });
478         }
479         */
480
481         //Post a Change message for all events except rename
482         void OnFileEvent(object sender, FileSystemEventArgs e)
483         {
484             //Ignore events that affect the cache folder
485             var filePath = e.FullPath;
486             if (Ignore(filePath))
487                 return;
488             _eventIdleBatch.Post(e);
489         }
490
491         //Post a Change message for moves containing the old and new names
492         void OnMoveEvent(object sender, MovedEventArgs e)
493         {
494             var oldFullPath = e.OldFullPath;
495             var fullPath = e.FullPath;
496             
497
498             //If the source path is one of the ignored folders, ignore
499             if (IgnorePaths(oldFullPath)) 
500                 return;
501
502             //TODO: Must prevent move propagation if the source folder is blocked by selective sync
503             //Ignore takes into account Selective Sync
504             if (Ignore(fullPath))
505                 return;
506
507             _eventIdleBatch.Post(e);
508         }
509
510
511
512         private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
513                                                                              {
514             {WatcherChangeTypes.Created,FileStatus.Created},
515             {WatcherChangeTypes.Changed,FileStatus.Modified},
516             {WatcherChangeTypes.Deleted,FileStatus.Deleted},
517             {WatcherChangeTypes.Renamed,FileStatus.Renamed}
518         };
519
520         private Dictionary<string, string> _ignoreFiles=new Dictionary<string, string>();
521
522         private WorkflowState UpdateFileStatus(WorkflowState state)
523         {
524             if (state==null)
525                 throw new ArgumentNullException("state");
526             if (String.IsNullOrWhiteSpace(state.Path))
527                 throw new ArgumentException("The state's Path can't be empty","state");
528             Contract.EndContractBlock();
529
530             var path = state.Path;
531             var status = _statusDict[state.TriggeringChange];
532             var oldStatus = Workflow.StatusKeeper.GetFileStatus(path);
533             if (status == oldStatus)
534             {
535                 state.Status = status;
536                 state.Skip = true;
537                 return state;
538             }
539             if (state.Status == FileStatus.Renamed)
540                 Workflow.ClearFileStatus(path);
541
542             state.Status = Workflow.SetFileStatus(path, status);
543             return state;
544         }
545
546         private WorkflowState UpdateOverlayStatus(WorkflowState state)
547         {
548             if (state==null)
549                 throw new ArgumentNullException("state");
550             Contract.EndContractBlock();
551
552             if (state.Skip)
553                 return state;
554
555             switch (state.Status)
556             {
557                 case FileStatus.Created:
558                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash).Wait();
559                     break;
560                 case FileStatus.Modified:
561                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified, state.ShortHash).Wait();
562                     break;
563                 case FileStatus.Deleted:
564                     //this.StatusAgent.RemoveFileOverlayStatus(state.Path);
565                     break;
566                 case FileStatus.Renamed:
567                     this.StatusKeeper.ClearFileStatus(state.OldPath);
568                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified, state.ShortHash).Wait();
569                     break;
570                 case FileStatus.Unchanged:
571                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal, state.ShortHash).Wait();
572                     break;
573             }
574
575             if (state.Status == FileStatus.Deleted)
576                 NativeMethods.RaiseChangeNotification(Path.GetDirectoryName(state.Path));
577             else
578                 NativeMethods.RaiseChangeNotification(state.Path);
579             return state;
580         }
581
582
583         private WorkflowState UpdateFileChecksum(WorkflowState state)
584         {
585             if (state.Skip)
586                 return state;
587
588             if (state.Status == FileStatus.Deleted)
589                 return state;
590
591             var path = state.Path;
592             //Skip calculation for folders
593             if (Directory.Exists(path))
594                 return state;
595
596             var info = new FileInfo(path);
597
598             using (StatusNotification.GetNotifier("Hashing {0}", "Finished Hashing {0}", info.Name))
599             {
600
601                 var shortHash = info.ComputeShortHash();
602
603                 string merkleHash = info.CalculateHash(StatusKeeper.BlockSize, StatusKeeper.BlockHash);
604                 StatusKeeper.UpdateFileChecksum(path, shortHash, merkleHash);
605
606                 state.Hash = merkleHash;
607                 return state;
608             }
609         }
610
611         //Does the file exist in the container's local folder?
612         public bool Exists(string relativePath)
613         {
614             if (String.IsNullOrWhiteSpace(relativePath))
615                 throw new ArgumentNullException("relativePath");
616             //A RootPath must be set before calling this method
617             if (String.IsNullOrWhiteSpace(RootPath))
618                 throw new InvalidOperationException("RootPath was not set");
619             Contract.EndContractBlock();
620             //Create the absolute path by combining the RootPath with the relativePath
621             var absolutePath=Path.Combine(RootPath, relativePath);
622             //Is this a valid file?
623             if (File.Exists(absolutePath))
624                 return true;
625             //Or a directory?
626             if (Directory.Exists(absolutePath))
627                 return true;
628             //Fail if it is neither
629             return false;
630         }
631
632         public static FileAgent GetFileAgent(AccountInfo accountInfo)
633         {
634             return GetFileAgent(accountInfo.AccountPath);
635         }
636
637         public static FileAgent GetFileAgent(string rootPath)
638         {
639             return AgentLocator<FileAgent>.Get(rootPath.ToLower());
640         }
641
642
643         public FileSystemInfo GetFileSystemInfo(string relativePath)
644         {
645             if (String.IsNullOrWhiteSpace(relativePath))
646                 throw new ArgumentNullException("relativePath");
647             //A RootPath must be set before calling this method
648             if (String.IsNullOrWhiteSpace(RootPath))
649                 throw new InvalidOperationException("RootPath was not set");            
650             Contract.EndContractBlock();            
651
652             var absolutePath = Path.Combine(RootPath, relativePath);
653
654             if (Directory.Exists(absolutePath))
655                 return new DirectoryInfo(absolutePath).WithProperCapitalization();
656             else
657                 return new FileInfo(absolutePath).WithProperCapitalization();
658             
659         }
660
661         public void Delete(string relativePath)
662         {
663             var absolutePath = Path.Combine(RootPath, relativePath).ToLower();
664             if (Log.IsDebugEnabled)
665                 Log.DebugFormat("Deleting {0}", absolutePath);
666             if (File.Exists(absolutePath))
667             {    
668                 try
669                 {
670                     File.Delete(absolutePath);
671                 }
672                 //The file may have been deleted by another thread. Just ignore the relevant exception
673                 catch (FileNotFoundException) { }
674             }
675             else if (Directory.Exists(absolutePath))
676             {
677                 try
678                 {
679                     Directory.Delete(absolutePath, true);
680                 }
681                 //The directory may have been deleted by another thread. Just ignore the relevant exception
682                 catch (DirectoryNotFoundException){}                
683             }
684         
685             //_ignoreFiles[absolutePath] = absolutePath;                
686             StatusKeeper.ClearFileStatus(absolutePath);
687         }
688     }
689 }