Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / FileAgent.cs @ 139ac1e8

History | View | Annotate | Download (19.5 kB)

1
#region
2
/* -----------------------------------------------------------------------
3
 * <copyright file="FileAgent.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;
43
using System.Collections.Generic;
44
using System.ComponentModel.Composition;
45
using System.Diagnostics;
46
using System.Diagnostics.Contracts;
47
using System.IO;
48
using System.Linq;
49
using System.Text;
50
using System.Threading.Tasks;
51
using Pithos.Interfaces;
52
using Pithos.Network;
53
using log4net;
54
using log4net.Core;
55

    
56
namespace Pithos.Core.Agents
57
{
58
//    [Export]
59
    public class FileAgent
60
    {
61
        Agent<WorkflowState> _agent;
62
        private FileSystemWatcher _watcher;
63
        private FileSystemWatcherAdapter _adapter;
64

    
65
        //[Import]
66
        public IStatusKeeper StatusKeeper { get; set; }
67
        //[Import]
68
        public IPithosWorkflow Workflow { get; set; }
69
        //[Import]
70
        public WorkflowAgent WorkflowAgent { get; set; }
71

    
72
        private AccountInfo AccountInfo { get; set; }
73

    
74
        private string RootPath { get;  set; }
75

    
76
        private static readonly ILog Log = LogManager.GetLogger("FileAgent");
77

    
78
        public void Start(AccountInfo accountInfo,string rootPath)
79
        {
80
            if (accountInfo==null)
81
                throw new ArgumentNullException("accountInfo");
82
            if (String.IsNullOrWhiteSpace(rootPath))
83
                throw new ArgumentNullException("rootPath");
84
            if (!Path.IsPathRooted(rootPath))
85
                throw new ArgumentException("rootPath must be an absolute path","rootPath");
86
            Contract.EndContractBlock();
87

    
88
            AccountInfo = accountInfo;
89
            RootPath = rootPath;
90
            _watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true};
91
            _adapter = new FileSystemWatcherAdapter(_watcher);
92

    
93
            _adapter.Changed += OnFileEvent;
94
            _adapter.Created += OnFileEvent;
95
            _adapter.Deleted += OnFileEvent;
96
            _adapter.Renamed += OnRenameEvent;
97
            _adapter.Moved += OnMoveEvent;
98
            _watcher.EnableRaisingEvents = true;
99

    
100

    
101
            _agent = Agent<WorkflowState>.Start(inbox =>
102
            {
103
                Action loop = null;
104
                loop = () =>
105
                {
106
                    var message = inbox.Receive();
107
                    var process=message.Then(Process,inbox.CancellationToken);                    
108
                    inbox.LoopAsync(process,loop,ex=>
109
                        Log.ErrorFormat("[ERROR] File Event Processing:\r{0}", ex));
110
                };
111
                loop();
112
            });
113
        }
114

    
115
        private Task<object> Process(WorkflowState state)
116
        {
117
            if (state==null)
118
                throw new ArgumentNullException("state");
119
            Contract.EndContractBlock();
120

    
121
            if (Ignore(state.Path))
122
                return CompletedTask<object>.Default;
123

    
124
            var networkState = NetworkGate.GetNetworkState(state.Path);
125
            //Skip if the file is already being downloaded or uploaded and 
126
            //the change is create or modify
127
            if (networkState != NetworkOperation.None &&
128
                (
129
                    state.TriggeringChange == WatcherChangeTypes.Created ||
130
                    state.TriggeringChange == WatcherChangeTypes.Changed
131
                ))
132
                return CompletedTask<object>.Default;
133

    
134
            try
135
            {
136
                UpdateFileStatus(state);
137
                UpdateOverlayStatus(state);
138
                UpdateFileChecksum(state);
139
                WorkflowAgent.Post(state);
140
            }
141
            catch (IOException exc)
142
            {
143
                if (File.Exists(state.Path))
144
                {
145
                    Log.WarnFormat("File access error occured, retrying {0}\n{1}", state.Path, exc);
146
                    _agent.Post(state);
147
                }
148
                else
149
                {
150
                    Log.WarnFormat("File {0} does not exist. Will be ignored\n{1}", state.Path, exc);
151
                }
152
            }
153
            catch (Exception exc)
154
            {
155
                Log.WarnFormat("Error occured while indexing{0}. The file will be skipped\n{1}",
156
                               state.Path, exc);
157
            }
158
            return CompletedTask<object>.Default;
159
        }
160

    
161
        public bool Pause
162
        {
163
            get { return _watcher == null || !_watcher.EnableRaisingEvents; }
164
            set
165
            {
166
                if (_watcher != null)
167
                    _watcher.EnableRaisingEvents = !value;                
168
            }
169
        }
170

    
171
        public string CachePath { get; set; }
172

    
173
        private List<string> _selectivePaths = new List<string>();
174
        public List<string> SelectivePaths
175
        {
176
            get { return _selectivePaths; }
177
            set { _selectivePaths = value; }
178
        }
179

    
180

    
181
        public void Post(WorkflowState workflowState)
182
        {
183
            if (workflowState == null)
184
                throw new ArgumentNullException("workflowState");
185
            Contract.EndContractBlock();
186

    
187
            _agent.Post(workflowState);
188
        }
189

    
190
        public void Stop()
191
        {
192
            if (_watcher != null)
193
            {
194
                _watcher.Changed -= OnFileEvent;
195
                _watcher.Created -= OnFileEvent;
196
                _watcher.Deleted -= OnFileEvent;
197
                _watcher.Renamed -= OnRenameEvent;
198
                _watcher.Dispose();
199
            }
200
            _watcher = null;
201

    
202
            if (_agent!=null)
203
                _agent.Stop();
204
        }
205

    
206
        // Enumerate all files in the Pithos directory except those in the Fragment folder
207
        // and files with a .ignore extension
208
        public IEnumerable<string> EnumerateFiles(string searchPattern="*")
209
        {
210
            var monitoredFiles = from filePath in Directory.EnumerateFileSystemEntries(RootPath, searchPattern, SearchOption.AllDirectories)
211
                                 where !Ignore(filePath)
212
                                 select filePath;
213
            return monitoredFiles;
214
        }
215

    
216
        public IEnumerable<FileInfo> EnumerateFileInfos(string searchPattern="*")
217
        {
218
            var rootDir = new DirectoryInfo(RootPath);
219
            var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
220
                                 where !Ignore(file.FullName)
221
                                 select file;
222
            return monitoredFiles;
223
        }                
224

    
225
        public IEnumerable<string> EnumerateFilesAsRelativeUrls(string searchPattern="*")
226
        {
227
            var rootDir = new DirectoryInfo(RootPath);
228
            var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
229
                                 where !Ignore(file.FullName)
230
                                 select file.AsRelativeUrlTo(RootPath);
231
            return monitoredFiles;
232
        }                
233

    
234
        public IEnumerable<string> EnumerateFilesSystemInfosAsRelativeUrls(string searchPattern="*")
235
        {
236
            var rootDir = new DirectoryInfo(RootPath);
237
            var monitoredFiles = from file in rootDir.EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories)
238
                                 where !Ignore(file.FullName)
239
                                 select file.AsRelativeUrlTo(RootPath);
240
            return monitoredFiles;
241
        }                
242

    
243

    
244
        
245

    
246
        private bool Ignore(string filePath)
247
        {
248
            //Ignore all first-level directories and files (ie at the container folders level)
249
            if (FoundBelowRoot(filePath, RootPath,1))
250
                return true;
251

    
252
            //Ignore first-level items under the "others" folder (ie at the accounts folders level).
253
            var othersPath = Path.Combine(RootPath, FolderConstants.OthersFolder);
254
            if (FoundBelowRoot(filePath, othersPath,1))
255
                return true;
256

    
257
            //Ignore second-level (container) folders under the "others" folder (ie at the container folders level). 
258
            if (FoundBelowRoot(filePath, othersPath,2))
259
                return true;            
260

    
261

    
262
            //Ignore anything happening in the cache path
263
            if (filePath.StartsWith(CachePath))
264
                return true;
265
            if (_ignoreFiles.ContainsKey(filePath.ToLower()))
266
                return true;
267

    
268
            //If selective synchronization is defined, 
269
            if (SelectivePaths.Count>0)
270
            {
271
                //Abort if the file is not located under any of the selected folders
272
                if (!SelectivePaths.Any(filePath.StartsWith))
273
                    return true;
274
            }
275

    
276
            return false;
277
        }
278

    
279
/*        private static bool FoundInRoot(string filePath, string rootPath)
280
        {
281
            //var rootDirectory = new DirectoryInfo(rootPath);
282

    
283
            //If the paths are equal, return true
284
            if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
285
                return true;
286

    
287
            //If the filepath is below the root path
288
            if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
289
            {
290
                //Get the relative path
291
                var relativePath = filePath.Substring(rootPath.Length + 1);
292
                //If the relativePath does NOT contains a path separator, we found a match
293
                return (!relativePath.Contains(@"\"));
294
            }
295

    
296
            //If the filepath is not under the root path, return false
297
            return false;            
298
        }*/
