Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ 78ebfd2d

History | View | Annotate | Download (9.7 kB)

1
// -----------------------------------------------------------------------
2
// <copyright file="FileInfoExtensions.cs" company="GRNET">
3
// Copyright 2011 GRNET S.A. All rights reserved.
4
// 
5
// Redistribution and use in source and binary forms, with or
6
// without modification, are permitted provided that the following
7
// conditions are met:
8
// 
9
//   1. Redistributions of source code must retain the above
10
//      copyright notice, this list of conditions and the following
11
//      disclaimer.
12
// 
13
//   2. Redistributions in binary form must reproduce the above
14
//      copyright notice, this list of conditions and the following
15
//      disclaimer in the documentation and/or other materials
16
//      provided with the distribution.
17
// 
18
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
// POSSIBILITY OF SUCH DAMAGE.
30
// 
31
// The views and conclusions contained in the software and
32
// documentation are those of the authors and should not be
33
// interpreted as representing official policies, either expressed
34
// or implied, of GRNET S.A.
35
// </copyright>
36
// -----------------------------------------------------------------------
37

    
38

    
39
using System.Diagnostics.Contracts;
40
using System.IO;
41
using System.Threading.Tasks;
42

    
43
namespace Pithos.Core.Agents
44
{
45
    using System;
46
    using System.Collections.Generic;
47
    using System.Linq;
48
    using System.Text;
49

    
50
    /// <summary>
51
    /// TODO: Update summary.
52
    /// </summary>
53
    public class FileSystemWatcherAdapter
54
    {
55
         public event FileSystemEventHandler Changed;
56
        public event FileSystemEventHandler Created;
57
        public event FileSystemEventHandler Deleted;
58
        public event RenamedEventHandler Renamed;
59
        public event MovedEventHandler Moved;
60

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

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

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

    
77
        private static void OnTimeout(object state)
78
        {
79
            throw new NotImplementedException();
80
        }
81

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

    
93
            //Handle any previously deleted event
94
            PropagateCachedDeleted(sender);
95

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

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

    
107
            //TODO: This requires synchronization of the _cachedDeletedFullPath field
108
            //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
109
            //Maybe, use a timer instead of a task
110
            
111
            TaskEx.Delay(PropagateDelay).ContinueWith(t =>
112
                                                           {
113
                                                               var myPath = e.FullPath;
114
                                                               if (this._cachedDeletedFullPath==myPath)
115
                                                                    PropagateCachedDeleted(sender);
116
                                                           });
117
        }
118

    
119
        private void OnRename(object sender, RenamedEventArgs e)
120
        {
121
            if (sender == null)
122
                throw new ArgumentNullException("sender");
123
            Contract.Ensures(_cachedDeletedFullPath == null);
124
            Contract.EndContractBlock();
125

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

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

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

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

    
170
        }
171

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

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

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

    
191
            if (!hasMoved)
192
                return false;
193

    
194
            try
195
            {
196
                //If the actual action is a Move, raise a Move event instead of the actual event
197
                var newDirectory = Path.GetDirectoryName(e.FullPath);
198
                var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
199
                if (Moved != null)
200
                    Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
201
            }
202
            finally
203
            {
204
                _cachedDeletedFullPath = null;
205
            }
206
            return true;
207
        }
208

    
209
        private void PropagateCachedDeleted(object sender)
210
        {
211
            if (sender == null)
212
                throw new ArgumentNullException("sender");
213
            Contract.Ensures(_cachedDeletedFullPath == null);
214
            Contract.EndContractBlock();
215

    
216
            //Nothing to handle if there is no cached deleted file
217
            if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
218
                return;
219
            
220
            var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
221
            var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
222

    
223
            if (Deleted!=null)
224
                Deleted(sender, new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));
225

    
226
            _cachedDeletedFullPath = null;
227
        }
228
    }
229
}