commit 78ebfd2d2993d6dac1cee4bab27568fbba77c68c
Author: Panagiotis Kanavos <pkanavos@gmail.com>
Date:   Thu Jan 19 23:10:35 2012 +0200

    Added FileSystemWatcherAdapter.cs to raise Moved events from the sequence of Deleted, Created events raised by FileSystemWatcher.
    Modified PreferencesViewModel.cs to URLEncode the token when opening the Pithos site

diff --git a/trunk/Pithos.Client.WPF/Preferences/PreferencesViewModel.cs b/trunk/Pithos.Client.WPF/Preferences/PreferencesViewModel.cs
index bcd7633..772c9ab 100644
--- a/trunk/Pithos.Client.WPF/Preferences/PreferencesViewModel.cs
+++ b/trunk/Pithos.Client.WPF/Preferences/PreferencesViewModel.cs
@@ -93,6 +93,7 @@ namespace Pithos.Client.WPF
 
         private static void CreateShortcut(string shortcutPath)
         {
+		//TODO: ###THROWS PERMISSIONS ERROR###
             var wshShell = new WshShellClass();
             var shortcut = (IWshRuntimeLibrary.IWshShortcut) wshShell.CreateShortcut(
                 shortcutPath);
diff --git a/trunk/Pithos.Core/Agents/FileAgent.cs b/trunk/Pithos.Core/Agents/FileAgent.cs
index 8218cc6..8f2d585 100644
--- a/trunk/Pithos.Core/Agents/FileAgent.cs
+++ b/trunk/Pithos.Core/Agents/FileAgent.cs
@@ -19,6 +19,7 @@ namespace Pithos.Core.Agents
     {
         Agent<WorkflowState> _agent;
         private FileSystemWatcher _watcher;
+        private FileSystemWatcherAdapter _adapter;
 
         //[Import]
         public IStatusKeeper StatusKeeper { get; set; }
@@ -45,12 +46,14 @@ namespace Pithos.Core.Agents
 
             AccountInfo = accountInfo;
             RootPath = rootPath;
-            _watcher = new FileSystemWatcher(rootPath);
-            _watcher.IncludeSubdirectories = true;            
-            _watcher.Changed += OnFileEvent;
-            _watcher.Created += OnFileEvent;
-            _watcher.Deleted += OnFileEvent;
-            _watcher.Renamed += OnRenameEvent;
+            _watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true};
+            _adapter = new FileSystemWatcherAdapter(_watcher);
+
+            _adapter.Changed += OnFileEvent;
+            _adapter.Created += OnFileEvent;
+            _adapter.Deleted += OnFileEvent;
+            _adapter.Renamed += OnRenameEvent;
+            _adapter.Moved += OnMoveEvent;
             _watcher.EnableRaisingEvents = true;
 
 
@@ -234,6 +237,25 @@ namespace Pithos.Core.Agents
             });
         }
 
+        //Post a Change message for renames containing the old and new names
+        void OnMoveEvent(object sender, MovedEventArgs e)
+        {
+            var oldFullPath = e.OldFullPath;
+            var fullPath = e.FullPath;
+            if (Ignore(oldFullPath) || Ignore(fullPath))
+                return;
+
+            _agent.Post(new WorkflowState
+            {
+                AccountInfo=AccountInfo,
+                OldPath = oldFullPath,
+                OldFileName = e.OldName,
+                Path = fullPath,
+                FileName = e.Name,
+                TriggeringChange = e.ChangeType
+            });
+        }
+
 
 
         private Dictionary<WatcherChangeTypes, FileStatus> _statusDict = new Dictionary<WatcherChangeTypes, FileStatus>
diff --git a/trunk/Pithos.Core/Agents/FileSystemWatcherAdapter.cs b/trunk/Pithos.Core/Agents/FileSystemWatcherAdapter.cs
new file mode 100644
index 0000000..e652f48
--- /dev/null
+++ b/trunk/Pithos.Core/Agents/FileSystemWatcherAdapter.cs
@@ -0,0 +1,229 @@
+﻿// -----------------------------------------------------------------------
+// <copyright file="FileInfoExtensions.cs" company="GRNET">
+// Copyright 2011 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>
+// -----------------------------------------------------------------------
+
+
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Pithos.Core.Agents
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Text;
+
+    /// <summary>
+    /// TODO: Update summary.
+    /// </summary>
+    public class FileSystemWatcherAdapter
+    {
+         public event FileSystemEventHandler Changed;
+        public event FileSystemEventHandler Created;
+        public event FileSystemEventHandler Deleted;
+        public event RenamedEventHandler Renamed;
+        public event MovedEventHandler Moved;
+
+        public FileSystemWatcherAdapter(FileSystemWatcher watcher)
+        {
+            if (watcher==null)
+                throw new ArgumentNullException("watcher");
+            Contract.EndContractBlock();
+
+            watcher.Changed += OnChangeOrCreate;
+            watcher.Created += OnChangeOrCreate;
+            watcher.Deleted += OnDeleted;
+            watcher.Renamed += OnRename;            
+            
+        }
+
+        private string _cachedDeletedFullPath;
+        private const int PropagateDelay = 10;        
+
+        private static void OnTimeout(object state)
+        {
+            throw new NotImplementedException();
+        }
+
+        private void OnDeleted(object sender, FileSystemEventArgs e)
+        {
+            if (sender == null)
+                throw new ArgumentNullException("sender");
+            if (e.ChangeType != WatcherChangeTypes.Deleted)
+                throw new ArgumentException("e");
+            if (string.IsNullOrWhiteSpace(e.FullPath))
+                throw new ArgumentException("e");
+            Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));
+            Contract.EndContractBlock();
+
+            //Handle any previously deleted event
+            PropagateCachedDeleted(sender);
+
+            //A delete event may be an actual delete event or the first event in a move action.
+            //To decide which action occured, we need to wait for the next action, so
+            //we  store the file path and return .
+            //A delete action will not be followed by any other event, so we need to add a watchdog 
+            //that will declare a Delete action after a short amount of time
+
+            //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents
+            //      as this is actually a MOVE operation
+            //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself
+            _cachedDeletedFullPath = e.FullPath;
+
+            //TODO: This requires synchronization of the _cachedDeletedFullPath field
+            //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives
+            //Maybe, use a timer instead of a task
+            
+            TaskEx.Delay(PropagateDelay).ContinueWith(t =>
+                                                           {
+                                                               var myPath = e.FullPath;
+                                                               if (this._cachedDeletedFullPath==myPath)
+                                                                    PropagateCachedDeleted(sender);
+                                                           });
+        }
+
+        private void OnRename(object sender, RenamedEventArgs e)
+        {
+            if (sender == null)
+                throw new ArgumentNullException("sender");
+            Contract.Ensures(_cachedDeletedFullPath == null);
+            Contract.EndContractBlock();
+
+            try
+            {
+                //Propagate any previous cached delete event
+                PropagateCachedDeleted(sender);
+                
+                if (Renamed!=null)
+                    Renamed(sender, e);
+                
+            }
+            finally
+            {
+                _cachedDeletedFullPath = null;    
+            }
+        }
+
+        private void OnChangeOrCreate(object sender, FileSystemEventArgs e)
+        {
+            if (sender == null)
+                throw new ArgumentNullException("sender");
+            if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))
+                throw new ArgumentException("e");
+            Contract.Ensures(_cachedDeletedFullPath == null);
+            Contract.EndContractBlock();
+
+            try
+            {
+                //A Move action results in a sequence of a Delete and a Create or Change event
+                //If the actual action is a Move, raise a Move event instead of the actual event
+                if (HandleMoved(sender, e))
+                    return;
+
+                //Otherwise, propagate the Delete event if it exists 
+                PropagateCachedDeleted(sender);
+                //and propagate the actual event
+                var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;
+                if (actualEvent != null)
+                    actualEvent(sender, e);
+            }
+            finally
+            {
+                //Finally, make sure the cached path is cleared
+                _cachedDeletedFullPath = null;
+            }
+
+        }
+
+        private bool HandleMoved(object sender, FileSystemEventArgs e)
+        {
+            if (sender == null)
+                throw new ArgumentNullException("sender");
+            if (!(e.ChangeType == WatcherChangeTypes.Created ||
+                                                  e.ChangeType == WatcherChangeTypes.Changed))
+                throw new ArgumentException("e");
+            Contract.EndContractBlock();
+
+            //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE
+            //instead of a sequence of independent actions
+            //One way to detect this would be to request that the full paths are NOT the same
+
+            var oldName = Path.GetFileName(_cachedDeletedFullPath);
+            //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path
+            var newName = Path.GetFileName(e.Name);
+            //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation
+            var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);
+
+            if (!hasMoved)
+                return false;
+
+            try
+            {
+                //If the actual action is a Move, raise a Move event instead of the actual event
+                var newDirectory = Path.GetDirectoryName(e.FullPath);
+                var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
+                if (Moved != null)
+                    Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));
+            }
+            finally
+            {
+                _cachedDeletedFullPath = null;
+            }
+            return true;
+        }
+
+        private void PropagateCachedDeleted(object sender)
+        {
+            if (sender == null)
+                throw new ArgumentNullException("sender");
+            Contract.Ensures(_cachedDeletedFullPath == null);
+            Contract.EndContractBlock();
+
+            //Nothing to handle if there is no cached deleted file
+            if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))
+                return;
+            
+            var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);
+            var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);
+
+            if (Deleted!=null)
+                Deleted(sender, new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));
+
+            _cachedDeletedFullPath = null;
+        }
+    }
+}
diff --git a/trunk/Pithos.Core/Agents/MovedEventArgs.cs b/trunk/Pithos.Core/Agents/MovedEventArgs.cs
new file mode 100644
index 0000000..9005450
--- /dev/null
+++ b/trunk/Pithos.Core/Agents/MovedEventArgs.cs
@@ -0,0 +1,82 @@
+﻿// -----------------------------------------------------------------------
+// <copyright file="FileInfoExtensions.cs" company="GRNET">
+// Copyright 2011 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>
+// -----------------------------------------------------------------------
+
+
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Security.Permissions;
+using System;
+
+namespace Pithos.Core.Agents
+{
+    
+
+    public delegate void MovedEventHandler(object sender, MovedEventArgs e);
+
+    public class MovedEventArgs : FileSystemEventArgs
+    {
+        private readonly string _oldName;
+        private readonly string _oldFullPath;
+
+        public string OldFullPath
+        {
+            get
+            {
+                new FileIOPermission(FileIOPermissionAccess.Read, Path.GetPathRoot(_oldFullPath)).Demand();
+                return _oldFullPath;
+            }
+        }
+
+        public string OldName
+        {
+            get { return _oldName; }
+        }
+
+        public MovedEventArgs(string newDirectory, string newName, string oldDirectory, string oldName)
+            : base(WatcherChangeTypes.Renamed, newDirectory, newName)
+        {
+            if (String.IsNullOrWhiteSpace(oldDirectory))
+                throw new ArgumentNullException("oldDirectory");
+            if (String.IsNullOrWhiteSpace(oldName))
+                throw new ArgumentNullException("oldName");
+            Contract.EndContractBlock();
+
+            _oldName = oldName;
+            _oldFullPath = Path.Combine(oldDirectory, oldName);
+        }
+    }
+}
diff --git a/trunk/Pithos.Core/Pithos.Core.csproj b/trunk/Pithos.Core/Pithos.Core.csproj
index 151f573..6558320 100644
--- a/trunk/Pithos.Core/Pithos.Core.csproj
+++ b/trunk/Pithos.Core/Pithos.Core.csproj
@@ -390,6 +390,8 @@
     <Compile Include="Agents\CollectionExtensions.cs" />
     <Compile Include="Agents\FileAgent.cs" />
     <Compile Include="Agents\FileInfoExtensions.cs" />
+    <Compile Include="Agents\FileSystemWatcherAdapter.cs" />
+    <Compile Include="Agents\MovedEventArgs.cs" />
     <Compile Include="Agents\NetworkAgent.cs" />
     <Compile Include="Agents\WorkflowAgent.cs" />
     <Compile Include="DynamicDictionary.cs" />