299

    
300

    
301
        private static bool FoundBelowRoot(string filePath, string rootPath,int level)
302
        {
303
            //var rootDirectory = new DirectoryInfo(rootPath);
304

    
305
            //If the paths are equal, return true
306
            if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
307
                return true;
308

    
309
            //If the filepath is below the root path
310
            if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
311
            {
312
                //Get the relative path
313
                var relativePath = filePath.Substring(rootPath.Length + 1);
314
                //If the relativePath does NOT contains a path separator, we found a match
315
                var levels=relativePath.ToCharArray().Count(c=>c=='\\')+1;                
316
                return levels==level;
317
            }
318

    
319
            //If the filepath is not under the root path, return false
320
            return false;            
321
        }
322

    
323
        //Post a Change message for all events except rename
324
        void OnFileEvent(object sender, FileSystemEventArgs e)
325
        {
326
            //Ignore events that affect the cache folder
327
            var filePath = e.FullPath;
328
            if (Ignore(filePath)) 
329
                return;
330

    
331
            _agent.Post(new WorkflowState{AccountInfo=AccountInfo, Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType });
332
        }
333

    
334

    
335
        //Post a Change message for renames containing the old and new names
336
        void OnRenameEvent(object sender, RenamedEventArgs e)
337
        {
338
            var oldFullPath = e.OldFullPath;
339
            var fullPath = e.FullPath;
340
            if (Ignore(oldFullPath) || Ignore(fullPath))
341
                return;
342

    
343
            _agent.Post(new WorkflowState
344
            {
345
                AccountInfo=AccountInfo,
346
                OldPath = oldFullPath,
347
                OldFileName = e.OldName,
348
                Path = fullPath,
349
                FileName = e.Name,
350
                TriggeringChange = e.ChangeType
351
            });
352
        }
