Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (12.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.Threading.Tasks;
45
using Pithos.Interfaces;
46

    
47
namespace Pithos.Core.Agents
48
{
49
    using System;
50

    
51
    /// <summary>
52
    /// Wraps a FileSystemWatcher and raises Move and child object events 
53
    /// </summary>
54
    public class FileSystemWatcherAdapter
55
    {
56
         public event FileSystemEventHandler Changed;
57
        public event FileSystemEventHandler Created;
58
        public event FileSystemEventHandler Deleted;
59
        public event RenamedEventHandler Renamed;
60
        public event MovedEventHandler Moved;
61

    
62
        public FileSystemWatcherAdapter(FileSystemWatcher watcher)
63
        {
64
            if (watcher==null)
65
                throw new ArgumentNullException("watcher");
66
            Contract.EndContractBlock();
67

    
68
            watcher.Changed += OnChangeOrCreate;
69
            watcher.Created += OnChangeOrCreate;
70
            watcher.Deleted += OnDeleted;
71
            watcher.Renamed += OnRename;            
72
            
73
        }
74

    
75
        private string _cachedDeletedFullPath;
76
        private const int PropagateDelay = 10;
77

    
78
        private void OnDeleted(object sender, FileSystemEventArgs e)
79
        {
80
            if (sender == null)
81
                throw new ArgumentNullException("sender");
82
            if (e.ChangeType != WatcherChangeTypes.Deleted)
83
                throw new ArgumentException("e");
84
            if (string.IsNullOrWhiteSpace(e.FullPath))
85
                throw new ArgumentException("e");
86
            Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));
87
            Contract.EndContractBlock();
88

    
89
            //Handle any previously deleted event
90
            PropagateCachedDeleted(sender);
91

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

    
98
            //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents
99
            //      as this is actually a MOVE operation
100
            //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself
101
            _cachedDeletedFullPath = e.FullPath;
102
            
103
            //TODO: This requires synchronization of the _cachedDeletedFullPath field
104
            //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
105
            //Maybe, use a timer instead of a task
106
            
107
            TaskEx.Delay(PropagateDelay).ContinueWith(t =>
108
                                                           {
109
                                                               var myPath = e.FullPath;
110
                                                               if (_cachedDeletedFullPath==myPath)
111
                                                                    PropagateCachedDeleted(sender);
112
                                                           });
113
        }
114

    
115
        private void OnRename(object sender, RenamedEventArgs e)
116
        {
117
            if (sender == null)
118
                throw new ArgumentNullException("sender");
119
            Contract.Ensures(_cachedDeletedFullPath == null);
120
            Contract.EndContractBlock();
121

    
122
            try
123
            {
124
                //Propagate any previous cached delete event
125
                PropagateCachedDeleted(sender);
126
                
127
                if (Renamed!=null)
128
                    Renamed(sender, e);
129
                
130
            }
131
            finally
132
            {
133
                _cachedDeletedFullPath = null;    
134
            }
135
        }
136

    
137
        private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
138
        {
139
            if (sender == null)
140
                throw new ArgumentNullException("sender");
141
            if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
142
                throw new ArgumentException("e");
143
            Contract.Ensures(_cachedDeletedFullPath == null);
144
            Contract.EndContractBlock();
145

    
146
            try
147
            {
148
                //A Move action results in a sequence of a Delete and a Create or Change event
149
                //If the actual action is a Move, raise a Move event instead of the actual event
150
                if (HandleMoved(sender, e))
151
                    return;
152

    
153
                //Otherwise, propagate the Delete event if it exists 
154
                PropagateCachedDeleted(sender);
155
                //and propagate the actual event
156
                var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
157

    
158
                if (actualEvent != null)
159
                {
160
                    actualEvent(sender, e);
161
                    //For Folders, raise Created events for all children
162
                    RaiseCreatedForChildren(sender,e);
163
                }
164
            }
165
            finally
166
            {
167
                //Finally, make sure the cached path is cleared
168
                _cachedDeletedFullPath = null;
169
            }
170

    
171
        }
172

    
173
        private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e)
174
        {
175
            Contract.Requires(sender!=null);
176
            Contract.Requires(e!=null);
177

    
178
            if (e.ChangeType != WatcherChangeTypes.Created)
179
                return;
180
            var dir= new DirectoryInfo(e.FullPath);
181
            //Skip if this is not a folder
182
            if (!dir.Exists)
183
                return;
184
            foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories))
185
            {
186
                var path = Path.GetDirectoryName(info.FullName);
187
                Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name));
188
            }            
189
        }
190

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

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

    
204
            var oldName = Path.GetFileName(_cachedDeletedFullPath);
205
            //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
206
            var newName = Path.GetFileName(e.Name);
207
            //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
208
            var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);
209

    
210
            if (!hasMoved)
211
                return false;
212

    
213
            try
214
            {
215
                //If the actual action is a Move, raise a Move event instead of the actual event
216
                var newDirectory = Path.GetDirectoryName(e.FullPath);
217
                var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
218

    
219
                if (Moved != null)
220
                {
221
                    Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
222
                    //If the moved item is a dictionary, we need to raise a change event for each child item
223
                    //When a directory is moved within the same volume, Windows raises events only for the directory object,
224
                    //not its children. This happens because the move actually changes a single directory entry. It doesn't
225
                    //affect the entries of the children.
226
                    var directory = new DirectoryInfo(e.FullPath);
227
                    if (directory.Exists)
228
                    {
229
                        foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
230
                        {
231
                            var newChildDirectory = Path.GetDirectoryName(child.FullName);
232

    
233
                            var relativePath=child.AsRelativeTo(newDirectory);
234
                            var relativeFolder = Path.GetDirectoryName(relativePath);
235
                            var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
236
                            Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name));
237
                        }
238
                    }
239

    
240
                }
241

    
242
            }
243
            finally
244
            {
245
                _cachedDeletedFullPath = null;
246
            }
247
            return true;
248
        }
249

    
250
        private void PropagateCachedDeleted(object sender)
251
        {
252
            if (sender == null)
253
                throw new ArgumentNullException("sender");
254
            Contract.Ensures(_cachedDeletedFullPath == null);
255
            Contract.EndContractBlock();
256

    
257
            //Nothing to handle if there is no cached deleted file
258
            if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
259
                return;
260
            
261
            var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
262
            var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
263

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

    
271
            _cachedDeletedFullPath = null;
272
        }
273
    }
274
}