PHP ftp_alloc() 函数

注意:ftp_alloc() 函数需要启用FTP扩展。在使用前请确保已安装并启用FTP扩展。

定义和用法

ftp_alloc() 函数用于在FTP服务器上为要上传的文件预分配空间。

这个函数向FTP服务器发送一个 ALLO 命令,请求为即将上传的文件分配指定大小的空间。这对于确保服务器有足够空间来接收大文件特别有用。

  • 预分配优势:可以避免在上传过程中因空间不足而失败
  • 服务器支持:不是所有FTP服务器都支持此功能
  • 常用场景:上传大文件前预先检查并分配空间

语法

ftp_alloc(resource $ftp_stream, int $filesize, string &$result = null): bool

参数值

参数 描述
$ftp_stream FTP连接资源。由 ftp_connect()ftp_ssl_connect() 函数返回的连接标识符。
$filesize 文件大小(以字节为单位)。要预分配的空间大小。
&$result 服务器响应(可选)。如果提供这个变量,服务器返回的文本响应将被存储在其中。

返回值

  • 成功时返回 true
  • 失败时返回 false
  • 如果服务器不支持 ALLO 命令,函数可能会失败

示例

示例 1:基本用法 - 为文件预分配空间

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

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

// 登录到FTP服务器
if (@ftp_login($conn_id, $ftp_user, $ftp_pass)) {
    echo "已连接到 $ftp_server,用户为 $ftp_user<br>";
} else {
    echo "无法以 $ftp_user 身份登录<br>";
    ftp_close($conn_id);
    exit;
}

// 启用被动模式(可选,根据服务器配置)
ftp_pasv($conn_id, true);

// 要上传的文件大小(例如:10MB)
$file_size = 10 * 1024 * 1024; // 10MB

// 尝试预分配空间
if (ftp_alloc($conn_id, $file_size, $result)) {
    echo "成功预分配了 $file_size 字节的空间<br>";
    if ($result) {
        echo "服务器响应: " . htmlspecialchars($result) . "<br>";
    }

    // 现在可以上传文件了
    $local_file = "large_file.zip";
    $remote_file = "uploads/large_file.zip";

    if (ftp_put($conn_id, $remote_file, $local_file, FTP_BINARY)) {
        echo "文件上传成功: $local_file -> $remote_file<br>";
    } else {
        echo "文件上传失败<br>";
    }
} else {
    echo "预分配空间失败<br>";
    if (isset($result)) {
        echo "服务器响应: " . htmlspecialchars($result) . "<br>";
    }
    echo "FTP服务器可能不支持 ALLO 命令,尝试直接上传...<br>";

    // 如果不支持预分配,尝试直接上传
    $local_file = "large_file.zip";
    $remote_file = "uploads/large_file.zip";

    if (ftp_put($conn_id, $remote_file, $local_file, FTP_BINARY)) {
        echo "文件上传成功(未预分配)<br>";
    } else {
        echo "文件上传失败<br>";
    }
}

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

示例 2:封装为可重用的函数

<?php
/**
 * 安全上传大文件到FTP服务器
 * @param string $server FTP服务器地址
 * @param string $username 用户名
 * @param string $password 密码
 * @param string $local_file 本地文件路径
 * @param string $remote_file 远程文件路径
 * @param bool $use_allocation 是否使用预分配
 * @return array 上传结果
 */
