Fix incorrect check in Selective Sync that prevented folder changes to propagate...
[pithos-ms-client] / trunk / Pithos.Core / Agents / FileAgent.cs
index 7aa7a30..b64773a 100644 (file)
-using System;
+#region
+/* -----------------------------------------------------------------------
+ * <copyright file="FileAgent.cs" company="GRNet">
+ * 
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and
+ * documentation are those of the authors and should not be
+ * interpreted as representing official policies, either expressed
+ * or implied, of GRNET S.A.
+ * </copyright>
+ * -----------------------------------------------------------------------
+ */
+#endregion
+using System;
 using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Diagnostics;
 using System.Diagnostics.Contracts;
 using System.IO;
 using System.Linq;
-using System.Text;
+using System.Reflection;
+using System.Threading.Tasks;
 using Pithos.Interfaces;
 using Pithos.Network;
+using log4net;
 
 namespace Pithos.Core.Agents
 {
-    [Export]
+//    [Export]
     public class FileAgent
     {
+        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+
         Agent<WorkflowState> _agent;
         private FileSystemWatcher _watcher;
+        private FileSystemWatcherAdapter _adapter;
 
-        [Import]
+        //[Import]
         public IStatusKeeper StatusKeeper { get; set; }
-        [Import]
+
+        public IStatusNotification StatusNotification { get; set; }
+        //[Import]
         public IPithosWorkflow Workflow { get; set; }
-        [Import]
+        //[Import]
         public WorkflowAgent WorkflowAgent { get; set; }
 
-        public string RootPath { get; private set; }
+        private AccountInfo AccountInfo { get; set; }
+
+        internal string RootPath { get;  set; }
+        
+        private FileEventIdleBatch _eventIdleBatch;
+
+        public TimeSpan IdleTimeout { get; set; }
+
+
+        private void ProcessBatchedEvents(Dictionary<string, FileSystemEventArgs[]> fileEvents)
+        {
+            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Uploading {0} files",fileEvents.Count));
+            foreach (var fileEvent in fileEvents)
+            {
+                var filePath = fileEvent.Key;
+                var changes = fileEvent.Value;
+                
+                if (Ignore(filePath)) continue;
+                                
+                foreach (var change in changes)
+                {
+                    if (change.ChangeType == WatcherChangeTypes.Renamed)
+                    {
+                        var rename = (MovedEventArgs) change;
+                        _agent.Post(new WorkflowState(change)
+                                        {
+                                            AccountInfo = AccountInfo,
+                                            OldPath = rename.OldFullPath,
+                                            OldFileName = Path.GetFileName(rename.OldName),
+                                            Path = rename.FullPath,
+                                            FileName = Path.GetFileName(rename.Name),
+                                            TriggeringChange = rename.ChangeType
+                                        });
+                    }
+                    else
+                        _agent.Post(new WorkflowState(change)
+                        {
+                            AccountInfo = AccountInfo,
+                            Path = change.FullPath,
+                            FileName = Path.GetFileName(change.Name),
+                            TriggeringChange = change.ChangeType
+                        });                        
+                }
+            }
+            StatusNotification.SetPithosStatus(PithosStatus.LocalComplete);
+        }
 
-        public void Start(string rootPath)
+        public void Start(AccountInfo accountInfo,string rootPath)
         {
+            if (accountInfo==null)
+                throw new ArgumentNullException("accountInfo");
+            if (String.IsNullOrWhiteSpace(rootPath))
+                throw new ArgumentNullException("rootPath");
+            if (!Path.IsPathRooted(rootPath))
+                throw new ArgumentException("rootPath must be an absolute path","rootPath");
+            if (IdleTimeout == null)
+                throw new InvalidOperationException("IdleTimeout must have a valid value");
+            Contract.EndContractBlock();
+
+            AccountInfo = accountInfo;
             RootPath = rootPath;
-            _watcher = new FileSystemWatcher(rootPath);
-            _watcher.Changed += OnFileEvent;
-            _watcher.Created += OnFileEvent;
-            _watcher.Deleted += OnFileEvent;
-            _watcher.Renamed += OnRenameEvent;
+
+            _eventIdleBatch = new FileEventIdleBatch((int)IdleTimeout.TotalMilliseconds, ProcessBatchedEvents);
+
+            _watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true,InternalBufferSize=8*4096};
+            _adapter = new FileSystemWatcherAdapter(_watcher);
+
+            _adapter.Changed += OnFileEvent;
+            _adapter.Created += OnFileEvent;
+            _adapter.Deleted += OnFileEvent;
+            //_adapter.Renamed += OnRenameEvent;
+            _adapter.Moved += OnMoveEvent;
             _watcher.EnableRaisingEvents = true;
 
 
@@ -43,30 +151,62 @@ namespace Pithos.Core.Agents
                 loop = () =>
                 {
                     var message = inbox.Receive();
-                    var process = message.ContinueWith(t =>
-                    {
-                        var state = t.Result;
-                        Process(state);
-                        inbox.DoAsync(loop);
-                    });
-
-                    process.ContinueWith(t =>
-                    {
-                        inbox.DoAsync(loop);
-                        if (t.IsFaulted)
-                        {
-                            var ex = t.Exception.InnerException;
-                            if (ex is OperationCanceledException)
-                                inbox.Stop();
-                            Trace.TraceError("[ERROR] File Event Processing:\r{0}", ex);
-                        }
-                    });
-
+                    var process=message.Then(Process,inbox.CancellationToken);                    
+                    inbox.LoopAsync(process,loop,ex=>
+                        Log.ErrorFormat("[ERROR] File Event Processing:\r{0}", ex));
                 };
                 loop();
             });
         }
 
