#region /* ----------------------------------------------------------------------- * * * 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. * * ----------------------------------------------------------------------- */ #endregion using System.Diagnostics.Contracts; using System.IO; using System.Threading.Tasks; using Pithos.Interfaces; namespace Pithos.Core.Agents { using System; /// /// Wraps a FileSystemWatcher and raises Move and child object events /// 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 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 (_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); //For Folders, raise Created events for all children RaiseCreatedForChildren(sender,e); } } finally { //Finally, make sure the cached path is cleared _cachedDeletedFullPath = null; } } private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e) { Contract.Requires(sender!=null); Contract.Requires(e!=null); if (e.ChangeType != WatcherChangeTypes.Created) return; var dir= new DirectoryInfo(e.FullPath); //Skip if this is not a folder if (!dir.Exists) return; foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories)) { var path = Path.GetDirectoryName(info.FullName); Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name)); } } 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)); //If the moved item is a dictionary, we need to raise a change event for each child item //When a directory is moved within the same volume, Windows raises events only for the directory object, //not its children. This happens because the move actually changes a single directory entry. It doesn't //affect the entries of the children. var directory = new DirectoryInfo(e.FullPath); if (directory.Exists) { foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { var newChildDirectory = Path.GetDirectoryName(child.FullName); var relativePath=child.AsRelativeTo(newDirectory); var relativeFolder = Path.GetDirectoryName(relativePath); var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name)); } } } } 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); //Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation //In this case we need to raise the proper events for all child objects of the deleted directory. //UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted //This should be done at a higher level, eg by checking the stored state if (Deleted != null) Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName)); _cachedDeletedFullPath = null; } } }