function ftp_upload_large_file($server, $username, $password, $local_file, $remote_file, $use_allocation = true) {
    $result = [
        'success' => false,
        'message' => '',
        'server_response' => ''
    ];

    // 验证本地文件是否存在
    if (!file_exists($local_file)) {
        $result['message'] = "本地文件不存在: $local_file";
        return $result;
    }

    // 获取文件大小
    $file_size = filesize($local_file);
    if ($file_size === false) {
        $result['message'] = "无法获取文件大小";
        return $result;
    }

    // 连接到FTP服务器
    $conn_id = @ftp_connect($server);
    if (!$conn_id) {
        $result['message'] = "无法连接到FTP服务器: $server";
        return $result;
    }

    // 登录
    if (!@ftp_login($conn_id, $username, $password)) {
        $result['message'] = "FTP登录失败";
        ftp_close($conn_id);
        return $result;
    }

    // 启用被动模式(提高通过防火墙的连接成功率)
    ftp_pasv($conn_id, true);

    // 如果启用预分配且文件较大(大于1MB)
    if ($use_allocation && $file_size > 1024 * 1024) {
        $allocation_result = '';
        if (ftp_alloc($conn_id, $file_size, $allocation_result)) {
            $result['server_response'] = $allocation_result;
            $result['message'] .= "已预分配 " . format_bytes($file_size) . " 空间<br>";
        } else {
            // 预分配失败,记录响应但不中断上传
            $result['server_response'] = $allocation_result;
            $result['message'] .= "预分配失败,服务器可能不支持此功能<br>";
        }
    }

    // 上传文件
    if (@ftp_put($conn_id, $remote_file, $local_file, FTP_BINARY)) {
        $result['success'] = true;
        $result['message'] .= "文件上传成功: " . basename($local_file) . " → $remote_file";
    } else {
        $result['message'] .= "文件上传失败";
    }

    // 关闭连接
    ftp_close($conn_id);

    return $result;
}

/**
 * 格式化字节数为易读格式
 */
function format_bytes($bytes, $precision = 2) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    $bytes /= pow(1024, $pow);

    return round($bytes, $precision) . ' ' . $units[$pow];
}

// 使用示例
echo "<h4>大文件上传示例:</h4>";

// 模拟上传
$upload_result = ftp_upload_large_file(
    "ftp.example.com",
    "username",
    "password",
    "/path/to/large_file.zip",
    "/uploads/large_file.zip",
    true  // 启用预分配
);

// 显示结果
if ($upload_result['success']) {
    echo "<div class='alert alert-success'>";
    echo "<strong>上传成功!</strong><br>";
    echo $upload_result['message'];
} else {
    echo "<div class='alert alert-danger'>";
    echo "<strong>上传失败!</strong><br>";
    echo $upload_result['message'];
}

if (!empty($upload_result['server_response'])) {
    echo "<br><strong>服务器响应:</strong> " .
         htmlspecialchars($upload_result['server_response']);
}

echo "</div>";
?>

示例 3:检查服务器是否支持 ALLO 命令

<?php
/**
 * 检查FTP服务器是否支持 ALLO 命令
 * @param resource $ftp_conn FTP连接资源
 * @return bool 是否支持预分配
 */
function ftp_supports_allocation($ftp_conn) {
    // 尝试发送一个小的ALLOC请求(1字节)
    $test_size = 1;
    $server_response = '';

    // 保存原始错误报告级别
    $old_error_level = error_reporting(0);

    $result = @ftp_alloc($ftp_conn, $test_size, $server_response);

    // 恢复错误报告级别
    error_reporting($old_error_level);

    // 如果返回true,说明服务器支持
    if ($result === true) {
        return true;
    }

    // 检查服务器响应中是否包含特定消息
    $negative_responses = [
        '502', // Command not implemented
        '504', // Command not implemented for that parameter
        '550', // Requested action not taken
    ];

    foreach ($negative_responses as $response) {
        if (strpos($server_response, $response) !== false) {
            return false;
        }
    }

    // 如果服务器返回其他响应,可能支持但空间不足等
    // 这种情况下我们认为服务器支持 ALLO 命令
    return !empty($server_response);
}

// 使用示例
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

// 连接到FTP服务器
$conn_id = ftp_connect($ftp_server);
if (!$conn_id) {
    die("无法连接到 $ftp_server");
}

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

echo "<h4>检查FTP服务器功能:</h4>";

// 检查是否支持 ALLO 命令
if (ftp_supports_allocation($conn_id)) {
    echo "<span class='text-success'>✓ FTP服务器支持预分配空间(ALLO命令)</span><br>";
} else {
    echo "<span class='text-warning'>✗ FTP服务器不支持预分配空间(ALLO命令)</span><br>";
}

// 检查其他常用命令支持
echo "<br><strong>其他FTP功能检查:</strong><br>";

// 检查是否支持被动模式
ftp_pasv($conn_id, true);
echo "✓ 支持被动模式<br>";

// 尝试获取系统类型(SYST命令)
$syst = ftp_systype($conn_id);
if ($syst !== false) {
    echo "✓ 系统类型: " . htmlspecialchars($syst) . "<br>";
} else {
    echo "✗ 无法获取系统类型<br>";
}

