#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;
using System.Diagnostics.Contracts;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Pithos.Interfaces;
using log4net;
namespace Pithos.Core.Agents
{
using System;
///
/// Wraps a FileSystemWatcher and raises Move and child object events
///
public class FileSystemWatcherAdapter
{
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
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;
watcher.Error += OnError;
}
private void OnError(object sender, ErrorEventArgs e)
{
var error = e.GetException();
Log.Error("FSW error",error);
}
private string _cachedDeletedFullPath;
private string CachedDeletedFullPath
{
get { return _cachedDeletedFullPath; }
set
{
Debug.Assert(Path.IsPathRooted(value));
if (!Path.IsPathRooted(value))
Log.WarnFormat("Storing a relative CachedDeletedFullPath: {0}",value);
_cachedDeletedFullPath = value;
}
}
///
/// Clears any cached deleted file path
///
///
/// This method was added to bypass the null checking in the property's setter
///
private void ClearCachedDeletedPath()
{
_cachedDeletedFullPath = null;
}
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.EndContractBlock();
TaskEx.Run(() => InnerOnDeleted(sender, e));
}
private void InnerOnDeleted(object sender, FileSystemEventArgs e)
{
//Handle any previously deleted event
if (Log.IsDebugEnabled)
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
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.EndContractBlock();
TaskEx.Run(() => InnerRename(sender, e));
}
private void InnerRename(object sender, RenamedEventArgs e)
{
try
{
if (Log.IsDebugEnabled)
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath);
//Propagate any previous cached delete event
PropagateCachedDeleted(sender);
if (Moved!= null)
{
try
{
Moved(sender, new MovedEventArgs(Path.GetDirectoryName(e.FullPath),Path.GetFileName(e.Name),Path.GetDirectoryName(e.OldFullPath),Path.GetFileName(e.OldName)));
}
catch (Exception exc)
{
Log.Error("Rename event error", exc);
throw;
}
var directory = new DirectoryInfo(e.FullPath);
if (directory.Exists)
{
var newDirectory = e.FullPath;
var oldDirectory = e.OldFullPath;
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
{
ClearCachedDeletedPath();
}
}
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.EndContractBlock();
TaskEx.Run(() => InnerChangeOrCreated(sender, e));
}
private void InnerChangeOrCreated(object sender, FileSystemEventArgs e)
{
try
{
if (Log.IsDebugEnabled)
Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath);
//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
ClearCachedDeletedPath();
}
}
private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e)
{
if(sender==null)
throw new ArgumentNullException("sender");
if (e==null)
throw new ArgumentNullException("e");
Contract.EndContractBlock();
if (e.ChangeType != WatcherChangeTypes.Created)
return;
var dir= new DirectoryInfo(e.FullPath);
//Skip if this is not a folder
if (!dir.Exists)
return;
try
{
foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories))
{
var path = Path.GetDirectoryName(info.FullName);
Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name));
}
}
catch (IOException)
{
TaskEx.Delay(1000)
.ContinueWith(_=>RaiseCreatedForChildren(sender,e));
}
}
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 oldPath = CachedDeletedFullPath;
var oldName = Path.GetFileName(oldPath);
var oldDirectory = Path.GetDirectoryName(oldPath);
//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 = (oldPath != e.FullPath && oldName == newName);
if (!hasMoved)
return false;
try
{
if (Log.IsDebugEnabled)
Log.DebugFormat("Moved for [{0}]", e.FullPath);
//If the actual action is a Move, raise a Move event instead of the actual event
var newDirectory = Path.GetDirectoryName(e.FullPath);
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
{
ClearCachedDeletedPath();
}
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 (Log.IsDebugEnabled)
Log.DebugFormat("Propagating delete for [{0}]", 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));
ClearCachedDeletedPath();
}
}
}