Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ dccd340f

History | View | Annotate | Download (15 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.EndContractBlock();
98

    
99
            TaskEx.Run(() => InnerOnDeleted(sender, e));
100
        }
101

    
102
        private void InnerOnDeleted(object sender, FileSystemEventArgs e)
103
        {
104
//Handle any previously deleted event
105
            if (Log.IsDebugEnabled)
106
                Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
107
            PropagateCachedDeleted(sender);
108

    
109
            //A delete event may be an actual delete event or the first event in a move action.
110
            //To decide which action occured, we need to wait for the next action, so
111
            //we  store the file path and return .
112
            //A delete action will not be followed by any other event, so we need to add a watchdog 
113
            //that will declare a Delete action after a short amount of time
114

    
115
            //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents
116
            //      as this is actually a MOVE operation
117
            //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself
118
            _cachedDeletedFullPath = e.FullPath;
119

    
120
            //TODO: This requires synchronization of the _cachedDeletedFullPath field
121
            //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
122
            //Maybe, use a timer instead of a task
123

    
124
            TaskEx.Delay(PropagateDelay).ContinueWith(t =>
125
                                                          {
126
                                                              var myPath = e.FullPath;
127
                                                              if (_cachedDeletedFullPath == myPath)
128
                                                                  PropagateCachedDeleted(sender);
129
                                                          });
130
        }
131

    
132
        private void OnRename(object sender, RenamedEventArgs e)
133
        {
134
            if (sender == null)
135
                throw new ArgumentNullException("sender");
136
            Contract.EndContractBlock();
137

    
138
            TaskEx.Run(() => InnerRename(sender, e));
139
        }
140

    
141
        private void InnerRename(object sender, RenamedEventArgs e)
142
        {
143
            try
144
            {
145
                if (Log.IsDebugEnabled)
146
                    Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
147
                //Propagate any previous cached delete event
148
                PropagateCachedDeleted(sender);
149

    
150
                if (Moved!= null)
151
                {
152
                    try
153
                    {
154

    
155
                        Moved(sender, new MovedEventArgs(e.FullPath,e.Name,e.OldFullPath,e.OldName));
156
                    }
157
                    catch (Exception exc)
158
                    {
159
                        Log.Error("Rename event error", exc);
160
                        throw;
161
                    }
162

    
163
                    var directory = new DirectoryInfo(e.FullPath);
164
                    if (directory.Exists)
165
                    {
166
                        var newDirectory = e.FullPath;
167
                        var oldDirectory = e.OldFullPath;
168

    
169
                        foreach (
170
                            var child in
171
                                directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
172
                        {
173
                            var newChildDirectory = Path.GetDirectoryName(child.FullName);
174

    
175
                            var relativePath = child.AsRelativeTo(newDirectory);
176
                            var relativeFolder = Path.GetDirectoryName(relativePath);
177
                            var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
178
                            Moved(sender,
179
                                  new MovedEventArgs(newChildDirectory, child.Name, oldChildDirectory,
180
                                                     child.Name));
181
                        }
182
                    }
183
                }
184
            }
185
            finally
186
            {
187
                _cachedDeletedFullPath = null;
188
            }
189
        }
190

    
191
        private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
192
        {
193
            if (sender == null)
194
                throw new ArgumentNullException("sender");
195
            if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
196
                throw new ArgumentException("e");
197
            Contract.EndContractBlock();
198
            TaskEx.Run(() => InnerChangeOrCreated(sender, e));
199

    
200
        }
201

    
202
        private void InnerChangeOrCreated(object sender, FileSystemEventArgs e)
203
        {
204
            try
205
            {
206
                if (Log.IsDebugEnabled)
207
                    Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath);
208
                //A Move action results in a sequence of a Delete and a Create or Change event
209
                //If the actual action is a Move, raise a Move event instead of the actual event
210
                if (HandleMoved(sender, e))
211
                    return;
212

    
213
                //Otherwise, propagate the Delete event if it exists 
214
                PropagateCachedDeleted(sender);
215
                //and propagate the actual event
216
                var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
217

    
218
                if (actualEvent != null)
219
                {
220
                    actualEvent(sender, e);
221
                    //For Folders, raise Created events for all children
222
                    RaiseCreatedForChildren(sender, e);
223
                }
224
            }
225
            finally
226
            {
227
                //Finally, make sure the cached path is cleared
228
                _cachedDeletedFullPath = null;
229
            }
230
        }