353

    
354
        //Post a Change message for renames containing the old and new names
355
        void OnMoveEvent(object sender, MovedEventArgs e)
356
        {
357
            var oldFullPath = e.OldFullPath;
358
            var fullPath = e.FullPath;
359
            if (Ignore(oldFullPath) || Ignore(fullPath))
360
                return;
361

    
362
            _agent.Post(new WorkflowState
363
            {
364
                AccountInfo=AccountInfo,
365
                OldPath = oldFullPath,
366
                OldFileName = e.OldName,
367
                Path = fullPath,
368
                FileName = e.Name,
369
                TriggeringChange = e.ChangeType
370
            });
371
        }
372

    
373

    
374

    
375
        private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
376
        {
377
            {WatcherChangeTypes.Created,FileStatus.Created},
378
            {WatcherChangeTypes.Changed,FileStatus.Modified},
379
            {WatcherChangeTypes.Deleted,FileStatus.Deleted},
380
            {WatcherChangeTypes.Renamed,FileStatus.Renamed}
381
        };
382

    
383
        private Dictionary<string,string> _ignoreFiles=new Dictionary<string, string>();
384

    
385
        private WorkflowState UpdateFileStatus(WorkflowState state)
386
        {
387
            if (state==null)
388
                throw new ArgumentNullException("state");
389
            if (String.IsNullOrWhiteSpace(state.Path))
390
                throw new ArgumentException("The state's Path can't be empty","state");
391
            Contract.EndContractBlock();
392

    
393
            var path = state.Path;
394
            var status = _statusDict[state.TriggeringChange];
395
            var oldStatus = Workflow.StatusKeeper.GetFileStatus(path);
396
            if (status == oldStatus)
397
            {
398
                state.Status = status;
399
                state.Skip = true;
400
                return state;
401
            }
402
            if (state.Status == FileStatus.Renamed)
403
                Workflow.ClearFileStatus(path);
404

    
405
            state.Status = Workflow.SetFileStatus(path, status);
406
            return state;
407
        }
