--- /dev/null
+// -----------------------------------------------------------------------\r
+// <copyright file="FileInfoExtensions.cs" company="GRNET">\r
+// Copyright 2011 GRNET S.A. All rights reserved.\r
+// \r
+// Redistribution and use in source and binary forms, with or\r
+// without modification, are permitted provided that the following\r
+// conditions are met:\r
+// \r
+// 1. Redistributions of source code must retain the above\r
+// copyright notice, this list of conditions and the following\r
+// disclaimer.\r
+// \r
+// 2. Redistributions in binary form must reproduce the above\r
+// copyright notice, this list of conditions and the following\r
+// disclaimer in the documentation and/or other materials\r
+// provided with the distribution.\r
+// \r
+// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS\r
+// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR\r
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\r
+// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\r
+// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+// POSSIBILITY OF SUCH DAMAGE.\r
+// \r
+// The views and conclusions contained in the software and\r
+// documentation are those of the authors and should not be\r
+// interpreted as representing official policies, either expressed\r
+// or implied, of GRNET S.A.\r
+// </copyright>\r
+// -----------------------------------------------------------------------\r
+\r
+\r
+using System.Diagnostics.Contracts;\r
+using System.IO;\r
+using System.Threading.Tasks;\r
+\r
+namespace Pithos.Core.Agents\r
+{\r
+ using System;\r
+ using System.Collections.Generic;\r
+ using System.Linq;\r
+ using System.Text;\r
+\r
+ /// <summary>\r
+ /// TODO: Update summary.\r
+ /// </summary>\r
+ public class FileSystemWatcherAdapter\r
+ {\r
+ public event FileSystemEventHandler Changed;\r
+ public event FileSystemEventHandler Created;\r
+ public event FileSystemEventHandler Deleted;\r
+ public event RenamedEventHandler Renamed;\r
+ public event MovedEventHandler Moved;\r
+\r
+ public FileSystemWatcherAdapter(FileSystemWatcher watcher)\r
+ {\r
+ if (watcher==null)\r
+ throw new ArgumentNullException("watcher");\r
+ Contract.EndContractBlock();\r
+\r
+ watcher.Changed += OnChangeOrCreate;\r
+ watcher.Created += OnChangeOrCreate;\r
+ watcher.Deleted += OnDeleted;\r
+ watcher.Renamed += OnRename; \r
+ \r
+ }\r
+\r
+ private string _cachedDeletedFullPath;\r
+ private const int PropagateDelay = 10; \r
+\r
+ private static void OnTimeout(object state)\r
+ {\r
+ throw new NotImplementedException();\r
+ }\r
+\r
+ private void OnDeleted(object sender, FileSystemEventArgs e)\r
+ {\r
+ if (sender == null)\r
+ throw new ArgumentNullException("sender");\r
+ if (e.ChangeType != WatcherChangeTypes.Deleted)\r
+ throw new ArgumentException("e");\r
+ if (string.IsNullOrWhiteSpace(e.FullPath))\r
+ throw new ArgumentException("e");\r
+ Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath));\r
+ Contract.EndContractBlock();\r
+\r
+ //Handle any previously deleted event\r
+ PropagateCachedDeleted(sender);\r
+\r
+ //A delete event may be an actual delete event or the first event in a move action.\r
+ //To decide which action occured, we need to wait for the next action, so\r
+ //we store the file path and return .\r
+ //A delete action will not be followed by any other event, so we need to add a watchdog \r
+ //that will declare a Delete action after a short amount of time\r
+\r
+ //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents\r
+ // as this is actually a MOVE operation\r
+ //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself\r
+ _cachedDeletedFullPath = e.FullPath;\r
+\r
+ //TODO: This requires synchronization of the _cachedDeletedFullPath field\r
+ //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives\r
+ //Maybe, use a timer instead of a task\r
+ \r
+ TaskEx.Delay(PropagateDelay).ContinueWith(t =>\r
+ {\r
+ var myPath = e.FullPath;\r
+ if (this._cachedDeletedFullPath==myPath)\r
+ PropagateCachedDeleted(sender);\r
+ });\r
+ }\r
+\r
+ private void OnRename(object sender, RenamedEventArgs e)\r
+ {\r
+ if (sender == null)\r
+ throw new ArgumentNullException("sender");\r
+ Contract.Ensures(_cachedDeletedFullPath == null);\r
+ Contract.EndContractBlock();\r
+\r
+ try\r
+ {\r
+ //Propagate any previous cached delete event\r
+ PropagateCachedDeleted(sender);\r
+ \r
+ if (Renamed!=null)\r
+ Renamed(sender, e);\r
+ \r
+ }\r
+ finally\r
+ {\r
+ _cachedDeletedFullPath = null; \r
+ }\r
+ }\r
+\r
+ private void OnChangeOrCreate(object sender, FileSystemEventArgs e)\r
+ {\r
+ if (sender == null)\r
+ throw new ArgumentNullException("sender");\r
+ if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed))\r
+ throw new ArgumentException("e");\r
+ Contract.Ensures(_cachedDeletedFullPath == null);\r
+ Contract.EndContractBlock();\r
+\r
+ try\r
+ {\r
+ //A Move action results in a sequence of a Delete and a Create or Change event\r
+ //If the actual action is a Move, raise a Move event instead of the actual event\r
+ if (HandleMoved(sender, e))\r
+ return;\r
+\r
+ //Otherwise, propagate the Delete event if it exists \r
+ PropagateCachedDeleted(sender);\r
+ //and propagate the actual event\r
+ var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed;\r
+ if (actualEvent != null)\r
+ actualEvent(sender, e);\r
+ }\r
+ finally\r
+ {\r
+ //Finally, make sure the cached path is cleared\r
+ _cachedDeletedFullPath = null;\r
+ }\r
+\r
+ }\r
+\r
+ private bool HandleMoved(object sender, FileSystemEventArgs e)\r
+ {\r
+ if (sender == null)\r
+ throw new ArgumentNullException("sender");\r
+ if (!(e.ChangeType == WatcherChangeTypes.Created ||\r
+ e.ChangeType == WatcherChangeTypes.Changed))\r
+ throw new ArgumentException("e");\r
+ Contract.EndContractBlock();\r
+\r
+ //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE\r
+ //instead of a sequence of independent actions\r
+ //One way to detect this would be to request that the full paths are NOT the same\r
+\r
+ var oldName = Path.GetFileName(_cachedDeletedFullPath);\r
+ //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path\r
+ var newName = Path.GetFileName(e.Name);\r
+ //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation\r
+ var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName);\r
+\r
+ if (!hasMoved)\r
+ return false;\r
+\r
+ try\r
+ {\r
+ //If the actual action is a Move, raise a Move event instead of the actual event\r
+ var newDirectory = Path.GetDirectoryName(e.FullPath);\r
+ var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);\r
+ if (Moved != null)\r
+ Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName));\r
+ }\r
+ finally\r
+ {\r
+ _cachedDeletedFullPath = null;\r
+ }\r
+ return true;\r
+ }\r
+\r
+ private void PropagateCachedDeleted(object sender)\r
+ {\r
+ if (sender == null)\r
+ throw new ArgumentNullException("sender");\r
+ Contract.Ensures(_cachedDeletedFullPath == null);\r
+ Contract.EndContractBlock();\r
+\r
+ //Nothing to handle if there is no cached deleted file\r
+ if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath))\r
+ return;\r
+ \r
+ var deletedFileName = Path.GetFileName(_cachedDeletedFullPath);\r
+ var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath);\r
+\r
+ if (Deleted!=null)\r
+ Deleted(sender, new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName));\r
+\r
+ _cachedDeletedFullPath = null;\r
+ }\r
+ }\r
+}\r
--- /dev/null
+// -----------------------------------------------------------------------\r
+// <copyright file="FileInfoExtensions.cs" company="GRNET">\r
+// Copyright 2011 GRNET S.A. All rights reserved.\r
+// \r
+// Redistribution and use in source and binary forms, with or\r
+// without modification, are permitted provided that the following\r
+// conditions are met:\r
+// \r
+// 1. Redistributions of source code must retain the above\r
+// copyright notice, this list of conditions and the following\r
+// disclaimer.\r
+// \r
+// 2. Redistributions in binary form must reproduce the above\r
+// copyright notice, this list of conditions and the following\r
+// disclaimer in the documentation and/or other materials\r
+// provided with the distribution.\r
+// \r
+// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS\r
+// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR\r
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\r
+// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\r
+// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+// POSSIBILITY OF SUCH DAMAGE.\r
+// \r
+// The views and conclusions contained in the software and\r
+// documentation are those of the authors and should not be\r
+// interpreted as representing official policies, either expressed\r
+// or implied, of GRNET S.A.\r
+// </copyright>\r
+// -----------------------------------------------------------------------\r
+\r
+\r
+using System.Diagnostics.Contracts;\r
+using System.IO;\r
+using System.Security.Permissions;\r
+using System;\r
+\r
+namespace Pithos.Core.Agents\r
+{\r
+ \r
+\r
+ public delegate void MovedEventHandler(object sender, MovedEventArgs e);\r
+\r
+ public class MovedEventArgs : FileSystemEventArgs\r
+ {\r
+ private readonly string _oldName;\r
+ private readonly string _oldFullPath;\r
+\r
+ public string OldFullPath\r
+ {\r
+ get\r
+ {\r
+ new FileIOPermission(FileIOPermissionAccess.Read, Path.GetPathRoot(_oldFullPath)).Demand();\r
+ return _oldFullPath;\r
+ }\r
+ }\r
+\r
+ public string OldName\r
+ {\r
+ get { return _oldName; }\r
+ }\r
+\r
+ public MovedEventArgs(string newDirectory, string newName, string oldDirectory, string oldName)\r
+ : base(WatcherChangeTypes.Renamed, newDirectory, newName)\r
+ {\r
+ if (String.IsNullOrWhiteSpace(oldDirectory))\r
+ throw new ArgumentNullException("oldDirectory");\r
+ if (String.IsNullOrWhiteSpace(oldName))\r
+ throw new ArgumentNullException("oldName");\r
+ Contract.EndContractBlock();\r
+\r
+ _oldName = oldName;\r
+ _oldFullPath = Path.Combine(oldDirectory, oldName);\r
+ }\r
+ }\r
+}\r