+        private Task<object> Process(WorkflowState state)
+        {
+            if (state==null)
+                throw new ArgumentNullException("state");
+            Contract.EndContractBlock();
+
+            if (Ignore(state.Path))
+                return CompletedTask<object>.Default;
+
+            var networkState = NetworkGate.GetNetworkState(state.Path);
+            //Skip if the file is already being downloaded or uploaded and 
+            //the change is create or modify
+            if (networkState != NetworkOperation.None &&
+                (
+                    state.TriggeringChange == WatcherChangeTypes.Created ||
+                    state.TriggeringChange == WatcherChangeTypes.Changed
+                ))
+                return CompletedTask<object>.Default;
+
+            try
+            {
+                //StatusKeeper.EnsureFileState(state.Path);
+                
+                UpdateFileStatus(state);
+                UpdateOverlayStatus(state);
+                UpdateFileChecksum(state);
+                WorkflowAgent.Post(state);
+            }
+            catch (IOException exc)
+            {
+                if (File.Exists(state.Path))
+                {
+                    Log.WarnFormat("File access error occured, retrying {0}\n{1}", state.Path, exc);
+                    _agent.Post(state);
+                }
+                else
+                {
+                    Log.WarnFormat("File {0} does not exist. Will be ignored\n{1}", state.Path, exc);
+                }
+            }
+            catch (Exception exc)
+            {
+                Log.WarnFormat("Error occured while indexing{0}. The file will be skipped\n{1}",
+                               state.Path, exc);
+            }
+            return CompletedTask<object>.Default;
+        }
+
         public bool Pause
         {
             get { return _watcher == null || !_watcher.EnableRaisingEvents; }
@@ -77,10 +217,24 @@ namespace Pithos.Core.Agents
             }
         }
 
-        public string FragmentsPath { get; set; }
+        public string CachePath { get; set; }
+
+        /*private List<string> _selectivePaths = new List<string>();
+        public List<string> SelectivePaths
+        {
+            get { return _selectivePaths; }
+            set { _selectivePaths = value; }
+        }
+*/
+        public Selectives Selectives { get; set; }
+
 
         public void Post(WorkflowState workflowState)
         {
+            if (workflowState == null)
+                throw new ArgumentNullException("workflowState");
+            Contract.EndContractBlock();
+
             _agent.Post(workflowState);
         }
 