// 尝试获取当前目录(PWD命令)
$pwd = ftp_pwd($conn_id);
if ($pwd !== false) {
    echo "✓ 当前目录: " . htmlspecialchars($pwd) . "<br>";
} else {
    echo "✗ 无法获取当前目录<br>";
}

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

示例 4:处理上传过程中的空间不足

<?php
/**
 * 安全上传文件,处理空间不足的情况
 */
function ftp_safe_upload($ftp_server, $ftp_user, $ftp_pass, $local_file, $remote_path) {
    // 连接和登录
    $conn_id = ftp_connect($ftp_server);
    if (!$conn_id) {
        return ['success' => false, 'error' => '连接失败'];
    }

    if (!ftp_login($conn_id, $ftp_user, $ftp_pass)) {
        ftp_close($conn_id);
        return ['success' => false, 'error' => '登录失败'];
    }

    ftp_pasv($conn_id, true);

    $file_size = filesize($local_file);
    $file_name = basename($local_file);
    $remote_file = rtrim($remote_path, '/') . '/' . $file_name;

    // 第一步:尝试预分配空间
    $allocation_result = '';
    if (ftp_alloc($conn_id, $file_size, $allocation_result)) {
        // 预分配成功
        echo "<div class='alert alert-success'>";
        echo "已成功预分配 " . format_filesize($file_size) . " 空间<br>";
        echo "服务器响应: " . htmlspecialchars($allocation_result) . "<br>";
        echo "</div>";
    } else {
        // 预分配失败,分析原因
        echo "<div class='alert alert-warning'>";
        echo "预分配请求失败<br>";

        if (strpos($allocation_result, '550') !== false) {
            // 550错误通常表示空间不足或权限问题
            echo "<strong>可能原因:</strong><br>";
            echo "1. FTP服务器空间不足<br>";
            echo "2. 没有写入权限<br>";
            echo "3. 文件已存在且不可覆盖<br><br>";

            // 尝试检查剩余空间
            $free_space = ftp_check_space($conn_id, $remote_path);
            if ($free_space !== false) {
                echo "可用空间: " . format_filesize($free_space) . "<br>";
                echo "所需空间: " . format_filesize($file_size) . "<br>";

                if ($free_space < $file_size) {
                    ftp_close($conn_id);
                    return [
                        'success' => false,
                        'error' => '服务器空间不足。需要: ' .
                                  format_filesize($file_size) .
                                  ',可用: ' . format_filesize($free_space)
                    ];
                }
            }
        }
        echo "服务器响应: " . htmlspecialchars($allocation_result) . "<br>";
        echo "</div>";
    }

    // 第二步:执行上传
    echo "<div class='alert alert-info'>开始上传文件...</div>";

    $upload_start = microtime(true);
    if (ftp_put($conn_id, $remote_file, $local_file, FTP_BINARY)) {
        $upload_time = microtime(true) - $upload_start;

        echo "<div class='alert alert-success'>";
        echo "<strong>文件上传成功!</strong><br>";
        echo "文件名: $file_name<br>";
        echo "文件大小: " . format_filesize($file_size) . "<br>";
        echo "上传时间: " . round($upload_time, 2) . " 秒<br>";
        echo "传输速度: " . format_filesize($file_size / $upload_time) . "/秒<br>";
        echo "</div>";

        ftp_close($conn_id);
        return ['success' => true, 'file' => $remote_file, 'size' => $file_size];
    } else {
        echo "<div class='alert alert-danger'>";
        echo "<strong>文件上传失败!</strong><br>";
        echo "可能的原因:<br>";
        echo "1. 连接中断<br>";
        echo "2. 权限不足<br>";
        echo "3. 服务器空间不足<br>";
        echo "</div>";

        ftp_close($conn_id);
        return ['success' => false, 'error' => '上传失败'];
    }
}

/**
 * 估算FTP服务器的可用空间(使用SIZE命令检查现有文件)
 * 注意:这不是标准FTP功能,某些服务器可能不支持
 */
function ftp_check_space($ftp_conn, $path) {
    // 尝试获取目录列表来估算空间
    // 这是一个简化的示例,实际实现可能更复杂
    $list = ftp_nlist($ftp_conn, $path);

    if ($list === false) {
        return false;
    }

    // 这里只是示例,实际中需要更复杂的逻辑
    // 来获取真实的磁盘空间信息
    return 100 * 1024 * 1024; // 假设有100MB空间
}

