PHP 內建 ftp_* 函數提供底層 FTP 協議支援,但直接使用較繁瑣。本文封裝一個 FtpClient 類,涵蓋連線管理、被動模式、檔案上傳/下載/移動/複製/刪除、目錄遞歸建立與錯誤回報,開箱即用。
<?php
/**
* FTP 操作封裝類
* 特性:自動被動模式、目錄遞歸建立、複製轉為下載+上傳、錯誤訊息收集
*/
class FtpClient {
private $conn;
public $lastError = '';
/** 建立連線並登入
* @param string $host
* @param int $port
* @param string $user
* @param string $pass
* @param int $timeout 秒
* @throws Exception 連線/登入失敗
*/
public function __construct($host, $port = 21, $user, $pass, $timeout = 30) {
$this->conn = @ftp_connect($host, $port, $timeout);
if (!$this->conn) throw new Exception("FTP 連線失敗: $host:$port");
if (!@ftp_login($this->conn, $user, $pass)) throw new Exception("FTP 登入失敗: $user");
@ftp_pasv($this->conn, true); // 被動模式,適應防火牆/NAT
}
/** 關閉連線 */
public function close() {
if ($this->conn) @ftp_close($this->conn);
$this->conn = null;
}
/** 解構子自動關閉 */
public function __destruct() { $this->close(); }
/** 取得最後錯誤 */
public function getLastError() { return $this->lastError; }
// ---------- 目錄操作 ----------
/** 遞歸建立遠端目錄 (含多層)
* @param string $remotePath 如 "data/2024/05/"
* @return bool
*/
private function mkDirRecursive($remotePath) {
$dirs = explode('/', trim($remotePath, '/'));
$current = '';
foreach ($dirs as $dir) {
if ($dir === '') continue;
$current .= "/$dir";
if (!@ftp_chdir($this->conn, $current)) {
if (!@ftp_mkdir($this->conn, $current)) {
$this->lastError = "建立目錄失敗: $current";
return false;
}
@ftp_chdir($this->conn, $current);
}
}
// 回到根目錄
for ($i = 0; $i < count($dirs); $i++) @ftp_cdup($this->conn);
return true;
}
// ---------- 檔案操作 ----------
/** 上傳本地檔案
* @param string $localPath 本地絕對路徑
* @param string $remotePath 遠端相對路徑 (含檔名)
* @param bool $autoMkdir 自動建立目標目錄
* @return bool
*/
public function upload($localPath, $remotePath, $autoMkdir = true) {
if (!is_file($localPath)) { $this->lastError = "本地檔案不存在: $localPath"; return false; }
if ($autoMkdir) {
$dir = dirname($remotePath);
if ($dir !== '.' && !$this->mkDirRecursive($dir)) return false;
}
if (!@ftp_put($this->conn, $remotePath, $localPath, FTP_BINARY)) {
$this->lastError = "上傳失敗: $localPath -> $remotePath";
return false;
}
return true;
}
/** 下載遠端檔案
* @param string $remotePath
* @param string $localPath
* @return bool
*/
public function download($remotePath, $localPath) {
$dir = dirname($localPath);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) { $this->lastError = "本地目錄建立失敗: $dir"; return false; }
if (!@ftp_get($this->conn, $localPath, $remotePath, FTP_BINARY)) {
$this->lastError = "下載失敗: $remotePath -> $localPath";
return false;
}
return true;
}
/** 移動/重命名遠端檔案 (FTP RENAME)
* @param string $oldPath
* @param string $newPath
* @param bool $autoMkdir
* @return bool
*/
public function move($oldPath, $newPath, $autoMkdir = true) {
if ($autoMkdir) {
$dir = dirname($newPath);
if ($dir !== '.' && !$this->mkDirRecursive($dir)) return false;
}
if (!@ftp_rename($this->conn, $oldPath, $newPath)) {
$this->lastError = "移動失敗: $oldPath -> $newPath";
return false;
}
return true;
}
/** 複製遠端檔案 (FTP 無 COPY,採下載+上傳)
* @param string $srcPath
* @param string $dstPath
* @param bool $autoMkdir
* @return bool
*/
public function copy($srcPath, $dstPath, $autoMkdir = true) {
$tmp = sys_get_temp_dir() . '/ftp_copy_' . uniqid() . '.tmp';
if (!$this->download($srcPath, $tmp)) return false;
$ok = $this->upload($tmp, $dstPath, $autoMkdir);
@unlink($tmp);
return $ok;
}
/** 刪除遠端檔案 */
public function delete($remotePath) {
if (!@ftp_delete($this->conn, $remotePath)) {
$this->lastError = "刪除失敗: $remotePath";
return false;
}
return true;
}
/** 列出目錄檔案 (簡易 nlist) */
public function listFiles($remotePath = '.') {
return @ftp_nlist($this->conn, $remotePath) ?: [];
}
/** 取得檔案大小 (bytes) */
public function fileSize($remotePath) {
$size = @ftp_size($this->conn, $remotePath);
return $size >= 0 ? $size : false; // -1 表目錄或不存在
}
}
?>
<?php
require_once 'FtpClient.php';
try {
$ftp = new FtpClient('ftp.example.com', 21, 'user', 'pass');
// 上傳
$ftp->upload('/local/path/video.mp4', 'uploads/2024/05/video.mp4');
// 複製
$ftp->copy('uploads/2024/05/video.mp4', 'backup/video.mp4');
// 移動
$ftp->move('uploads/2024/05/video.mp4', 'archive/video.mp4');
// 列表
print_r($ftp->listFiles('uploads'));
$ftp->close();
} catch (Exception $e) {
echo "FTP 錯誤: " . $e->getMessage();
}
?>
若不需要完整類,僅需單一函數建立遠端目錄:
function ftpMkdirRecursive($host, $user, $pass, $remotePath, $port = 21) {
$conn = ftp_connect($host, $port);
if (!$conn || !ftp_login($conn, $user, $pass)) return false;
ftp_pasv($conn, true);
$dirs = explode('/', trim($remotePath, '/'));
$cur = '';
foreach ($dirs as $d) {
$cur .= "/$d";
if (!@ftp_chdir($conn, $cur) && !@ftp_mkdir($conn, $cur)) { ftp_close($conn); return false; }
}
ftp_close($conn);
return true;
}
ftp_pasv($conn, true),否則資料連線常被防火牆擋住導致掛起。FTP_BINARY 適用所有檔案;僅純文字可用 FTP_ASCII。/,即使 Windows 伺服器也建議統一用正斜線。ftp_connect 第 3 參數或 ini_set('default_socket_timeout', 300)。@ 抑制警告,改由回傳值與 $lastError 統一處理,生產環境建議改用 error_get_last() 記錄詳細資訊。