PHP ftp_put() 函数

PHP ftp_put() 函数用于将本地文件上传到FTP服务器。

特点:这是一个阻塞函数,会在文件传输完成前一直等待,适合小文件传输或不需要同时执行其他任务的场景。

语法

ftp_put(
    resource $ftp,
    string $remote_file,
    string $local_file,
    int $mode = FTP_BINARY,
    int $startpos = 0
): bool

参数说明

参数 描述
ftp 必需。FTP连接的标识符,由ftp_connect()ftp_ssl_connect()返回
remote_file 必需。FTP服务器上要创建的文件路径
local_file 必需。本地文件路径,包含要上传的数据
mode 可选。传输模式,可以是:
  • FTP_ASCII - 文本模式(ASCII)
  • FTP_BINARY - 二进制模式(默认)
startpos 可选。从本地文件的指定位置开始上传(断点续传)。注意:需要服务器支持

返回值

  • 成功时返回 true
  • 失败时返回 false

与ftp_nb_put()的区别

特性 ftp_put() ftp_nb_put()
阻塞性 阻塞模式(同步) 非阻塞模式(异步)
执行方式 脚本等待传输完成 传输过程中可执行其他代码
适用场景 小文件或简单传输 大文件传输或需要并发的场景
进度跟踪 不支持 支持(通过配合ftp_nb_continue())
返回值 布尔值(成功/失败) 状态码(FTP_FAILED/FTP_FINISHED/FTP_MOREDATA)
代码复杂度 简单,直接调用 复杂,需要循环处理

示例

示例1:基本用法 - 上传文件到FTP服务器

<?php
// 连接FTP服务器
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

$ftp_conn = ftp_connect($ftp_server);
if (!$ftp_conn) {
    die("无法连接到 $ftp_server");
}

// 登录
if (!ftp_login($ftp_conn, $ftp_user, $ftp_pass)) {
    die("登录失败");
}

// 启用被动模式(推荐)
ftp_pasv($ftp_conn, true);

// 检查本地文件是否存在
$local_file = "upload.txt";
if (!file_exists($local_file)) {
    die("本地文件不存在: $local_file");
}

// 上传文件
$remote_file = "uploads/upload.txt";
echo "开始上传: $local_file -> $remote_file\n";

if (ftp_put($ftp_conn, $remote_file, $local_file, FTP_ASCII)) {
    echo "文件上传成功!\n";

    // 验证上传
    $remote_size = ftp_size($ftp_conn, $remote_file);
    $local_size = filesize($local_file);

    if ($remote_size == $local_size) {
        echo "文件大小验证通过: $local_size 字节\n";
    } else {
        echo "警告: 本地文件大小 ($local_size) 与远程文件大小 ($remote_size) 不一致\n";
    }
} else {
    echo "文件上传失败!\n";

    // 获取更详细的错误信息
    $error = error_get_last();
    if ($error) {
        echo "错误信息: " . $error['message'] . "\n";
    }
}

// 关闭FTP连接
ftp_close($ftp_conn);
?>

示例2:带完整错误处理和验证的上传

<?php
class FTPUploader {
    private $ftp_conn;
    private $upload_log = [];

    public function connect($server, $username, $password, $port = 21, $timeout = 30) {
        $this->log("正在连接到服务器: $server:$port");

        $this->ftp_conn = ftp_connect($server, $port, $timeout);
        if (!$this->ftp_conn) {
            $this->log("连接失败: 无法连接到服务器");
            return false;
        }

        // 设置超时选项
        ftp_set_option($this->ftp_conn, FTP_TIMEOUT_SEC, $timeout);

        $this->log("连接成功,正在登录...");

        if (!@ftp_login($this->ftp_conn, $username, $password)) {
            $this->log("登录失败: 用户名或密码错误");
            ftp_close($this->ftp_conn);
            $this->ftp_conn = null;
            return false;
        }

        $this->log("登录成功");

        // 尝试开启被动模式
        if (@ftp_pasv($this->ftp_conn, true)) {
            $this->log("被动模式已启用");
        } else {
            $this->log("警告: 无法启用被动模式,使用主动模式");
        }

        return true;
    }

    public function uploadFile($local_file, $remote_path, $mode = FTP_BINARY) {
        $this->log("开始上传文件: $local_file");

        // 验证本地文件
        if (!file_exists($local_file)) {
            $this->log("错误: 本地文件不存在");
            return false;
        }

        if (!is_readable($local_file)) {
            $this->log("错误: 本地文件不可读");
            return false;
        }

        $file_size = filesize($local_file);
        $this->log("文件大小: " . $this->formatBytes($file_size));

        // 构建远程文件路径
        $filename = basename($local_file);
        $remote_file = rtrim($remote_path, '/') . '/' . $filename;

        // 检查远程目录是否存在,如果不存在则创建
        $remote_dir = dirname($remote_file);
        if (!$this->remoteDirectoryExists($remote_dir)) {
            $this->log("远程目录不存在,正在创建: $remote_dir");
            if (!$this->createRemoteDirectory($remote_dir)) {
                $this->log("错误: 无法创建远程目录");
                return false;
            }
        }

        // 检查远程文件是否已存在
        $remote_size = @ftp_size($this->ftp_conn, $remote_file);
        if ($remote_size != -1) {
            $this->log("警告: 远程文件已存在,大小: " . $this->formatBytes($remote_size));

            // 可以选择覆盖或跳过
            // 这里选择覆盖
            $this->log("将覆盖现有文件");
        }

        $start_time = time();
        $this->log("开始传输...");

        // 执行上传
        if (@ftp_put($this->ftp_conn, $remote_file, $local_file, $mode)) {
            $upload_time = time() - $start_time;
            $this->log("上传成功!耗时: {$upload_time}秒");

            // 验证上传
            $new_remote_size = ftp_size($this->ftp_conn, $remote_file);
            if ($new_remote_size == $file_size) {
                $this->log("验证通过: 文件大小匹配 (" . $this->formatBytes($file_size) . ")");

                // 计算上传速度
                if ($upload_time > 0) {
                    $speed = $file_size / $upload_time;
                    $this->log("平均速度: " . $this->formatBytes($speed) . "/s");
                }
            } else {
                $this->log("警告: 文件大小不匹配 (本地: " . $this->formatBytes($file_size) .
                          ", 远程: " . $this->formatBytes($new_remote_size) . ")");
            }

            return true;
        } else {
            $this->log("上传失败");

            // 尝试获取错误信息
            $error = error_get_last();
            if ($error) {
                $this->log("错误详情: " . $error['message']);
            }

            return false;
        }
    }

    private function remoteDirectoryExists($directory) {
        // 尝试切换到目录来检查是否存在
        $current_dir = @ftp_pwd($this->ftp_conn);
        if ($current_dir === false) {
            return false;
        }

        if (@ftp_chdir($this->ftp_conn, $directory)) {
            // 切换回原目录
            ftp_chdir($this->ftp_conn, $current_dir);
            return true;
        }

        return false;
    }

    private function createRemoteDirectory($directory) {
        // 去除开头的斜杠
        $directory = ltrim($directory, '/');
        $parts = explode('/', $directory);

        $current_path = '';
        foreach ($parts as $part) {
            if (!empty($part)) {
                $current_path .= '/' . $part;

                if (!$this->remoteDirectoryExists($current_path)) {
                    if (!@ftp_mkdir($this->ftp_conn, $current_path)) {
                        return false;
                    }
                    $this->log("创建目录: $current_path");
                }
            }
        }

        return true;
    }