408

    
409
        private WorkflowState UpdateOverlayStatus(WorkflowState state)
410
        {
411
            if (state==null)
412
                throw new ArgumentNullException("state");
413
            Contract.EndContractBlock();
414

    
415
            if (state.Skip)
416
                return state;
417

    
418
            switch (state.Status)
419
            {
420
                case FileStatus.Created:
421
                case FileStatus.Modified:
422
                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
423
                    break;
424
                case FileStatus.Deleted:
425
                    //this.StatusAgent.RemoveFileOverlayStatus(state.Path);
426
                    break;
427
                case FileStatus.Renamed:
428
                    this.StatusKeeper.ClearFileStatus(state.OldPath);
429
                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
430
                    break;
431
                case FileStatus.Unchanged:
432
                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal);
433
                    break;
434
            }
435

    
436
            if (state.Status == FileStatus.Deleted)
437
                NativeMethods.RaiseChangeNotification(Path.GetDirectoryName(state.Path));
438
            else
439
                NativeMethods.RaiseChangeNotification(state.Path);
440
            return state;
441
        }
442

    
443

    
444
        private WorkflowState UpdateFileChecksum(WorkflowState state)
445
        {
446
            if (state.Skip)
447
                return state;
448

    
449
            if (state.Status == FileStatus.Deleted)
450
                return state;
451

    
452
            var path = state.Path;
453
            //Skip calculation for folders
454
            if (Directory.Exists(path))
455
                return state;
456

    
457
            var info = new FileInfo(path);
458
            string hash = info.CalculateHash(StatusKeeper.BlockSize,StatusKeeper.BlockHash);
459
            StatusKeeper.UpdateFileChecksum(path, hash);
460

    
461
            state.Hash = hash;
462
            return state;
463
        }
464

    
465
        //Does the file exist in the container's local folder?
466
        public bool Exists(string relativePath)
467
        {
468
            if (String.IsNullOrWhiteSpace(relativePath))
469
                throw new ArgumentNullException("relativePath");
470
            //A RootPath must be set before calling this method
471
            if (String.IsNullOrWhiteSpace(RootPath))
472
                throw new InvalidOperationException("RootPath was not set");
473
            Contract.EndContractBlock();
474
            //Create the absolute path by combining the RootPath with the relativePath
475
            var absolutePath=Path.Combine(RootPath, relativePath);
476
            //Is this a valid file?
477
            if (File.Exists(absolutePath))
478
                return true;
479
            //Or a directory?
480
            if (Directory.Exists(absolutePath))
481
                return true;
482
            //Fail if it is neither
483
            return false;
484
        }
485

    
486
        public FileSystemInfo GetFileSystemInfo(string relativePath)
487
        {
488
            if (String.IsNullOrWhiteSpace(relativePath))
489
                throw new ArgumentNullException("relativePath");
490
            //A RootPath must be set before calling this method
491
            if (String.IsNullOrWhiteSpace(RootPath))
492
                throw new InvalidOperationException("RootPath was not set");            
493
            Contract.EndContractBlock();            
494

    
495
            var absolutePath = Path.Combine(RootPath, relativePath);
496

    
497
            if (Directory.Exists(absolutePath))
498
                return new DirectoryInfo(absolutePath).WithProperCapitalization();
499
            else
500
                return new FileInfo(absolutePath).WithProperCapitalization();
501
            
502
        }
503

    
504
        public void Delete(string relativePath)
505
        {
506
            var absolutePath = Path.Combine(RootPath, relativePath).ToLower();
507
            if (File.Exists(absolutePath))
508
            {    
509
                try
510
                {
511
                    File.Delete(absolutePath);
512
                }
513
                //The file may have been deleted by another thread. Just ignore the relevant exception
514
                catch (FileNotFoundException) { }
515
            }
516
            else if (Directory.Exists(absolutePath))
517
            {
518
                try
519
                {
520
                    Directory.Delete(absolutePath, true);
521
                }
522
                //The directory may have been deleted by another thread. Just ignore the relevant exception
523
                catch (DirectoryNotFoundException){}                
524
            }
525
        
526
            //_ignoreFiles[absolutePath] = absolutePath;                
527
            StatusKeeper.ClearFileStatus(absolutePath);
528
        }
529
    }
530
}