在容器化、K8s 普及前,FTP 仍是許多虛擬主機、老舊項目、嵌入式設備的唯一發布通道。本文實戰拆解一套生產級 PHP FTP 同步工具,核心亮點:基於 mtime 的增量檢測、遞歸目錄遍歷、斷點續傳雛形、CodeIgniter 風格的 FTP 封裝類。
FilerFile::Zip()策略:維護一個 .~~~time.php 緩存文件,存儲 [文件路徑 => mtime] 映射。每次掃描對比當前 mtime,只返回變更文件。
<?php
class FilerFile {
private $time_path;
private $fctimes = [];
public function Zip($dir) {
$this->time_path = rtrim($dir, "/") . "/.~~~time.php";
$filelist = $this->GetFileList($dir);
// 保存最新 mtime 映射
file_put_contents($this->time_path, "fctimes, true) . ";");
return $filelist;
}
private function GetFileList($dir, $path = "") {
static $tmpp = "";
if (!$tmpp) $tmpp = $dir;
$d = dir($dir);
$files = [];
while ($f = $d->read()) {
// 排除規則:隱藏文件、.svn、配置排除列表
if ($f == '.' || $f == '..' || $f[0] == '.' || in_array($f, $_GET['exclude'])) continue;
$newdir = rtrim($dir, "/") . "/" . $f;
$abspath = $newdir;
if (is_dir($newdir)) {
$files = array_merge($files, $this->GetFileList($newdir, $newdir));
} else {
// 記錄 mtime
$this->fctimes[$abspath] = filemtime($abspath);
// 對比緩存
if (!$this->getFileByFilectime($abspath)) continue; // 未變更跳過
// 返回相對路徑
$relPath = str_replace($tmpp, "", $dir) . "/" . $f;
$files[] = ltrim($relPath, "/");
}
}
return $files;
}
private function getFileByFilectime($file) {
static $time_data;
if (!$time_data) $time_data = @include_once($this->time_path) ?: [];
// 關鍵:對比 mtime 而非 ctime,避免權限變更觸發誤傳
return ($time_data[$file] ?? null) != filemtime($file) ? $file : false;
}
}
?>
優化建議:大項目掃描耗時,建議引入 inotify 監聽或數據庫記錄文件哈希(MD5/SHA1)實現內容級去重。
Ftp 類參考 CodeIgniter FTP 庫設計,統一錯誤處理、被動模式、調試日誌、資源管理。
<?php
class Ftp {
private $conn_id = false;
private $debug = true;
private $passive = true;
public function connect($config) {
$this->_init($config);
$this->conn_id = @ftp_connect($this->hostname, $this->port);
$this->_login() && ftp_pasv($this->conn_id, true);
return true;
}
// 上傳:自動判斷 ASCII/BINARY 模式
public function upload($local, $remote, $mode = 'auto', $perm = null) {
$mode = ($mode == 'auto') ? $this->_settype($this->_getext($local)) : $mode;
$mode = ($mode == 'ascii') ? FTP_ASCII : FTP_BINARY;
$res = @ftp_put($this->conn_id, $remote, $local, $mode);
if ($res && $perm) $this->chmod($remote, $perm);
return $res;
}
// 遞歸創建遠程目錄(FTP 無 mkdir -p)
public function mkdir($path, $perm = null) {
$dirs = explode('/', $path);
$current = '';
foreach ($dirs as $dir) {
$current .= $dir . '/';
if (!$this->chgdir($current, true)) { // 抑制錯誤嘗試進入
@ftp_mkdir($this->conn_id, $dir);
$this->chgdir($current);
}
}
return true;
}
// 遞歸刪除目錄
public function delete_dir($path) {
$list = $this->filelist($path);
foreach ($list as $item) {
if (!$this->delete_file($item)) $this->delete_dir($item); // 可能是子目錄
}
return @ftp_rmdir($this->conn_id, $path);
}
// 錯誤日誌統一入口
private function _error($msg) {
file_put_contents('/tmp/ftp_err.log',
"[".date('Y-m-d H:i:s')."] host:{$this->hostname} user:{$this->username} msg:{$msg}\n",
FILE_APPEND
);
}
}
?>
<?php
// deploy.php - 命令行執行: php deploy.php
set_time_limit(0);
ini_set('memory_limit', '512M');
$config = [
'hostname' => 'ftp.example.com',
'username' => 'deploy_user',
'password' => 'strong_password',
'port' => 21,
];
$localRoot = '/var/www/myproject';
$remoteRoot = '/htdocs';
// 1. 掃描增量
$scanner = new FilerFile();
$_GET['exclude'] = ['.git', 'vendor', 'node_modules', 'storage/logs', '.env'];
$changedFiles = $scanner->Zip($localRoot);
if (empty($changedFiles)) {
echo "無文件變更,退出部署\n";
exit(0);
}
echo "檢測到 " . count($changedFiles) . " 個文件變更,開始上傳...\n";
// 2. 連接 FTP
$ftp = new Ftp();
$ftp->connect($config);
// 3. 確保遠程根目錄存在
$ftp->mkdir($remoteRoot);
// 4. 批量上傳
$success = $failed = 0;
foreach ($changedFiles as $relFile) {
$local = $localRoot . '/' . $relFile;
$remote = $remoteRoot . '/' . $relFile;
// 確保遠程目錄存在
$remoteDir = dirname($remote);
$ftp->mkdir($remoteDir);
if ($ftp->upload($local, $remote)) {
echo "[OK] $relFile\n";
$success++;
} else {
echo "[FAIL] $relFile\n";
$failed++;
}
}
$ftp->close();
echo "部署完成:成功 $success,失敗 $failed\n";
exit($failed > 0 ? 1 : 0);
?>
deploy 階段調用 php deploy.php,憑證存儲在 CI Variables。ftp-deploy-action 或自定義 Docker 容器運行 PHP 腳本。ftp_ssl_connect (FTPS) 或改用 phpseclib 實現 SFTP,明文 FTP 極不安全。/backup/YYYYMMDD_HHIISS/,失敗時一鍵恢復。pcntl_fork 或 parallel 扩展並發上傳,加速部署。這套 FTP 自動化方案雖屬「傳統技術棧」,但其增量掃描緩存設計、FTP 協議完整封裝、錯誤處理規範化的工程思想值得借鑒。現代項目建議優先選擇 Rsync over SSH、Docker Registry、K8s RollingUpdate,若受限於 FTP 環境,請務必升級為 FTPS/SFTP 並引入上述安全加固措施。