PHP ftp_fget() 函数

ftp_fget() 函数从FTP服务器下载一个文件并写入到已打开的文件指针中。

注意:ftp_get()函数不同,ftp_fget()需要预先打开一个本地文件句柄,然后将内容写入该句柄。这使得它更适合需要自定义文件处理的场景。

语法

ftp_fget(
    resource $ftp_stream,
    resource $handle,
    string $remote_file,
    int $mode = FTP_BINARY,
    int $resumepos = 0
): bool

参数说明

参数 描述
$ftp_stream 必需。FTP连接的资源标识符,由ftp_connect()ftp_ssl_connect()函数返回。
$handle 必需。已打开的本地文件指针,用于写入下载的内容。必须使用fopen()等函数打开,且以写入模式(如"w"、"wb")打开。
$remote_file 必需。要下载的远程文件路径。
$mode 可选。传输模式。可以是:
  • FTP_ASCII - ASCII文本模式
  • FTP_BINARY - 二进制模式(默认)
  • FTP_AUTORESUME - 自动恢复传输
$resumepos 可选。远程文件中开始下载的位置(字节偏移量)。默认为0。

返回值

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

示例

示例1:基本使用

从FTP服务器下载文件到本地:

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

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

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

// 开启被动模式
ftp_pasv($conn_id, true);

// 远程文件
$remote_file = "public_html/document.pdf";
$local_file = "downloaded_document.pdf";

// 打开本地文件句柄(写入模式,二进制)
$handle = fopen($local_file, "wb");
if ($handle === false) {
    ftp_close($conn_id);
    die("无法创建本地文件: $local_file");
}

// 从FTP服务器下载文件
if (ftp_fget($conn_id, $handle, $remote_file, FTP_BINARY)) {
    echo "文件下载成功: $local_file\n";

    // 获取文件大小
    fseek($handle, 0, SEEK_END);
    $file_size = ftell($handle);
    echo "文件大小: " . formatBytes($file_size) . "\n";
} else {
    echo "文件下载失败\n";
}

// 关闭文件句柄
fclose($handle);

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

// 辅助函数:格式化文件大小
function formatBytes($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];
}
?>

示例2:下载文本文件(ASCII模式)

<?php
/**
 * 下载文本文件并使用ASCII模式转换行结束符
 */
function downloadTextFile($ftp_server, $ftp_user, $ftp_pass, $remote_file, $local_file) {
    $conn_id = ftp_connect($ftp_server);
    if (!$conn_id) {
        throw new Exception("无法连接到FTP服务器");
    }

    if (!ftp_login($conn_id, $ftp_user, $ftp_pass)) {
        ftp_close($conn_id);
        throw new Exception("FTP登录失败");
    }

    ftp_pasv($conn_id, true);

    // 打开本地文件(文本模式)
    $handle = fopen($local_file, "w");
    if (!$handle) {
        ftp_close($conn_id);
        throw new Exception("无法创建本地文件");
    }

    try {
        // 使用ASCII模式下载(自动转换行结束符)
        if (!ftp_fget($conn_id, $handle, $remote_file, FTP_ASCII)) {
            throw new Exception("文件下载失败");
        }

        echo "文本文件下载成功\n";

        // 返回到文件开头并读取内容
        rewind($handle);
        $content = stream_get_contents($handle);
        $line_count = substr_count($content, "\n") + 1;

        echo "文件信息:\n";
        echo "- 文件路径: $local_file\n";
        echo "- 行数: $line_count\n";
        echo "- 字符数: " . strlen($content) . "\n";

    } finally {
        // 确保资源被释放
        fclose($handle);
        ftp_close($conn_id);
    }

    return true;
}

