<?php
/**
* FTP 操作類
* 支持:上傳、下載、移動、複製、刪除、創建目錄、列表
*/
class FtpClient {
private $conn;
private $connected = false;
public $lastError = '';
/**
* 構造函數:連接並登錄
* @param string $host FTP 服務器地址
* @param int $port 端口,默認 21
* @param string $user 用戶名
* @param string $pass 密碼
* @param int $timeout 連接超時秒數
* @throws Exception
*/
public function __construct(string $host, int $port = 21, string $user, string $pass, int $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)) {
$this->close();
throw new Exception("FTP 登錄失敗:用戶名或密碼錯誤");
}
// 啟用被動模式(穿透防火牆/NAT)
ftp_pasv($this->conn, true);
$this->connected = true;
}
/** 檢查連接狀態 */
private function ensureConnected(): void {
if (!$this->connected) {
throw new Exception('FTP 連接已關閉或未初始化');
}
}
/**
* 遞歸創建遠程目錄
* @param string $remotePath 如 "a/b/c"
* @return bool
*/
public function mkdirRecursive(string $remotePath): bool {
$this->ensureConnected();
$dirs = explode('/', trim($remotePath, '/'));
$current = '';
foreach ($dirs as $dir) {
$current .= "/$dir";
// 嘗試進入,失敗則創建
if (!@ftp_chdir($this->conn, $current)) {
if (!@ftp_mkdir($this->conn, $current)) {
$this->lastError = "創建目錄失敗:$current";
return false;
}
if (!@ftp_chdir($this->conn, $current)) {
$this->lastError = "進入目錄失敗:$current";
return false;
}
}
}
// 返回根目錄
@ftp_chdir($this->conn, '/');
return true;
}
/**
* 上傳文件
* @param string $localPath 本地文件路徑
* @param string $remotePath 遠程完整路徑(含文件名)
* @param int $mode FTP_BINARY | FTP_ASCII
* @return bool
*/
public function upload(string $localPath, string $remotePath, int $mode = FTP_BINARY): bool {
$this->ensureConnected();
if (!is_file($localPath)) {
$this->lastError = "本地文件不存在:$localPath";
return false;
}
// 確保遠程目錄存在
$remoteDir = dirname($remotePath);
if ($remoteDir !== '.' && $remoteDir !== '/') {
$this->mkdirRecursive($remoteDir);
}
$result = @ftp_put($this->conn, $remotePath, $localPath, $mode);
if (!$result) $this->lastError = "上傳失敗:$localPath -> $remotePath";
return $result;
}
/**
* 下載文件
* @param string $remotePath 遠程文件路徑
* @param string $localPath 本地保存路徑
* @param int $mode FTP_BINARY | FTP_ASCII
* @return bool
*/
public function download(string $remotePath, string $localPath, int $mode = FTP_BINARY): bool {
$this->ensureConnected();
$localDir = dirname($localPath);
if (!is_dir($localDir)) mkdir($localDir, 0755, true);
$result = @ftp_get($this->conn, $localPath, $remotePath, $mode);
if (!$result) $this->lastError = "下載失敗:$remotePath -> $localPath";
return $result;
}
/**
* 移動/重命名文件(同服務器內)
* @param string $from 來源路徑
* @param string $to 目標路徑
* @return bool
*/
public function move(string $from, string $to): bool {
$this->ensureConnected();
$toDir = dirname($to);
if ($toDir !== '.' && $toDir !== '/') {
$this->mkdirRecursive($toDir);
}
$result = @ftp_rename($this->conn, $from, $to);
if (!$result) $this->lastError = "移動失敗:$from -> $to";
return $result;
}
/**
* 複製文件(FTP 無原生複製,採用 下載->上傳)
* @param string $from 來源路徑
* @param string $to 目標路徑
* @param string $tmpDir 本地臨時目錄
* @return bool
*/
public function copy(string $from, string $to, string $tmpDir = '/tmp'): bool {
$this->ensureConnected();
$tmpFile = $tmpDir . '/ftp_copy_' . uniqid() . '.tmp';
if (!$this->download($from, $tmpFile)) return false;
$result = $this->upload($tmpFile, $to);
@unlink($tmpFile); // 清理臨時文件
return $result;
}
/**
* 刪除文件
* @param string $remotePath
* @return bool
*/
public function delete(string $remotePath): bool {
$this->ensureConnected();
$result = @ftp_delete($this->conn, $remotePath);
if (!$result) $this->lastError = "刪除失敗:$remotePath";
return $result;
}
/**
* 刪除目錄(遞歸)
* @param string $remoteDir
* @return bool
*/
public function deleteDir(string $remoteDir): bool {
$this->ensureConnected();
$list = $this->listFiles($remoteDir);
if ($list === false) return false;
foreach ($list as $item) {
if ($item['type'] === 'dir') {
$this->deleteDir($item['path']);
} else {
$this->delete($item['path']);
}
}
return @ftp_rmdir($this->conn, $remoteDir);
}
/**
* 獲取文件列表
* @param string $remoteDir
* @return array|false [['name'=>'', 'path'=>'', 'type'=>'file|dir', 'size'=>0, 'time'=>0], ...]
*/
public function listFiles(string $remoteDir = '.'): array|false {
$this->ensureConnected();
$raw = @ftp_nlist($this->conn, $remoteDir);
if ($raw === false) {
$this->lastError = "列表獲取失敗:$remoteDir";
return false;
}
$result = [];
foreach ($raw as $item) {
$fullPath = rtrim($remoteDir, '/') . '/' . $item;
// 簡易判斷類型:嘗試進入目錄
$isDir = @ftp_chdir($this->conn, $fullPath);
if ($isDir) @ftp_chdir($this->conn, $remoteDir); // 返回
$result[] = [
'name' => $item,
'path' => $fullPath,
'type' => $isDir ? 'dir' : 'file',
'size' => $isDir ? 0 : (ftp_size($this->conn, $fullPath) ?: 0),
'time' => ftp_mdtm($this->conn, $fullPath) ?: 0
];
}
return $result;
}
/** 獲取文件大小 */
public function fileSize(string $remotePath): int {
$this->ensureConnected();
return ftp_size($this->conn, $remotePath) ?: -1;
}
/** 關閉連接 */
public function close(): void {
if ($this->connected && $this->conn) {
@ftp_close($this->conn);
$this->connected = false;
}
}
/** 析構函數自動關閉 */
public function __destruct() {
$this->close();
}
}
<?php
require_once 'FtpClient.php';
try {
$ftp = new FtpClient('ftp.example.com', 21, 'username', 'password');
// 上傳
$ftp->upload('/local/path/image.jpg', '/remote/images/2024/01/image.jpg');
// 下載
$ftp->download('/remote/backup.sql', '/local/backup/backup_' . date('Ymd') . '.sql');
// 移動
$ftp->move('/remote/temp/file.txt', '/remote/archive/file.txt');
// 複製
$ftp->copy('/remote/data/config.ini', '/remote/backup/config.ini');
// 列表
$files = $ftp->listFiles('/remote/images');
foreach ($files as $f) {
echo "{$f['name']} ({$f['type']}) {$f['size']} bytes\n";
}
// 刪除
$ftp->delete('/remote/old/temp.log');
$ftp->close();
} catch (Exception $e) {
echo "FTP 錯誤:" . $e->getMessage();
}
?>
ftp_ssl_connect() 實現 FTPSftp_fget()/ftp_fput() + ftp_seek() 實現大文件斷點續傳vfsStream 或 Mock FTP Server 測試ftp_chmod() 或 ftp_site("CHMOD 644 $file")set_time_limit() 和 FTP 連接超時這個類封裝了 FTP 核心操作,遵循單一職責原則,生產環境可直接使用或作為基礎擴展。