    public function batchUpload($local_dir, $remote_dir, $mode = FTP_BINARY, $extensions = []) {
        $this->log("开始批量上传: $local_dir -> $remote_dir");

        if (!is_dir($local_dir)) {
            $this->log("错误: 本地目录不存在");
            return false;
        }

        // 获取本地文件列表
        $files = scandir($local_dir);
        $success_count = 0;
        $fail_count = 0;
        $total_files = 0;

        foreach ($files as $file) {
            if ($file == '.' || $file == '..') {
                continue;
            }

            $local_file = rtrim($local_dir, '/') . '/' . $file;

            // 如果是目录,递归处理
            if (is_dir($local_file)) {
                $this->log("跳过目录: $file");
                continue;
            }

            // 检查扩展名过滤
            if (!empty($extensions)) {
                $file_ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
                if (!in_array($file_ext, $extensions)) {
                    $this->log("跳过文件(扩展名不匹配): $file");
                    continue;
                }
            }

            $total_files++;
            $this->log("处理文件 ($total_files): $file");

            if ($this->uploadFile($local_file, $remote_dir, $mode)) {
                $success_count++;
            } else {
                $fail_count++;
            }
        }

        $this->log("批量上传完成");
        $this->log("总计: $total_files 个文件, 成功: $success_count, 失败: $fail_count");

        return [
            'total' => $total_files,
            'success' => $success_count,
            'failed' => $fail_count
        ];
    }

    private function log($message) {
        $timestamp = date('Y-m-d H:i:s');
        $log_entry = "[$timestamp] $message";
        $this->upload_log[] = $log_entry;
        echo $log_entry . "\n";
    }

    public function getLog() {
        return $this->upload_log;
    }

    private function formatBytes($bytes, $precision = 2) {
        if ($bytes <= 0) return '0 Bytes';

        $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
        $base = log($bytes, 1024);
        $pow = floor($base);
        $pow = min($pow, count($units) - 1);

        return round(pow(1024, $base - $pow), $precision) . ' ' . $units[$pow];
    }

    public function __destruct() {
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
            $this->log("FTP连接已关闭");
        }
    }
}

// 使用示例
try {
    $uploader = new FTPUploader();

    // 连接服务器
    if ($uploader->connect('localhost', 'user', 'pass')) {
        // 上传单个文件
        $uploader->uploadFile('test.txt', '/uploads', FTP_ASCII);

        // 批量上传图片
        $uploader->batchUpload('/path/to/images', '/uploads/images', FTP_BINARY, ['jpg', 'png', 'gif']);

        // 获取日志
        $log = $uploader->getLog();
        file_put_contents('ftp_upload.log', implode("\n", $log));
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例3:断点续传实现

<?php
function resumeUpload($ftp_conn, $local_file, $remote_file, $mode = FTP_BINARY) {
    if (!file_exists($local_file)) {
        return ['success' => false, 'message' => '本地文件不存在'];
    }

    $local_size = filesize($local_file);
    $resume_pos = 0;

    // 检查远程文件是否已存在(部分上传)
    $remote_size = @ftp_size($ftp_conn, $remote_file);
    if ($remote_size != -1 && $remote_size < $local_size) {
        // 文件已存在但未完成上传
        $resume_pos = $remote_size;
        echo "发现未完成的上传,将从 {$resume_pos} 字节处继续\n";
    }

    // 如果从断点处开始,需要特殊处理
    if ($resume_pos > 0) {
        // 使用 ftp_fput 配合文件指针实现断点续传
        $handle = fopen($local_file, 'r');
        if (!$handle) {
            return ['success' => false, 'message' => '无法打开本地文件'];
        }

        // 移动文件指针到续传位置
        fseek($handle, $resume_pos);

        // 使用 fput 上传(支持断点位置)
        $result = ftp_fput($ftp_conn, $remote_file, $handle, $mode, $resume_pos);

        fclose($handle);

        if ($result) {
            return ['success' => true, 'message' => '续传成功', 'resumed' => true];
        } else {
            return ['success' => false, 'message' => '续传失败'];
        }
    } else {
        // 全新上传,使用 ftp_put
        $result = ftp_put($ftp_conn, $remote_file, $local_file, $mode);

        if ($result) {
            return ['success' => true, 'message' => '上传成功', 'resumed' => false];
        } else {
            return ['success' => false, 'message' => '上传失败'];
        }
    }
}

// 带断点记录的上传函数
class ResumeFTPUpload {
    private $ftp_conn;
    private $resume_file = 'ftp_resume.json';

    public function __construct($server, $username, $password) {
        $this->ftp_conn = ftp_connect($server);
        if (!$this->ftp_conn) {
            throw new Exception("无法连接到FTP服务器");
        }

        if (!ftp_login($this->ftp_conn, $username, $password)) {
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->ftp_conn, true);
    }

    public function smartUpload($local_file, $remote_file, $mode = FTP_BINARY) {
        if (!file_exists($local_file)) {
            throw new Exception("本地文件不存在: $local_file");
        }

        $local_size = filesize($local_file);
        $file_key = md5($local_file . $remote_file);

        // 加载断点记录
        $resume_data = $this->loadResumeData();
        $resume_pos = 0;

        if (isset($resume_data[$file_key])) {
            $record = $resume_data[$file_key];

            // 检查文件是否已更改(通过修改时间)
            $file_mtime = filemtime($local_file);
            if ($record['mtime'] == $file_mtime && $record['size'] == $local_size) {
                $resume_pos = $record['position'];
                echo "发现断点记录,将从 {$resume_pos} 字节处继续\n";
            } else {
                echo "文件已修改,重新开始上传\n";
                unset($resume_data[$file_key]);
            }
        }

        // 检查远程文件实际大小
        $remote_size = @ftp_size($this->ftp_conn, $remote_file);
        if ($remote_size != -1) {
            if ($remote_size < $local_size) {
                // 远程文件存在但较小,可能是未完成的上传
                if ($remote_size > $resume_pos) {
                    $resume_pos = $remote_size;
                    echo "根据远程文件大小调整断点位置: {$resume_pos}\n";
                }
            } elseif ($remote_size == $local_size) {
                echo "文件已完整上传\n";
                return true;
            } else {
                echo "警告: 远程文件比本地文件大,将覆盖\n";
            }
        }

        if ($resume_pos > 0) {
            // 使用 ftp_fput 进行续传
            return $this->resumeUploadWithProgress($local_file, $remote_file, $mode, $resume_pos, $file_key, $local_size);
        } else {
            // 全新上传
            return $this->newUpload($local_file, $remote_file, $mode, $file_key, $local_size);
        }
    }

    private function resumeUploadWithProgress($local_file, $remote_file, $mode, $startpos, $file_key, $total_size) {
        $handle = fopen($local_file, 'r');
        if (!$handle) {
            throw new Exception("无法打开本地文件");
        }

        // 移动文件指针到续传位置
        fseek($handle, $startpos);

        // 注意:ftp_fput 不支持通过 ftp_put 那样的 startpos 参数
        // 需要使用 ftp_nb_fput 来实现带进度的续传
        // 这里简化处理,直接使用 fput

        echo "开始续传...\n";
        $result = ftp_fput($this->ftp_conn, $remote_file, $handle, $mode, $startpos);

        fclose($handle);

        if ($result) {
            // 清除断点记录
            $this->clearResumeData($file_key);
            echo "续传完成\n";
            return true;
        } else {
            // 保存断点
            $this->saveResumeData($file_key, $local_file, $remote_file, $startpos);
            echo "续传失败,已保存断点\n";
            return false;
        }
    }

    private function newUpload($local_file, $remote_file, $mode, $file_key, $total_size) {
        echo "开始新文件上传...\n";

        $result = ftp_put($this->ftp_conn, $remote_file, $local_file, $mode);

        if ($result) {
            echo "上传完成\n";
            return true;
        } else {
            echo "上传失败\n";
            return false;
        }
    }

    private function loadResumeData() {
        if (file_exists($this->resume_file)) {
            $data = json_decode(file_get_contents($this->resume_file), true);
            return is_array($data) ? $data : [];
        }
        return [];
    }

    private function saveResumeData($key, $local_file, $remote_file, $position) {
        $data = $this->loadResumeData();

        $data[$key] = [
            'local_file' => $local_file,
            'remote_file' => $remote_file,
            'position' => $position,
            'size' => filesize($local_file),
            'mtime' => filemtime($local_file),
            'timestamp' => time()
        ];

        file_put_contents($this->resume_file, json_encode($data, JSON_PRETTY_PRINT));
    }

    private function clearResumeData($key) {
        $data = $this->loadResumeData();
        if (isset($data[$key])) {
            unset($data[$key]);
            file_put_contents($this->resume_file, json_encode($data, JSON_PRETTY_PRINT));
        }
    }

    public function __destruct() {
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
        }
    }
}

// 使用示例
try {
    $uploader = new ResumeFTPUpload('localhost', 'user', 'pass');

    $local_file = 'large_file.zip';
    $remote_file = '/uploads/large_file.zip';

    if ($uploader->smartUpload($local_file, $remote_file, FTP_BINARY)) {
        echo "文件上传成功\n";
    } else {
        echo "文件上传失败或部分完成\n";
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

注意事项

  • 确保本地文件存在且有读取权限
  • 确保FTP用户有目标目录的写入权限
  • 大文件上传可能耗时较长,脚本可能超时,需要设置适当的超时时间
  • 断点续传功能需要服务器支持REST命令
  • 二进制模式(FTP_BINARY)适用于所有文件类型,特别是图像、压缩包等
  • 文本模式(FTP_ASCII)会自动转换换行符,可能改变文件内容
  • 上传过程中如果网络中断,可能需要手动清理不完整的文件
  • 注意服务器存储空间限制

最佳实践建议

推荐做法
  • 使用ftp_pasv($ftp_conn, true)启用被动模式
  • 上传前检查本地文件大小和权限
  • 上传后验证文件大小以确保完整性
  • 使用二进制模式传输所有文件类型
  • 实现日志记录以便排查问题
  • 为大文件上传设置适当的超时时间
  • 实现断点续传功能以提高可靠性
避免做法
  • 不要在不检查返回值的情况下假设上传成功
  • 避免在上传过程中修改源文件
  • 不要使用文本模式上传二进制文件
  • 避免在不存在的目录中上传文件
  • 不要忽略错误处理和异常捕获
  • 避免在循环中重复打开和关闭FTP连接
  • 不要忘记设置适当的超时时间

常见问题解答

有以下几种解决方案:

  1. 增加超时时间:使用 ftp_set_option($ftp_conn, FTP_TIMEOUT_SEC, 300) 设置更长的超时时间(300秒)
  2. 使用非阻塞模式:对于超大文件,使用 ftp_nb_put() 配合 ftp_nb_continue()
  3. 增加PHP执行时间:在脚本开始时使用 set_time_limit(0) 取消时间限制
  4. 分块上传:将大文件分割成小块,分别上传后再合并
  5. 压缩文件:上传前压缩文件,减少传输量

可以通过以下方式验证上传是否成功:

  1. 检查返回值ftp_put() 返回 truefalse
  2. 验证文件大小:上传后使用 ftp_size() 获取远程文件大小,与本地文件大小比较
  3. 计算文件哈希:计算本地文件的MD5或SHA1哈希,下载远程文件后验证哈希值
  4. 尝试下载验证:上传后立即下载文件并比较内容
  5. 检查文件修改时间:使用 ftp_mdtm() 获取远程文件修改时间

模式 说明 适用场景
FTP_ASCII 文本模式,会自动转换换行符(CR/LF)。在Windows和Unix系统间传输文本文件时保持格式正确。 .txt, .html, .php, .css, .js 等文本文件
FTP_BINARY 二进制模式,原样传输文件,不做任何转换。 .jpg, .png, .zip, .exe, .pdf 等所有非文本文件

建议:如果不确定文件类型,或者需要确保文件内容完全不变,总是使用 FTP_BINARY 模式。

相关函数

  • ftp_nb_put() - 非阻塞方式上传文件
  • ftp_fput() - 通过文件指针上传文件
  • ftp_get() - 从FTP服务器下载文件
  • ftp_mkdir() - 在FTP服务器上创建目录
  • ftp_delete() - 删除FTP服务器上的文件
  • ftp_rename() - 重命名FTP服务器上的文件
  • ftp_size() - 获取远程文件大小