/**
 * 格式化文件大小
 */
function format_filesize($bytes) {
    if ($bytes >= 1073741824) {
        return number_format($bytes / 1073741824, 2) . ' GB';
    } elseif ($bytes >= 1048576) {
        return number_format($bytes / 1048576, 2) . ' MB';
    } elseif ($bytes >= 1024) {
        return number_format($bytes / 1024, 2) . ' KB';
    } elseif ($bytes > 1) {
        return $bytes . ' bytes';
    } elseif ($bytes == 1) {
        return $bytes . ' byte';
    } else {
        return '0 bytes';
    }
}

// 模拟上传(在实际应用中需要真实FTP服务器)
echo "<h4>安全文件上传流程:</h4>";
echo "<p>此示例演示了使用ftp_alloc()进行安全文件上传的完整流程。</p>";

// 注意:在实际运行中需要提供真实的FTP服务器信息
// $result = ftp_safe_upload("server", "user", "pass", "file.txt", "/uploads");
?>

示例 5:批量上传大文件

<?php
/**
 * 批量上传大文件到FTP服务器
 */
class FTPBatchUploader {
    private $conn_id;
    private $server;
    private $username;
    private $password;
    private $use_allocation;
    private $log = [];

    public function __construct($server, $username, $password, $use_allocation = true) {
        $this->server = $server;
        $this->username = $username;
        $this->password = $password;
        $this->use_allocation = $use_allocation;
    }

    public function connect() {
        $this->conn_id = @ftp_connect($this->server);
        if (!$this->conn_id) {
            $this->log("无法连接到FTP服务器: {$this->server}");
            return false;
        }

        if (!@ftp_login($this->conn_id, $this->username, $this->password)) {
            $this->log("FTP登录失败");
            ftp_close($this->conn_id);
            return false;
        }

        ftp_pasv($this->conn_id, true);
        $this->log("已连接到FTP服务器: {$this->server}");
        return true;
    }

    public function uploadFiles($files, $remote_base_path) {
        if (!$this->conn_id) {
            $this->log("未连接到FTP服务器");
            return false;
        }

        $results = [];
        $total_size = 0;
        $success_count = 0;
        $fail_count = 0;

        foreach ($files as $local_file => $remote_path) {
            $this->log("开始处理: $local_file");

            if (!file_exists($local_file)) {
                $this->log("文件不存在: $local_file", 'error');
                $results[$local_file] = ['success' => false, 'error' => '文件不存在'];
                $fail_count++;
                continue;
            }

            $file_size = filesize($local_file);
            $remote_file = rtrim($remote_base_path, '/') . '/' . ltrim($remote_path, '/');

            // 预分配空间(如果启用)
            if ($this->use_allocation && $file_size > 1024 * 1024) {
                $alloc_result = '';
                if (ftp_alloc($this->conn_id, $file_size, $alloc_result)) {
                    $this->log("已预分配 " . $this->formatSize($file_size) . " 空间");
                } else {
                    $this->log("预分配失败: " . htmlspecialchars($alloc_result), 'warning');
                }
            }

            // 上传文件
            $start_time = microtime(true);
            if (ftp_put($this->conn_id, $remote_file, $local_file, FTP_BINARY)) {
                $upload_time = microtime(true) - $start_time;
                $speed = $file_size / $upload_time;

                $this->log("上传成功: " . basename($local_file) .
                          " (" . $this->formatSize($file_size) .
                          ", " . round($upload_time, 2) . "秒, " .
                          $this->formatSize($speed) . "/秒)", 'success');

                $results[$local_file] = [
                    'success' => true,
                    'remote_path' => $remote_file,
                    'size' => $file_size,
                    'time' => $upload_time,
                    'speed' => $speed
                ];

                $total_size += $file_size;
                $success_count++;
            } else {
                $this->log("上传失败: " . basename($local_file), 'error');
                $results[$local_file] = ['success' => false, 'error' => '上传失败'];
                $fail_count++;
            }
        }

        $this->log("批量上传完成: $success_count 个成功, $fail_count 个失败");
        $this->log("总上传大小: " . $this->formatSize($total_size));

        return [
            'total' => count($files),
            'success' => $success_count,
            'failed' => $fail_count,
            'total_size' => $total_size,
            'results' => $results
        ];
    }

