Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ 174bbb6e

History | View | Annotate | Download (15.2 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.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));
98
            Contract.EndContractBlock();
99

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

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

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

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

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

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

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

    
140
            TaskEx.Run(() => InnerRename(sender, e));
141
        }
142

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

    
152
                if (Moved!= null)
153
                {
154
                    try
155
                    {
156

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

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

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

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

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

    
203
        }
204

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

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

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

    
235
        private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e)
236
        {
237
            Contract.Requires(sender!=null);
238
            Contract.Requires(e!=null);
239

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

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

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

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

    
281
            if (!hasMoved)
282
                return false;
283

    
284
            try
285
            {
286
                if (Log.IsDebugEnabled)
287
                    Log.DebugFormat("Moved for [{0}]",  e.FullPath);
288

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

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

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

    
314
                }
315

    
316
            }
317
            finally
318
            {
319
                _cachedDeletedFullPath = null;
320
            }
321
            return true;
322
        }
323

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

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

    
338
            if (Log.IsDebugEnabled)
339
                Log.DebugFormat("Propagating delete for [{0}]", _cachedDeletedFullPath);
340

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

    
348
            _cachedDeletedFullPath = null;
349
        }
350
    }
351
}