//******************************************************************************************************
// FtpFileWatcher.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 05/22/2003 - J. Ritchie Carroll
// Generated original version of source code.
// 08/06/2009 - Josh L. Patterson
// Edited Comments.
// 09/14/2009 - Stephen C. Wills
// Added new header and license agreement.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Timers;
namespace GSF.Net.Ftp
{
///
/// FTP File Watcher
///
///
/// Monitors for file changes over an FTP session.
///
[ToolboxBitmap(typeof(FtpFileWatcher)), DefaultProperty("Server"), DefaultEvent("FileAdded"), Description("Monitors for file changes over an FTP session")]
public class FtpFileWatcher : Component
{
#region [ Members ]
// Events
///
/// Raised when new file is added to monitored FTP directory.
///
///
/// is reference to newly added file.
///
public event EventHandler> FileAdded;
///
/// Raised when file is deleted from monitored FTP directory.
///
///
/// is reference to file that was removed.
///
public event EventHandler> FileDeleted;
///
/// Raised when new status messages come from the FTP file watcher.
///
///
/// is status message from FTP file watcher.
///
public event EventHandler> Status;
///
/// Raised when FTP command has been sent.
///
///
/// is sent FTP command.
///
public event EventHandler> CommandSent;
///
/// Raised when FTP response has been received.
///
///
/// is received FTP response.
///
public event EventHandler> ResponseReceived;
// Fields
private FtpClient m_session;
private string m_username;
private string m_password;
private string m_watchDirectory;
private Timer m_watchTimer;
private Timer m_restartTimer;
private readonly List m_currentFiles;
private readonly List m_newFiles;
private bool m_enabled;
private bool m_notifyOnComplete;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Constructs a new FTP file watcher using the default settings.
///
public FtpFileWatcher()
{
m_enabled = true;
m_notifyOnComplete = true;
m_currentFiles = new List();
m_newFiles = new List();
m_session = new FtpClient(false);
m_session.CommandSent += OnCommandSent;
m_session.ResponseReceived += OnResponseReceived;
// Define a timer to watch for new files
m_watchTimer = new Timer();
m_watchTimer.Elapsed += WatchTimer_Elapsed;
m_watchTimer.AutoReset = false;
m_watchTimer.Interval = 5000;
m_watchTimer.Enabled = false;
// Define a timer for FTP connection in case of availability failures
m_restartTimer = new Timer();
m_restartTimer.Elapsed += RestartTimer_Elapsed;
m_restartTimer.AutoReset = false;
m_restartTimer.Interval = 10000;
m_restartTimer.Enabled = false;
}
///
/// Constructs a new FTP file watcher using the specified settings.
///
/// Set to true to not be case sensitive with FTP file and directory names.
/// Set to true to notify after file has completed uploading -or- set to false for immediate notification of new file.
public FtpFileWatcher(bool caseInsensitive, bool notifyOnComplete)
: this()
{
m_session.CaseInsensitive = caseInsensitive;
m_notifyOnComplete = notifyOnComplete;
}
///
/// Initializes a new instance of the class.
///
/// object that contains the .
public FtpFileWatcher(IContainer container)
: this()
{
if ((object)container != null)
container.Add(this);
}
#endregion
#region [ Properties ]
///
/// Gets or sets FTP server name (DNS name or IP) to watch.
///
///
/// FTP server name should not be prefixed with FTP://.
///
[Browsable(true), Category("Configuration"), Description("Specify FTP server name (do not prefix with ftp://).")]
public virtual string Server
{
get
{
return m_session.Server;
}
set
{
m_session.Server = value;
}
}
///
/// Gets or sets FTP server port to use, defaults to 21.
///
///
/// This only needs to be changed if the FTP server is established on a non-standard port number.
///
[Browsable(true), Category("Configuration"), Description("Specify FTP server port, if needed."), DefaultValue(21)]
public int Port
{
get
{
return m_session.Port;
}
set
{
m_session.Port = value;
}
}
///
/// Gets or sets FTP case sensitivity of file and directory names.
///
///
/// Set to true to not be case sensitive with FTP file and directory names.
///
[Browsable(true), Category("Configuration"), Description("Set to True to not be case sensitive with FTP file and directory names."), DefaultValue(false)]
public bool CaseInsensitive
{
get
{
return m_session.CaseInsensitive;
}
set
{
m_session.CaseInsensitive = value;
}
}
///
/// Gets or sets interval, in seconds, to scan for file changes on monitored FTP directory.
///
///
/// Specify interval in seconds to poll FTP directory for file changes.
///
[Browsable(true), Category("Configuration"), Description("Specify interval in seconds to poll FTP directory for file changes."), DefaultValue(5)]
public virtual int WatchInterval
{
get
{
return (int)(m_watchTimer.Interval / 1000);
}
set
{
m_watchTimer.Enabled = false;
m_watchTimer.Interval = value * 1000;
m_watchTimer.Enabled = m_enabled;
}
}
///
/// Gets or sets name of FTP directory name to monitor. Leave blank to monitor initial FTP session directory.
///
[Browsable(true), Category("Configuration"), Description("Specify FTP directory to monitor. Leave blank to monitor initial FTP session directory."), DefaultValue("")]
public virtual string Directory
{
get
{
return m_watchDirectory;
}
set
{
m_watchDirectory = value;
ConnectToWatchDirectory();
Reset();
}
}
///
/// Sets flag for notification time. Set to true to only notify when a file is finished uploading, set to False to get an immediate notification when a new file is detected.
///
[Browsable(true), Category("Configuration"), Description("Set to True to only be notified of new FTP files when upload is complete. This monitors file size changes at each WatchInterval."), DefaultValue(true)]
public virtual bool NotifyOnComplete
{
get
{
return m_notifyOnComplete;
}
set
{
m_notifyOnComplete = value;
Reset();
}
}
///
/// Gets or sets enabled state of the object.
///
[Browsable(true), Category("Configuration"), Description("Determines if FTP file watcher is enabled."), DefaultValue(true)]
public virtual bool Enabled
{
get
{
return m_enabled;
}
set
{
m_enabled = value;
Reset();
}
}
///
/// Returns true if FTP file watcher session is connected.
///
[Browsable(false)]
public virtual bool IsConnected
{
get
{
return m_session.IsConnected;
}
}
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
try
{
if (!m_disposed)
{
if (disposing)
{
Close();
if ((object)m_session != null)
{
m_session.CommandSent -= OnCommandSent;
m_session.ResponseReceived -= OnResponseReceived;
m_session.Dispose();
}
m_session = null;
if ((object)m_watchTimer != null)
{
m_watchTimer.Elapsed -= WatchTimer_Elapsed;
m_watchTimer.Dispose();
}
m_watchTimer = null;
if ((object)m_restartTimer != null)
{
m_restartTimer.Elapsed -= RestartTimer_Elapsed;
m_restartTimer.Dispose();
}
m_restartTimer = null;
}
}
m_disposed = true;
}
finally
{
base.Dispose(disposing);
}
}
///
/// Closes FTP session and clears resources used by the .
///
public virtual void Close()
{
m_currentFiles.Clear();
m_newFiles.Clear();
m_watchTimer.Enabled = false;
m_restartTimer.Enabled = false;
CloseSession();
}
///
/// Connects to FTP server and enables file watching if is true.
///
/// A value as the username.
/// A value as the password.
public virtual void Connect(string userName, string password)
{
if (!string.IsNullOrEmpty(userName))
m_username = userName;
if (!string.IsNullOrEmpty(password))
m_password = password;
try
{
// Attempt to connect to FTP server
m_session.Connect(m_username, m_password);
OnStatus("FTP file watcher connected to \"ftp://" + m_username + "@" + m_session.Server + "\"");
ConnectToWatchDirectory();
m_watchTimer.Enabled = m_enabled;
// FTP servers can be fickle creatues, so after a successful connection we setup the
// restart timer to reconnect every thirty minutes whether we need to or not :)
m_restartTimer.Interval = 1800000;
m_restartTimer.Enabled = true;
}
catch (FtpExceptionBase ex)
{
// If this fails, we'll try again in a moment. The FTP server may be down...
OnStatus("FTP file watcher failed to connect to \"ftp://" + m_username + "@" + m_session.Server + "\" - trying again in 10 seconds..." + "\r\n" + "\t" + "Exception: " + ex.Message);
RestartConnectCycle();
}
}
///
/// Clones FTP session used by file watcher so it can be used for other purposes.
///
/// New connected FTP session matching settings defined for FTP file watcher.
public virtual FtpClient CloneFtpSession()
{
// This method is just for convenience. We can't allow the end user to use the
// actual internal directory for sending files or other work because it is
// constantly being refreshed/used etc., so we instead create a new FTP Session
// based on the current internal session and watch directory information
FtpClient newSession = new FtpClient(m_session.CaseInsensitive);
newSession.Server = m_session.Server;
newSession.Connect(m_username, m_password);
newSession.SetCurrentDirectory(m_watchDirectory);
return newSession;
}
///
/// Resets and restarts FTP session used by FTP file watcher.
///
public virtual void Reset()
{
m_restartTimer.Enabled = false;
m_watchTimer.Enabled = false;
m_currentFiles.Clear();
m_newFiles.Clear();
m_watchTimer.Enabled = m_enabled;
if (!m_session.IsConnected)
RestartConnectCycle();
}
///
/// Raises event.
///
/// A status message.
protected void OnStatus(string status)
{
if ((object)Status != null)
Status(this, new EventArgs("[" + DateTime.UtcNow + "] " + status));
}
///
/// Raises event.
///
/// A file.
protected void OnFileAdded(FtpFile file)
{
if ((object)FileAdded != null)
FileAdded(this, new EventArgs(file));
}
///
/// Raises event.
///
/// A file.
protected void OnFileDeleted(FtpFile file)
{
if ((object)FileDeleted != null)
FileDeleted(this, new EventArgs(file));
}
///
/// Raises event.
///
/// A command.
protected void OnCommandSent(string command)
{
if ((object)CommandSent != null)
CommandSent(this, new EventArgs(command));
}
private void OnCommandSent(object sender, EventArgs e)
{
OnCommandSent(e.Argument);
}
///
/// Raises event.
///
/// A response.
protected void OnResponseReceived(string response)
{
if ((object)ResponseReceived != null)
ResponseReceived(this, new EventArgs(response));
}
private void OnResponseReceived(object sender, EventArgs e)
{
OnCommandSent(e.Argument);
}
private void ConnectToWatchDirectory()
{
if (m_session.IsConnected)
{
m_session.SetCurrentDirectory(m_watchDirectory);
if (m_watchDirectory.Length > 0)
OnStatus("FTP file watcher monitoring directory \"" + m_watchDirectory + "\"");
else
OnStatus("No FTP file watcher directory specified - monitoring initial folder");
}
}
// This method is synchronized in case user sets watch interval too tight...
[MethodImpl(MethodImplOptions.Synchronized)]
private void WatchTimer_Elapsed(object sender, ElapsedEventArgs e)
{
// We attempt to access the FTP Session and refresh the current directory, if this fails
// we are going to restart the connect cycle
try
{
// Refresh the file listing for the current directory
m_session.CurrentDirectory.Refresh();
}
catch (FtpExceptionBase ex)
{
RestartConnectCycle();
OnStatus("FTP file watcher is no longer connected to server \"" + m_session.Server + "\" - restarting connect cycle." + "\r\n" + "\t" + "Exception: " + ex.Message);
}
if ((object)m_session != null)
{
if (m_session.IsConnected)
{
//Dictionary.ValueCollection.Enumerator currentFiles = m_session.CurrentDirectory.Files.GetEnumerator();
FtpFile newFile;
List removedFiles = new List();
int x, index;
// Check for new files
foreach (FtpFile currentFile in m_session.CurrentDirectory.Files)
{
if (m_notifyOnComplete)
{
// See if any new files are finished downloading
index = m_newFiles.BinarySearch(currentFile);
if (index >= 0)
{
newFile = m_newFiles[index];
if (newFile.Size == currentFile.Size)
{
// File size has not changed since last directory refresh, so we will
// notify user of new file...
m_currentFiles.Add(currentFile);
m_currentFiles.Sort();
m_newFiles.RemoveAt(index);
OnFileAdded(currentFile);
}
else
{
newFile.Size = currentFile.Size;
}
}
else if (m_currentFiles.BinarySearch(currentFile) < 0)
{
m_newFiles.Add(currentFile);
m_newFiles.Sort();
}
}
else if (m_currentFiles.BinarySearch(currentFile) < 0)
{
// If user wants an immediate notification of new files, we'll give it to them...
m_currentFiles.Add(currentFile);
m_currentFiles.Sort();
OnFileAdded(currentFile);
}
}
// Check for removed files
for (x = 0; x < m_currentFiles.Count; x++)
{
if ((object)m_session.CurrentDirectory.FindFile(m_currentFiles[x].Name) == null)
{
removedFiles.Add(x);
OnFileDeleted(m_currentFiles[x]);
}
}
// Remove files that have been deleted
if (removedFiles.Count > 0)
{
removedFiles.Sort();
// We remove items in desc order to maintain index integrity
for (x = removedFiles.Count - 1; x >= 0; x--)
{
m_currentFiles.RemoveAt(removedFiles[x]);
}
removedFiles.Clear();
}
m_watchTimer.Enabled = m_enabled;
}
else
{
RestartConnectCycle();
OnStatus("FTP file watcher is no longer connected to server \"" + m_session.Server + "\" - restarting connect cycle.");
}
}
}
private void CloseSession()
{
try
{
m_session.Close();
}
catch
{
}
}
private void RestartConnectCycle()
{
m_restartTimer.Enabled = false;
m_restartTimer.Interval = 10000;
m_restartTimer.Enabled = true;
}
private void RestartTimer_Elapsed(object sender, ElapsedEventArgs e)
{
// Attempt to close the FTP Session if it is open...
CloseSession();
// Try to reestablish connection
m_watchTimer.Enabled = false;
Connect(null, null);
}
#endregion
}
}