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() | 符號連結建立/讀取 |
<?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];
}
?>
<?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];
}
?>
<?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
// 取得檔案資訊
$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 檔案系統操作看似簡單,但生產環境需考量:權限管理、路徑遍歷防護、大檔案記憶體控制、併發鎖定、跨平台相容性。建議:
file_get_contents()/file_put_contents() 簡單場景fopen() + fread()/fwrite()RecursiveDirectoryIterator 物件導向 APIrealpath() + 基礎目錄前綴驗證flock()SplFileInfo 取代分散的 file* 函數