Monitoring FTP uploads

I still support a couple of desktop applications which use FTP to upload error reports to a web server. These reports can be infrequent, and it’s easy to get out of the habit of monitoring the FTP directory. A couple of days ago I decided to let the process manage itself by creating a simple PHP script to monitor the folder and send me an email when new files were uploaded.

1. The .ini file

To avoid rework if I needed to check other FTP servers in the future, I first created ftp01.ini to hold the FTP credentials and the address for email notifications.

[ftp]
dir =''
mode = 'pasv'
port = 21
pass = '[FTP PASSWORD]'
recurse = 0
url = '[URL TO FTP SERVER]'
user = '[FTP USERNAME]'

[email]
address[] = '[MY EMAIL ADDRESS]'

  • If the directory you want to check isn’t the directory you’re in after logging in with the supplied credentials, then dir can be set accordingly.
  • If mode is empty the FTP login will be active.
  • It’s intended that if recurse is set to 1 the script will also check files in any subdirectories, but I’ve not (yet) implemented this as I don’t need it at the moment.
  • The email->address parameter is an array, which allows me to send notifications to more than one person if I wish to – just add further email addresses on subsequent lines in the same format.

I’ve saved this .ini file above the webroot. Whatever directory it’s stored in needs to be writable by the web server, as it’s where the script will create a record of existing files (it only notifies me when new files arrive).

2. The entry point

On my own web server I have a folder of utility scripts, and I created ftpcheck.php there.

<?php

/*
* To test, use
* - http://[DEV BOX URL]/ftpcheck.php?uid=a4e859f27afd446e82cda7b535ee9c36
* - https://[PRODUCTION URL]/ftpcheck.php?uid=a4e859f27afd446e82cda7b535ee9c36
*
* Cron task
* - curl -s https://[PRODUCTION URL]/ftpcheck.php?uid=a4e859f27afd446e82cda7b535ee9c36 >/dev/null 2>&1
*/

require "ftpalert.php";
$ftp = new ftpalert;
echo $ftp->execute('../etc/ftp01.ini');

  • Note the unique string on the URLs: in the ftpalert class, this helps ensure that only a trusted source can run the script. You can create a unique one here.
  • You don’t have to echo the returned value from the ftpalert class, but it’s useful for pre-production testing.
  • If I had multiple FTP servers I wanted to check, I could call the final line of my script multiple times, each with a different .ini file.

3. The class that does the work

The PHP class ftpalert.php is stored in the same directory as the above ftpcheck.php.

<?php

/*
* A simple class that checks whether new files have been uploaded to a specified ftp directory and sends an email
* notification if they have.
*/

class ftpalert {

    protected
        $data = "",
        $from = "[SENDER EMAIL ADDRESS - I USE A REAL ONE FROM THE SAME DOMAIN TO ENSURE VALIDITY]",
        $guid = "[YOUR UNIQUE GUID AS DESCRIBED IN THE PREVIOUS SECTION]",
        $sock = NULL,
    $vars = NULL;

    public function __construct() {
        // Nowt
    }

    public function execute($ini) {
        if ($this->valid($_GET['uid'])) {
            $this->loadVars($ini);
            $this->ftpOpen();
            $list = $this->ftpList();
            $this->ftpClose();
            $out = $this->ftpCompare($list);
            $count = count($list);
            $sent = $this->emailSend($out);

            return "files: $count, email: $sent";
        }
        else {
            die();
        }
    }

    // -----------------------------------------------------------------------

    private function emailSend($list) {
        if (count($list) > 0) {
            $eol = PHP_EOL;
            $headers = '';
            $headers .= "From: $this->from$eol";
            $headers .= "Reply-to: $this->from$eol";
            $headers .= "Return-Path: $this->from$eol";
            $server = $this->vars['ftp']['url'];
            $subject = "New files from $server";
            $body = "The following new files have been uploaded to $server:$eol$eol";

            foreach ($list as $item) {
                $body .= " - $item$eol";
            }

            $body .= $eol . "This is an automated email message. If you have received it in error, please discard it.$eol$eol";

            foreach ($this->vars['email']['address'] as $recipient) {
                mail($recipient, $subject, $body, $headers);
            }

            return count($this->vars['email']['address']);
        }
        else {
            return '0';
        }
    }

    private function ftpClose() {
        ftp_close($this->sock);
    }

    private function ftpCompare($list) {
        if (file_exists($this->data)) {
            $data = unserialize(file_get_contents($this->data));
            $list = array_diff($list, $data);
        }

        file_put_contents($this->data, serialize($list));

        return $list;
    }

    private function ftpList() {
        $list = ftp_rawlist($this->sock, '/');

        if (is_array($list)) {
            $items = array();

            foreach ($list as $item) {
                $chunks = preg_split("/\s+/", $item);

                if ($chunks[0][0] !== 'd') {
                    $items[] = $chunks[count($chunks) - 1];
                }
            }

            return $items;
        }

        die("Could not retrieve list");
    }

    private function ftpOpen() {
        $this->sock = ftp_connect($this->vars['ftp']['url']);

        if ($this->sock === FALSE) {
            die("Could not connect");
        }

        if (!ftp_login($this->sock, $this->vars['ftp']['user'], $this->vars['ftp']['pass'])) {
            die('Could not log in');
        }

        if ($this->vars['ftp']['mode'] === 'pasv') {
            $pasv = ftp_pasv($this->sock, TRUE);
        }
    }

    private function loadVars($ini) {
        if (file_exists($ini)) {
            // Save the ini file path; that's where we'll save the output
            $path = pathinfo($ini);
            $dir = $path['dirname'];
            $file = $path['filename'];
            $this->data = "$dir/$file.data";

            $this->vars = parse_ini_file($ini, TRUE);
        }
        else {
            die("Could not load ini file $ini");
        }
    }

    private function valid($uid) {
        return (isset($uid) && $uid === $this->guid);
    }

}

  • The error messages are deliberately slightly vague. If you have the source code you can immediately see what’s gone wrong, but they might keep an unauthorised user guessing.
  • The ftpList() function uses ftp_rawlist rather than ftp_nlist so the date can be parsed and included in the email message. This isn’t necessary for me at the moment, though.
  • This class could be stored in the same directory as the .ini file for added security: just change the path in ftpcheck.php.
  • Don’t forget to set restrictive permissions on the PHP files.

4. The cron task

On the web server I have a cron task that runs this once a day. In my particular case there’s no need for a more frequent check.

0    4    *    *    1-6    curl -s https://[PRODUCTION URL]/ftpcheck.php?uid=a4e859f27afd446e82cda7b535ee9c36 >/dev/null 2>&1

  • I run this cron task Monday to Saturday at 4.00am server time, so it shows up in my inbox at 9.00am NZT, which is about when I’m ready for it. I don’t want to hear from it on Sunday.