Statistics
| Branch: | Revision:

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
}