// ----------------------------------------------------------------------- // // 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. // // ----------------------------------------------------------------------- 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; /// /// TODO: Update 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; } } }