2 /* -----------------------------------------------------------------------
3 * <copyright file="FileAgent.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
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.
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.
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.
39 * -----------------------------------------------------------------------
43 using System.Collections.Generic;
44 using System.ComponentModel.Composition;
45 using System.Diagnostics;
46 using System.Diagnostics.Contracts;
50 using System.Threading.Tasks;
51 using Pithos.Interfaces;
56 namespace Pithos.Core.Agents
59 public class FileAgent
61 Agent<WorkflowState> _agent;
62 private FileSystemWatcher _watcher;
63 private FileSystemWatcherAdapter _adapter;
66 public IStatusKeeper StatusKeeper { get; set; }
68 public IPithosWorkflow Workflow { get; set; }
70 public WorkflowAgent WorkflowAgent { get; set; }
72 private AccountInfo AccountInfo { get; set; }
74 private string RootPath { get; set; }
76 private static readonly ILog Log = LogManager.GetLogger("FileAgent");
78 public void Start(AccountInfo accountInfo,string rootPath)
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();
88 AccountInfo = accountInfo;
90 _watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true};
91 _adapter = new FileSystemWatcherAdapter(_watcher);
93 _adapter.Changed += OnFileEvent;
94 _adapter.Created += OnFileEvent;
95 _adapter.Deleted += OnFileEvent;
96 _adapter.Renamed += OnRenameEvent;
97 _adapter.Moved += OnMoveEvent;
98 _watcher.EnableRaisingEvents = true;
101 _agent = Agent<WorkflowState>.Start(inbox =>
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));
115 private Task<object> Process(WorkflowState state)
118 throw new ArgumentNullException("state");
119 Contract.EndContractBlock();
121 if (Ignore(state.Path))
122 return CompletedTask<object>.Default;
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 &&
129 state.TriggeringChange == WatcherChangeTypes.Created ||
130 state.TriggeringChange == WatcherChangeTypes.Changed
132 return CompletedTask<object>.Default;
136 UpdateFileStatus(state);
137 UpdateOverlayStatus(state);
138 UpdateFileChecksum(state);
139 WorkflowAgent.Post(state);
141 catch (IOException exc)
143 if (File.Exists(state.Path))
145 Log.WarnFormat("File access error occured, retrying {0}\n{1}", state.Path, exc);
150 Log.WarnFormat("File {0} does not exist. Will be ignored\n{1}", state.Path, exc);
153 catch (Exception exc)
155 Log.WarnFormat("Error occured while indexing{0}. The file will be skipped\n{1}",
158 return CompletedTask<object>.Default;
163 get { return _watcher == null || !_watcher.EnableRaisingEvents; }
166 if (_watcher != null)
167 _watcher.EnableRaisingEvents = !value;
171 public string CachePath { get; set; }
173 private List<string> _selectivePaths = new List<string>();
174 public List<string> SelectivePaths
176 get { return _selectivePaths; }
177 set { _selectivePaths = value; }
181 public void Post(WorkflowState workflowState)
183 if (workflowState == null)
184 throw new ArgumentNullException("workflowState");
185 Contract.EndContractBlock();
187 _agent.Post(workflowState);
192 if (_watcher != null)
194 _watcher.Changed -= OnFileEvent;
195 _watcher.Created -= OnFileEvent;
196 _watcher.Deleted -= OnFileEvent;
197 _watcher.Renamed -= OnRenameEvent;
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="*")
210 var monitoredFiles = from filePath in Directory.EnumerateFileSystemEntries(RootPath, searchPattern, SearchOption.AllDirectories)
211 where !Ignore(filePath)
213 return monitoredFiles;
216 public IEnumerable<FileInfo> EnumerateFileInfos(string searchPattern="*")
218 var rootDir = new DirectoryInfo(RootPath);
219 var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
220 where !Ignore(file.FullName)
222 return monitoredFiles;
225 public IEnumerable<string> EnumerateFilesAsRelativeUrls(string searchPattern="*")
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;
234 public IEnumerable<string> EnumerateFilesSystemInfosAsRelativeUrls(string searchPattern="*")
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;
246 private bool Ignore(string filePath)
248 //Ignore all first-level directories and files (ie at the container folders level)
249 if (FoundBelowRoot(filePath, RootPath,1))
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))
257 //Ignore second-level (container) folders under the "others" folder (ie at the container folders level).
258 if (FoundBelowRoot(filePath, othersPath,2))
262 //Ignore anything happening in the cache path
263 if (filePath.StartsWith(CachePath))
265 if (_ignoreFiles.ContainsKey(filePath.ToLower()))
268 //Ignore if selective synchronization is defined,
269 return SelectivePaths.Count > 0
270 //And the target file is not below any of the selective paths
271 && !SelectivePaths.Any(filePath.IsAtOrDirectlyBelow);
274 /* private static bool FoundInRoot(string filePath, string rootPath)
276 //var rootDirectory = new DirectoryInfo(rootPath);
278 //If the paths are equal, return true
279 if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
282 //If the filepath is below the root path
283 if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
285 //Get the relative path
286 var relativePath = filePath.Substring(rootPath.Length + 1);
287 //If the relativePath does NOT contains a path separator, we found a match
288 return (!relativePath.Contains(@"\"));
291 //If the filepath is not under the root path, return false
296 private static bool FoundBelowRoot(string filePath, string rootPath,int level)
298 //var rootDirectory = new DirectoryInfo(rootPath);
300 //If the paths are equal, return true
301 if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
304 //If the filepath is below the root path
305 if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
307 //Get the relative path
308 var relativePath = filePath.Substring(rootPath.Length + 1);
309 //If the relativePath does NOT contains a path separator, we found a match
310 var levels=relativePath.ToCharArray().Count(c=>c=='\\')+1;
311 return levels==level;
314 //If the filepath is not under the root path, return false
318 //Post a Change message for all events except rename
319 void OnFileEvent(object sender, FileSystemEventArgs e)
321 //Ignore events that affect the cache folder
322 var filePath = e.FullPath;
323 if (Ignore(filePath))
326 _agent.Post(new WorkflowState{AccountInfo=AccountInfo, Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType });
330 //Post a Change message for renames containing the old and new names
331 void OnRenameEvent(object sender, RenamedEventArgs e)
333 var oldFullPath = e.OldFullPath;
334 var fullPath = e.FullPath;
335 if (Ignore(oldFullPath) || Ignore(fullPath))
338 _agent.Post(new WorkflowState
340 AccountInfo=AccountInfo,
341 OldPath = oldFullPath,
342 OldFileName = e.OldName,
345 TriggeringChange = e.ChangeType
349 //Post a Change message for renames containing the old and new names
350 void OnMoveEvent(object sender, MovedEventArgs e)
352 var oldFullPath = e.OldFullPath;
353 var fullPath = e.FullPath;
354 if (Ignore(oldFullPath) || Ignore(fullPath))
357 _agent.Post(new WorkflowState
359 AccountInfo=AccountInfo,
360 OldPath = oldFullPath,
361 OldFileName = e.OldName,
364 TriggeringChange = e.ChangeType
370 private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
372 {WatcherChangeTypes.Created,FileStatus.Created},
373 {WatcherChangeTypes.Changed,FileStatus.Modified},
374 {WatcherChangeTypes.Deleted,FileStatus.Deleted},
375 {WatcherChangeTypes.Renamed,FileStatus.Renamed}
378 private Dictionary<string, string> _ignoreFiles=new Dictionary<string, string>();
380 private WorkflowState UpdateFileStatus(WorkflowState state)
383 throw new ArgumentNullException("state");
384 if (String.IsNullOrWhiteSpace(state.Path))
385 throw new ArgumentException("The state's Path can't be empty","state");
386 Contract.EndContractBlock();
388 var path = state.Path;
389 var status = _statusDict[state.TriggeringChange];
390 var oldStatus = Workflow.StatusKeeper.GetFileStatus(path);
391 if (status == oldStatus)
393 state.Status = status;
397 if (state.Status == FileStatus.Renamed)
398 Workflow.ClearFileStatus(path);
400 state.Status = Workflow.SetFileStatus(path, status);
404 private WorkflowState UpdateOverlayStatus(WorkflowState state)
407 throw new ArgumentNullException("state");
408 Contract.EndContractBlock();
413 switch (state.Status)
415 case FileStatus.Created:
416 case FileStatus.Modified:
417 this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
419 case FileStatus.Deleted:
420 //this.StatusAgent.RemoveFileOverlayStatus(state.Path);
422 case FileStatus.Renamed:
423 this.StatusKeeper.ClearFileStatus(state.OldPath);
424 this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
426 case FileStatus.Unchanged:
427 this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal);
431 if (state.Status == FileStatus.Deleted)
432 NativeMethods.RaiseChangeNotification(Path.GetDirectoryName(state.Path));
434 NativeMethods.RaiseChangeNotification(state.Path);
439 private WorkflowState UpdateFileChecksum(WorkflowState state)
444 if (state.Status == FileStatus.Deleted)
447 var path = state.Path;
448 //Skip calculation for folders
449 if (Directory.Exists(path))
452 var info = new FileInfo(path);
453 string hash = info.CalculateHash(StatusKeeper.BlockSize,StatusKeeper.BlockHash);
454 StatusKeeper.UpdateFileChecksum(path, hash);
460 //Does the file exist in the container's local folder?
461 public bool Exists(string relativePath)
463 if (String.IsNullOrWhiteSpace(relativePath))
464 throw new ArgumentNullException("relativePath");
465 //A RootPath must be set before calling this method
466 if (String.IsNullOrWhiteSpace(RootPath))
467 throw new InvalidOperationException("RootPath was not set");
468 Contract.EndContractBlock();
469 //Create the absolute path by combining the RootPath with the relativePath
470 var absolutePath=Path.Combine(RootPath, relativePath);
471 //Is this a valid file?
472 if (File.Exists(absolutePath))
475 if (Directory.Exists(absolutePath))
477 //Fail if it is neither
481 public static FileAgent GetFileAgent(AccountInfo accountInfo)
483 return GetFileAgent(accountInfo.AccountPath);
486 public static FileAgent GetFileAgent(string rootPath)
488 return AgentLocator<FileAgent>.Get(rootPath.ToLower());
492 public FileSystemInfo GetFileSystemInfo(string relativePath)
494 if (String.IsNullOrWhiteSpace(relativePath))
495 throw new ArgumentNullException("relativePath");
496 //A RootPath must be set before calling this method
497 if (String.IsNullOrWhiteSpace(RootPath))
498 throw new InvalidOperationException("RootPath was not set");
499 Contract.EndContractBlock();
501 var absolutePath = Path.Combine(RootPath, relativePath);
503 if (Directory.Exists(absolutePath))
504 return new DirectoryInfo(absolutePath).WithProperCapitalization();
506 return new FileInfo(absolutePath).WithProperCapitalization();
510 public void Delete(string relativePath)
512 var absolutePath = Path.Combine(RootPath, relativePath).ToLower();
513 if (File.Exists(absolutePath))
517 File.Delete(absolutePath);
519 //The file may have been deleted by another thread. Just ignore the relevant exception
520 catch (FileNotFoundException) { }
522 else if (Directory.Exists(absolutePath))
526 Directory.Delete(absolutePath, true);
528 //The directory may have been deleted by another thread. Just ignore the relevant exception
529 catch (DirectoryNotFoundException){}
532 //_ignoreFiles[absolutePath] = absolutePath;
533 StatusKeeper.ClearFileStatus(absolutePath);