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

PHP FTP 操作函數完整封裝:目錄遞歸、檔案傳輸與編碼轉換實戰

     編輯:PHP 相關知識

PHP FTP 操作函數完整封裝:目錄遞歸、檔案傳輸與編碼轉換實戰

FTP 協議在批量檔案同步、備份、跨伺服器部署等場景仍廣泛使用。本文提供一個生產級的 FTP 操作類,涵蓋連線管理、目錄遞歸掃描、檔案上傳下載、編碼轉換、斷點續傳與錯誤重試機制。

核心 FTP 類實現

<?php
class FtpClient {
    private $conn;
    private $config;
    private $passiveMode = true;
    private $timeout = 30;
    
    public function __construct(array $config) {
        $this->config = array_merge([
            'host' => '127.0.0.1',
            'port' => 21,
            'user' => 'anonymous',
            'pass' => 'anonymous',
            'ssl' => false,
            'timeout' => 30,
        ], $config);
    }
    
    public function connect() {
        $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) {
            throw new RuntimeException("無法連線至 FTP 伺服器 {$this->config['host']}:{$this->config['port']}");
        }
        
        if (!@ftp_login($this->conn, $this->config['user'], $this->config['pass'])) {
            throw new RuntimeException("FTP 登入失敗:用戶 {$this->config['user']}");
        }
        
        ftp_pasv($this->conn, $this->passiveMode);
        ftp_set_option($this->conn, FTP_TIMEOUT_SEC, $this->config['timeout']);
        
        return $this;
    }
    
    public function disconnect() {
        if ($this->conn) {
            ftp_close($this->conn);
            $this->conn = null;
        }
    }
    
    // 目錄遞歸掃描(支援深度限制)
    public function scanDirectory($remotePath, $maxDepth = -1, $currentDepth = 0) {
        if ($maxDepth >= 0 && $currentDepth > $maxDepth) return [];
        
        $list = ftp_rawlist($this->conn, $remotePath);
        if ($list === false) return [];
        
        $result = ['dirs' => [], 'files' => []];
        foreach ($list as $entry) {
            $parsed = $this->parseRawListEntry($entry);
            if (!$parsed) continue;
            
            $fullPath = $remotePath . '/' . $parsed['name'];
            
            if ($parsed['type'] === 'dir') {
                $result['dirs'][$parsed['name']] = [
                    'path' => $fullPath,
                    'perms' => $parsed['perms'],
                    'date' => $parsed['date'],
                    'children' => $this->scanDirectory($fullPath, $maxDepth, $currentDepth + 1),
                ];
            } else {
                $result['files'][$parsed['name']] = [
                    'path' => $fullPath,
                    'size' => $parsed['size'],
                    'perms' => $parsed['perms'],
                    'date' => $parsed['date'],
                ];
            }
        }
        return $result;
    }
    
    private function parseRawListEntry($entry) {
        // 解析 ftp_rawlist 輸出(格式因伺服器而異,此處為 Unix 風格)
        // drwxr-xr-x 2 user group 4096 Jan 15 10:30 dirname
        // -rw-r--r-- 1 user group 1234 Jan 15 10:30 filename.txt
        if (preg_match('/^([d-])([rwxsStT-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+[\d:]{4,5})\s+(.+)$/', $entry, $m)) {
            return [
                'type' => $m[1] === 'd' ? 'dir' : 'file',
                'perms' => $m[2],
                'size' => (int)$m[3],
                'date' => strtotime($m[4]),
                'name' => $m[5],
            ];
        }
        return null;
    }
    
    // 檔案下載(支援斷點續傳)
    public function download($remoteFile, $localFile, $resume = false) {
        $mode = FTP_BINARY;
        $localSize = $resume && file_exists($localFile) ? filesize($localFile) : 0;
        
        if ($localSize > 0) {
            $remoteSize = ftp_size($this->conn, $remoteFile);
            if ($remoteSize == $localSize) return true; // 已完整
            if ($remoteSize < $localSize) $localSize = 0; // 遠端較小,重新下載
        }
        
        $fp = fopen($localFile, $localSize > 0 ? 'ab' : 'wb');
        if (!$fp) throw new RuntimeException("無法開啟本地檔案:$localFile");
        
        $result = ftp_nb_fget($this->conn, $fp, $remoteFile, $mode, $localSize);
        while ($result == FTP_MOREDATA) {
            $result = ftp_nb_continue($this->conn);
        }
        fclose($fp);
        
        if ($result != FTP_FINISHED) {
            throw new RuntimeException("下載失敗:$remoteFile -> $localFile");
        }
        return true;
    }
    
    // 檔案上傳(支援斷點續傳)
    public function upload($localFile, $remoteFile, $resume = false) {
        if (!file_exists($localFile)) throw new RuntimeException("本地檔案不存在:$localFile");
        
        $mode = FTP_BINARY;
        $remoteSize = $resume ? ftp_size($this->conn, $remoteFile) : 0;
        $localSize = filesize($localFile);
        
        if ($remoteSize >= $localSize) return true; // 已完整
        
        $fp = fopen($localFile, 'rb');
        if (!$fp) throw new RuntimeException("無法讀取本地檔案:$localFile");
        
        if ($remoteSize > 0) {
            fseek($fp, $remoteSize);
        }
        
        $result = ftp_nb_fput($this->conn, $remoteFile, $fp, $mode, $remoteSize);
        while ($result == FTP_MOREDATA) {
            $result = ftp_nb_continue($this->conn);
        }
        fclose($fp);
        
        if ($result != FTP_FINISHED) {
            throw new RuntimeException("上傳失敗:$localFile -> $remoteFile");
        }
        return true;
    }
    
    // 目錄同步(鏡像模式)
    public function syncDirectory($localDir, $remoteDir, $deleteExtra = false, $dryRun = false) {
        $localFiles = $this->scanLocal($localDir);
        $remoteFiles = $this->scanRemote($remoteDir);
        
        $actions = ['upload' => [], 'download' => [], 'delete' => [], 'skip' => []];
        
        // 比對邏輯...
        // 實際實現需比對 mtime、size、checksum
        
        return $actions;
    }
    
    // 編碼轉換下載(適用於 legacy 系統)
    public function downloadWithEncoding($remoteFile, $localFile, $fromEncoding = 'GBK', $toEncoding = 'UTF-8') {
        $tempFile = tempnam(sys_get_temp_dir(), 'ftp_');
        $this->download($remoteFile, $tempFile);
        
        $content = file_get_contents($tempFile);
        $content = iconv($fromEncoding, $toEncoding . '//IGNORE', $content);
        file_put_contents($localFile, $content);
        unlink($tempFile);
    }
    
    // 被動模式切換
    public function setPassiveMode($enabled) {
        $this->passiveMode = $enabled;
        if ($this->conn) ftp_pasv($this->conn, $enabled);
    }
    
    // 錯誤重試裝飾器
    public function withRetry(callable $operation, $maxRetries = 3, $delay = 1) {
        $lastException = null;
        for ($i = 0; $i <= $maxRetries; $i++) {
            try {
                return $operation($this);
            } catch (Exception $e) {
                $lastException = $e;
                if ($i < $maxRetries) {
                    sleep($delay * pow(2, $i)); // 指數退避
                    $this->reconnect();
                }
            }
        }
        throw $lastException;
    }
    
    private function reconnect() {
        $this->disconnect();
        $this->connect();
    }
    
    public function __destruct() {
        $this->disconnect();
    }
}

實戰應用範例

1. 批量下載目錄(含編碼轉換)

$ftp = new FtpClient([
    'host' => 'files.example.com',
    'user' => 'backup_user',
    'pass' => 'secure_password',
    'ssl' => true, // FTPS
])->connect();

$structure = $ftp->scanDirectory('/data/exports', 2); // 最大深度 2

foreach ($structure['files'] as $name => $info) {
    if (strpos($name, '.csv') !== false) {
        $localPath = "/var/backups/exports/{$name}";
        $ftp->withRetry(function($client) use ($info, $localPath) {
            $client->downloadWithEncoding($info['path'], $localPath, 'BIG5', 'UTF-8');
        });
        echo "下載完成:$name\n";
    }
}

$ftp->disconnect();

2. 部署腳本:上傳建構產物

$ftp = new FtpClient([
    'host' => 'deploy.example.com',
    'user' => 'deployer',
    'pass' => getenv('FTP_DEPLOY_PASS'),
])->connect();

$buildDir = '/workspace/build/dist';
$remoteBase = '/var/www/releases/' . date('Ymd_His');

$ftp->withRetry(function($client) use ($buildDir, $remoteBase) {
    $client->syncDirectory($buildDir, $remoteBase, true);
});

echo "部署完成:$remoteBase\n";
$ftp->disconnect();

FTP 協議限制與替代方案

限制影響建議替代
明文傳輸(預設)帳號密碼、資料可被竊聽FTPS (SSL/TLS) 或 SFTP (SSH)
主動模式防火牆穿透困難需開放大量高位埠強制被動模式 + 固定被動埠範圍
無完整性校驗傳輸錯誤難以偵測應用層 checksum (MD5/SHA256) 驗證
目錄列表格式不標準解析脆弱MLSD 指令 (RFC 3659) 或改用 SFTP
單連線吞吐量低大檔案傳輸慢多連線並行 / lftp / rsync over SSH

遷移至 SFTP 建議

// 使用 phpseclib (純 PHP SFTP)
use phpseclib\Net\SFTP;

$sftp = new SFTP('host', 22);
if (!$sftp->login('user', 'password')) {
    throw new Exception('SFTP 登入失敗');
}

// 上傳
$sftp->put('/remote/path/file.txt', '/local/path/file.txt', NET_SFTP_LOCAL_FILE);

// 下載
$sftp->get('/local/path/file.txt', '/remote/path/file.txt');

// 目錄掃描
$files = $sftp->nlist('/remote/path');

FTP 類封裝應包含連線池、非阻塞傳輸、進度回呼、完整性校驗等企業級特性。新專案優先選擇 SFTP/rsync,既有 FTP 系統建議加裝 FTPS 與應用層校驗機制。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved