ccFileSlinger – simple directory monitor that emails new files

I got a few emails asking about the “makeshift security system” I referenced in this article. Well, perhaps calling it a “system” is quite a bit of an exaggeration, but I will outline what I did and share the associated code that I wrote for this purpose.

The idea is quite simple. Logitech Quickcam software allows motion-activated recording. When the motion sensor is activated, a video of configured duration  is recorded to the specified directory. Naturally, the problem is that if someone takes your computer, the recorded files are gone with it, which most certainly constitutes a system failure :-).

In attempt to mitigate this obvious shortcoming, I threw together a quick application that runs alongside Quickcam software and watches the directory that was configured as Quickcam’s output directory for new files. When a new a file is found, it promptly emails it to the specified email address.

Welcome multi-purpose tool – ccFileSlinger… Basic features are:

  • Monitors specified directory for new files (files that appear after the monitoring has begun)
  • Waits for the file to be written completely
  • Emails the file as an attachment using specified SMTP server and credentials to the configured email address
  • Works with any file (not just avi, does not have to be used with Quickcam)
  • Free, of course – Download binaries and source code
Configuration and Execution
ccFileSlinger is a command-line application that takes one argument – the directory that should be monitored. Email configuration is done via a config file – ccFileSlinger.exe.config.
<setting name="email_to_address" serializeAs="String">
    <value>me@server.com</value>
</setting>
<setting name="email_from_address" serializeAs="String">
    <value>fromme@server.com</value>
</setting>
<setting name="smtp_server" serializeAs="String">
    <value>mail.server.com</value>
</setting>
<setting name="smtp_user" serializeAs="String">
    <value>user@server.com</value>
</setting>
<setting name="smtp_password" serializeAs="String">
    <value>server_password</value>
</setting>
Before running the application, open the config file in  a text editor (i.e. Notepad) and substitute your desired email settings:
  • me@server.com – set this to the email address to which you want the email sent
  • fromme@server.com – set the to the email address that should be used as the ‘from’ address on the email
  • mail.server.com – set this to your SMTP mail server
  • user@server.com – set this to the user name that should be used to access the SMTP server
  • server_password – set this to the password that should be associated with the user
Once this is all set, go to the ccFileSlinger directory in the command prompt and start it with the desired parameter – e.g:
C:\ccFileSlinger>ccFileSlinger.exe “C:\Documents and Settings\user\My Documents\My Videos\QuickCam”
The Code
First, let me say that I did not mean for this to be production level code and it’s just something I put together in less than an hour. So if you have comments, improvements, or fixes, please pass them along.
The app consists of 3 classes:
  • FileSlingerEntry: entry point to the application that sets up the directory watcher and the emailer and handles data relay between the two.
  • DirectoryWatcher: monitors specified directory for new files; instantiated with the directory name and callback delegate that is called when new files are discovered
  • Emailer: sends email messages via SMTP
FileSlingerEntry
class FileSlingerEntry
{
    static Emailer m_oEmailer = new Emailer(FileSlinger.Emailer.Default.smtp_server, 
                            FileSlinger.Emailer.Default.smtp_user, 
                            FileSlinger.Emailer.Default.smtp_password);
    /// <summary>
    /// Application entry point
    /// </summary>
    /// <param name="args">expecting one argument representing the directory that should 
    /// be watched</param>
    static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Please supply the directory that should be watched...");
            return;
        }
        AppDomain.CurrentDomain.UnhandledException += 
               new UnhandledExceptionEventHandler(FileSlingerException);             
        DirectoryWatcher.NewFileCallback oFileCallback = 
               new DirectoryWatcher.NewFileCallback(ProcessNewFile);              
        DirectoryWatcher oWatcher = new DirectoryWatcher(args[0], oFileCallback);
        Console.WriteLine("Press 'Any' key to exit...");                 // pause to exit
        Console.ReadKey();
        oWatcher.Dispose();
    }
    /// <summary>
    /// Will be called by DirectoryWatcher when new file is detected in the watch 
    /// directory
    /// The function emails the file to using the SMTP and user settings specified in 
    /// the config file</summary>
    /// <param name="strFilePath">full path to a file that was found</param>
    static void ProcessNewFile(string strFilePath)
    {
        //Console.WriteLine("Will email a file at {0}", strFilePath);
        string strMessage = @"Hi, FYI new file was found in the watched directory 
              and it is attached...";
        string strSubject = "[FileSlinger] New File Alert";
        m_oEmailer.EmailFile(FileSlinger.Emailer.Default.email_to_address, 
               FileSlinger.Emailer.Default.email_from_address, strMessage, strSubject, 
               strFilePath);
    }
    /// <summary>
    /// Handles (reports) any un-caught exceptions in the application
    /// </summary>
    /// <param name="sender">The application domain that handled the 
    /// System.AppDomain.UnhandledException event</param>
    /// <param name="args">event data</param>
    static void FileSlingerException(object sender, UnhandledExceptionEventArgs args)
    {
        Exception e = (Exception)args.ExceptionObject;
        Console.WriteLine("An exception has occured in the application: {0}:{1} ", 
             e.Message, e.StackTrace);
    }
}

DirectoryWatcher

/// <summary>
/// Timur Kovalev (http://www.creativecodedesign.com): This class watches the 
/// specified directory for new (previosuly unprocessed) files and 
/// when a new file is discovered, it calls the specified delegate
/// </summary>
public class DirectoryWatcher : IDisposable
{
    /// <summary>
    /// Defintion for a callback that will get called when a new file is found
    /// </summary>
    /// <param name="strFilePath">path to the new file</param>
    public delegate void NewFileCallback(string strFilePath);
    FileSystemWatcher m_oFSWatcher = null;  
    NewFileCallback m_oCallback = null;         
    /// <summary>
    /// Instantiates a directory watcher for the specified directory and 
    /// issues a callback when new files are discovered
    /// </summary>
    /// <param name="strDirectory">Directory that should be watched - note, the  
    /// directory should already exist (or an exception will be thrown)</param>
    /// <param name="oCallback">a callback function that should be called when 
    /// a new file is discovered</param>
    public DirectoryWatcher(string strDirectory, NewFileCallback oCallback)
    {
        m_oCallback = oCallback;                                  // store the callback
        m_oFSWatcher = new FileSystemWatcher(strDirectory);
        m_oFSWatcher.Created += 
                new FileSystemEventHandler(FileSystemObjectCreated);  
        m_oFSWatcher.IncludeSubdirectories = false; // don't look at subdirectories
        m_oFSWatcher.EnableRaisingEvents = true;    // start the monitoring
    }
    /// <summary>
    /// Called by FileSystemWatcher object when a new file is found in the target 
    /// directory </summary>
    /// <param name="sender">The source of the event</param>
    /// <param name="e">Event Data</param>
    private void FileSystemObjectCreated(object sender, FileSystemEventArgs e)
    {
        //Console.WriteLine("Found File {0}", e.FullPath);
        FileInfo oFileInfo = new FileInfo(e.FullPath);         // get the file info
        while (true)                                    // a little hackery here.. :-/
        {
            try
            {
                using(FileStream oStream = oFileInfo.OpenWrite()); // try to open a file 
                break;                   // 'using' will close the stream, so break
            }
            catch (IOException) // and exception will occur if it is still being written
            {
                //Console.WriteLine("waiting for write completion...");
                System.Threading.Thread.Sleep(1000);               // wait a seconds
                continue;                                        // and try again...        
            }
        }            
        m_oCallback(e.FullPath);
    }
    /// <summary>
    /// Cleans up after the instance explicitly
    /// </summary>
    public void Dispose()
    {
        if (m_oFSWatcher != null)
        {
            m_oFSWatcher.EnableRaisingEvents = false;
            m_oFSWatcher.Dispose();
         }
    }
 }
 
Emailer
/// <summary>
/// Timur Kovalev (http://www.creativecodedesign.com): A class that allows email 
/// messages to be sent via SMTP
/// </summary>
public class Emailer
{
    string m_strServer = null;
    string m_strUser = null;
    string m_strPassword = null;
    /// <summary>
    /// Instantiates the SMTP emailer class
    /// </summary>
    /// <param name="strServer">SMTP server that should be used</param>
    /// <param name="strUser">the username for the server</param>
    /// <param name="strPassword">the password for the server</param>
    public Emailer(string strServer, string strUser, string strPassword)
    {
        m_strServer = strServer;
        m_strUser = strUser;
        m_strPassword = strPassword;
    }       
    /// <summary>
    /// Sends a message with an attached file
    /// </summary>
    /// <param name="strToAddress">the address to which the message 
    /// should be sent</param>
    /// <param name="strFromAddress">the address that should be used as 
    /// the 'from' address</param>
    /// <param name="strMessage">the body of the email</param>
    /// <param name="strSubject">the subject for the email</param>
    /// <param name="strFileName">full path to the file that should be 
    /// attached to the email</param>
    public void EmailFile(string strToAddress, string strFromAddress, string strMessage, 
                                 string strSubject, string strFileName)
    {
        try
        {                
            SmtpClient smtpClient = new SmtpClient();
            NetworkCredential customCredentials = new NetworkCredential(m_strUser, 
                  m_strPassword);
            MailMessage message = new MailMessage();
            smtpClient.Host = m_strServer;
            smtpClient.UseDefaultCredentials = false;
            smtpClient.Credentials = customCredentials;
            message.From = new MailAddress(strFromAddress);
            message.Subject = strSubject;
            message.IsBodyHtml = true;
            message.To.Add(strToAddress);
            message.Body = strMessage;                
            using(Attachment oAttachedFile = new Attachment(strFileName))
            {
                message.Attachments.Add(oAttachedFile);
                smtpClient.Send(message);      
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(@"An exception has occured while trying to send an 
                  email - {0}:{1}",  ex.Message, ex.StackTrace);
            Console.WriteLine("Server {0}\n User: {1}\n", m_strServer, m_strUser);
        }
    }
}

Not much to say about the code – it’s pretty self explanatory. The unfortunate hacky bit in the DirectoryWatcher has to do with the fact that the FileSystemObjectCreated event is fired as soon as the file appears in the directory, at which time it may still be written to by the application that has created it. (btw, trying to look for FileSystemObjectChanged does not solve that problem…). Thus, we attempt to open the file for writing, which will fail as long as there is another process writing to it. In case of the IO exception we wait.

The problem with this is that IO exception can be raised for other reasons (e.g. File was created and immediately deleted – depending on the application). Hence a check for the actual exception message would be warranted. This was not a use case for me, so I left it as is. Change as you see fit.

Revision History:

v1.0 – Initial Revision

Download: