首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP FTP 上傳檔案完整指南:ftp_put、ftp_fput、斷點續傳、二進制模式與錯誤處理

     編輯:PHP 相關知識

核心上傳函數對比

函數參數適用場景
ftp_put(conn, remote, local, mode)本地檔案路徑小檔案、簡單上傳
ftp_fput(conn, remote, handle, mode, startpos=0)已開啟檔案指標大檔案、斷點續傳、記憶體控制
ftp_nb_fput(...)非阻塞版本進度條、長連線、並發控制

傳輸模式:必須正確選擇

  • FTP_BINARY (預設):**所有非純文本檔案** (圖片、壓縮包、PDF、執行檔、資料庫備份)
  • FTP_ASCII:**純文本檔案** (.txt, .csv, .sql, .php, .html, .css, .js) — 會自動轉換換行符 (CRLF ↔ LF)

錯誤後果:二進制檔用 ASCII 模式會損壞;文本檔用 Binary 模式換行符可能不符預期。

基礎上傳:ftp_put

<?php
function ftpUploadSimple(
  string $host, string $user, string $pass,
  string $localFile, string $remoteFile,
  bool $ssl = false, int $mode = FTP_BINARY
): bool {
  $conn = $ssl ? @ftp_ssl_connect($host) : @ftp_connect($host);
  if (!$conn) throw new Exception("連線失敗: $host");
  
  if (!@ftp_login($conn, $user, $pass)) {
    ftp_close($conn);
    throw new Exception("登入失敗");
  }
  
  ftp_pasv($conn, true);
  
  // 可選:確保遠端目錄存在
  $remoteDir = dirname($remoteFile);
  if ($remoteDir !== '.') {
    ftp_mkdir_recursive($conn, $remoteDir); // 需自行實現遞迴建立
  }
  
  $result = ftp_put($conn, $remoteFile, $localFile, $mode);
  ftp_close($conn);
  
  if (!$result) throw new Exception("上傳失敗: $localFile -> $remoteFile");
  return true;
}

// 輔助:遞迴建立遠端目錄
function ftp_mkdir_recursive($conn, string $dir): bool {
  $parts = explode('/', trim($dir, '/'));
  $path = '';
  foreach ($parts as $part) {
    $path .= '/' . $part;
    if (!@ftp_chdir($conn, $path)) {
      if (!@ftp_mkdir($conn, $path)) return false;
      @ftp_chdir($conn, $path);
    }
  }
  return true;
}

進階:ftp_fput 大檔案/斷點續傳

<?php
function ftpUploadResume(
  string $host, string $user, string $pass,
  string $localFile, string $remoteFile,
  bool $ssl = false, int $mode = FTP_BINARY
): bool {
  $conn = $ssl ? @ftp_ssl_connect($host) : @ftp_connect($host);
  if (!$conn) throw new Exception("連線失敗");
  if (!@ftp_login($conn, $user, $pass)) throw new Exception("登入失敗");
  ftp_pasv($conn, true);
  
  // 1. 確保遠端目錄
  ftp_mkdir_recursive($conn, dirname($remoteFile));
  
  // 2. 檢查遠端已上傳大小 (續傳)
  $remoteSize = ftp_size($conn, $remoteFile);
  $localSize = filesize($localFile);
  
  if ($remoteSize >= $localSize) {
    ftp_close($conn);
    return true; // 已完成
  }
  
  // 3. 開啟本地檔案,跳過已上傳部分
  $fp = fopen($localFile, 'rb');
  if (!$fp) throw new Exception("無法開啟本地檔案");
  fseek($fp, $remoteSize);
  
  // 4. 續傳上傳
  $result = ftp_fput($conn, $remoteFile, $fp, $mode, $remoteSize);
  fclose($fp);
  ftp_close($conn);
  
  if (!$result) throw new Exception("續傳上傳失敗");
  return true;
}

非阻塞上傳:ftp_nb_fput (支援進度條)

<?php
function ftpUploadNonBlocking(
  string $host, string $user, string $pass,
  string $localFile, string $remoteFile,
  bool $ssl = false,
  callable $progressCallback = null
): bool {
  $conn = $ssl ? @ftp_ssl_connect($host) : @ftp_connect($host);
  if (!$conn) throw new Exception("連線失敗");
  if (!@ftp_login($conn, $user, $pass)) throw new Exception("登入失敗");
  ftp_pasv($conn, true);
  ftp_mkdir_recursive($conn, dirname($remoteFile));
  
  $fp = fopen($localFile, 'rb');
  if (!$fp) throw new Exception("無法開啟本地檔案");
  
  $totalSize = filesize($localFile);
  $ret = ftp_nb_fput($conn, $remoteFile, $fp, FTP_BINARY, 0);
  
  while ($ret === FTP_MOREDATA) {
    if ($progressCallback) {
      $uploaded = ftell($fp);
      $progressCallback($uploaded, $totalSize);
    }
    $ret = ftp_nb_continue($conn);
  }
  
  fclose($fp);
  ftp_close($conn);
  
  if ($ret !== FTP_FINISHED) throw new Exception("非阻塞上傳失敗");
  return true;
}

完整封裝類:支援多種模式、自動重試、日誌

<?php
class FtpUploader {
  private $conn;
  private $config;
  
  public function __construct(array $config) {
    $this->config = array_merge([
      'host' => '', 'user' => '', 'pass' => '',
      'ssl' => false, 'port' => 21, 'pasv' => true,
      'timeout' => 90, 'maxRetry' => 3
    ], $config);
  }
  
  private function connect() {
    for ($i = 0; $i < $this->config['maxRetry']; $i++) {
      $this->conn = $this->config['ssl'] 
        ? @ftp_ssl_connect($this->config['host'], $this->config['port'], $this->config['timeout'])
        : @ftp_connect($this->config['host'], $this->config['port'], $this->config['timeout']);
      if ($this->conn && @ftp_login($this->conn, $this->config['user'], $this->config['pass'])) {
        ftp_pasv($this->conn, $this->config['pasv']);
        return;
      }
      @ftp_close($this->conn);
      sleep(1 << $i);
    }
    throw new Exception("FTP 連線失敗,重試 {$this->config['maxRetry']} 次");
  }
  
  public function upload(
    string $localPath,
    string $remotePath,
    int $mode = FTP_BINARY,
    bool $resume = true,
    callable $progress = null
  ): bool {
    $this->connect();
    
    // 確保遠端目錄
    $this->mkdirRecursive(dirname($remotePath));
    
    $localSize = filesize($localPath);
    $remoteSize = $resume ? @ftp_size($this->conn, $remotePath) : -1;
    if ($remoteSize >= $localSize) return true;
    
    $fp = fopen($localPath, 'rb');
    if (!$fp) throw new Exception("本地檔案不可讀: $localPath");
    if ($remoteSize > 0) fseek($fp, $remoteSize);
    
    $startPos = $remoteSize > 0 ? $remoteSize : 0;
    $func = $progress ? 'ftp_nb_fput' : 'ftp_fput';
    
    $ret = $func($this->conn, $remotePath, $fp, $mode, $startPos);
    
    if ($progress && $func === 'ftp_nb_fput') {
      while ($ret === FTP_MOREDATA) {
        $progress(ftell($fp), $localSize);
        $ret = ftp_nb_continue($this->conn);
      }
    }
    
    fclose($fp);
    @ftp_close($this->conn);
    $this->conn = null;
    
    if ($ret !== true && $ret !== FTP_FINISHED) {
      throw new Exception("上傳失敗: $localPath -> $remotePath");
    }
    return true;
  }
  
  private function mkdirRecursive(string $dir): void {
    $cwd = ftp_pwd($this->conn);
    $parts = explode('/', trim($dir, '/'));
    $path = '';
    foreach ($parts as $part) {
      $path .= '/' . $part;
      if (!@ftp_chdir($this->conn, $path)) {
        @ftp_mkdir($this->conn, $path);
        @ftp_chdir($this->conn, $path);
      }
    }
    @ftp_chdir($this->conn, $cwd);
  }
  
  public function __destruct() {
    if ($this->conn) @ftp_close($this->conn);
  }
}

// 使用
$uploader = new FtpUploader([
  'host' => 'ftp.example.com',
  'user' => 'backup_user',
  'pass' => 'secret',
  'ssl'  => true,
]);

$uploader->upload(
  '/local/backup/db_20240101.sql.gz',
  '/remote/backups/db_20240101.sql.gz',
  FTP_BINARY,
  true, // 斷點續傳
  function($up, $total) {
    echo "\r上傳進度: " . round($up/$total*100, 1) . "%";
  }
);
echo "\n上傳完成\n";

常見問題排查表

現象可能原因解決方案
上傳 0 位元組 / 損壞二進制檔用 FTP_ASCII強制 FTP_BINARY
連線逾時主動模式被防火牆阻擋ftp_pasv(true)
權限拒絕FTP 用戶無寫入權限檢查伺服器權限、目錄擁有者
中文檔名亂碼FTP 協議編碼不一致檔名 URL encode / Base64 / 改用 SFTP
大檔案記憶體爆掉用 ftp_put 讀整檔入記憶體改用 ftp_fput / ftp_nb_fput 流式
續傳失敗伺服器不支援 REST/APPE檢查 ftp_raw($conn, 'REST 0') 或改用 lftp

生產環境建議

  1. 改用 SFTP (ssh2_sftp / phpseclib):單通道、加密、標準化、無模式困擾
  2. 異步佇列:上傳任務入 Redis/RabbitMQ,Worker 處理,API 秒回應
  3. 完整性校驗:上傳後比對 MD5/SHA256,確保傳輸無損壞
  4. 限速與並發控制:避免佔滿頻寬影響其他業務
  5. 詳細日誌:檔案、大小、耗時、速度、結果、錯誤碼,便於審計排查
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved