#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;
}
}
}