@@ -88,15 +242,12 @@ namespace Pithos.Core.Agents
         {
             if (_watcher != null)
             {
-                _watcher.Changed -= OnFileEvent;
-                _watcher.Created -= OnFileEvent;
-                _watcher.Deleted -= OnFileEvent;
-                _watcher.Renamed -= OnRenameEvent;
                 _watcher.Dispose();
             }
             _watcher = null;
 
-            _agent.Stop();
+            if (_agent!=null)
+                _agent.Stop();
         }
 
         // Enumerate all files in the Pithos directory except those in the Fragment folder
@@ -127,27 +278,104 @@ namespace Pithos.Core.Agents
             return monitoredFiles;
         }                
 
+        public IEnumerable<string> EnumerateFilesSystemInfosAsRelativeUrls(string searchPattern="*")
+        {
+            var rootDir = new DirectoryInfo(RootPath);
+            var monitoredFiles = from file in rootDir.EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories)
+                                 where !Ignore(file.FullName)
+                                 select file.AsRelativeUrlTo(RootPath);
+            return monitoredFiles;
+        }                
+
 
         
 
         private bool Ignore(string filePath)
         {
-            if (filePath.StartsWith(FragmentsPath))
+            //Ignore all first-level directories and files (ie at the container folders level)
+            if (FoundBelowRoot(filePath, RootPath,1))
                 return true;
-            return false;
+
+            //Ignore first-level items under the "others" folder (ie at the accounts folders level).
+            var othersPath = Path.Combine(RootPath, FolderConstants.OthersFolder);
+            if (FoundBelowRoot(filePath, othersPath,1))
+                return true;
+
+            //Ignore second-level (container) folders under the "others" folder (ie at the container folders level). 
+            if (FoundBelowRoot(filePath, othersPath,2))
+                return true;            
+
+
+            //Ignore anything happening in the cache path
+            if (filePath.StartsWith(CachePath))
+                return true;
+            if (_ignoreFiles.ContainsKey(filePath.ToLower()))
+                return true;
+            
+            //If selective sync is enabled, propagate folder events
+            if (Selectives.IsSelectiveEnabled(AccountInfo.AccountKey) && Directory.Exists(filePath))
+                return false;
+            //Ignore if selective synchronization is defined, 
+            //And the target file is not below any of the selective paths
+            return !Selectives.IsSelected(AccountInfo, filePath);
+        }
+
+/*        private static bool FoundInRoot(string filePath, string rootPath)
+        {
+            //var rootDirectory = new DirectoryInfo(rootPath);
+
+            //If the paths are equal, return true
+            if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
+                return true;
+
+            //If the filepath is below the root path
+            if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
+            {
+                //Get the relative path
+                var relativePath = filePath.Substring(rootPath.Length + 1);
+                //If the relativePath does NOT contains a path separator, we found a match
+                return (!relativePath.Contains(@"\"));
+            }
+
+            //If the filepath is not under the root path, return false
+            return false;            
+        }*/
+
+
+        private static bool FoundBelowRoot(string filePath, string rootPath,int level)
+        {
+            //var rootDirectory = new DirectoryInfo(rootPath);
+
+            //If the paths are equal, return true
+            if (filePath.Equals(rootPath, StringComparison.InvariantCultureIgnoreCase))
+                return true;
+
+            //If the filepath is below the root path
+            if (filePath.StartsWith(rootPath,StringComparison.InvariantCulture))
+            {
+                //Get the relative path
+                var relativePath = filePath.Substring(rootPath.Length + 1);
+                //If the relativePath does NOT contains a path separator, we found a match
+                var levels=relativePath.ToCharArray().Count(c=>c=='\\')+1;                
+                return levels==level;
+            }
+
+            //If the filepath is not under the root path, return false
+            return false;            
         }
 
         //Post a Change message for all events except rename
         void OnFileEvent(object sender, FileSystemEventArgs e)
         {
-            //Ignore events that affect the Fragments folder
+            //Ignore events that affect the cache folder
             var filePath = e.FullPath;
             if (Ignore(filePath)) 
                 return;
-            _agent.Post(new WorkflowState { Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType });
+            _eventIdleBatch.Post(e);
         }
 
 