231

    
232
        private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e)
233
        {
234
            Contract.Requires(sender!=null);
235
            Contract.Requires(e!=null);
236

    
237
            if (e.ChangeType != WatcherChangeTypes.Created)
238
                return;
239
            var dir= new DirectoryInfo(e.FullPath);
240
            //Skip if this is not a folder
241
            if (!dir.Exists)
242
                return;
243
            try
244
            {
245
                foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories))
246
                {
247
                    var path = Path.GetDirectoryName(info.FullName);
248
                    Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name));
249
                }
250
            }
251
            catch (IOException exc)
252
            {
253
                TaskEx.Delay(1000)
254
                    .ContinueWith(_=>RaiseCreatedForChildren(sender,e));
255
                
256
            }
257
        }
258

    
259
        private bool HandleMoved(object sender, FileSystemEventArgs e)
260
        {
261
            if (sender == null)
262
                throw new ArgumentNullException("sender");
263
            if (!(e.ChangeType == WatcherChangeTypes.Created ||
264
                                                  e.ChangeType == WatcherChangeTypes.Changed))
265
                throw new ArgumentException("e");
266
            Contract.EndContractBlock();
267

    
268
            //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE
269
            //instead of a sequence of independent actions
270
            //One way to detect this would be to request that the full paths are NOT the same
271

    
272
            var oldName = Path.GetFileName(_cachedDeletedFullPath);
273
            //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
274
            var newName = Path.GetFileName(e.Name);
275
            //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
276
            var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);
277

    
278
            if (!hasMoved)
279
                return false;
280

    
281
            try
282
            {
283
                if (Log.IsDebugEnabled)
284
                    Log.DebugFormat("Moved for [{0}]",  e.FullPath);
285

    
286
                //If the actual action is a Move, raise a Move event instead of the actual event
287
                var newDirectory = Path.GetDirectoryName(e.FullPath);
288
                var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
289

    
290
                if (Moved != null)
291
                {
292
                    Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
293
                    //If the moved item is a dictionary, we need to raise a change event for each child item
294
                    //When a directory is moved within the same volume, Windows raises events only for the directory object,
295
                    //not its children. This happens because the move actually changes a single directory entry. It doesn't
296
                    //affect the entries of the children.
297
                    var directory = new DirectoryInfo(e.FullPath);
298
                    if (directory.Exists)
299
                    {
300
                        foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
301
                        {
302
                            var newChildDirectory = Path.GetDirectoryName(child.FullName);
303

    
304
                            var relativePath=child.AsRelativeTo(newDirectory);
305
                            var relativeFolder = Path.GetDirectoryName(relativePath);
306
                            var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
307
                            Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name));
308
                        }
309
                    }
310

    
311
                }
312

    
313
            }
314
            finally
315
            {
316
                _cachedDeletedFullPath = null;
317
            }
318
            return true;
319
        }
320

    
321
        private void PropagateCachedDeleted(object sender)
322
        {
323
            if (sender == null)
324
                throw new ArgumentNullException("sender");
325
            Contract.Ensures(_cachedDeletedFullPath == null);
326
            Contract.EndContractBlock();
327

    
328
            //Nothing to handle if there is no cached deleted file
329
            if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
330
                return;
331
            
332
            var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
333
            var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
334

    
335
            if (Log.IsDebugEnabled)
336
                Log.DebugFormat("Propagating delete for [{0}]", _cachedDeletedFullPath);
337

    
338
            //Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation
339
            //In this case we need to raise the proper events for all child objects of the deleted directory.
340
            //UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted
341
            //This should be done at a higher level, eg by checking the stored state
342
            if (Deleted != null)            
343
                Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));
344

    
345
            _cachedDeletedFullPath = null;
346
        }
347
    }
348
}