plupload-handler-php icon indicating copy to clipboard operation
plupload-handler-php copied to clipboard

PluploadFTP.php new feature

Open bvicil opened this issue 8 years ago • 1 comments

in some of our project we need to send files above php max-limit with chunking and do not want to chmod upload directories.

so i write FTP add-on to PluploadHandler.php.

first for chunk file name issue you must add these lines to javascript: BeforeUpload: function (up, file) { // Called right before the upload for a given file starts, can be used to cancel it if required up.settings.multipart_params = { filename: file.name }; }

so we must add some extra lines to PluploadHandler.php

FIND define('PLUPLOAD_SECURITY_ERR', 105); ADD AFTER define('PLUPLOAD_FTP_ERR', 106); define('PLUPLOAD_FTPUP_ERR', 107); define('PLUPLOAD_FTPCH_ERR', 108); define('PLUPLOAD_CHDIR_ERR', 109); define('PLUPLOAD_CHFILE_ERR', 110);

find PLUPLOAD_SECURITY_ERR => "File didn't pass security check." ADD AFTER , PLUPLOAD_FTP_ERR => "Failed to connect FTP server.", PLUPLOAD_FTPUP_ERR => "Failed to send file with FTP.", PLUPLOAD_FTPCH_ERR => "Failed to combine file with FTP.", PLUPLOAD_CHDIR_ERR => "Failed to open part file.", PLUPLOAD_CHFILE_ERR => "Cannot open parted file."

then change appropriate changes in upload.php (mine is this)

`require_once("PluploadHandler.php"); require_once("PluploadFTP.php");

$settings = array( 'target_dir' => 'path/to/upload', //absolute ftp directory 'rel_dir' => 'path/to/upload', //relative directory from script 'allow_extensions' => 'jpg,png', 'ftphost' => 'ftp.domain.tld', // ftp host address 'ftpuser' => 'ftpuser', // ftp user 'ftppass' => 'ftppass' // ftp pass );

PluploadFTP::no_cache_headers(); PluploadFTP::cors_headers(); if (!PluploadFTP::handleFTP($settings)) { die(json_encode(array( 'OK' => 0, 'error' => array( 'code' => PluploadHandler::get_error_code(), 'message' => PluploadHandler::get_error_message() ) ))); } else { die(json_encode(array('OK' => 1))); }`

and the new PluploadFTP.php file is this: `<?php class PluploadFTP extends PluploadHandler { /** * Handle FTP Upload * * @return nothing * @param array Configuration Files */ static function handleFTP($conf = array()) { // 5 minutes execution time @set_time_limit(5 * 60);

    parent::$_error = null; // start fresh

    $conf = self::$conf = array_merge(array(
        'file_data_name' => 'file',
        'tmp_dir' => ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload",
        'target_dir' => false,
        'rel_dir' => false,
        'cleanup' => true,
        'max_file_age' => 5 * 3600,
        'chunk' => isset($_REQUEST['chunk']) ? intval($_REQUEST['chunk']) : 0,
        'chunks' => isset($_REQUEST['chunks']) ? intval($_REQUEST['chunks']) : 0,
        'file_name' => isset($_REQUEST['name']) ? $_REQUEST['name'] : false,
        'filename' => isset($_REQUEST['filename']) ? $_REQUEST['filename'] : false,
        'allow_extensions' => false,
        'delay' => 0,
        'cb_sanitize_file_name' => array(__CLASS__, 'sanitize_file_name'),
        'cb_check_file' => false,
        'ftphost' => false,
        'ftpuser' => false,
        'ftppass' => false,
    ), $conf);

    try {
        if (!$conf['file_name']) {
            if (!empty($_FILES)) {
                $conf['file_name'] = $_FILES[$conf['file_data_name']]['name'];
            } else {
                throw new Exception('', PLUPLOAD_INPUT_ERR);
            }
        }

        // Cleanup outdated temp files and folders
        if ($conf['cleanup']) {
            self::cleanupFTP();
        }

        // Fake network congestion
        if ($conf['delay']) {
            usleep($conf['delay']);
        }

        if (is_callable($conf['cb_sanitize_file_name'])) {
            $file_name = call_user_func($conf['cb_sanitize_file_name'], $conf['file_name']);
        } else {
            $file_name = $conf['file_name'];
        }

        if($conf['filename']) $file_name = $conf['filename'];

        // Check if file type is allowed
        if ($conf['allow_extensions']) {
            if (is_string($conf['allow_extensions'])) {
                $conf['allow_extensions'] = preg_split('{\s*,\s*}', $conf['allow_extensions']);
            }

            if (!in_array(strtolower(pathinfo($file_name, PATHINFO_EXTENSION)), $conf['allow_extensions'])) {
                throw new Exception('', PLUPLOAD_TYPE_ERR);
            }
        }

        $file_path = rtrim($conf['target_dir'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file_name;
        $tmp_path = $file_path . ".part";

        // Write file or chunk to appropriate temp location
        if ($conf['chunks']) {
            self::write_file_toFTP("$file_path.dir.part" . DIRECTORY_SEPARATOR . $conf['chunk']);
            // Check if all chunks already uploaded
            if ($conf['chunk'] == $conf['chunks'] - 1) {
                self::write_chunks_to_fileFTP("$file_path.dir.part", $tmp_path);
            }
        } else {
            self::write_file_toFTP($tmp_path);
        }


        // Upload complete write a temp file to the final destination
        if (!$conf['chunks'] || $conf['chunk'] == $conf['chunks'] - 1) {
            if (is_callable($conf['cb_check_file']) && !call_user_func($conf['cb_check_file'], $tmp_path)) {
                //@self::rrmdirFTP($tmp_path);
                throw new Exception('', PLUPLOAD_SECURITY_ERR);
            }

            $conn_id = self::ftpConnect();
            ftp_rename($conn_id, $tmp_path, $file_path);
            ftp_close($conn_id);

            return array(
                'name' => $file_name,
                'path' => $file_path,
                'size' => filesize($file_path)
            );
        }

        // ok so far
        return true;

    } catch (Exception $ex) {
        parent::$_error = $ex->getCode();
        return false;
    }
}

/**
 * Writes either a multipart/form-data message or a binary stream
 * to the specified file with FTP.
 *
 * @throws Exception In case of error generates exception with the corresponding code
 *
 * @param string $file_path The path to write the file to
 * @param bool $file_data_name The name of the multipart field
 */
static function write_file_toFTP($file_path, $file_data_name = false)
{
    if (!$file_data_name) $file_data_name = self::$conf['file_data_name'];

    if (!empty($_FILES) && isset($_FILES[$file_data_name])) {
        if ($_FILES[$file_data_name]["error"] || !is_uploaded_file($_FILES[$file_data_name]["tmp_name"])) {
            throw new Exception('', PLUPLOAD_MOVE_ERR);
        }

        $file_path = str_replace("\\","/", substr($file_path, 1, strlen($file_path)));

        $destination = "ftp://".self::$conf['ftpuser'].":".self::$conf['ftppass']."@".self::$conf['ftphost']."/".$file_path;
        $ch = curl_init();
        $localfile = $_FILES[$file_data_name]['tmp_name'];
        $fp = fopen($localfile, 'r');
        curl_setopt($ch, CURLOPT_URL, $destination);
        curl_setopt($ch, CURLOPT_UPLOAD, 1);
        curl_setopt($ch, CURLOPT_INFILE, $fp);
        curl_setopt($ch, CURLOPT_INFILESIZE, filesize($localfile));
        curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);
        curl_exec ($ch);
        $error_no = curl_errno($ch);
        curl_close ($ch);
        if ($error_no != 0) throw new Exception('', PLUPLOAD_FTPUP_ERR);

    } else {
        // Handle binary streams
        if (!$in = @fopen("php://input", "rb")) throw new Exception('', PLUPLOAD_INPUT_ERR);

        $myresult = self::ftpupload($in, $file_path);
        if($myresult != 0) throw new Exception('', PLUPLOAD_FTPCH_ERR);

        @fclose($in);
    }
}


/**
 * Combine chunks from the specified folder into the single file with FTP.
 *
 * @throws Exception In case of error generates exception with the corresponding code
 *
 * @param string $file_path The file to write the chunks to
 */
static function write_chunks_to_fileFTP($chunk_dir, $file_path)
{
    $base_dir = dirname($file_path);
    $newFile = str_replace($base_dir.DIRECTORY_SEPARATOR, "", $chunk_dir);

    $chunk_file = self::$conf['rel_dir'].DIRECTORY_SEPARATOR.$newFile;

    for ($i = 0; $i < self::$conf['chunks']; $i++) {
        $chunk_path = $chunk_file . DIRECTORY_SEPARATOR . $i;
        if (!file_exists($chunk_path)) throw new Exception('', PLUPLOAD_CHDIR_ERR);
        if (!$in = @fopen($chunk_path, "rb")) throw new Exception('', PLUPLOAD_CHFILE_ERR);

        $myresult = self::ftpupload($chunk_path, $file_path);
        if($myresult != 0) throw new Exception('', PLUPLOAD_FTPCH_ERR);

        fclose($in);
    }

    // Cleanup
    self::rrmdirFTP($newFile);
}


/**
 * Concise way to recursively remove a directory with FTP
 *
 * @throws Exception In case of error generates exception with the corresponding code
 *
 * @param string $dir Directory to remove
 */
private static function rrmdirFTP($dir)
{
    $dir = self::$conf['target_dir'].DIRECTORY_SEPARATOR.$dir;

    $conn_id = self::ftpConnect();
    ftp_chdir($conn_id, $dir);

    $contents = ftp_nlist($conn_id, ".");

    foreach($contents as $file){
        if(is_dir(self::$conf['rel_dir'].DIRECTORY_SEPARATOR.$file))
            self::rrmdirFTP($file);
        else
            @ftp_delete($conn_id, $file);
    }
    ftp_rmdir($conn_id, $dir);
    ftp_close($conn_id);
}


/**
 * Cleanup outdated temp files and folders
 *
 */
private static function cleanupFTP()
{
    /*
    $uploadDir = self::$conf['target_dir'];

    $conn_id = self::ftpConnect();
    ftp_chdir($conn_id, $uploadDir);

    $contents = ftp_nlist($conn_id, ".");
    foreach($contents as $file){
        if(is_dir(self::$conf['rel_dir'].DIRECTORY_SEPARATOR.$file)){
            $is_part = substr($file, strlen($file) -5);
            if($is_part == ".part"){
                if(time() - ftp_mdtm($conn_id, $file) < self::$conf['max_file_age']){
                    continue;
                }
                self::rrmdirFTP($file);
            }
        }
    }
    ftp_close($conn_id);
    */
}

/**
 * Connect to FTP server
 *
 * @return resource ftp resource
 *
 * @throws Exception In case of error generates exception with the corresponding code
 *
 * */
private static function ftpConnect(){
    $conn_id = ftp_connect(self::$conf['ftphost']);
    if (!$conn_id) throw new Exception('', PLUPLOAD_FTP_ERR);

    ftp_login($conn_id, self::$conf['ftpuser'], self::$conf['ftppass']);
    ftp_pasv($conn_id, true);

    return $conn_id;
}

/**
 * FTP upload with append mode
 *
 * @return bool true/error code
 *
 * @throws Exception In case of error generates exception with the corresponding code
 *
 * @param string $chunk_file chunk file
 * @param string $file_path last file
 *
 */
private static function ftpupload( $chunk_file , $file_path )
{
    $file_path = str_replace(DIRECTORY_SEPARATOR,"/", substr($file_path, 1, strlen($file_path)));
    $destination = "ftp://".self::$conf['ftpuser'].":".self::$conf['ftppass']."@".self::$conf['ftphost']."/".$file_path;

    $ch = curl_init();

    if (!$fp = @fopen($chunk_file, "r")) throw new Exception('', PLUPLOAD_INPUT_ERR);

    curl_setopt($ch, CURLOPT_UPLOAD, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, 300);
    curl_setopt($ch, CURLE_OPERATION_TIMEOUTED, 300);
    curl_setopt($ch, CURLOPT_URL, $destination);
    curl_setopt($ch, CURLOPT_FTPAPPEND, TRUE ); // APPEND FLAG
    curl_setopt($ch, CURLOPT_INFILE, $fp);
    curl_setopt($ch, CURLOPT_INFILESIZE, filesize($chunk_file));
    curl_exec($ch);
    fclose ($fp);
    $errorMsg = '';
    $errorMsg = curl_error($ch);
    $errorNumber = curl_errno($ch);
    curl_close($ch);
    return $errorNumber;
}

}`

so i have got issues about; i can not check last modified time of chunk directory, so i can not cleanupFTP directory. because the capabilities of ftp_mdtm is not working correctly.

i use this script and it works with chunks correctly.

thanks for your help.

cheers.

bvicil avatar May 18 '16 11:05 bvicil

this code is of interest but it has been damaged by github because it's in a comment. can you put it up somewhere so it can be obtained as intended?

rob-lindman avatar Dec 09 '16 14:12 rob-lindman