#!/usr/local/bin/php
<?
/*
* lazyftp.php - Keeps remote FTP files in sync with local files.
*
* Copyright (c) 2003 by Ryan Grove (ryan@wonko.com).
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*
***************************************************************************
* LazyFTP is a relatively straightforward PHP script that keeps remote
* files in sync with local files via FTP. It does this by monitoring the
* local directory tree for additions, changes, and deletions, and uploading
* or deleting files on the FTP server when necessary.
*
* This version of LazyFTP does not create or delete remote directories, so
* you should make sure your remote tree is an exact mirror of your local
* tree before you run LazyFTP.
*
* You can read more about LazyFTP at http://wonko.com/lazyftp/.
*/
 
############################################################################
# User-configurable settings
############################
 
/** Local directory to monitor for changes. */
$localdir = "";
 
/** FTP server hostname or IP address. */
$ftpserver = "";
 
/** FTP server port. */
$ftpport = 21;
 
/** FTP username. */
$ftpuser = "anonymous";
 
/** FTP password. */
$ftppass = "anon@anon.com";
 
/** FTP directory to which changed files should be uploaded. */
$ftpdirectory = "";
 
/** Whether or not to use passive mode for file transfers. */
$passive = false;
 
/** Whether or not to connect using SSL-FTP. */
$ssl = false;
 
/** Whether or not to output verbose status messages. */
$verbose = false;
 
/** Interval at which files are checked for changes (in seconds). */
$interval = 5;
 
############################################################################
# End of user-configurable settings
###################################
 
set_time_limit(0);
 
define("VERSION", "1.0.0");
define("_DEBUG", false);
 
/** FTP class. */
$ftp = new ftp;
 
/** Recursive list of files and directories under $localdir. */
$files = array();
 
// Parse commandline arguments.
parseArgs();
 
// Say hello.
echo "\n";
echo "LazyFTP v".VERSION."\n";
echo "Copyright (c) 2003 Ryan Grove. All rights reserved.\n\n";
 
// Make sure $localdir is valid.
if (!is_dir($localdir))
    error("Not a valid directory: $localdir");
    
message("Monitoring $localdir.");
 
// Main program loop.
while(1 == 1)
{
    $oldfiles = $files;
    $files    = array();
    $changed  = array();
    $deleted  = array();
    
    clearstatcache();
    getFileList($localdir);
    
    // Discover added and changed files.
    foreach($files as $file)
    {
        if (!in_array($file, $oldfiles))
        {
            $changed[] = $file['name'];
            
            // Update the $oldfiles array so it doesn't think this file was
            // deleted as a result of the different 'modified' time.
            $oldfiles[$file['name']] = $file;
        }
    }
    
    // Discover deleted files.
    foreach($oldfiles as $oldfile)
    {
        if (!in_array($oldfile, $files))
            $deleted[] = $oldfile['name'];
    }
    
    // Upload changed files.
    if (count($oldfiles) > 0 && (count($changed) > 0 || count($deleted) > 0))
    {
        // Make sure we're connected to the FTP server.
        if ($ftp->connect($ftpserver, $ftpport, $ftpuser, $ftppass, $ssl, $passive))
        {
            // Upload each changed file.
            foreach($changed as $file)
            {
                $remotedir  = $ftpdirectory.str_replace($localdir, "", dirname($file));
                $remotefile = escapeshellcmd(basename($file));
                
                if ($ftp->chdir($remotedir))
                {
                    if ($ftp->uploadFile($file, $remotefile))
                        message("Uploaded $file.");
                    else
                        message("Error: Could not upload $file to $remotedir/$remotefile.");
                }
                else
                {
                    message("Error: Could not change remote directory to $remotedir.");
                }                
            }
            
            // Remove each deleted file.
            foreach($deleted as $file)
            {
                $remotedir  = $ftpdirectory.str_replace($localdir, "", dirname($file));
                $remotefile = escapeshellcmd(basename($file));
                
                if ($ftp->chdir($remotedir))
                {
                    if ($ftp->delete($remotefile))
                        message("Deleted $remotedir/$remotefile.");
                    else
                        message("Error: Could not delete $remotedir/$remotefile.");
                }
                else
                {
                    message("Error: Could not change remote directory to $remotedir.");
                }
            }
        }
        else
        {
            message("Error: Could not establish FTP connection.");
        }
    }        
    
    sleep($interval);
}
 
############################################################################
# Functions
###########
 
/**
* Prints an error message to standard output, then exits. Kinda like
* message() only less friendly.
*
* @param message Error message to print.
* @see message()
*/
function error($message)
{
    echo "[".date("m/d/Y H:i:s")."] Fatal Error: $message\n";
    exit;    
}
 
/**
* Prints a message to standard output if verbose mode is on. Prefaces the
* message with a timestamp and puts a newline at the end if <i>break</i> is
* true.
*
* @param message Message to print.
* @param break Whether or not to add a newline at the end.
* @see error()
*/
function message($message, $break = true)
{
    global $verbose;
    
    echo $verbose ? "[".date("m/d/Y H:i:s")."] $message\n" : "";
}
 
/**
* Parses commandline arguments.
*/
function parseArgs()
{
    global $localdir, $ftpserver, $ftpport, $ftpuser, $ftppass, $ftpdirectory, $passive, $verbose;
    
    for ($i = 1; $i < $_SERVER["argc"]; $i++)
    {
        switch($_SERVER["argv"][$i])
        {
            case "-i":
            case "--interval":
                $i++;
                $interval = $_SERVER["argv"][$i];
                break;
                
            case "-l":
            case "--localdir":
                $i++;
                $localdir = $_SERVER["argv"][$i];
                break;
                
            case "-p":
            case "--pass":
            case "--password":
            case "--ftppass":
            case "--ftppassword":
                $i++;
                $ftppass = $_SERVER["argv"][$i];
                break;
                
            case "-P":
            case "--passive":
            case "--passivemode":
            case "--pasv":
                $passive = true;
                break;
                
            case "-r":
            case "--remotedir":
            case "--remotedirectory":
            case "--ftpdir":
            case "--ftpdirectory":
                $i++;
                $ftpdirectory = $_SERVER["argv"][$i];
                break;
                
            case "-s":
            case "--server":
            case "--ftpserver":
            case "--host":
                $i++;
                $ftpserver = $_SERVER["argv"][$i];
                break;
                
            case "-S":
            case "--ssl":
            case "--secure":
                $ssl = true;
                break;
                
            case "-u":
            case "--user":
            case "--username":
            case "--ftpuser":
            case "--ftpusername":
                $i++;
                $ftpuser = $_SERVER["argv"][$i];
                break;
                
            case "-v":
            case "--verbose":
                $verbose = true;
                break;
                
            case "-?":
            case "-h":
            case "--help":
                echo "Usage: lazyftp [option <parameter>...]\n";
                echo "\n";
                echo " -i <interval>   Interval at which files are checked for changes (in seconds).\n";
                echo " -l <localdir>   Local directory to monitor for changes.\n";
                echo " -p <pass>       FTP password.\n";
                echo " -P              Use passive mode for file transfers.\n";
                echo " -r <remotedir>  FTP directory to which changed files should be uploaded.\n";
                echo " -s              FTP server hostname or IP address.\n";
                echo " -S              Connect using SSL-FTP.\n";
                echo " -u <user>       FTP username (if not specified, login will be anonymous).\n";
                echo " -v              Enable verbose status messages.\n";
                echo "\n";
                echo "Example:\n";
                echo "  lazyftp.php -l /my/files -s ftp.pants.com -r /home/monkey -v\n";
                echo "\n";
                echo "These options can be set permanently by editing lazyftp.php.\n";
                exit;
                break;
 
            default:
        }
    }
}
 
/**
* Iterates through a directory tree recursively and builds an array of
* files and their last modified time.
*
* @param dirname Directory to iterate through.
*/
function getFileList($dirname)
{
    global $files, $localdir;
    
    $d = dir($dirname);
    
    while($entry = $d->read())
    {
        if ($entry != '.' && $entry != '..')
        {
            if (is_dir($dirname.'/'.$entry))
            {
                getFileList($dirname.'/'.$entry);
            }
            else
            {
                $file = $dirname.'/'.$entry;
                $files[$file] = array('name' => $file, 'modified' => filemtime($file));
            }
        }
    }
    
    $d->close();
}
    
############################################################################
# Classes
#########
 
class ftp
{
    /** <i>true</i> when connected to a server, <i>false</i> otherwise. */    
    var $connected;
    
    /** Resource handle for the current connection (if any). */
    var $conn;
    
    /** System type of the remote server (if supported). */
    var $systype;
    
    function ftp()
    {
        $this->connected = false;
        $this->ssl       = false;        
    }
 
    /**
     * Establishes a connection with an FTP server if one is not already
     * open.
     *
     * @param server Server to connect to.
     * @param port Server port to connect to.
     * @param user Login username (leave empty for anonymous).
     * @param pass Login password (leave empty for anonymous).
     * @param ssl Set to <i>true</i> to attempt an SSL-FTP connection.
     *    Default is <i>false</i>.
     * @param passive Set to <i>true</i> to turn passive mode on. Default is
     *    <i>false</i>.
     * @param timeout Timeout (in seconds) for all network operations on
     *    this connection. Default is 90.
     * @return <i>true</i> on success (or if a connection is already open),
     *    <i>false</i> on failure or error.
     */
    function connect($server, $port = 21, $user = "anonymous", $pass = "anon@anon.com", $ssl = false, $passive = false, $timeout = 90)
    {
        // Already connected.
        if ($this->connected)
            return true;
 
        if ($user == "")
            $user = "anonymous";
            
        if ($pass == "")
            $pass = "anon@anon.com";
        
        // Attempt to connect.        
        $ssl ? $this->conn = @ftp_ssl_connect($server, $port, $timeout) :
            $this->conn = @ftp_connect($server, $port, $timeout);
            
        if ($this->conn === false)
        {
            // Couldn't connect.
            $this->connected = false;
            echo _DEBUG ? "Could not connect to $server on port $port.<br />\n" : "";
            return false;
        }
        else
        {
            // Connected, now let's try logging in.
            if (@ftp_login($this->conn, $user, $pass))
            {
                if ($passive)
                    @ftp_pasv($this->conn, true);
                
                $this->systype = @ftp_systype($this->conn);                
                $this->connected = true;
                return true;
            }
            else
            {
                $this->connected = false;
                echo _DEBUG ? "Invalid username or password.<br />\n" : "";
                return false;                
            }            
        }
    }
    
    /**
     * Deletes the specified file from the server.
     *
     * @param filename File to delete.
     * @return <i>true</i> on success, <i>false</i> on failure.
     */
    function delete($filename)
    {
        if ($this->connected === false)
        {
            echo _DEBUG ? "Can't delete file when no connection is open.<br />\n" : "";
            return false;
        }
        
        if (@ftp_delete($this->conn, $filename))
        {
            return true;
        }
        else
        {
            echo _DEBUG ? "Could not delete file $filename.<br />\n" : "";
            return false;
        }
    }
    
    /**
     * Disconnects from the server and closes the current connection (if
     * any).
     *
     * @return <i>true</i> on success, <i>false</i> on failure.
     */
    function disconnect()
    {
        if ($this->connected)
        {
            @ftp_quit($this->conn);
            return true;
        }
        else
        {
            echo _DEBUG ? "Cannot disconnect when not connected.<br />\n" : "";
            return false;    
        }
    }
 
    /**
     * Changes to the specified directory.
     *
     * @param directory Directory to change to.
     * @return <i>true</i> on success, <i>false</i> on failure.
     */
    function chdir($directory)
    {
        if ($this->connected === false)
        {
            echo _DEBUG ? "Can't change directory when no connection is open.<br />\n" : "";
            return false;
        }
        
        if (@ftp_chdir($this->conn, $directory))
        {
            return true;
        }
        else
        {
            echo _DEBUG ? "Could not change directory to $directory.<br />\n" : "";
            return false;
        }
    }
    
    /**
     * Returns the name of the current working directory.
     *
     * @return Name of current directory, or <i>false</i> on failure.
     */
    function pwd()
    {
        if ($this->connected === false)
        {
            echo _DEBUG ? "Can't get working directory name when no connection is open.<br />\n" : "";
            return false;
        }
        
        if ($pwd = @ftp_pwd($this->conn))
        {
            return $pwd;
        }
        else
        {
            echo _DEBUG ? "Could not get working directory name.<br />\n" : "";
            return false;
        }
    }
    
    /**
     * Uploads a file to the server in the current directory.
     *
     * @param localfile Filename of the file to upload on the local machine.
     * @param remotefile Filename of the file on the remote machine.
     * @param mode File mode (one of the constants FTP_ASCII or FTP_BINARY).
     * @return <i>true</i> on success, <i>false</i> on failure.
     */
    function uploadFile($localfile, $remotefile, $mode = FTP_BINARY)
    {
        if ($this->connected === false)
        {
            echo _DEBUG ? "Can't upload file when no connection is open.<br />\n" : "";
            return false;
        }
        
        if (@ftp_put($this->conn, $remotefile, $localfile, $mode))
        {
            return true;
        }
        else
        {
            echo _DEBUG ? "Upload failed.<br />\n" : "";
            return false;
        }
    }
}
?>