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

PHP 檔案系統操作完整指南:目錄遍歷、檔案複製、權限管理與實戰範例

     編輯:PHP 相關知識

PHP 提供豐富的檔案系統函數族群,支援從基本的讀寫到複雜的目錄遞迴、權限控制、檔案鎖定等操作。本文系統整理核心函數、常見陷阱、安全最佳實踐,並提供可直接用於生產環境的工具類封裝。

核心函數分類速查

類別函數說明
目錄操作opendir()開啟目錄控制代碼
readdir()讀取下一項目
closedir()關閉目錄控制代碼
scandir()一次性掃描目錄回傳陣列
mkdir()建立目錄(支援遞迴)
rmdir()刪除空目錄
檔案資訊file_exists()檢查檔案/目錄是否存在
is_file() / is_dir()判斷類型
filesize()取得檔案大小(位元組)
filemtime() / fileatime() / filectime()修改/存取/狀態改變時間
fileperms()取得權限位元遮罩
fileowner() / filegroup()擁有者 UID/GID
realpath()解析絕對路徑(解析符號連結)
pathinfo()解析路徑資訊(目錄、檔名、副檔名)
讀寫操作fopen() / fclose()開啟/關閉檔案控制代碼
fread() / fwrite()二進位讀寫
fgets() / fgetcsv()逐行/CSV 讀取
file_get_contents() / file_put_contents()一次性讀寫(便利封裝)
copy()複製檔案
unlink()刪除檔案
進階控制flock()檔案鎖定(共享/獨佔)
chmod() / chown() / chgrp()權限/擁有者變更
touch()更新存取/修改時間
symlink() / readlink()符號連結建立/讀取

目錄遍歷:三種主流方式比較

1. 經典 opendir/readdir/closedir(記憶體效率最高)

<?php
function listDirClassic(string $path): array {
    $files = [];
    $dirs = [];
    
    if (!is_dir($path)) {
        throw new InvalidArgumentException("Not a directory: $path");
    }
    
    $handle = opendir($path);
    if ($handle === false) {
        throw new RuntimeException("Cannot open directory: $path");
    }
    
    while (($entry = readdir($handle)) !== false) {
        if ($entry === '.' || $entry === '..') continue;
        $fullPath = $path . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($fullPath)) {
            $dirs[] = $entry;
        } else {
            $files[] = $entry;
        }
    }
    closedir($handle);
    
    return ['dirs' => $dirs, 'files' => $files];
}
?>

2. scandir() 簡潔版(一次性載入記憶體)

<?php
function listDirScandir(string $path): array {
    $entries = scandir($path) ?: [];
    $files = [];
    $dirs = [];
    foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') continue;
        $fullPath = $path . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($fullPath)) {
            $dirs[] = $entry;
        } else {
            $files[] = $entry;
        }
    }
    return ['dirs' => $dirs, 'files' => $files];
}
?>

3. RecursiveDirectoryIterator + RegexIterator(現代、功能強大)

<?php
function listDirRecursive(string $path, string $pattern = '/*'): array {
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );
    
    $filtered = new RegexIterator($iterator, $pattern);
    
    $result = ['dirs' => [], 'files' => []];
    foreach ($filtered as $fileInfo) {
        $relative = substr($fileInfo->getPathname(), strlen($path) + 1);
        if ($fileInfo->isDir()) {
            $result['dirs'][] = $relative;
        } else {
            $result['files'][] = $relative;
        }
    }
    return $result;
}

// 使用:只列 .php 檔案
// listDirRecursive('/var/www', '/\.php$/');
?&t;

檔案複製與備份:生產級實作

<?php
class FileBackup
{
    public function backup(string $source, string $destDir, bool $timestamp = true): string
    {
        if (!is_file($source)) {
            throw new InvalidArgumentException("Source not a file: $source");
        }
        
        if (!is_dir($destDir)) {
            mkdir($destDir, 0755, true);
        }
        
        $info = pathinfo($source);
        $filename = $info['filename'];
        $ext = isset($info['extension']) ? '.' . $info['extension'] : '';
        
        if ($timestamp) {
            $filename .= '_' . date('Ymd_His');
        }
        $filename .= $ext;
        
        $destPath = $destDir . DIRECTORY_SEPARATOR . $filename;
        
        // 使用 copy() 內建函數(流式複製,記憶體友善)
        if (!copy($source, $destPath)) {
            throw new RuntimeException("Copy failed: $source -> $destPath");
        }
        
        // 保留原檔案權限與時間
        $perms = fileperms($source);
        chmod($destPath, $perms & 0777);
        touch($destPath, filemtime($source), fileatime($source));
        
        return $destPath;
    }
    
    public function rotateBackups(string $destDir, int $keep = 7): void
    {
        $files = glob($destDir . DIRECTORY_SEPARATOR . '*');
        usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // 新到舊
        foreach (array_slice($files, $keep) as $old) {
            @unlink($old);
        }
    }
}

// 使用範例
$backup = new FileBackup();
$backup->backup('/var/www/config.php', '/backups/config');
$backup->rotateBackups('/backups/config', 30); // 保留 30 份
?>

安全最佳實踐:防止路徑遍歷攻擊

<?php
function safePath(string $baseDir, string $userInput): string
{
    // 1. 正規化基礎目錄(絕對路徑、無符號連結)
    $baseDir = realpath($baseDir);
    if ($baseDir === false) {
        throw new InvalidArgumentException("Base directory not found: $baseDir");
    }
    $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    
    // 2. 移除使用者輸入中的路徑遍歷嘗試
    $userInput = ltrim($userInput, DIRECTORY_SEPARATOR);
    $userInput = str_replace(['../', '..\\'], '', $userInput); // 基礎過濾
    
    // 3. 組合並再次正規化
    $fullPath = realpath($baseDir . $userInput);
    
    // 4. 關鍵檢查:確保最終路徑在基礎目錄內
    if ($fullPath === false || strpos($fullPath, $baseDir) !== 0) {
        throw new RuntimeException("Path traversal attempt detected: $userInput");
    }
    
    return $fullPath;
}

// 使用範例
// $userFile = $_GET['file'] ?? '';
// $safeFile = safePath('/var/www/uploads', $userFile);
// readfile($safeFile);
?>

大檔案處理:記憶體友善的串流讀寫

<?php
function streamCopy(string $source, string $dest, int $bufferSize = 8192): int
{
    $src = fopen($source, 'rb');
    if (!$src) throw new RuntimeException("Cannot open source: $source");
    
    $dst = fopen($dest, 'wb');
    if (!$dst) {
        fclose($src);
        throw new RuntimeException("Cannot open dest: $dest");
    }
    
    $total = 0;
    while (!feof($src)) {
        $chunk = fread($src, $bufferSize);
        if ($chunk === false) break;
        $written = fwrite($dst, $chunk);
        if ($written === false) break;
        $total += $written;
    }
    
    fclose($src);
    fclose($dst);
    return $total;
}

// 逐行處理大型 CSV/日誌
function processLargeFile(string $path, callable $lineHandler): void
{
    $handle = fopen($path, 'r');
    if (!$handle) throw new RuntimeException("Cannot open: $path");
    
    while (($line = fgets($handle)) !== false) {
        $lineHandler(rtrim($line, "\r\n"));
    }
    fclose($handle);
}

// 使用:計算大檔案行數
// $count = 0;
// processLargeFile('/var/log/nginx/access.log', fn() => $count++);
?>

檔案鎖定:防止併發衝突

<?php
function lockedWrite(string $file, string $data, int $flags = LOCK_EX): bool
{
    $fp = fopen($file, 'c'); // 開啟寫入,不截斷,建立檔案
    if (!$fp) return false;
    
    $locked = flock($fp, $flags);
    if ($locked) {
        ftruncate($fp, 0); // 截斷
        fwrite($fp, $data);
        fflush($fp);
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return $locked;
}

function lockedRead(string $file): ?string
{
    $fp = fopen($file, 'r');
    if (!$fp) return null;
    
    $locked = flock($fp, LOCK_SH);
    if (!$locked) {
        fclose($fp);
        return null;
    }
    
    $data = stream_get_contents($fp);
    flock($fp, LOCK_UN);
    fclose($fp);
    return $data;
}
?>

常見錯誤與避坑指南

錯誤原因解決
fopen(): failed to open stream: Permission denied目錄/檔案權限不足檢查 PHP-FPM/Apache 使用者對目錄的寫入權限,必要時用 ACL
mkdir(): Permission denied父目錄不可寫確保父目錄可寫,或用 mkdir($path, 0755, true) 遞迴建立
filesize(): stat failed檔案不存在或超過 2GB (32位系統)先 file_exists(),大檔案用 SplFileInfo::getSize()
unlink(): Resource temporarily unavailable檔案被其他程序鎖定確保檔案未被開啟,Windows 下常見
中文檔名亂碼編碼不一致(Windows GBK vs Linux UTF-8)統一用 UTF-8,必要時 iconv('UTF-8', 'GBK', $filename)
符號連結攻擊攻擊者建立連結指向敏感檔案使用 realpath() 驗證、open_basedir 限制

現代 PHP 物件導向封裝:SplFileInfo 與 FilesystemIterator

<?php
// 取得檔案資訊
$info = new SplFileInfo('/var/www/index.php');
echo $info->getFilename();      // index.php
echo $info->getExtension();     // php
echo $info->getSize();          // 檔案大小(支援 >2GB)
echo $info->getMTime();         // 修改時間戳
echo $info->getPerms();         // 權限
echo $info->isReadable();       // 可讀
echo $info->isWritable();       // 可寫
echo $info->getRealPath();      // 絕對路徑

// 目錄迭代(支援過濾、遞迴)
$dir = new FilesystemIterator('/var/www', FilesystemIterator::SKIP_DOTS);
foreach ($dir as $fileInfo) {
    if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') {
        echo $fileInfo->getFilename() . "\n";
    }
}

// 遞迴 + 過濾
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator('/var/www', FilesystemIterator::SKIP_DOTS)
);
$phpFiles = new RegexIterator($iterator, '/\.php$/');
foreach ($phpFiles as $file) {
    echo $file->getPathname() . "\n";
}
?>

總結

PHP 檔案系統操作看似簡單,但生產環境需考量:權限管理、路徑遍歷防護、大檔案記憶體控制、併發鎖定、跨平台相容性。建議:

  1. 優先使用 file_get_contents()/file_put_contents() 簡單場景
  2. 大檔案/串流場景用 fopen() + fread()/fwrite()
  3. 目錄遍歷用 RecursiveDirectoryIterator 物件導向 API
  4. 所有使用者輸入路徑必須經過 realpath() + 基礎目錄前綴驗證
  5. 關鍵寫入操作加上檔案鎖 flock()
  6. 善用 SplFileInfo 取代分散的 file* 函數
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved