Changes for directories
[pithos-ms-client] / trunk / Pithos.Core / Agents / FileAgent.cs
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
6 using System.IO;
7 using System.Linq;
8 using System.Text;
9 using System.Threading.Tasks;
10 using Pithos.Interfaces;
11 using Pithos.Network;
12 using log4net;
13 using log4net.Core;
14
15 namespace Pithos.Core.Agents
16 {
17 //    [Export]
18     public class FileAgent
19     {
20         Agent<WorkflowState> _agent;
21         private FileSystemWatcher _watcher;
22
23         //[Import]
24         public IStatusKeeper StatusKeeper { get; set; }
25         //[Import]
26         public IPithosWorkflow Workflow { get; set; }
27         //[Import]
28         public WorkflowAgent WorkflowAgent { get; set; }
29
30         private AccountInfo AccountInfo { get; set; }
31
32         private string RootPath { get;  set; }
33
34         private static readonly ILog Log = LogManager.GetLogger("FileAgent");
35
36         public void Start(AccountInfo accountInfo,string rootPath)
37         {
38             if (accountInfo==null)
39                 throw new ArgumentNullException("accountInfo");
40             if (String.IsNullOrWhiteSpace(rootPath))
41                 throw new ArgumentNullException("rootPath");
42             if (!Path.IsPathRooted(rootPath))
43                 throw new ArgumentException("rootPath must be an absolute path","rootPath");
44             Contract.EndContractBlock();
45
46             AccountInfo = accountInfo;
47             RootPath = rootPath;
48             _watcher = new FileSystemWatcher(rootPath);
49             _watcher.IncludeSubdirectories = true;            
50             _watcher.Changed += OnFileEvent;
51             _watcher.Created += OnFileEvent;
52             _watcher.Deleted += OnFileEvent;
53             _watcher.Renamed += OnRenameEvent;
54             _watcher.EnableRaisingEvents = true;
55
56
57             _agent = Agent<WorkflowState>.Start(inbox =>
58             {
59                 Action loop = null;
60                 loop = () =>
61                 {
62                     var message = inbox.Receive();
63                     var process=message.Then(Process,inbox.CancellationToken);
64
65                     inbox.LoopAsync(process,loop,ex=>
66                         Log.ErrorFormat("[ERROR] File Event Processing:\r{0}", ex));
67                 };
68                 loop();
69             });
70         }
71
72         private Task<object> Process(WorkflowState state)
73         {
74             if (state==null)
75                 throw new ArgumentNullException("state");
76             Contract.EndContractBlock();
77
78             if (Ignore(state.Path))
79                 return CompletedTask<object>.Default;
80
81             var networkState = NetworkGate.GetNetworkState(state.Path);
82             //Skip if the file is already being downloaded or uploaded and 
83             //the change is create or modify
84             if (networkState != NetworkOperation.None &&
85                 (
86                     state.TriggeringChange == WatcherChangeTypes.Created ||
87                     state.TriggeringChange == WatcherChangeTypes.Changed
88                 ))
89                 return CompletedTask<object>.Default;
90
91             try
92             {
93                 UpdateFileStatus(state);
94                 UpdateOverlayStatus(state);
95                 UpdateFileChecksum(state);
96                 WorkflowAgent.Post(state);
97             }
98             catch (IOException exc)
99             {
100                 if (File.Exists(state.Path))
101                 {
102                     Log.WarnFormat("File access error occured, retrying {0}\n{1}", state.Path, exc);
103                     _agent.Post(state);
104                 }
105                 else
106                 {
107                     Log.WarnFormat("File {0} does not exist. Will be ignored\n{1}", state.Path, exc);
108                 }
109             }
110             catch (Exception exc)
111             {
112                 Log.WarnFormat("Error occured while indexing{0}. The file will be skipped\n{1}",
113                                state.Path, exc);
114             }
115             return CompletedTask<object>.Default;
116         }
117
118         public bool Pause
119         {
120             get { return _watcher == null || !_watcher.EnableRaisingEvents; }
121             set
122             {
123                 if (_watcher != null)
124                     _watcher.EnableRaisingEvents = !value;                
125             }
126         }
127
128         public string CachePath { get; set; }
129
130         private List<string> _selectivePaths = new List<string>();
131         public List<string> SelectivePaths
132         {
133             get { return _selectivePaths; }
134             set { _selectivePaths = value; }
135         }
136
137
138         public void Post(WorkflowState workflowState)
139         {
140             if (workflowState == null)
141                 throw new ArgumentNullException("workflowState");
142             Contract.EndContractBlock();
143
144             _agent.Post(workflowState);
145         }
146
147         public void Stop()
148         {
149             if (_watcher != null)
150             {
151                 _watcher.Changed -= OnFileEvent;
152                 _watcher.Created -= OnFileEvent;
153                 _watcher.Deleted -= OnFileEvent;
154                 _watcher.Renamed -= OnRenameEvent;
155                 _watcher.Dispose();
156             }
157             _watcher = null;
158
159             if (_agent!=null)
160                 _agent.Stop();
161         }
162
163         // Enumerate all files in the Pithos directory except those in the Fragment folder
164         // and files with a .ignore extension
165         public IEnumerable<string> EnumerateFiles(string searchPattern="*")
166         {
167             var monitoredFiles = from filePath in Directory.EnumerateFileSystemEntries(RootPath, searchPattern, SearchOption.AllDirectories)
168                                  where !Ignore(filePath)
169                                  select filePath;
170             return monitoredFiles;
171         }
172
173         public IEnumerable<FileInfo> EnumerateFileInfos(string searchPattern="*")
174         {
175             var rootDir = new DirectoryInfo(RootPath);
176             var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
177                                  where !Ignore(file.FullName)
178                                  select file;
179             return monitoredFiles;
180         }                
181
182         public IEnumerable<string> EnumerateFilesAsRelativeUrls(string searchPattern="*")
183         {
184             var rootDir = new DirectoryInfo(RootPath);
185             var monitoredFiles = from file in rootDir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)
186                                  where !Ignore(file.FullName)
187                                  select file.AsRelativeUrlTo(RootPath);
188             return monitoredFiles;
189         }                
190
191
192         
193
194         private bool Ignore(string filePath)
195         {
196             var pithosPath = Path.Combine(RootPath, "pithos");
197             if (pithosPath.Equals(filePath, StringComparison.InvariantCultureIgnoreCase))
198                 return true;
199             if (filePath.StartsWith(CachePath))
200                 return true;
201             if (_ignoreFiles.ContainsKey(filePath.ToLower()))
202                 return true;
203             return false;
204         }
205
206         //Post a Change message for all events except rename
207         void OnFileEvent(object sender, FileSystemEventArgs e)
208         {
209             //Ignore events that affect the cache folder
210             var filePath = e.FullPath;
211             if (Ignore(filePath)) 
212                 return;
213           /*  if (Directory.Exists(filePath))
214                 return;    */        
215             _agent.Post(new WorkflowState{AccountInfo=AccountInfo, Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType });
216         }
217
218
219         //Post a Change message for renames containing the old and new names
220         void OnRenameEvent(object sender, RenamedEventArgs e)
221         {
222             var oldFullPath = e.OldFullPath;
223             var fullPath = e.FullPath;
224             if (Ignore(oldFullPath) || Ignore(fullPath))
225                 return;
226
227             _agent.Post(new WorkflowState
228             {
229                 AccountInfo=AccountInfo,
230                 OldPath = oldFullPath,
231                 OldFileName = e.OldName,
232                 Path = fullPath,
233                 FileName = e.Name,
234                 TriggeringChange = e.ChangeType
235             });
236         }
237
238
239
240         private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
241         {
242             {WatcherChangeTypes.Created,FileStatus.Created},
243             {WatcherChangeTypes.Changed,FileStatus.Modified},
244             {WatcherChangeTypes.Deleted,FileStatus.Deleted},
245             {WatcherChangeTypes.Renamed,FileStatus.Renamed}
246         };
247
248         private Dictionary<string,string> _ignoreFiles=new Dictionary<string, string>();
249
250         private WorkflowState UpdateFileStatus(WorkflowState state)
251         {
252             if (state==null)
253                 throw new ArgumentNullException("state");
254             if (String.IsNullOrWhiteSpace(state.Path))
255                 throw new ArgumentException("The state's Path can't be empty","state");
256             Contract.EndContractBlock();
257
258             var path = state.Path;
259             var status = _statusDict[state.TriggeringChange];
260             var oldStatus = Workflow.StatusKeeper.GetFileStatus(path);
261             if (status == oldStatus)
262             {
263                 state.Status = status;
264                 state.Skip = true;
265                 return state;
266             }
267             if (state.Status == FileStatus.Renamed)
268                 Workflow.ClearFileStatus(path);
269
270             state.Status = Workflow.SetFileStatus(path, status);
271             return state;
272         }
273
274         private WorkflowState UpdateOverlayStatus(WorkflowState state)
275         {
276             if (state==null)
277                 throw new ArgumentNullException("state");
278             Contract.EndContractBlock();
279
280             if (state.Skip)
281                 return state;
282
283             switch (state.Status)
284             {
285                 case FileStatus.Created:
286                 case FileStatus.Modified:
287                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
288                     break;
289                 case FileStatus.Deleted:
290                     //this.StatusAgent.RemoveFileOverlayStatus(state.Path);
291                     break;
292                 case FileStatus.Renamed:
293                     this.StatusKeeper.ClearFileStatus(state.OldPath);
294                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
295                     break;
296                 case FileStatus.Unchanged:
297                     this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal);
298                     break;
299             }
300
301             if (state.Status == FileStatus.Deleted)
302                 NativeMethods.RaiseChangeNotification(Path.GetDirectoryName(state.Path));
303             else
304                 NativeMethods.RaiseChangeNotification(state.Path);
305             return state;
306         }
307
308
309         private WorkflowState UpdateFileChecksum(WorkflowState state)
310         {
311             if (state.Skip)
312                 return state;
313
314             if (state.Status == FileStatus.Deleted)
315                 return state;
316
317             var path = state.Path;
318             //Skip calculation for folders
319             if (Directory.Exists(path))
320                 return state;
321
322             var info = new FileInfo(path);
323             string hash = info.CalculateHash(StatusKeeper.BlockSize,StatusKeeper.BlockHash);
324             StatusKeeper.UpdateFileChecksum(path, hash);
325
326             state.Hash = hash;
327             return state;
328         }
329
330         //Does the file exist in the container's local folder?
331         public bool Exists(string relativePath)
332         {
333             if (String.IsNullOrWhiteSpace(relativePath))
334                 throw new ArgumentNullException("relativePath");
335             //A RootPath must be set before calling this method
336             if (String.IsNullOrWhiteSpace(RootPath))
337                 throw new InvalidOperationException("RootPath was not set");
338             Contract.EndContractBlock();
339             //Create the absolute path by combining the RootPath with the relativePath
340             var absolutePath=Path.Combine(RootPath, relativePath);
341             //Is this a valid file?
342             if (File.Exists(absolutePath))
343                 return true;
344             //Or a directory?
345             if (Directory.Exists(absolutePath))
346                 return true;
347             //Fail if it is neither
348             return false;
349         }
350
351         public FileSystemInfo GetFileSystemInfo(string relativePath)
352         {
353             if (String.IsNullOrWhiteSpace(relativePath))
354                 throw new ArgumentNullException("relativePath");
355             //A RootPath must be set before calling this method
356             if (String.IsNullOrWhiteSpace(RootPath))
357                 throw new InvalidOperationException("RootPath was not set");            
358             Contract.EndContractBlock();            
359
360             var absolutePath = Path.Combine(RootPath, relativePath);
361
362             if (Directory.Exists(absolutePath))
363                 return new DirectoryInfo(absolutePath).WithProperCapitalization();
364             else
365                 return new FileInfo(absolutePath).WithProperCapitalization();
366             
367         }
368
369         public void Delete(string relativePath)
370         {
371             var absolutePath = Path.Combine(RootPath, relativePath).ToLower();
372             if (File.Exists(absolutePath))
373             {    
374                 try
375                 {
376                     File.Delete(absolutePath);
377                 }
378                 //The file may have been deleted by another thread. Just ignore the relevant exception
379                 catch (FileNotFoundException) { }
380             }
381             else if (Directory.Exists(absolutePath))
382             {
383                 try
384                 {
385                     Directory.Delete(absolutePath, true);
386                 }
387                 //The directory may have been deleted by another thread. Just ignore the relevant exception
388                 catch (DirectoryNotFoundException){}                
389             }
390         
391             //_ignoreFiles[absolutePath] = absolutePath;                
392             StatusKeeper.ClearFileStatus(absolutePath);
393         }
394     }
395 }