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 |
可选。传输模式,可以是:
|
startpos |
可选。从本地文件的指定位置开始上传(断点续传)。注意:需要服务器支持 |
truefalse| 特性 | ftp_put() | ftp_nb_put() |
|---|---|---|
| 阻塞性 | 阻塞模式(同步) | 非阻塞模式(异步) |
| 执行方式 | 脚本等待传输完成 | 传输过程中可执行其他代码 |
| 适用场景 | 小文件或简单传输 | 大文件传输或需要并发的场景 |
| 进度跟踪 | 不支持 | 支持(通过配合ftp_nb_continue()) |
| 返回值 | 布尔值(成功/失败) | 状态码(FTP_FAILED/FTP_FINISHED/FTP_MOREDATA) |
| 代码复杂度 | 简单,直接调用 | 复杂,需要循环处理 |
<?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);
?>
<?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";
}
?>
<?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";
}
?>
REST命令FTP_BINARY)适用于所有文件类型,特别是图像、压缩包等FTP_ASCII)会自动转换换行符,可能改变文件内容ftp_pasv($ftp_conn, true)启用被动模式有以下几种解决方案:
ftp_set_option($ftp_conn, FTP_TIMEOUT_SEC, 300) 设置更长的超时时间(300秒)ftp_nb_put() 配合 ftp_nb_continue()set_time_limit(0) 取消时间限制可以通过以下方式验证上传是否成功:
ftp_put() 返回 true 或 falseftp_size() 获取远程文件大小,与本地文件大小比较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() - 获取远程文件大小