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