    public function disconnect() {
        if ($this->conn_id) {
            ftp_close($this->conn_id);
            $this->log("已断开FTP连接");
            $this->conn_id = null;
        }
    }

    private function log($message, $type = 'info') {
        $timestamp = date('Y-m-d H:i:s');
        $log_entry = "[$timestamp] [$type] $message";
        $this->log[] = $log_entry;

        // 同时输出到页面(在实际应用中可能只记录到日志文件)
        $css_class = '';
        switch ($type) {
            case 'error': $css_class = 'text-danger'; break;
            case 'warning': $css_class = 'text-warning'; break;
            case 'success': $css_class = 'text-success'; break;
            default: $css_class = 'text-info'; break;
        }

        echo "<div class='$css_class'>$log_entry</div>";
    }

    private function formatSize($bytes) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        if ($bytes == 0) return '0 B';
        $i = floor(log($bytes, 1024));
        return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
    }

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

// 使用示例
echo "<h4>批量大文件上传示例:</h4>";

// 模拟文件列表
$files_to_upload = [
    '/path/to/large_file1.zip' => 'backups/file1.zip',
    '/path/to/large_file2.iso' => 'backups/file2.iso',
    '/path/to/document.pdf' => 'docs/document.pdf',
    '/path/to/video.mp4' => 'media/video.mp4'
];

// 创建上传器实例
$uploader = new FTPBatchUploader(
    "ftp.example.com",
    "username",
    "password",
    true  // 启用预分配
);

// 连接服务器
if ($uploader->connect()) {
    // 执行批量上传
    $results = $uploader->uploadFiles($files_to_upload, '/uploads');

    // 显示统计信息
    echo "<div class='alert alert-info mt-3'>";
    echo "<strong>批量上传统计:</strong><br>";
    echo "总文件数: {$results['total']}<br>";
    echo "成功: {$results['success']}<br>";
    echo "失败: {$results['failed']}<br>";
    echo "总大小: " . $uploader->formatSize($results['total_size']) . "<br>";
    echo "</div>";

    // 断开连接
    $uploader->disconnect();
} else {
    echo "<div class='alert alert-danger'>无法连接到FTP服务器</div>";
}
?>

注意事项

  • 服务器支持:不是所有FTP服务器都支持 ALLO 命令,使用前应检查服务器兼容性
  • FTP扩展:需要启用PHP的FTP扩展(--enable-ftp
  • 错误处理:函数可能因为多种原因失败,应实现适当的错误处理机制
  • 连接状态:必须在有效的FTP连接上调用此函数
  • 被动模式:在某些网络环境下,可能需要启用被动模式
  • 性能影响:预分配操作会增加一次服务器往返,可能略微影响性能
  • 替代方案:如果服务器不支持,应考虑其他方法确保空间足够
  • 安全性:FTP协议本身不安全,考虑使用FTPS(FTP over SSL)

相关FTP函数

函数 描述
ftp_connect() 建立FTP连接
ftp_ssl_connect() 建立安全的FTP over SSL连接
ftp_login() 登录FTP服务器
ftp_put() 上传文件到FTP服务器
ftp_get() 从FTP服务器下载文件
ftp_nb_put() 非阻塞方式上传文件
ftp_size() 获取远程文件大小
ftp_pasv() 启用或禁用被动模式
ftp_close() 关闭FTP连接

最佳实践

  1. 检查服务器支持:使用前检查FTP服务器是否支持 ALLO 命令
  2. 错误处理:总是检查 ftp_alloc() 的返回值并处理失败情况
  3. 使用被动模式:在防火墙或NAT环境下使用被动模式提高连接成功率
  4. 超时设置:为大型文件上传设置合理的超时时间
  5. 日志记录:记录上传过程和服务器响应,便于调试
  6. 使用FTPS:生产环境中使用FTP over SSL确保数据安全
  7. 分块上传:对于极大文件,考虑分块上传以提供更好的用户体验
  8. 验证文件完整性:上传后验证文件大小或计算校验和

浏览器支持

该函数是 PHP 后端函数,与浏览器无关。需要 PHP 4.0 或更高版本,且启用 FTP 扩展。