Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (11.4 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
    using System.Collections.Generic;
51
    using System.Linq;
52
    using System.Text;
53

    
54
    /// <summary>
55
    /// TODO: Update summary.
56
    /// </summary>
57
    public class FileSystemWatcherAdapter
58
    {
59
         public event FileSystemEventHandler Changed;
60
        public event FileSystemEventHandler Created;
61
        public event FileSystemEventHandler Deleted;
62
        public event RenamedEventHandler Renamed;
63
        public event MovedEventHandler Moved;
64

    
65
        public FileSystemWatcherAdapter(FileSystemWatcher watcher)
66
        {
67
            if (watcher==null)
68
                throw new ArgumentNullException("watcher");
69
            Contract.EndContractBlock();
70

    
71
            watcher.Changed += OnChangeOrCreate;
72
            watcher.Created += OnChangeOrCreate;
73
            watcher.Deleted += OnDeleted;
74
            watcher.Renamed += OnRename;            
75
            
76
        }
77

    
78
        private string _cachedDeletedFullPath;
79
        private const int PropagateDelay = 10;        
80

    
81
        private static void OnTimeout(object state)
82
        {
83
            throw new NotImplementedException();
84
        }
85

    
86
        private void OnDeleted(object sender, FileSystemEventArgs e)
87
        {
88
            if (sender == null)
89
                throw new ArgumentNullException("sender");
90
            if (e.ChangeType != WatcherChangeTypes.Deleted)
91
                throw new ArgumentException("e");
92
            if (string.IsNullOrWhiteSpace(e.FullPath))
93
                throw new ArgumentException("e");
94
            Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));
95
            Contract.EndContractBlock();
96

    
97
            //Handle any previously deleted event
98
            PropagateCachedDeleted(sender);
99

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

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

    
123
        private void OnRename(object sender, RenamedEventArgs e)
124
        {
125
            if (sender == null)
126
                throw new ArgumentNullException("sender");
127
            Contract.Ensures(_cachedDeletedFullPath == null);
128
            Contract.EndContractBlock();
129

    
130
            try
131
            {
132
                //Propagate any previous cached delete event
133
                PropagateCachedDeleted(sender);
134
                
135
                if (Renamed!=null)
136
                    Renamed(sender, e);
137
                
138
            }
139
            finally
140
            {
141
                _cachedDeletedFullPath = null;    
142
            }
143
        }
144

    
145
        private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
146
        {
147
            if (sender == null)
148
                throw new ArgumentNullException("sender");
149
            if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
150
                throw new ArgumentException("e");
151
            Contract.Ensures(_cachedDeletedFullPath == null);
152
            Contract.EndContractBlock();
153

    
154
            try
155
            {
156
                //A Move action results in a sequence of a Delete and a Create or Change event
157
                //If the actual action is a Move, raise a Move event instead of the actual event
158
                if (HandleMoved(sender, e))
159
                    return;
160

    
161
                //Otherwise, propagate the Delete event if it exists 
162
                PropagateCachedDeleted(sender);
163
                //and propagate the actual event
164
                var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
165
                if (actualEvent != null)
166
                    actualEvent(sender, e);
167
            }
168
            finally
169
            {
170
                //Finally, make sure the cached path is cleared
171
                _cachedDeletedFullPath = null;
172
            }
173

    
174
        }
175

    
176
        private bool HandleMoved(object sender, FileSystemEventArgs e)
177
        {
178
            if (sender == null)
179
                throw new ArgumentNullException("sender");
180
            if (!(e.ChangeType == WatcherChangeTypes.Created ||
181
                                                  e.ChangeType == WatcherChangeTypes.Changed))
182
                throw new ArgumentException("e");
183
            Contract.EndContractBlock();
184

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

    
189
            var oldName = Path.GetFileName(_cachedDeletedFullPath);
190
            //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
191
            var newName = Path.GetFileName(e.Name);
192
            //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
193
            var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);
194

    
195
            if (!hasMoved)
196
                return false;
197

    
198
            try
199
            {
200
                //If the actual action is a Move, raise a Move event instead of the actual event
201
                var newDirectory = Path.GetDirectoryName(e.FullPath);
202
                var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
203

    
204
                if (Moved != null)
205
                {
206
                    Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
207
                    //If the moved item is a dictionary, we need to raise a change event for each child item
208
                    //When a directory is moved within the same volume, Windows raises events only for the directory object,
209
                    //not its children. This happens because the move actually changes a single directory entry. It doesn't
210
                    //affect the entries of the children.
211
                    var directory = new DirectoryInfo(e.FullPath);
212
                    if (directory.Exists)
213
                    {
214
                        foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
215
                        {
216
                            var newChildDirectory = Path.GetDirectoryName(child.FullName);
217

    
218
                            var relativePath=child.AsRelativeTo(newDirectory);
219
                            var relativeFolder = Path.GetDirectoryName(relativePath);
220
                            var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder);
221
                            Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name));
222
                        }
223
                    }
224

    
225
                }
226

    
227
            }
228
            finally
229
            {
230
                _cachedDeletedFullPath = null;
231
            }
232
            return true;
233
        }
234

    
235
        private void PropagateCachedDeleted(object sender)
236
        {
237
            if (sender == null)
238
                throw new ArgumentNullException("sender");
239
            Contract.Ensures(_cachedDeletedFullPath == null);
240
            Contract.EndContractBlock();
241

    
242
            //Nothing to handle if there is no cached deleted file
243
            if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
244
                return;
245
            
246
            var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
247
            var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
248

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

    
256
            _cachedDeletedFullPath = null;
257
        }
258
    }
259
}