+/*
         //Post a Change message for renames containing the old and new names
         void OnRenameEvent(object sender, RenamedEventArgs e)
         {
@@ -158,6 +386,7 @@ namespace Pithos.Core.Agents
 
             _agent.Post(new WorkflowState
             {
+                AccountInfo=AccountInfo,
                 OldPath = oldFullPath,
                 OldFileName = e.OldName,
                 Path = fullPath,
@@ -165,55 +394,41 @@ namespace Pithos.Core.Agents
                 TriggeringChange = e.ChangeType
             });
         }
+*/
 
-
-        private void Process(WorkflowState state)
+        //Post a Change message for moves containing the old and new names
+        void OnMoveEvent(object sender, MovedEventArgs e)
         {
-            Debug.Assert(!Ignore(state.Path));            
-
-            var networkState = NetworkGate.GetNetworkState(state.Path);
-            //Skip if the file is already being downloaded or uploaded and 
-            //the change is create or modify
-            if (networkState != NetworkOperation.None &&
-                (
-                    state.TriggeringChange == WatcherChangeTypes.Created ||
-                    state.TriggeringChange == WatcherChangeTypes.Changed
-                ))
+            var oldFullPath = e.OldFullPath;
+            var fullPath = e.FullPath;
+            if (Ignore(oldFullPath) || Ignore(fullPath))
                 return;
 
-            try
-            {
-                UpdateFileStatus(state);
-                UpdateOverlayStatus(state);
-                UpdateFileChecksum(state);
-                WorkflowAgent.Post(state);
-            }
-            catch (IOException exc)
-            {
-                Trace.TraceWarning("File access error occured, retrying {0}\n{1}", state.Path, exc);
-                _agent.Post(state);
-            }
-            catch (Exception exc)
-            {
-                Trace.TraceWarning("Error occured while indexing{0. The file will be skipped}\n{1}", state.Path, exc);                
-            }
+            _eventIdleBatch.Post(e);
         }
 
+
+
         private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
-        {
+                                                                             {
             {WatcherChangeTypes.Created,FileStatus.Created},
             {WatcherChangeTypes.Changed,FileStatus.Modified},
             {WatcherChangeTypes.Deleted,FileStatus.Deleted},
             {WatcherChangeTypes.Renamed,FileStatus.Renamed}
         };
 
+        private Dictionary<string, string> _ignoreFiles=new Dictionary<string, string>();
+
         private WorkflowState UpdateFileStatus(WorkflowState state)
         {
-            Debug.Assert(!state.Path.Contains("fragments"));
-            Debug.Assert(!state.Path.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase));
+            if (state==null)
+                throw new ArgumentNullException("state");
+            if (String.IsNullOrWhiteSpace(state.Path))
+                throw new ArgumentException("The state's Path can't be empty","state");
+            Contract.EndContractBlock();
 
-            string path = state.Path;
-            FileStatus status = _statusDict[state.TriggeringChange];
+            var path = state.Path;
+            var status = _statusDict[state.TriggeringChange];
             var oldStatus = Workflow.StatusKeeper.GetFileStatus(path);
             if (status == oldStatus)
             {
@@ -230,24 +445,30 @@ namespace Pithos.Core.Agents
 
         private WorkflowState UpdateOverlayStatus(WorkflowState state)
         {
+            if (state==null)
+                throw new ArgumentNullException("state");
+            Contract.EndContractBlock();
+
             if (state.Skip)
                 return state;
 
             switch (state.Status)
             {
                 case FileStatus.Created:
+                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash);
+                    break;
                 case FileStatus.Modified:
-                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
+                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash);
                     break;
                 case FileStatus.Deleted:
-                    this.StatusKeeper.RemoveFileOverlayStatus(state.Path);
+                    //this.StatusAgent.RemoveFileOverlayStatus(state.Path);
                     break;
                 case FileStatus.Renamed:
-                    this.StatusKeeper.RemoveFileOverlayStatus(state.OldPath);
-                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified);
+                    this.StatusKeeper.ClearFileStatus(state.OldPath);
+                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash);
                     break;
                 case FileStatus.Unchanged:
-                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal);
+                    this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal,state.ShortHash);
                     break;
             }
 
@@ -272,12 +493,19 @@ namespace Pithos.Core.Agents
             if (Directory.Exists(path))
                 return state;
 
-            string hash = Signature.CalculateMD5(path);
+            var info = new FileInfo(path);
 
-            StatusKeeper.UpdateFileChecksum(path, hash);
+            using (StatusNotification.GetNotifier("Hashing {0}", "Finished Hashing {0}", info.Name))
+            {
 
-            state.Hash = hash;
-            return state;
+                var shortHash = info.ComputeShortHash();
+
+                string merkleHash = info.CalculateHash(StatusKeeper.BlockSize, StatusKeeper.BlockHash);
+                StatusKeeper.UpdateFileChecksum(path, shortHash, merkleHash);
+
+                state.Hash = merkleHash;
+                return state;
+            }
         }
 
         //Does the file exist in the container's local folder?
@@ -295,13 +523,24 @@ namespace Pithos.Core.Agents
             if (File.Exists(absolutePath))
                 return true;
             //Or a directory?
-            if (Directory.Exists(RootPath))
+            if (Directory.Exists(absolutePath))
                 return true;
             //Fail if it is neither
             return false;
         }
 
-        public FileInfo GetFileInfo(string relativePath)
+        public static FileAgent GetFileAgent(AccountInfo accountInfo)
+        {
+            return GetFileAgent(accountInfo.AccountPath);
+        }
+
+        public static FileAgent GetFileAgent(string rootPath)
+        {
+            return AgentLocator<FileAgent>.Get(rootPath.ToLower());
+        }
+
+
+        public FileSystemInfo GetFileSystemInfo(string relativePath)
         {
             if (String.IsNullOrWhiteSpace(relativePath))
                 throw new ArgumentNullException("relativePath");
@@ -311,10 +550,40 @@ namespace Pithos.Core.Agents
             Contract.EndContractBlock();            
 
             var absolutePath = Path.Combine(RootPath, relativePath);
-            Debug.Assert(File.Exists(absolutePath));
 
-            return new FileInfo(absolutePath);
+            if (Directory.Exists(absolutePath))
+                return new DirectoryInfo(absolutePath).WithProperCapitalization();
+            else
+                return new FileInfo(absolutePath).WithProperCapitalization();
             
         }
+
+        public void Delete(string relativePath)
+        {
+            var absolutePath = Path.Combine(RootPath, relativePath).ToLower();
+            if (Log.IsDebugEnabled)
+                Log.DebugFormat("Deleting {0}", absolutePath);
+            if (File.Exists(absolutePath))
+            {    
+                try
+                {
+                    File.Delete(absolutePath);
+                }
+                //The file may have been deleted by another thread. Just ignore the relevant exception
+                catch (FileNotFoundException) { }
+            }
+            else if (Directory.Exists(absolutePath))
+            {
+                try
+                {
+                    Directory.Delete(absolutePath, true);
+                }
+                //The directory may have been deleted by another thread. Just ignore the relevant exception
+                catch (DirectoryNotFoundException){}                
+            }
+        
+            //_ignoreFiles[absolutePath] = absolutePath;                
+            StatusKeeper.ClearFileStatus(absolutePath);
+        }
     }
 }