// 使用示例
try {
    downloadTextFile(
        "ftp.example.com",
        "username",
        "password",
        "public_html/logs/access.log",
        "local_access.log"
    );
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例3:断点续传(恢复下载)

<?php
/**
 * 支持断点续传的文件下载
 */
function resumeDownload($ftp_server, $ftp_user, $ftp_pass, $remote_file, $local_file) {
    $conn_id = ftp_connect($ftp_server);
    if (!$conn_id) {
        return ["success" => false, "message" => "无法连接到FTP服务器"];
    }

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

    ftp_pasv($conn_id, true);

    // 检查远程文件是否存在
    $remote_size = ftp_size($conn_id, $remote_file);
    if ($remote_size == -1) {
        ftp_close($conn_id);
        return ["success" => false, "message" => "远程文件不存在"];
    }

    // 检查本地文件是否存在及大小
    $local_size = 0;
    $mode = "wb"; // 默认写二进制模式

    if (file_exists($local_file)) {
        $local_size = filesize($local_file);

        if ($local_size < $remote_size) {
            // 部分下载,需要续传
            $mode = "ab"; // 追加模式
            echo "发现部分下载的文件,从 $local_size 字节处继续下载\n";
        } elseif ($local_size == $remote_size) {
            ftp_close($conn_id);
            return ["success" => true, "message" => "文件已完整下载"];
        } else {
            // 本地文件比远程大,重新下载
            $mode = "wb";
            $local_size = 0;
            echo "本地文件异常,重新下载\n";
        }
    }

    // 打开文件句柄
    $handle = fopen($local_file, $mode);
    if (!$handle) {
        ftp_close($conn_id);
        return ["success" => false, "message" => "无法打开本地文件"];
    }

    // 如果是从中间开始下载,移动文件指针到末尾
    if ($mode == "ab") {
        fseek($handle, 0, SEEK_END);
    }

    // 执行下载(指定从哪个位置开始)
    if (ftp_fget($conn_id, $handle, $remote_file, FTP_BINARY, $local_size)) {
        fclose($handle);
        ftp_close($conn_id);

        $final_size = filesize($local_file);
        $downloaded = $final_size - $local_size;

        return [
            "success" => true,
            "message" => "下载完成",
            "total_size" => $remote_size,
            "downloaded" => $downloaded,
            "local_size" => $final_size
        ];
    } else {
        fclose($handle);
        ftp_close($conn_id);
        return ["success" => false, "message" => "下载失败"];
    }
}

// 使用示例
$result = resumeDownload(
    "ftp.example.com",
    "username",
    "password",
    "public_html/large_file.zip",
    "local_large_file.zip"
);

if ($result['success']) {
    echo "下载成功!\n";
    echo "远程文件大小: " . formatBytes($result['total_size']) . "\n";
    echo "本次下载: " . formatBytes($result['downloaded']) . "\n";
    echo "本地文件大小: " . formatBytes($result['local_size']) . "\n";
} else {
    echo "下载失败: " . $result['message'] . "\n";
}
?>

示例4:下载并实时处理文件内容

<?php
/**
 * 下载文件并实时处理内容(逐行读取大文件)
 */
class StreamingFTPDownloader {
    private $conn;
    private $temp_file;
    private $temp_handle;

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

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

        ftp_pasv($this->conn, true);

        // 创建临时文件
        $this->temp_file = tempnam(sys_get_temp_dir(), 'ftp_');
        $this->temp_handle = fopen($this->temp_file, "w+b");

        if (!$this->temp_handle) {
            throw new Exception("无法创建临时文件");
        }
    }

    /**
     * 下载文件并逐行处理
     */
    public function downloadAndProcess($remote_file, callable $processor) {
        // 下载文件到临时句柄
        if (!ftp_fget($this->conn, $this->temp_handle, $remote_file, FTP_BINARY)) {
            throw new Exception("文件下载失败");
        }

        // 回到文件开头
        rewind($this->temp_handle);

        $line_number = 0;
        $processed_lines = 0;

        // 逐行读取并处理
        while (!feof($this->temp_handle)) {
            $line = fgets($this->temp_handle);
            $line_number++;

            if ($line !== false) {
                $result = $processor($line, $line_number);
                if ($result !== false) {
                    $processed_lines++;
                }
            }
        }

        return [
            'total_lines' => $line_number,
            'processed_lines' => $processed_lines
        ];
    }

    /**
     * 下载文件并搜索内容
     */
    public function downloadAndSearch($remote_file, $search_pattern) {
        $results = [];

        $processor = function($line, $line_number) use ($search_pattern, &$results) {
            if (preg_match($search_pattern, $line)) {
                $results[] = [
                    'line' => $line_number,
                    'content' => trim($line)
                ];
                return true;
            }
            return false;
        };

        $this->downloadAndProcess($remote_file, $processor);

        return $results;
    }

    /**
     * 下载并过滤CSV文件
     */
    public function downloadAndFilterCSV($remote_file, $filter_callback) {
        $filtered_data = [];

        $processor = function($line, $line_number) use ($filter_callback, &$filtered_data) {
            $data = str_getcsv($line);

            if ($line_number === 1) {
                // 第一行可能是标题
                $filtered_data[] = $data;
                return true;
            }

            if ($filter_callback($data)) {
                $filtered_data[] = $data;
                return true;
            }

            return false;
        };

        $this->downloadAndProcess($remote_file, $processor);

        return $filtered_data;
    }

    public function __destruct() {
        if ($this->temp_handle) {
            fclose($this->temp_handle);
        }

        if ($this->temp_file && file_exists($this->temp_file)) {
            unlink($this->temp_file);
        }

        if ($this->conn && is_resource($this->conn)) {
            ftp_close($this->conn);
        }
    }
}

// 使用示例
try {
    $downloader = new StreamingFTPDownloader("ftp.example.com", "username", "password");

    // 示例1:搜索包含"error"的日志行
    echo "搜索错误日志...\n";
    $errors = $downloader->downloadAndSearch(
        "logs/application.log",
        '/error/i'
    );

    echo "找到 " . count($errors) . " 个错误:\n";
    foreach ($errors as $error) {
        echo "[行 {$error['line']}] {$error['content']}\n";
    }

    // 示例2:过滤CSV文件
    echo "\n过滤CSV文件...\n";
    $filtered = $downloader->downloadAndFilterCSV(
        "data/sales.csv",
        function($row) {
            // 只保留金额大于1000的行
            return isset($row[3]) && floatval($row[3]) > 1000;
        }
    );

    echo "过滤后保留 " . count($filtered) . " 行数据\n";

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例5:Web界面文件下载

<?php
// Web界面文件下载示例
session_start();

// FTP配置
$ftp_config = [
    'server' => 'ftp.example.com',
    'username' => 'webuser',
    'password' => 'password',
    'root_dir' => '/public_html/files/'
];

// 处理下载请求
if (isset($_GET['download']) && !empty($_GET['file'])) {
    $remote_file = basename($_GET['file']);
    $full_remote_path = $ftp_config['root_dir'] . $remote_file;
    $local_filename = $remote_file;

    $conn = ftp_connect($ftp_config['server']);
    if ($conn && ftp_login($conn, $ftp_config['username'], $ftp_config['password'])) {
        ftp_pasv($conn, true);

        // 检查文件是否存在
        $file_size = ftp_size($conn, $full_remote_path);
        if ($file_size != -1) {
            // 设置HTTP头
            header('Content-Description: File Transfer');
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="' . $local_filename . '"');
            header('Content-Transfer-Encoding: binary');
            header('Expires: 0');
            header('Cache-Control: must-revalidate');
            header('Pragma: public');
            header('Content-Length: ' . $file_size);

            // 创建临时文件句柄
            $temp_file = tmpfile();
            if ($temp_file && ftp_fget($conn, $temp_file, $full_remote_path, FTP_BINARY)) {
                // 输出文件内容
                rewind($temp_file);
                fpassthru($temp_file);
                fclose($temp_file);
                exit;
            }
        }

        ftp_close($conn);
    }

    // 如果下载失败,返回错误
    header('HTTP/1.1 404 Not Found');
    echo "文件下载失败";
    exit;
}

// 获取文件列表
$file_list = [];
$conn = ftp_connect($ftp_config['server']);
if ($conn && ftp_login($conn, $ftp_config['username'], $ftp_config['password'])) {
    ftp_pasv($conn, true);

    $files = ftp_nlist($conn, $ftp_config['root_dir']);
    if ($files !== false) {
        foreach ($files as $file) {
            $filename = basename($file);
            if ($filename != '.' && $filename != '..') {
                $file_size = ftp_size($conn, $ftp_config['root_dir'] . $filename);
                if ($file_size != -1) {
                    $file_list[] = [
                        'name' => $filename,
                        'size' => $file_size,
                        'formatted_size' => formatFileSize($file_size),
                        'modified' => ftp_mdtm($conn, $ftp_config['root_dir'] . $filename)
                    ];
                }
            }
        }
    }
    ftp_close($conn);
}

// 辅助函数
function formatFileSize($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';
    } else {
        return $bytes . ' 字节';
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FTP文件下载中心</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <style>
        .file-item:hover {
            background-color: #f8f9fa;
        }
        .file-icon {
            color: #6c757d;
            font-size: 1.2em;
        }
        .download-progress {
            display: none;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h1 class="mb-4"><i class="fas fa-cloud-download-alt me-2"></i>FTP文件下载中心</h1>

        <div class="card">
            <div class="card-header">
                <h5 class="mb-0">可用文件</h5>
            </div>
            <div class="card-body">
                <?php if (empty($file_list)): ?>
                    <p class="text-muted">没有可用的文件</p>
                <?php else: ?>
                    <div class="table-responsive">
                        <table class="table table-hover">
                            <thead>
                                <tr>
                                    <th>文件名</th>
                                    <th>大小</th>
                                    <th>修改时间</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ($file_list as $file): ?>
                                    <tr class="file-item">
                                        <td>
                                            <i class="fas fa-file file-icon me-2"></i>
                                            <?php echo htmlspecialchars($file['name']); ?>
                                        </td>
                                        <td><?php echo $file['formatted_size']; ?></td>
                                        <td>
                                            <?php echo $file['modified'] ? date('Y-m-d H:i:s', $file['modified']) : '未知'; ?>
                                        </td>
                                        <td>
                                            <a href="?download=1&file=<?php echo urlencode($file['name']); ?>"
                                               class="btn btn-sm btn-primary download-btn"
                                               data-filename="<?php echo htmlspecialchars($file['name']); ?>">
                                                <i class="fas fa-download me-1"></i>下载
                                            </a>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php endif; ?>
            </div>
        </div>

        <!-- 下载进度提示 -->
        <div id="downloadProgress" class="download-progress">
            <div class="alert alert-info">
                <h5><i class="fas fa-spinner fa-spin me-2"></i>正在下载...</h5>
                <p id="downloadingFile"></p>
                <div class="progress mt-2">
                    <div class="progress-bar progress-bar-striped progress-bar-animated"
                         style="width: 100%"></div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 下载按钮点击事件
            document.querySelectorAll('.download-btn').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    var filename = this.dataset.filename;
                    document.getElementById('downloadingFile').textContent =
                        '正在下载: ' + filename;
                    document.getElementById('downloadProgress').style.display = 'block';

                    // 滚动到进度提示
                    document.getElementById('downloadProgress').scrollIntoView({
                        behavior: 'smooth'
                    });
                });
            });

            // 如果URL中有下载参数,显示进度
            if (window.location.search.includes('download=1')) {
                document.getElementById('downloadProgress').style.display = 'block';
            }
        });
    </script>
</body>
</html>

传输模式说明

FTP传输模式:

模式常量 描述 适用文件类型
FTP_ASCII 1 ASCII文本模式,自动转换行结束符 .txt, .html, .php, .css, .js等文本文件
FTP_BINARY 2 二进制模式,原样传输字节 .jpg, .png, .zip, .exe, .pdf等二进制文件
FTP_AUTORESUME -1 自动恢复模式,尝试从断点续传 大文件下载,不稳定网络环境

与ftp_get()的区别

对比项 ftp_fget() ftp_get()
文件处理 写入到已打开的文件指针 直接写入到指定路径的文件
灵活性 高(可以控制文件打开方式) 低(自动创建文件)
内存使用 可以流式处理大文件 需要足够磁盘空间
断点续传 支持(通过$resumepos参数) 不支持
适用场景 需要自定义处理、大文件下载、实时处理 简单文件下载、小文件传输

常见问题

  1. 文件句柄问题:文件句柄未以正确的模式打开(需写入模式)
  2. 权限问题:FTP用户没有读取远程文件的权限
  3. 路径错误:远程文件路径不正确或文件不存在
  4. 网络问题:连接中断或超时
  5. 磁盘空间:本地磁盘空间不足
  6. 防火墙:FTP被动模式被防火墙阻止

<?php
// 根据文件扩展名选择传输模式
function getTransferMode($filename) {
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    $text_extensions = ['txt', 'html', 'htm', 'php', 'css', 'js', 'json', 'xml', 'csv'];
    $binary_extensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'zip', 'rar', 'exe', 'doc', 'xls'];

    if (in_array($extension, $text_extensions)) {
        return FTP_ASCII;
    } elseif (in_array($extension, $binary_extensions)) {
        return FTP_BINARY;
    } else {
        // 默认使用二进制模式
        return FTP_BINARY;
    }
}

// 使用示例
$filename = "document.pdf";
$mode = getTransferMode($filename);
echo "文件 {$filename} 使用模式: " . ($mode == FTP_ASCII ? "ASCII" : "二进制") . "\n";
?>

  • 设置超时时间:使用ftp_set_option($conn, FTP_TIMEOUT_SEC, 300)增加超时
  • 使用被动模式:ftp_pasv($conn, true)避免防火墙问题
  • 实现断点续传:使用$resumepos参数
  • 分块下载:将大文件分成多个部分下载
  • 错误重试:实现自动重试机制
  • 进度监控:显示下载进度,提高用户体验

相关函数

函数 描述
ftp_get() 从FTP服务器下载文件到本地路径
ftp_put() 上传文件到FTP服务器
ftp_nb_fget() 非阻塞方式从FTP服务器下载文件到文件指针
fopen() 打开文件或URL
fclose() 关闭已打开的文件指针
最佳实践:
  • 使用ftp_fget()处理大文件,避免内存溢出
  • 二进制文件使用FTP_BINARY模式,文本文件使用FTP_ASCII模式
  • 下载完成后检查文件完整性(如比较文件大小)
  • 使用临时文件处理敏感数据,处理完成后删除
  • 实现错误处理和日志记录机制
  • 对于公开下载,考虑添加下载次数统计