9.1 文件基础操作
9.1.1 文件信息获取
<?php
// 文件信息获取
echo "=== 文件信息获取 ===\n";
$filename = 'test.txt';
// 创建测试文件
file_put_contents($filename, "这是一个测试文件\n包含多行内容\n用于演示文件操作");
echo "--- 基本文件信息 ---\n";
// 检查文件是否存在
if (file_exists($filename)) {
echo "文件存在: $filename\n";
// 文件大小
$size = filesize($filename);
echo "文件大小: $size 字节\n";
// 文件类型
$type = filetype($filename);
echo "文件类型: $type\n";
// 文件权限
$perms = fileperms($filename);
echo "文件权限: " . decoct($perms & 0777) . "\n";
// 文件所有者
$owner = fileowner($filename);
echo "文件所有者ID: $owner\n";
// 文件组
$group = filegroup($filename);
echo "文件组ID: $group\n";
// 最后访问时间
$atime = fileatime($filename);
echo "最后访问时间: " . date('Y-m-d H:i:s', $atime) . "\n";
// 最后修改时间
$mtime = filemtime($filename);
echo "最后修改时间: " . date('Y-m-d H:i:s', $mtime) . "\n";
// 最后改变时间(包括权限、所有者等变化)
$ctime = filectime($filename);
echo "最后改变时间: " . date('Y-m-d H:i:s', $ctime) . "\n";
// 文件inode
$inode = fileinode($filename);
echo "文件inode: $inode\n";
} else {
echo "文件不存在: $filename\n";
}
echo "\n--- 文件状态检查 ---\n";
// 各种文件状态检查
$checks = [
'is_file' => '是否为普通文件',
'is_dir' => '是否为目录',
'is_link' => '是否为符号链接',
'is_readable' => '是否可读',
'is_writable' => '是否可写',
'is_executable' => '是否可执行'
];
foreach ($checks as $function => $description) {
$result = $function($filename) ? '是' : '否';
echo "$description: $result\n";
}
// 获取详细的文件统计信息
echo "\n--- 详细文件统计 ---\n";
$stat = stat($filename);
if ($stat) {
$statInfo = [
'dev' => '设备号',
'ino' => 'inode号',
'mode' => '模式',
'nlink' => '链接数',
'uid' => '用户ID',
'gid' => '组ID',
'rdev' => '设备类型',
'size' => '大小',
'atime' => '访问时间',
'mtime' => '修改时间',
'ctime' => '改变时间',
'blksize' => '块大小',
'blocks' => '块数'
];
foreach ($statInfo as $key => $description) {
if (isset($stat[$key])) {
$value = $stat[$key];
if (in_array($key, ['atime', 'mtime', 'ctime'])) {
$value = date('Y-m-d H:i:s', $value) . " ($value)";
}
echo "$description: $value\n";
}
}
}
// 清理测试文件
unlink($filename);
?>
9.1.2 文件读取操作
<?php
// 文件读取操作
echo "=== 文件读取操作 ===\n";
// 创建测试文件
$filename = 'read_test.txt';
$content = "第一行内容\n第二行内容\n第三行内容\n包含中文:你好世界\n数字:12345\n特殊字符:@#$%^&*()";
file_put_contents($filename, $content);
echo "--- 一次性读取整个文件 ---\n";
// 1. file_get_contents() - 最常用的方法
$data = file_get_contents($filename);
echo "file_get_contents()读取结果:\n$data\n\n";
// 2. file() - 读取为数组,每行一个元素
$lines = file($filename);
echo "file()读取结果(数组):\n";
foreach ($lines as $lineNum => $line) {
echo "行 " . ($lineNum + 1) . ": " . trim($line) . "\n";
}
echo "\n";
// 3. file() 去除换行符
$lines = file($filename, FILE_IGNORE_NEW_LINES);
echo "file()读取结果(去除换行符):\n";
foreach ($lines as $lineNum => $line) {
echo "行 " . ($lineNum + 1) . ": $line\n";
}
echo "\n";
echo "--- 逐行读取文件 ---\n";
// 使用fopen、fgets逐行读取
$handle = fopen($filename, 'r');
if ($handle) {
echo "使用fgets()逐行读取:\n";
$lineNum = 1;
while (($line = fgets($handle)) !== false) {
echo "行 $lineNum: " . trim($line) . "\n";
$lineNum++;
}
fclose($handle);
} else {
echo "无法打开文件\n";
}
echo "\n";
echo "--- 按块读取文件 ---\n";
// 使用fread按指定大小读取
$handle = fopen($filename, 'r');
if ($handle) {
echo "使用fread()按块读取(每次10字节):\n";
$chunkNum = 1;
while (!feof($handle)) {
$chunk = fread($handle, 10);
if ($chunk !== false && $chunk !== '') {
echo "块 $chunkNum: " . json_encode($chunk) . "\n";
$chunkNum++;
}
}
fclose($handle);
}
echo "\n";
echo "--- 文件指针操作 ---\n";
$handle = fopen($filename, 'r');
if ($handle) {
// 获取当前指针位置
echo "初始指针位置: " . ftell($handle) . "\n";
// 读取前10个字符
$data = fread($handle, 10);
echo "读取前10个字符: " . json_encode($data) . "\n";
echo "读取后指针位置: " . ftell($handle) . "\n";
// 移动指针到文件开头
rewind($handle);
echo "rewind后指针位置: " . ftell($handle) . "\n";
// 使用fseek移动指针
fseek($handle, 5, SEEK_SET); // 从文件开头偏移5字节
echo "fseek(5, SEEK_SET)后指针位置: " . ftell($handle) . "\n";
// 从当前位置向前移动
fseek($handle, 3, SEEK_CUR);
echo "fseek(3, SEEK_CUR)后指针位置: " . ftell($handle) . "\n";
// 从文件末尾向前移动
fseek($handle, -5, SEEK_END);
echo "fseek(-5, SEEK_END)后指针位置: " . ftell($handle) . "\n";
// 读取当前位置的数据
$data = fread($handle, 5);
echo "从当前位置读取5个字符: " . json_encode($data) . "\n";
fclose($handle);
}
echo "\n--- 二进制文件读取 ---\n";
// 创建二进制测试文件
$binaryFile = 'binary_test.bin';
$binaryData = pack('C*', 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00, 0xFF, 0x01, 0x02, 0x03);
file_put_contents($binaryFile, $binaryData);
// 读取二进制文件
$handle = fopen($binaryFile, 'rb'); // 'rb'模式用于二进制读取
if ($handle) {
echo "二进制文件内容:\n";
while (!feof($handle)) {
$byte = fread($handle, 1);
if ($byte !== false && $byte !== '') {
$hex = bin2hex($byte);
$dec = ord($byte);
$char = ($dec >= 32 && $dec <= 126) ? $byte : '.';
echo "字节: 0x$hex ($dec) '$char'\n";
}
}
fclose($handle);
}
// 清理测试文件
unlink($filename);
unlink($binaryFile);
?>
9.1.3 文件写入操作
<?php
// 文件写入操作
echo "=== 文件写入操作 ===\n";
echo "--- 一次性写入文件 ---\n";
// 1. file_put_contents() - 最常用的方法
$filename = 'write_test.txt';
$content = "这是通过file_put_contents写入的内容\n";
// 写入文件(覆盖模式)
$bytes = file_put_contents($filename, $content);
echo "写入字节数: $bytes\n";
echo "文件内容: " . file_get_contents($filename) . "\n";
// 追加写入
$appendContent = "这是追加的内容\n";
$bytes = file_put_contents($filename, $appendContent, FILE_APPEND);
echo "追加字节数: $bytes\n";
echo "文件内容: " . file_get_contents($filename) . "\n";
// 使用锁定写入
$lockContent = "这是使用锁定写入的内容\n";
$bytes = file_put_contents($filename, $lockContent, FILE_APPEND | LOCK_EX);
echo "锁定写入字节数: $bytes\n";
echo "文件内容: " . file_get_contents($filename) . "\n";
echo "--- 使用文件句柄写入 ---\n";
$filename2 = 'handle_write_test.txt';
// 打开文件进行写入
$handle = fopen($filename2, 'w'); // 'w'模式:写入,截断文件
if ($handle) {
// 使用fwrite写入
$data1 = "第一行数据\n";
$bytes1 = fwrite($handle, $data1);
echo "fwrite写入字节数: $bytes1\n";
// 使用fputs写入(fwrite的别名)
$data2 = "第二行数据\n";
$bytes2 = fputs($handle, $data2);
echo "fputs写入字节数: $bytes2\n";
// 使用fprintf格式化写入
$bytes3 = fprintf($handle, "格式化数据: %s = %d\n", "数量", 100);
echo "fprintf写入字节数: $bytes3\n";
fclose($handle);
echo "文件内容: " . file_get_contents($filename2) . "\n";
} else {
echo "无法打开文件进行写入\n";
}
echo "--- 不同写入模式演示 ---\n";
$modeFile = 'mode_test.txt';
// 准备初始内容
file_put_contents($modeFile, "初始内容\n");
echo "初始内容: " . file_get_contents($modeFile);
// 模式说明
$modes = [
'w' => '写入模式(截断)',
'a' => '追加模式',
'r+' => '读写模式(不截断)',
'w+' => '读写模式(截断)',
'a+' => '读写追加模式'
];
foreach ($modes as $mode => $description) {
echo "\n--- $description ($mode) ---\n";
// 重置文件内容
file_put_contents($modeFile, "初始内容\n");
$handle = fopen($modeFile, $mode);
if ($handle) {
// 尝试写入
fwrite($handle, "新内容\n");
// 如果是读写模式,尝试读取
if (strpos($mode, '+') !== false || $mode === 'r+') {
rewind($handle); // 回到文件开头
$content = fread($handle, 1024);
echo "读取到的内容: " . json_encode($content) . "\n";
}
fclose($handle);
echo "最终文件内容: " . file_get_contents($modeFile);
}
}
echo "\n--- 二进制文件写入 ---\n";
$binaryFile = 'binary_write_test.bin';
// 写入二进制数据
$handle = fopen($binaryFile, 'wb'); // 'wb'模式用于二进制写入
if ($handle) {
// 写入不同类型的二进制数据
// 写入字节序列
$bytes = pack('C*', 0x48, 0x65, 0x6C, 0x6C, 0x6F); // "Hello"
fwrite($handle, $bytes);
// 写入整数(小端序)
$int32 = pack('V', 0x12345678);
fwrite($handle, $int32);
// 写入浮点数
$float = pack('f', 3.14159);
fwrite($handle, $float);
fclose($handle);
// 读取并显示二进制文件内容
$binaryContent = file_get_contents($binaryFile);
echo "二进制文件大小: " . strlen($binaryContent) . " 字节\n";
echo "十六进制内容: " . bin2hex($binaryContent) . "\n";
}
echo "\n--- 文件写入安全性 ---\n";
// 安全写入函数
function safeFileWrite($filename, $data, $mode = 'w') {
// 检查目录是否存在
$dir = dirname($filename);
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new Exception("无法创建目录: $dir");
}
}
// 检查文件是否可写
if (file_exists($filename) && !is_writable($filename)) {
throw new Exception("文件不可写: $filename");
}
// 使用临时文件进行原子写入
$tempFile = $filename . '.tmp.' . uniqid();
$handle = fopen($tempFile, $mode);
if (!$handle) {
throw new Exception("无法创建临时文件: $tempFile");
}
try {
// 获取独占锁
if (!flock($handle, LOCK_EX)) {
throw new Exception("无法获取文件锁");
}
// 写入数据
$bytes = fwrite($handle, $data);
if ($bytes === false) {
throw new Exception("写入失败");
}
// 强制写入磁盘
if (!fflush($handle)) {
throw new Exception("刷新缓冲区失败");
}
fclose($handle);
// 原子性重命名
if (!rename($tempFile, $filename)) {
throw new Exception("重命名临时文件失败");
}
return $bytes;
} catch (Exception $e) {
fclose($handle);
if (file_exists($tempFile)) {
unlink($tempFile);
}
throw $e;
}
}
// 测试安全写入
try {
$safeFile = 'safe_test.txt';
$bytes = safeFileWrite($safeFile, "这是安全写入的内容\n");
echo "安全写入成功,字节数: $bytes\n";
echo "文件内容: " . file_get_contents($safeFile);
unlink($safeFile);
} catch (Exception $e) {
echo "安全写入失败: " . $e->getMessage() . "\n";
}
// 清理测试文件
$testFiles = [$filename, $filename2, $modeFile, $binaryFile];
foreach ($testFiles as $file) {
if (file_exists($file)) {
unlink($file);
}
}
?>
9.2 目录操作
9.2.1 目录基础操作
<?php
// 目录基础操作
echo "=== 目录基础操作 ===\n";
echo "--- 目录信息获取 ---\n";
// 获取当前工作目录
$currentDir = getcwd();
echo "当前工作目录: $currentDir\n";
// 获取脚本所在目录
$scriptDir = __DIR__;
echo "脚本所在目录: $scriptDir\n";
// 获取目录名和基名
$path = '/path/to/some/file.txt';
echo "路径: $path\n";
echo "目录名: " . dirname($path) . "\n";
echo "基名: " . basename($path) . "\n";
echo "基名(无扩展名): " . basename($path, '.txt') . "\n";
// 路径信息解析
$pathInfo = pathinfo($path);
echo "路径信息:\n";
foreach ($pathInfo as $key => $value) {
echo " $key: $value\n";
}
echo "\n--- 目录存在性检查 ---\n";
$testDir = 'test_directory';
echo "检查目录是否存在: $testDir\n";
if (is_dir($testDir)) {
echo "目录存在\n";
} else {
echo "目录不存在\n";
}
// 检查是否为目录
$items = ['.', '..', $currentDir, __FILE__];
foreach ($items as $item) {
$isDir = is_dir($item) ? '是' : '否';
echo "$item 是目录: $isDir\n";
}
echo "\n--- 创建目录 ---\n";
// 创建单级目录
if (!is_dir($testDir)) {
if (mkdir($testDir)) {
echo "成功创建目录: $testDir\n";
} else {
echo "创建目录失败: $testDir\n";
}
}
// 创建多级目录
$nestedDir = 'parent/child/grandchild';
if (!is_dir($nestedDir)) {
if (mkdir($nestedDir, 0755, true)) { // 第三个参数true表示递归创建
echo "成功创建多级目录: $nestedDir\n";
} else {
echo "创建多级目录失败: $nestedDir\n";
}
}
// 设置目录权限
if (is_dir($testDir)) {
if (chmod($testDir, 0755)) {
echo "成功设置目录权限: $testDir\n";
}
// 获取目录权限
$perms = fileperms($testDir);
echo "目录权限: " . decoct($perms & 0777) . "\n";
}
echo "\n--- 目录遍历 ---\n";
// 使用opendir、readdir遍历目录
echo "使用opendir/readdir遍历当前目录:\n";
$handle = opendir('.');
if ($handle) {
while (($entry = readdir($handle)) !== false) {
$type = is_dir($entry) ? '[DIR]' : '[FILE]';
echo " $type $entry\n";
}
closedir($handle);
}
echo "\n使用scandir遍历目录:\n";
$entries = scandir('.');
if ($entries) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') continue;
$type = is_dir($entry) ? '[DIR]' : '[FILE]';
$size = is_file($entry) ? filesize($entry) : 0;
echo " $type $entry ($size bytes)\n";
}
}
echo "\n--- 递归目录遍历 ---\n";
// 递归遍历目录函数
function listDirectory($dir, $prefix = '') {
if (!is_dir($dir)) {
return;
}
$entries = scandir($dir);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $entry;
$type = is_dir($fullPath) ? '[DIR]' : '[FILE]';
$size = is_file($fullPath) ? filesize($fullPath) : 0;
echo "$prefix$type $entry";
if (is_file($fullPath)) {
echo " ($size bytes)";
}
echo "\n";
// 如果是目录,递归遍历
if (is_dir($fullPath)) {
listDirectory($fullPath, $prefix . ' ');
}
}
}
echo "递归遍历parent目录:\n";
if (is_dir('parent')) {
listDirectory('parent');
}
echo "\n--- 使用DirectoryIterator ---\n";
// 使用DirectoryIterator遍历目录
echo "使用DirectoryIterator遍历当前目录:\n";
try {
$iterator = new DirectoryIterator('.');
foreach ($iterator as $fileInfo) {
if ($fileInfo->isDot()) continue;
$type = $fileInfo->isDir() ? '[DIR]' : '[FILE]';
$name = $fileInfo->getFilename();
$size = $fileInfo->isFile() ? $fileInfo->getSize() : 0;
$mtime = date('Y-m-d H:i:s', $fileInfo->getMTime());
echo " $type $name ($size bytes, $mtime)\n";
}
} catch (Exception $e) {
echo "DirectoryIterator错误: " . $e->getMessage() . "\n";
}
echo "\n--- 使用RecursiveDirectoryIterator ---\n";
// 递归遍历目录
echo "使用RecursiveDirectoryIterator递归遍历:\n";
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('.', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $fileInfo) {
$depth = $iterator->getDepth();
$prefix = str_repeat(' ', $depth);
$type = $fileInfo->isDir() ? '[DIR]' : '[FILE]';
$name = $fileInfo->getFilename();
$relativePath = $iterator->getSubPathName();
echo "$prefix$type $name ($relativePath)\n";
}
} catch (Exception $e) {
echo "RecursiveDirectoryIterator错误: " . $e->getMessage() . "\n";
}
?>
9.2.2 目录高级操作
<?php
// 目录高级操作
echo "=== 目录高级操作 ===\n";
echo "--- 目录复制 ---\n";
// 递归复制目录函数
function copyDirectory($source, $destination) {
if (!is_dir($source)) {
throw new InvalidArgumentException("源目录不存在: $source");
}
// 创建目标目录
if (!is_dir($destination)) {
if (!mkdir($destination, 0755, true)) {
throw new RuntimeException("无法创建目标目录: $destination");
}
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$targetPath = $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
if ($item->isDir()) {
if (!is_dir($targetPath) && !mkdir($targetPath, 0755, true)) {
throw new RuntimeException("无法创建目录: $targetPath");
}
} else {
if (!copy($item->getPathname(), $targetPath)) {
throw new RuntimeException("无法复制文件: {$item->getPathname()} -> $targetPath");
}
}
}
}
// 创建测试目录结构
$sourceDir = 'source_test';
$destDir = 'dest_test';
// 创建源目录和文件
mkdir($sourceDir, 0755, true);
mkdir($sourceDir . '/subdir', 0755, true);
file_put_contents($sourceDir . '/file1.txt', '文件1内容');
file_put_contents($sourceDir . '/file2.txt', '文件2内容');
file_put_contents($sourceDir . '/subdir/file3.txt', '子目录文件内容');
try {
copyDirectory($sourceDir, $destDir);
echo "目录复制成功: $sourceDir -> $destDir\n";
// 验证复制结果
echo "验证复制结果:\n";
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($destDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$relativePath = str_replace($destDir . DIRECTORY_SEPARATOR, '', $file->getPathname());
$type = $file->isDir() ? '[DIR]' : '[FILE]';
echo " $type $relativePath\n";
if ($file->isFile()) {
echo " 内容: " . file_get_contents($file->getPathname()) . "\n";
}
}
} catch (Exception $e) {
echo "目录复制失败: " . $e->getMessage() . "\n";
}
echo "\n--- 目录移动/重命名 ---\n";
$oldName = 'old_directory';
$newName = 'new_directory';
// 创建测试目录
mkdir($oldName);
file_put_contents($oldName . '/test.txt', '测试内容');
if (rename($oldName, $newName)) {
echo "目录重命名成功: $oldName -> $newName\n";
// 验证重命名结果
if (is_dir($newName) && !is_dir($oldName)) {
echo "重命名验证成功\n";
echo "新目录内容: " . file_get_contents($newName . '/test.txt') . "\n";
}
} else {
echo "目录重命名失败\n";
}
echo "\n--- 目录删除 ---\n";
// 递归删除目录函数
function removeDirectory($dir) {
if (!is_dir($dir)) {
return false;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先删除子项
);
foreach ($iterator as $item) {
if ($item->isDir()) {
if (!rmdir($item->getPathname())) {
throw new RuntimeException("无法删除目录: {$item->getPathname()}");
}
} else {
if (!unlink($item->getPathname())) {
throw new RuntimeException("无法删除文件: {$item->getPathname()}");
}
}
}
return rmdir($dir);
}
// 删除测试目录
$dirsToRemove = [$sourceDir, $destDir, $newName];
foreach ($dirsToRemove as $dir) {
if (is_dir($dir)) {
try {
if (removeDirectory($dir)) {
echo "成功删除目录: $dir\n";
} else {
echo "删除目录失败: $dir\n";
}
} catch (Exception $e) {
echo "删除目录异常: $dir - " . $e->getMessage() . "\n";
}
}
}
echo "\n--- 目录权限管理 ---\n";
$permTestDir = 'perm_test';
mkdir($permTestDir);
// 设置不同权限
$permissions = [
0755 => '所有者读写执行,组和其他用户读执行',
0644 => '所有者读写,组和其他用户只读',
0700 => '仅所有者读写执行',
0777 => '所有用户读写执行'
];
foreach ($permissions as $perm => $description) {
if (chmod($permTestDir, $perm)) {
$currentPerm = fileperms($permTestDir);
$octPerm = decoct($currentPerm & 0777);
echo "设置权限 $perm ($description): 当前权限 $octPerm\n";
// 检查权限
$readable = is_readable($permTestDir) ? '是' : '否';
$writable = is_writable($permTestDir) ? '是' : '否';
$executable = is_executable($permTestDir) ? '是' : '否';
echo " 可读: $readable, 可写: $writable, 可执行: $executable\n";
}
}
// 清理
rmdir($permTestDir);
echo "\n--- 目录监控 ---\n";
// 目录变化监控类
class DirectoryMonitor {
private $directory;
private $lastSnapshot;
public function __construct($directory) {
$this->directory = $directory;
$this->lastSnapshot = $this->takeSnapshot();
}
private function takeSnapshot() {
$snapshot = [];
if (!is_dir($this->directory)) {
return $snapshot;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->directory, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$relativePath = str_replace($this->directory . DIRECTORY_SEPARATOR, '', $file->getPathname());
$snapshot[$relativePath] = [
'type' => $file->isDir() ? 'dir' : 'file',
'size' => $file->isFile() ? $file->getSize() : 0,
'mtime' => $file->getMTime()
];
}
return $snapshot;
}
public function checkChanges() {
$currentSnapshot = $this->takeSnapshot();
$changes = [
'added' => [],
'modified' => [],
'deleted' => []
];
// 检查新增和修改
foreach ($currentSnapshot as $path => $info) {
if (!isset($this->lastSnapshot[$path])) {
$changes['added'][] = $path;
} elseif ($this->lastSnapshot[$path]['mtime'] !== $info['mtime'] ||
$this->lastSnapshot[$path]['size'] !== $info['size']) {
$changes['modified'][] = $path;
}
}
// 检查删除
foreach ($this->lastSnapshot as $path => $info) {
if (!isset($currentSnapshot[$path])) {
$changes['deleted'][] = $path;
}
}
$this->lastSnapshot = $currentSnapshot;
return $changes;
}
}
// 测试目录监控
$monitorDir = 'monitor_test';
mkdir($monitorDir);
file_put_contents($monitorDir . '/initial.txt', '初始文件');
$monitor = new DirectoryMonitor($monitorDir);
echo "开始监控目录: $monitorDir\n";
// 模拟一些变化
sleep(1); // 确保时间戳不同
file_put_contents($monitorDir . '/new_file.txt', '新文件');
file_put_contents($monitorDir . '/initial.txt', '修改后的内容');
mkdir($monitorDir . '/new_subdir');
$changes = $monitor->checkChanges();
echo "检测到的变化:\n";
foreach ($changes as $type => $files) {
if (!empty($files)) {
echo " $type: " . implode(', ', $files) . "\n";
}
}
// 清理
removeDirectory($monitorDir);
?>
9.3 文件上传处理
9.3.1 基础文件上传
<?php
// 文件上传处理
echo "=== 文件上传处理 ===\n";
// 模拟$_FILES数组(实际使用中来自表单上传)
$simulatedFiles = [
'upload' => [
'name' => 'test_image.jpg',
'type' => 'image/jpeg',
'tmp_name' => '/tmp/php_upload_temp',
'error' => UPLOAD_ERR_OK,
'size' => 1024000
]
];
echo "--- 文件上传错误处理 ---\n";
// 文件上传错误代码说明
$uploadErrors = [
UPLOAD_ERR_OK => '上传成功',
UPLOAD_ERR_INI_SIZE => '文件大小超过php.ini中upload_max_filesize的值',
UPLOAD_ERR_FORM_SIZE => '文件大小超过表单中MAX_FILE_SIZE的值',
UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
UPLOAD_ERR_NO_FILE => '没有文件被上传',
UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹',
UPLOAD_ERR_CANT_WRITE => '文件写入失败',
UPLOAD_ERR_EXTENSION => '文件上传被扩展程序阻止'
];
foreach ($uploadErrors as $code => $message) {
echo "错误代码 $code: $message\n";
}
echo "\n--- 文件上传验证函数 ---\n";
// 文件上传验证类
class FileUploadValidator {
private $allowedTypes = [];
private $maxSize = 0;
private $allowedExtensions = [];
public function __construct($allowedTypes = [], $maxSize = 0, $allowedExtensions = []) {
$this->allowedTypes = $allowedTypes;
$this->maxSize = $maxSize;
$this->allowedExtensions = $allowedExtensions;
}
public function validate($fileInfo) {
$errors = [];
// 检查上传错误
if ($fileInfo['error'] !== UPLOAD_ERR_OK) {
$errors[] = $this->getUploadErrorMessage($fileInfo['error']);
return $errors;
}
// 检查文件大小
if ($this->maxSize > 0 && $fileInfo['size'] > $this->maxSize) {
$errors[] = "文件大小超过限制 (" . $this->formatBytes($this->maxSize) . ")";
}
// 检查文件类型
if (!empty($this->allowedTypes)) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($fileInfo['tmp_name']);
if (!in_array($mimeType, $this->allowedTypes)) {
$errors[] = "不允许的文件类型: $mimeType";
}
}
// 检查文件扩展名
if (!empty($this->allowedExtensions)) {
$extension = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $this->allowedExtensions)) {
$errors[] = "不允许的文件扩展名: $extension";
}
}
// 检查文件名安全性
if (!$this->isSecureFilename($fileInfo['name'])) {
$errors[] = "文件名包含不安全字符";
}
return $errors;
}
private function getUploadErrorMessage($errorCode) {
$messages = [
UPLOAD_ERR_INI_SIZE => '文件大小超过服务器限制',
UPLOAD_ERR_FORM_SIZE => '文件大小超过表单限制',
UPLOAD_ERR_PARTIAL => '文件上传不完整',
UPLOAD_ERR_NO_FILE => '没有选择文件',
UPLOAD_ERR_NO_TMP_DIR => '临时目录不存在',
UPLOAD_ERR_CANT_WRITE => '文件写入失败',
UPLOAD_ERR_EXTENSION => '文件上传被阻止'
];
return $messages[$errorCode] ?? '未知上传错误';
}
private function formatBytes($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.2f %s", $bytes / pow(1024, $factor), $units[$factor]);
}
private function isSecureFilename($filename) {
// 检查危险字符
$dangerousChars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|'];
foreach ($dangerousChars as $char) {
if (strpos($filename, $char) !== false) {
return false;
}
}
// 检查文件名长度
if (strlen($filename) > 255) {
return false;
}
return true;
}
}
// 测试文件上传验证
$validator = new FileUploadValidator(
['image/jpeg', 'image/png', 'image/gif'], // 允许的MIME类型
2 * 1024 * 1024, // 2MB大小限制
['jpg', 'jpeg', 'png', 'gif'] // 允许的扩展名
);
// 创建测试文件
$testFile = 'test_upload.jpg';
file_put_contents($testFile, str_repeat('x', 1000)); // 创建1KB测试文件
$testFileInfo = [
'name' => 'test_image.jpg',
'type' => 'image/jpeg',
'tmp_name' => $testFile,
'error' => UPLOAD_ERR_OK,
'size' => filesize($testFile)
];
$errors = $validator->validate($testFileInfo);
if (empty($errors)) {
echo "文件验证通过\n";
} else {
echo "文件验证失败:\n";
foreach ($errors as $error) {
echo " - $error\n";
}
}
echo "\n--- 安全文件上传处理 ---\n";
// 安全文件上传处理类
class SecureFileUpload {
private $uploadDir;
private $validator;
public function __construct($uploadDir, FileUploadValidator $validator) {
$this->uploadDir = rtrim($uploadDir, '/\\') . DIRECTORY_SEPARATOR;
$this->validator = $validator;
// 确保上传目录存在
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function handleUpload($fileInfo) {
// 验证文件
$errors = $this->validator->validate($fileInfo);
if (!empty($errors)) {
throw new InvalidArgumentException('文件验证失败: ' . implode(', ', $errors));
}
// 生成安全的文件名
$safeFilename = $this->generateSafeFilename($fileInfo['name']);
$targetPath = $this->uploadDir . $safeFilename;
// 确保目标文件不存在(避免覆盖)
$counter = 1;
$originalPath = $targetPath;
while (file_exists($targetPath)) {
$pathInfo = pathinfo($originalPath);
$targetPath = $pathInfo['dirname'] . DIRECTORY_SEPARATOR .
$pathInfo['filename'] . "_$counter." . $pathInfo['extension'];
$counter++;
}
// 移动上传文件
if (!move_uploaded_file($fileInfo['tmp_name'], $targetPath)) {
// 对于测试,使用copy代替move_uploaded_file
if (!copy($fileInfo['tmp_name'], $targetPath)) {
throw new RuntimeException('文件移动失败');
}
}
// 设置文件权限
chmod($targetPath, 0644);
return [
'original_name' => $fileInfo['name'],
'saved_name' => basename($targetPath),
'path' => $targetPath,
'size' => $fileInfo['size'],
'type' => $fileInfo['type']
];
}
private function generateSafeFilename($originalName) {
// 获取文件扩展名
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// 生成唯一文件名
$timestamp = time();
$random = bin2hex(random_bytes(8));
return $timestamp . '_' . $random . '.' . $extension;
}
}
// 测试安全文件上传
$uploadDir = 'uploads';
$uploader = new SecureFileUpload($uploadDir, $validator);
try {
$result = $uploader->handleUpload($testFileInfo);
echo "文件上传成功:\n";
foreach ($result as $key => $value) {
echo " $key: $value\n";
}
} catch (Exception $e) {
echo "文件上传失败: " . $e->getMessage() . "\n";
}
// 清理测试文件
unlink($testFile);
if (is_dir($uploadDir)) {
$files = glob($uploadDir . '/*');
foreach ($files as $file) {
unlink($file);
}
rmdir($uploadDir);
}
?>
9.3.2 多文件上传处理
<?php
// 多文件上传处理
echo "=== 多文件上传处理 ===\n";
// 多文件上传处理类
class MultiFileUpload {
private $uploader;
private $results = [];
public function __construct(SecureFileUpload $uploader) {
$this->uploader = $uploader;
}
public function handleMultipleUploads($filesArray) {
$this->results = [];
// 处理多文件数组格式
$normalizedFiles = $this->normalizeFilesArray($filesArray);
foreach ($normalizedFiles as $index => $fileInfo) {
try {
if ($fileInfo['error'] === UPLOAD_ERR_NO_FILE) {
continue; // 跳过空文件
}
$result = $this->uploader->handleUpload($fileInfo);
$this->results[] = [
'success' => true,
'index' => $index,
'data' => $result
];
} catch (Exception $e) {
$this->results[] = [
'success' => false,
'index' => $index,
'error' => $e->getMessage(),
'filename' => $fileInfo['name']
];
}
}
return $this->results;
}
private function normalizeFilesArray($filesArray) {
$normalized = [];
if (isset($filesArray['name']) && is_array($filesArray['name'])) {
// 处理HTML表单中的多文件上传格式
$fileCount = count($filesArray['name']);
for ($i = 0; $i < $fileCount; $i++) {
$normalized[] = [
'name' => $filesArray['name'][$i],
'type' => $filesArray['type'][$i],
'tmp_name' => $filesArray['tmp_name'][$i],
'error' => $filesArray['error'][$i],
'size' => $filesArray['size'][$i]
];
}
} else {
// 处理单个文件或已规范化的数组
$normalized = is_array($filesArray) ? $filesArray : [$filesArray];
}
return $normalized;
}
public function getResults() {
return $this->results;
}
public function getSuccessfulUploads() {
return array_filter($this->results, function($result) {
return $result['success'];
});
}
public function getFailedUploads() {
return array_filter($this->results, function($result) {
return !$result['success'];
});
}
public function getUploadSummary() {
$total = count($this->results);
$successful = count($this->getSuccessfulUploads());
$failed = count($this->getFailedUploads());
return [
'total' => $total,
'successful' => $successful,
'failed' => $failed,
'success_rate' => $total > 0 ? round(($successful / $total) * 100, 2) : 0
];
}
}
// 创建测试文件
$testFiles = [];
for ($i = 1; $i <= 3; $i++) {
$filename = "test_file_$i.txt";
file_put_contents($filename, "测试文件 $i 的内容");
$testFiles[] = [
'name' => "upload_file_$i.txt",
'type' => 'text/plain',
'tmp_name' => $filename,
'error' => UPLOAD_ERR_OK,
'size' => filesize($filename)
];
}
// 添加一个错误文件
$testFiles[] = [
'name' => 'error_file.txt',
'type' => 'text/plain',
'tmp_name' => '',
'error' => UPLOAD_ERR_NO_FILE,
'size' => 0
];
// 测试多文件上传
$uploadDir = 'multi_uploads';
$validator = new FileUploadValidator(
['text/plain', 'image/jpeg', 'image/png'],
1024 * 1024, // 1MB
['txt', 'jpg', 'jpeg', 'png']
);
$uploader = new SecureFileUpload($uploadDir, $validator);
$multiUploader = new MultiFileUpload($uploader);
$results = $multiUploader->handleMultipleUploads($testFiles);
echo "多文件上传结果:\n";
foreach ($results as $result) {
if ($result['success']) {
echo " ✓ 文件 {$result['index']}: {$result['data']['original_name']} -> {$result['data']['saved_name']}\n";
} else {
echo " ✗ 文件 {$result['index']}: {$result['filename']} - {$result['error']}\n";
}
}
$summary = $multiUploader->getUploadSummary();
echo "\n上传摘要:\n";
echo " 总文件数: {$summary['total']}\n";
echo " 成功: {$summary['successful']}\n";
echo " 失败: {$summary['failed']}\n";
echo " 成功率: {$summary['success_rate']}%\n";
// 清理测试文件
for ($i = 1; $i <= 3; $i++) {
$filename = "test_file_$i.txt";
if (file_exists($filename)) {
unlink($filename);
}
}
if (is_dir($uploadDir)) {
$files = glob($uploadDir . '/*');
foreach ($files as $file) {
unlink($file);
}
rmdir($uploadDir);
}
?>
9.4 文件权限与安全
9.4.1 文件权限管理
<?php
// 文件权限管理
echo "=== 文件权限管理 ===\n";
echo "--- 权限基础概念 ---\n";
// 创建测试文件
$testFile = 'permission_test.txt';
file_put_contents($testFile, '权限测试文件');
echo "权限表示方法:\n";
echo " 八进制: 0755 (rwxr-xr-x)\n";
echo " 二进制: 111101101\n";
echo " 符号: rwxr-xr-x\n";
echo "\n权限位含义:\n";
echo " r (4): 读权限\n";
echo " w (2): 写权限\n";
echo " x (1): 执行权限\n";
echo "\n权限组:\n";
echo " 所有者 (user): 文件所有者的权限\n";
echo " 组 (group): 文件所属组的权限\n";
echo " 其他 (other): 其他用户的权限\n";
echo "\n--- 权限操作函数 ---\n";
// 权限管理类
class FilePermissionManager {
public static function getPermissions($file) {
if (!file_exists($file)) {
throw new InvalidArgumentException("文件不存在: $file");
}
$perms = fileperms($file);
return [
'octal' => decoct($perms & 0777),
'symbolic' => self::permissionsToSymbolic($perms),
'readable' => is_readable($file),
'writable' => is_writable($file),
'executable' => is_executable($file)
];
}
public static function setPermissions($file, $permissions) {
if (!file_exists($file)) {
throw new InvalidArgumentException("文件不存在: $file");
}
// 支持八进制字符串和整数
if (is_string($permissions)) {
$permissions = octdec($permissions);
}
if (!chmod($file, $permissions)) {
throw new RuntimeException("设置权限失败: $file");
}
return true;
}
public static function permissionsToSymbolic($perms) {
$symbolic = '';
// 文件类型
if (($perms & 0xC000) == 0xC000) {
$symbolic .= 's'; // Socket
} elseif (($perms & 0xA000) == 0xA000) {
$symbolic .= 'l'; // Symbolic Link
} elseif (($perms & 0x8000) == 0x8000) {
$symbolic .= '-'; // Regular file
} elseif (($perms & 0x6000) == 0x6000) {
$symbolic .= 'b'; // Block special
} elseif (($perms & 0x4000) == 0x4000) {
$symbolic .= 'd'; // Directory
} elseif (($perms & 0x2000) == 0x2000) {
$symbolic .= 'c'; // Character special
} elseif (($perms & 0x1000) == 0x1000) {
$symbolic .= 'p'; // FIFO pipe
} else {
$symbolic .= 'u'; // Unknown
}
// 所有者权限
$symbolic .= (($perms & 0x0100) ? 'r' : '-');
$symbolic .= (($perms & 0x0080) ? 'w' : '-');
$symbolic .= (($perms & 0x0040) ?
(($perms & 0x0800) ? 's' : 'x') :
(($perms & 0x0800) ? 'S' : '-'));
// 组权限
$symbolic .= (($perms & 0x0020) ? 'r' : '-');
$symbolic .= (($perms & 0x0010) ? 'w' : '-');
$symbolic .= (($perms & 0x0008) ?
(($perms & 0x0400) ? 's' : 'x') :
(($perms & 0x0400) ? 'S' : '-'));
// 其他用户权限
$symbolic .= (($perms & 0x0004) ? 'r' : '-');
$symbolic .= (($perms & 0x0002) ? 'w' : '-');
$symbolic .= (($perms & 0x0001) ?
(($perms & 0x0200) ? 't' : 'x') :
(($perms & 0x0200) ? 'T' : '-'));
return $symbolic;
}
public static function addPermission($file, $permission) {
$currentPerms = fileperms($file) & 0777;
$newPerms = $currentPerms | $permission;
return self::setPermissions($file, $newPerms);
}
public static function removePermission($file, $permission) {
$currentPerms = fileperms($file) & 0777;
$newPerms = $currentPerms & ~$permission;
return self::setPermissions($file, $newPerms);
}
public static function hasPermission($file, $permission) {
$currentPerms = fileperms($file) & 0777;
return ($currentPerms & $permission) === $permission;
}
}
// 测试权限管理
echo "当前文件权限:\n";
$perms = FilePermissionManager::getPermissions($testFile);
foreach ($perms as $key => $value) {
$valueStr = is_bool($value) ? ($value ? '是' : '否') : $value;
echo " $key: $valueStr\n";
}
echo "\n--- 权限修改测试 ---\n";
$testPermissions = [
0644 => 'rw-r--r--',
0755 => 'rwxr-xr-x',
0600 => 'rw-------',
0777 => 'rwxrwxrwx'
];
foreach ($testPermissions as $perm => $description) {
try {
FilePermissionManager::setPermissions($testFile, $perm);
$newPerms = FilePermissionManager::getPermissions($testFile);
echo "设置权限 $perm: {$newPerms['symbolic']} (八进制: {$newPerms['octal']})\n";
// 测试权限检查
$readable = $newPerms['readable'] ? '可读' : '不可读';
$writable = $newPerms['writable'] ? '可写' : '不可写';
$executable = $newPerms['executable'] ? '可执行' : '不可执行';
echo " 状态: $readable, $writable, $executable\n";
} catch (Exception $e) {
echo "设置权限失败: " . $e->getMessage() . "\n";
}
}
echo "\n--- 权限位操作 ---\n";
// 重置为基础权限
FilePermissionManager::setPermissions($testFile, 0644);
echo "初始权限: " . FilePermissionManager::getPermissions($testFile)['octal'] . "\n";
// 添加执行权限
FilePermissionManager::addPermission($testFile, 0111); // 所有用户执行权限
echo "添加执行权限后: " . FilePermissionManager::getPermissions($testFile)['octal'] . "\n";
// 移除写权限
FilePermissionManager::removePermission($testFile, 0222); // 所有用户写权限
echo "移除写权限后: " . FilePermissionManager::getPermissions($testFile)['octal'] . "\n";
// 检查特定权限
$hasReadPerm = FilePermissionManager::hasPermission($testFile, 0444);
echo "是否有读权限: " . ($hasReadPerm ? '是' : '否') . "\n";
$hasWritePerm = FilePermissionManager::hasPermission($testFile, 0222);
echo "是否有写权限: " . ($hasWritePerm ? '是' : '否') . "\n";
// 清理
unlink($testFile);
?>
9.4.2 文件安全检查
<?php
// 文件安全检查
echo "=== 文件安全检查 ===\n";
// 文件安全检查类
class FileSecurityChecker {
private static $dangerousExtensions = [
'php', 'php3', 'php4', 'php5', 'phtml', 'phps',
'asp', 'aspx', 'jsp', 'jspx',
'exe', 'com', 'bat', 'cmd', 'scr',
'sh', 'bash', 'csh', 'ksh',
'pl', 'py', 'rb', 'js'
];
private static $dangerousMimeTypes = [
'application/x-php',
'application/x-httpd-php',
'text/x-php',
'application/x-executable',
'application/x-msdownload',
'application/x-sh'
];
public static function checkFilename($filename) {
$issues = [];
// 检查文件名长度
if (strlen($filename) > 255) {
$issues[] = '文件名过长';
}
// 检查危险字符
$dangerousChars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
foreach ($dangerousChars as $char) {
if (strpos($filename, $char) !== false) {
$issues[] = "包含危险字符: $char";
}
}
// 检查文件扩展名
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (in_array($extension, self::$dangerousExtensions)) {
$issues[] = "危险的文件扩展名: $extension";
}
// 检查双扩展名
if (preg_match('/\.(\w+)\.(\w+)$/', $filename, $matches)) {
if (in_array(strtolower($matches[1]), self::$dangerousExtensions)) {
$issues[] = "双扩展名中包含危险扩展名: {$matches[1]}";
}
}
// 检查隐藏文件
if (strpos(basename($filename), '.') === 0) {
$issues[] = '隐藏文件';
}
return $issues;
}
public static function checkFileContent($filepath) {
$issues = [];
if (!file_exists($filepath)) {
$issues[] = '文件不存在';
return $issues;
}
// 检查MIME类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($filepath);
if (in_array($mimeType, self::$dangerousMimeTypes)) {
$issues[] = "危险的MIME类型: $mimeType";
}
// 检查文件头(魔数)
$handle = fopen($filepath, 'rb');
if ($handle) {
$header = fread($handle, 16);
fclose($handle);
// 检查PHP标签
if (strpos($header, '<?php') !== false || strpos($header, '<?=') !== false) {
$issues[] = '包含PHP代码标签';
}
// 检查脚本标签
if (stripos($header, '<script') !== false) {
$issues[] = '包含脚本标签';
}
// 检查可执行文件头
$executableHeaders = [
"\x4D\x5A" => 'Windows PE',
"\x7F\x45\x4C\x46" => 'Linux ELF',
"\xCA\xFE\xBA\xBE" => 'Java Class',
"\xFE\xED\xFA\xCE" => 'Mach-O'
];
foreach ($executableHeaders as $magic => $type) {
if (strpos($header, $magic) === 0) {
$issues[] = "可执行文件: $type";
}
}
}
return $issues;
}
public static function checkFileSize($filepath, $maxSize = null) {
$issues = [];
if (!file_exists($filepath)) {
$issues[] = '文件不存在';
return $issues;
}
$size = filesize($filepath);
// 检查文件大小
if ($maxSize && $size > $maxSize) {
$issues[] = "文件过大: " . self::formatBytes($size) . " > " . self::formatBytes($maxSize);
}
// 检查空文件
if ($size === 0) {
$issues[] = '空文件';
}
return $issues;
}
public static function checkDirectory($dirpath) {
$issues = [];
if (!is_dir($dirpath)) {
$issues[] = '目录不存在';
return $issues;
}
// 检查目录权限
if (!is_readable($dirpath)) {
$issues[] = '目录不可读';
}
if (!is_writable($dirpath)) {
$issues[] = '目录不可写';
}
// 检查目录遍历漏洞
$realPath = realpath($dirpath);
if ($realPath !== $dirpath) {
$issues[] = '可能存在目录遍历漏洞';
}
return $issues;
}
public static function sanitizeFilename($filename) {
// 移除路径分隔符
$filename = basename($filename);
// 移除危险字符
$filename = preg_replace('/[^\w\-_\.]/', '_', $filename);
// 限制长度
if (strlen($filename) > 255) {
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$basename = pathinfo($filename, PATHINFO_FILENAME);
$maxBasenameLength = 255 - strlen($extension) - 1;
$filename = substr($basename, 0, $maxBasenameLength) . '.' . $extension;
}
// 确保不是隐藏文件
if (strpos($filename, '.') === 0) {
$filename = 'file_' . $filename;
}
return $filename;
}
private static function formatBytes($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.2f %s", $bytes / pow(1024, $factor), $units[$factor]);
}
}
echo "--- 文件名安全检查 ---\n";
$testFilenames = [
'normal_file.txt',
'script.php',
'file..txt',
'very_long_filename_' . str_repeat('x', 250) . '.txt',
'.hidden_file',
'file<script>.txt',
'file.php.txt',
'file\\path.txt'
];
foreach ($testFilenames as $filename) {
$issues = FileSecurityChecker::checkFilename($filename);
echo "文件名: $filename\n";
if (empty($issues)) {
echo " ✓ 安全\n";
} else {
echo " ✗ 问题: " . implode(', ', $issues) . "\n";
}
$sanitized = FileSecurityChecker::sanitizeFilename($filename);
if ($sanitized !== $filename) {
echo " 清理后: $sanitized\n";
}
echo "\n";
}
echo "--- 文件内容安全检查 ---\n";
// 创建测试文件
$testFiles = [
'normal.txt' => '这是正常的文本内容',
'php_script.txt' => '<?php echo "Hello World"; ?>',
'html_script.txt' => '<script>alert("XSS")</script>',
'binary.bin' => "\x4D\x5A\x90\x00" // PE文件头
];
foreach ($testFiles as $filename => $content) {
file_put_contents($filename, $content);
$issues = FileSecurityChecker::checkFileContent($filename);
echo "文件: $filename\n";
if (empty($issues)) {
echo " ✓ 内容安全\n";
} else {
echo " ✗ 问题: " . implode(', ', $issues) . "\n";
}
$sizeIssues = FileSecurityChecker::checkFileSize($filename, 1024);
if (!empty($sizeIssues)) {
echo " 大小问题: " . implode(', ', $sizeIssues) . "\n";
}
echo "\n";
unlink($filename);
}
echo "--- 目录安全检查 ---\n";
$testDir = 'security_test_dir';
mkdir($testDir);
$dirIssues = FileSecurityChecker::checkDirectory($testDir);
echo "目录: $testDir\n";
if (empty($dirIssues)) {
echo " ✓ 目录安全\n";
} else {
echo " ✗ 问题: " . implode(', ', $dirIssues) . "\n";
}
rmdir($testDir);
?>
9.5 文件锁定与并发控制
9.5.1 文件锁定机制
<?php
// 文件锁定机制
echo "=== 文件锁定机制 ===\n";
echo "--- 基础文件锁定 ---\n";
// 文件锁定类型
echo "文件锁定类型:\n";
echo " LOCK_SH: 共享锁(读锁)\n";
echo " LOCK_EX: 独占锁(写锁)\n";
echo " LOCK_UN: 释放锁\n";
echo " LOCK_NB: 非阻塞锁\n";
// 基础锁定示例
$lockFile = 'lock_test.txt';
file_put_contents($lockFile, "初始内容\n");
echo "\n--- 独占锁示例 ---\n";
$handle = fopen($lockFile, 'r+');
if ($handle) {
echo "尝试获取独占锁...\n";
if (flock($handle, LOCK_EX)) {
echo "✓ 获取独占锁成功\n";
// 模拟长时间操作
echo "执行文件操作...\n";
fwrite($handle, "添加的内容 - " . date('Y-m-d H:i:s') . "\n");
// 释放锁
flock($handle, LOCK_UN);
echo "✓ 释放锁成功\n";
} else {
echo "✗ 获取独占锁失败\n";
}
fclose($handle);
}
echo "\n--- 共享锁示例 ---\n";
$handle1 = fopen($lockFile, 'r');
$handle2 = fopen($lockFile, 'r');
if ($handle1 && $handle2) {
echo "尝试获取两个共享锁...\n";
if (flock($handle1, LOCK_SH)) {
echo "✓ 第一个共享锁获取成功\n";
if (flock($handle2, LOCK_SH)) {
echo "✓ 第二个共享锁获取成功\n";
// 读取文件内容
$content1 = fread($handle1, 1024);
rewind($handle2);
$content2 = fread($handle2, 1024);
echo "两个进程都可以同时读取文件\n";
flock($handle2, LOCK_UN);
echo "✓ 第二个共享锁释放\n";
}
flock($handle1, LOCK_UN);
echo "✓ 第一个共享锁释放\n";
}
fclose($handle1);
fclose($handle2);
}
echo "\n--- 非阻塞锁示例 ---\n";
// 文件锁定管理类
class FileLockManager {
private $lockHandles = [];
public function acquireLock($filename, $mode = LOCK_EX, $blocking = true) {
$handle = fopen($filename, 'c+');
if (!$handle) {
throw new RuntimeException("无法打开文件: $filename");
}
$lockMode = $blocking ? $mode : ($mode | LOCK_NB);
if (flock($handle, $lockMode)) {
$this->lockHandles[$filename] = $handle;
return true;
} else {
fclose($handle);
return false;
}
}
public function releaseLock($filename) {
if (isset($this->lockHandles[$filename])) {
$handle = $this->lockHandles[$filename];
flock($handle, LOCK_UN);
fclose($handle);
unset($this->lockHandles[$filename]);
return true;
}
return false;
}
public function writeToFile($filename, $data) {
if (!isset($this->lockHandles[$filename])) {
throw new RuntimeException("文件未锁定: $filename");
}
$handle = $this->lockHandles[$filename];
fseek($handle, 0, SEEK_END);
return fwrite($handle, $data);
}
public function readFromFile($filename) {
if (!isset($this->lockHandles[$filename])) {
throw new RuntimeException("文件未锁定: $filename");
}
$handle = $this->lockHandles[$filename];
rewind($handle);
return fread($handle, filesize($filename));
}
public function __destruct() {
foreach ($this->lockHandles as $filename => $handle) {
$this->releaseLock($filename);
}
}
}
// 测试锁定管理器
$lockManager = new FileLockManager();
try {
// 尝试获取非阻塞锁
if ($lockManager->acquireLock($lockFile, LOCK_EX, false)) {
echo "✓ 获取非阻塞独占锁成功\n";
$lockManager->writeToFile($lockFile, "锁定管理器写入的内容\n");
echo "✓ 写入数据成功\n";
$content = $lockManager->readFromFile($lockFile);
echo "读取内容: " . trim($content) . "\n";
$lockManager->releaseLock($lockFile);
echo "✓ 释放锁成功\n";
} else {
echo "✗ 获取非阻塞独占锁失败\n";
}
} catch (Exception $e) {
echo "锁定操作异常: " . $e->getMessage() . "\n";
}
// 清理
unlink($lockFile);
?>
9.5.2 并发安全的文件操作
<?php
// 并发安全的文件操作
echo "=== 并发安全的文件操作 ===\n";
// 安全的计数器类
class SafeCounter {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
// 初始化计数器文件
if (!file_exists($filename)) {
file_put_contents($filename, '0');
}
}
public function increment() {
$handle = fopen($this->filename, 'r+');
if (!$handle) {
throw new RuntimeException("无法打开计数器文件");
}
try {
// 获取独占锁
if (!flock($handle, LOCK_EX)) {
throw new RuntimeException("无法获取文件锁");
}
// 读取当前值
$current = (int)fread($handle, 1024);
// 增加计数
$new = $current + 1;
// 写回文件
rewind($handle);
ftruncate($handle, 0);
fwrite($handle, (string)$new);
return $new;
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
public function getValue() {
$handle = fopen($this->filename, 'r');
if (!$handle) {
return 0;
}
try {
if (flock($handle, LOCK_SH)) {
$value = (int)fread($handle, 1024);
return $value;
}
return 0;
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
public function reset() {
$handle = fopen($this->filename, 'w');
if (!$handle) {
throw new RuntimeException("无法重置计数器文件");
}
try {
if (flock($handle, LOCK_EX)) {
fwrite($handle, '0');
return true;
}
return false;
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
}
// 测试安全计数器
$counterFile = 'safe_counter.txt';
$counter = new SafeCounter($counterFile);
echo "--- 安全计数器测试 ---\n";
// 重置计数器
$counter->reset();
echo "初始值: " . $counter->getValue() . "\n";
// 模拟并发增加
for ($i = 0; $i < 5; $i++) {
$newValue = $counter->increment();
echo "增加后: $newValue\n";
}
echo "最终值: " . $counter->getValue() . "\n";
// 安全的日志写入类
class SafeLogger {
private $filename;
private $maxSize;
public function __construct($filename, $maxSize = 1048576) { // 1MB
$this->filename = $filename;
$this->maxSize = $maxSize;
}
public function log($level, $message) {
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] [$level] $message\n";
$handle = fopen($this->filename, 'a');
if (!$handle) {
throw new RuntimeException("无法打开日志文件");
}
try {
// 获取独占锁
if (!flock($handle, LOCK_EX)) {
throw new RuntimeException("无法获取日志文件锁");
}
// 检查文件大小
$size = ftell($handle);
if ($size > $this->maxSize) {
$this->rotateLog();
}
// 写入日志
fwrite($handle, $logEntry);
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
private function rotateLog() {
$backupFile = $this->filename . '.old';
// 如果备份文件存在,删除它
if (file_exists($backupFile)) {
unlink($backupFile);
}
// 重命名当前日志文件
rename($this->filename, $backupFile);
}
public function getLogs($lines = 10) {
if (!file_exists($this->filename)) {
return [];
}
$handle = fopen($this->filename, 'r');
if (!$handle) {
return [];
}
try {
if (flock($handle, LOCK_SH)) {
$content = file_get_contents($this->filename);
$allLines = explode("\n", trim($content));
return array_slice($allLines, -$lines);
}
return [];
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
}
echo "\n--- 安全日志测试 ---\n";
$logFile = 'safe_log.txt';
$logger = new SafeLogger($logFile, 1024); // 1KB限制用于测试
// 写入一些日志
$logger->log('INFO', '应用程序启动');
$logger->log('DEBUG', '调试信息');
$logger->log('WARNING', '警告信息');
$logger->log('ERROR', '错误信息');
// 读取日志
$logs = $logger->getLogs(5);
echo "最近的日志条目:\n";
foreach ($logs as $log) {
if (!empty($log)) {
echo " $log\n";
}
}
// 清理测试文件
unlink($counterFile);
if (file_exists($logFile)) {
unlink($logFile);
}
if (file_exists($logFile . '.old')) {
unlink($logFile . '.old');
}
?>
9.6 特殊文件格式处理
9.6.1 CSV文件处理
<?php
// CSV文件处理
echo "=== CSV文件处理 ===\n";
echo "--- CSV写入 ---\n";
// CSV写入示例
$csvFile = 'test_data.csv';
// 准备测试数据
$data = [
['姓名', '年龄', '城市', '薪资'],
['张三', 25, '北京', 8000],
['李四', 30, '上海', 12000],
['王五', 28, '广州', 10000],
['赵六', 35, '深圳', 15000]
];
// 使用fputcsv写入CSV
$handle = fopen($csvFile, 'w');
if ($handle) {
// 设置UTF-8 BOM(用于Excel正确显示中文)
fwrite($handle, "\xEF\xBB\xBF");
foreach ($data as $row) {
fputcsv($handle, $row);
}
fclose($handle);
echo "✓ CSV文件写入成功\n";
}
echo "\n--- CSV读取 ---\n";
// 使用fgetcsv读取CSV
$handle = fopen($csvFile, 'r');
if ($handle) {
// 跳过BOM
if (fread($handle, 3) !== "\xEF\xBB\xBF") {
rewind($handle);
}
$rowNum = 0;
while (($row = fgetcsv($handle)) !== false) {
$rowNum++;
echo "行 $rowNum: " . implode(' | ', $row) . "\n";
}
fclose($handle);
}
echo "\n--- CSV处理类 ---\n";
// CSV处理类
class CSVProcessor {
private $filename;
private $delimiter;
private $enclosure;
private $escape;
public function __construct($filename, $delimiter = ',', $enclosure = '"', $escape = '\\') {
$this->filename = $filename;
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escape = $escape;
}
public function writeData($data, $headers = null) {
$handle = fopen($this->filename, 'w');
if (!$handle) {
throw new RuntimeException("无法创建CSV文件: {$this->filename}");
}
try {
// 写入UTF-8 BOM
fwrite($handle, "\xEF\xBB\xBF");
// 写入表头
if ($headers) {
fputcsv($handle, $headers, $this->delimiter, $this->enclosure, $this->escape);
}
// 写入数据
foreach ($data as $row) {
fputcsv($handle, $row, $this->delimiter, $this->enclosure, $this->escape);
}
return true;
} finally {
fclose($handle);
}
}
public function readData($hasHeaders = true) {
if (!file_exists($this->filename)) {
throw new InvalidArgumentException("CSV文件不存在: {$this->filename}");
}
$handle = fopen($this->filename, 'r');
if (!$handle) {
throw new RuntimeException("无法读取CSV文件: {$this->filename}");
}
try {
// 检查并跳过BOM
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
$data = [];
$headers = null;
if ($hasHeaders) {
$headers = fgetcsv($handle, 0, $this->delimiter, $this->enclosure, $this->escape);
}
while (($row = fgetcsv($handle, 0, $this->delimiter, $this->enclosure, $this->escape)) !== false) {
if ($hasHeaders && $headers) {
$data[] = array_combine($headers, $row);
} else {
$data[] = $row;
}
}
return [
'headers' => $headers,
'data' => $data
];
} finally {
fclose($handle);
}
}
public function filterData($filterCallback) {
$result = $this->readData(true);
$filteredData = array_filter($result['data'], $filterCallback);
return [
'headers' => $result['headers'],
'data' => array_values($filteredData)
];
}
public function sortData($sortField, $ascending = true) {
$result = $this->readData(true);
usort($result['data'], function($a, $b) use ($sortField, $ascending) {
$valueA = $a[$sortField] ?? '';
$valueB = $b[$sortField] ?? '';
// 尝试数值比较
if (is_numeric($valueA) && is_numeric($valueB)) {
$comparison = $valueA <=> $valueB;
} else {
$comparison = strcasecmp($valueA, $valueB);
}
return $ascending ? $comparison : -$comparison;
});
return [
'headers' => $result['headers'],
'data' => $result['data']
];
}
}
// 测试CSV处理类
$processor = new CSVProcessor('employees.csv');
// 写入员工数据
$employees = [
['张三', 25, '开发部', 8000],
['李四', 30, '设计部', 12000],
['王五', 28, '开发部', 10000],
['赵六', 35, '管理部', 15000],
['钱七', 26, '开发部', 9000]
];
$headers = ['姓名', '年龄', '部门', '薪资'];
$processor->writeData($employees, $headers);
echo "✓ 员工数据写入成功\n";
// 读取数据
$result = $processor->readData(true);
echo "\n读取的数据:\n";
foreach ($result['data'] as $employee) {
echo " {$employee['姓名']} - {$employee['年龄']}岁 - {$employee['部门']} - {$employee['薪资']}元\n";
}
// 过滤数据(开发部员工)
$filtered = $processor->filterData(function($employee) {
return $employee['部门'] === '开发部';
});
echo "\n开发部员工:\n";
foreach ($filtered['data'] as $employee) {
echo " {$employee['姓名']} - {$employee['薪资']}元\n";
}
// 按薪资排序
$sorted = $processor->sortData('薪资', false); // 降序
echo "\n按薪资排序(降序):\n";
foreach ($sorted['data'] as $employee) {
echo " {$employee['姓名']} - {$employee['薪资']}元\n";
}
// 清理
unlink($csvFile);
unlink('employees.csv');
?>
9.6.2 JSON文件处理
<?php
// JSON文件处理
echo "=== JSON文件处理 ===\n";
echo "--- JSON基础操作 ---\n";
// JSON文件处理类
class JSONFileManager {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
}
public function write($data, $prettyPrint = true) {
$flags = JSON_UNESCAPED_UNICODE;
if ($prettyPrint) {
$flags |= JSON_PRETTY_PRINT;
}
$json = json_encode($data, $flags);
if ($json === false) {
throw new RuntimeException('JSON编码失败: ' . json_last_error_msg());
}
$bytes = file_put_contents($this->filename, $json, LOCK_EX);
if ($bytes === false) {
throw new RuntimeException("写入JSON文件失败: {$this->filename}");
}
return $bytes;
}
public function read($assoc = true) {
if (!file_exists($this->filename)) {
throw new InvalidArgumentException("JSON文件不存在: {$this->filename}");
}
$content = file_get_contents($this->filename);
if ($content === false) {
throw new RuntimeException("读取JSON文件失败: {$this->filename}");
}
$data = json_decode($content, $assoc);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('JSON解码失败: ' . json_last_error_msg());
}
return $data;
}
public function update($callback) {
$data = $this->read(true);
$newData = $callback($data);
return $this->write($newData);
}
public function append($newItem, $arrayKey = null) {
return $this->update(function($data) use ($newItem, $arrayKey) {
if ($arrayKey) {
if (!isset($data[$arrayKey]) || !is_array($data[$arrayKey])) {
$data[$arrayKey] = [];
}
$data[$arrayKey][] = $newItem;
} else {
if (!is_array($data)) {
$data = [];
}
$data[] = $newItem;
}
return $data;
});
}
public function merge($newData) {
return $this->update(function($data) use ($newData) {
if (is_array($data) && is_array($newData)) {
return array_merge_recursive($data, $newData);
}
return $newData;
});
}
public function validate($schema = null) {
try {
$data = $this->read(true);
// 基础验证
if (!is_array($data) && !is_object($data)) {
return ['valid' => false, 'error' => 'JSON数据必须是对象或数组'];
}
// 如果提供了schema,进行schema验证
if ($schema && is_callable($schema)) {
$schemaResult = $schema($data);
if (!$schemaResult) {
return ['valid' => false, 'error' => 'Schema验证失败'];
}
}
return ['valid' => true];
} catch (Exception $e) {
return ['valid' => false, 'error' => $e->getMessage()];
}
}
}
// 测试JSON文件管理
$jsonFile = 'test_data.json';
$jsonManager = new JSONFileManager($jsonFile);
// 写入初始数据
$initialData = [
'application' => [
'name' => 'PHP教程示例',
'version' => '1.0.0',
'author' => 'PHP开发者'
],
'users' => [
['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com']
],
'settings' => [
'debug' => true,
'timezone' => 'Asia/Shanghai'
]
];
$jsonManager->write($initialData);
echo "✓ JSON数据写入成功\n";
// 读取数据
$data = $jsonManager->read();
echo "\n应用信息: {$data['application']['name']} v{$data['application']['version']}\n";
echo "用户数量: " . count($data['users']) . "\n";
// 添加新用户
$newUser = ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'];
$jsonManager->append($newUser, 'users');
echo "\n✓ 新用户添加成功\n";
// 更新设置
$jsonManager->merge([
'settings' => [
'cache_enabled' => true,
'max_users' => 1000
]
]);
echo "✓ 设置更新成功\n";
// 验证JSON文件
$validation = $jsonManager->validate(function($data) {
// 简单的schema验证
return isset($data['application']) &&
isset($data['users']) &&
is_array($data['users']) &&
isset($data['settings']);
});
if ($validation['valid']) {
echo "✓ JSON文件验证通过\n";
} else {
echo "✗ JSON文件验证失败: {$validation['error']}\n";
}
// 显示最终数据
$finalData = $jsonManager->read();
echo "\n最终用户列表:\n";
foreach ($finalData['users'] as $user) {
echo " ID: {$user['id']}, 姓名: {$user['name']}, 邮箱: {$user['email']}\n";
}
echo "\n最终设置:\n";
foreach ($finalData['settings'] as $key => $value) {
$valueStr = is_bool($value) ? ($value ? 'true' : 'false') : $value;
echo " $key: $valueStr\n";
}
// 配置文件管理示例
echo "\n--- 配置文件管理 ---\n";
class ConfigManager {
private $jsonManager;
private $config;
public function __construct($configFile) {
$this->jsonManager = new JSONFileManager($configFile);
$this->loadConfig();
}
private function loadConfig() {
try {
$this->config = $this->jsonManager->read(true);
} catch (Exception $e) {
// 如果配置文件不存在,创建默认配置
$this->config = $this->getDefaultConfig();
$this->save();
}
}
private function getDefaultConfig() {
return [
'database' => [
'host' => 'localhost',
'port' => 3306,
'username' => 'root',
'password' => '',
'database' => 'test'
],
'cache' => [
'enabled' => true,
'ttl' => 3600,
'driver' => 'file'
],
'logging' => [
'level' => 'info',
'file' => 'app.log'
]
];
}
public function get($key, $default = null) {
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return $default;
}
$value = $value[$k];
}
return $value;
}
public function set($key, $value) {
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $k) {
if (!isset($config[$k])) {
$config[$k] = [];
}
$config = &$config[$k];
}
$config = $value;
return $this->save();
}
public function save() {
return $this->jsonManager->write($this->config);
}
public function getAll() {
return $this->config;
}
}
// 测试配置管理
$configFile = 'app_config.json';
$config = new ConfigManager($configFile);
echo "数据库主机: " . $config->get('database.host') . "\n";
echo "缓存启用: " . ($config->get('cache.enabled') ? '是' : '否') . "\n";
echo "日志级别: " . $config->get('logging.level') . "\n";
// 更新配置
$config->set('database.host', '192.168.1.100');
$config->set('cache.ttl', 7200);
$config->set('app.name', 'PHP教程应用');
echo "\n✓ 配置更新成功\n";
echo "新的数据库主机: " . $config->get('database.host') . "\n";
echo "新的缓存TTL: " . $config->get('cache.ttl') . "\n";
echo "应用名称: " . $config->get('app.name') . "\n";
// 清理测试文件
unlink($jsonFile);
unlink($configFile);
?>
9.7 路径操作与文件系统函数
9.7.1 路径处理
<?php
// 路径处理
echo "=== 路径处理 ===\n";
echo "--- 基础路径函数 ---\n";
$testPaths = [
'/home/user/documents/file.txt',
'C:\\Users\\User\\Documents\\file.txt',
'../relative/path/file.txt',
'./current/file.txt',
'simple_file.txt'
];
foreach ($testPaths as $path) {
echo "\n路径: $path\n";
echo " 目录名: " . dirname($path) . "\n";
echo " 基名: " . basename($path) . "\n";
echo " 扩展名: " . pathinfo($path, PATHINFO_EXTENSION) . "\n";
echo " 文件名(无扩展名): " . pathinfo($path, PATHINFO_FILENAME) . "\n";
$pathInfo = pathinfo($path);
echo " 完整路径信息:\n";
foreach ($pathInfo as $key => $value) {
echo " $key: $value\n";
}
}
echo "\n--- 路径操作类 ---\n";
// 路径操作类
class PathHelper {
public static function normalize($path) {
// 统一路径分隔符
$path = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);
// 处理相对路径
$parts = explode(DIRECTORY_SEPARATOR, $path);
$normalized = [];
foreach ($parts as $part) {
if ($part === '' || $part === '.') {
continue;
} elseif ($part === '..') {
if (!empty($normalized) && end($normalized) !== '..') {
array_pop($normalized);
} else {
$normalized[] = $part;
}
} else {
$normalized[] = $part;
}
}
$result = implode(DIRECTORY_SEPARATOR, $normalized);
// 保持绝对路径的前导分隔符
if (strpos($path, DIRECTORY_SEPARATOR) === 0) {
$result = DIRECTORY_SEPARATOR . $result;
}
return $result;
}
public static function join(...$parts) {
$path = implode(DIRECTORY_SEPARATOR, $parts);
return self::normalize($path);
}
public static function isAbsolute($path) {
if (DIRECTORY_SEPARATOR === '\\') {
// Windows
return preg_match('/^[a-zA-Z]:\\/', $path) || strpos($path, '\\\\') === 0;
} else {
// Unix-like
return strpos($path, '/') === 0;
}
}
public static function makeAbsolute($path, $basePath = null) {
if (self::isAbsolute($path)) {
return self::normalize($path);
}
$basePath = $basePath ?: getcwd();
return self::normalize(self::join($basePath, $path));
}
public static function getRelativePath($from, $to) {
$from = self::normalize($from);
$to = self::normalize($to);
$fromParts = explode(DIRECTORY_SEPARATOR, $from);
$toParts = explode(DIRECTORY_SEPARATOR, $to);
// 找到公共前缀
$commonLength = 0;
$minLength = min(count($fromParts), count($toParts));
for ($i = 0; $i < $minLength; $i++) {
if ($fromParts[$i] === $toParts[$i]) {
$commonLength++;
} else {
break;
}
}
// 构建相对路径
$relativeParts = [];
// 添加 ".." 用于回到公共祖先
for ($i = $commonLength; $i < count($fromParts); $i++) {
$relativeParts[] = '..';
}
// 添加目标路径的剩余部分
for ($i = $commonLength; $i < count($toParts); $i++) {
$relativeParts[] = $toParts[$i];
}
return empty($relativeParts) ? '.' : implode(DIRECTORY_SEPARATOR, $relativeParts);
}
public static function changeExtension($path, $newExtension) {
$pathInfo = pathinfo($path);
$newExtension = ltrim($newExtension, '.');
$newPath = $pathInfo['dirname'] . DIRECTORY_SEPARATOR . $pathInfo['filename'];
if (!empty($newExtension)) {
$newPath .= '.' . $newExtension;
}
return self::normalize($newPath);
}
public static function ensureDirectory($path) {
$directory = is_dir($path) ? $path : dirname($path);
if (!is_dir($directory)) {
if (!mkdir($directory, 0755, true)) {
throw new RuntimeException("无法创建目录: $directory");
}
}
return $directory;
}
public static function findFiles($directory, $pattern = '*', $recursive = false) {
$files = [];
if (!is_dir($directory)) {
return $files;
}
$globPattern = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $pattern;
$matches = glob($globPattern);
foreach ($matches as $match) {
if (is_file($match)) {
$files[] = $match;
}
}
if ($recursive) {
$subdirs = glob(rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
foreach ($subdirs as $subdir) {
$files = array_merge($files, self::findFiles($subdir, $pattern, true));
}
}
return $files;
}
}
// 测试路径操作
echo "--- 路径规范化测试 ---\n";
$testPaths = [
'./test/../file.txt',
'/home/user/../user/documents/./file.txt',
'folder//subfolder///file.txt'
];
foreach ($testPaths as $path) {
$normalized = PathHelper::normalize($path);
echo "原路径: $path\n";
echo "规范化: $normalized\n\n";
}
echo "--- 路径连接测试 ---\n";
$joined = PathHelper::join('home', 'user', 'documents', 'file.txt');
echo "连接路径: $joined\n";
$joined2 = PathHelper::join('/var', 'www', '../log', 'app.log');
echo "连接路径2: $joined2\n";
echo "\n--- 绝对路径测试 ---\n";
$relativePath = 'documents/file.txt';
$absolutePath = PathHelper::makeAbsolute($relativePath);
echo "相对路径: $relativePath\n";
echo "绝对路径: $absolutePath\n";
echo "\n--- 相对路径计算 ---\n";
$from = '/home/user/projects/app';
$to = '/home/user/documents/file.txt';
$relative = PathHelper::getRelativePath($from, $to);
echo "从: $from\n";
echo "到: $to\n";
echo "相对路径: $relative\n";
echo "\n--- 扩展名更改 ---\n";
$originalFile = '/path/to/document.txt';
$newFile = PathHelper::changeExtension($originalFile, 'pdf');
echo "原文件: $originalFile\n";
echo "新文件: $newFile\n";
// 创建测试目录结构
$testDir = 'path_test';
PathHelper::ensureDirectory($testDir . '/subdir1/subdir2');
file_put_contents($testDir . '/file1.txt', 'test');
file_put_contents($testDir . '/file2.log', 'test');
file_put_contents($testDir . '/subdir1/file3.txt', 'test');
file_put_contents($testDir . '/subdir1/subdir2/file4.txt', 'test');
echo "\n--- 文件查找测试 ---\n";
$txtFiles = PathHelper::findFiles($testDir, '*.txt', false);
echo "当前目录的txt文件:\n";
foreach ($txtFiles as $file) {
echo " $file\n";
}
$allTxtFiles = PathHelper::findFiles($testDir, '*.txt', true);
echo "\n递归查找的txt文件:\n";
foreach ($allTxtFiles as $file) {
echo " $file\n";
}
// 清理测试目录
function removeDirectory($dir) {
if (!is_dir($dir)) return;
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
removeDirectory($testDir);
?>
9.8 本章小结
本章详细介绍了PHP中的文件系统操作,涵盖了以下主要内容:
核心知识点
文件基础操作
- 文件信息获取(大小、权限、时间戳等)
- 文件读取(一次性读取、逐行读取、按块读取)
- 文件写入(覆盖、追加、二进制写入)
- 文件指针操作和二进制文件处理
目录操作
- 目录创建、删除、重命名
- 目录遍历(递归和非递归)
- 目录权限管理
- 目录监控和变化检测
文件上传处理
- 上传错误处理和验证
- 安全的文件上传实现
- 多文件上传处理
- 文件类型和大小限制
文件权限与安全
- 文件权限管理(读、写、执行)
- 文件安全检查(文件名、内容、大小)
- 危险文件类型识别
- 文件名清理和验证
文件锁定与并发控制
- 文件锁定机制(共享锁、独占锁)
- 并发安全的文件操作
- 安全计数器和日志系统实现
特殊文件格式处理
- CSV文件读写和处理
- JSON文件操作和配置管理
- 数据过滤、排序和验证
路径操作
- 路径规范化和连接
- 绝对路径和相对路径转换
- 文件查找和目录遍历
- 跨平台路径处理
最佳实践
安全性
- 始终验证用户输入的文件名和路径
- 使用白名单验证文件类型
- 设置适当的文件权限
- 防止目录遍历攻击
性能优化
- 使用适当的读写缓冲区大小
- 对大文件使用流式处理
- 合理使用文件锁定避免死锁
- 缓存文件信息减少系统调用
错误处理
- 检查文件操作的返回值
- 使用异常处理机制
- 提供有意义的错误信息
- 实现优雅的降级处理
代码组织
- 封装文件操作到专门的类中
- 使用依赖注入提高可测试性
- 分离业务逻辑和文件操作
- 遵循单一职责原则
9.9 实践练习
练习1:文件备份系统
创建一个文件备份系统,要求: - 支持增量备份和完整备份 - 压缩备份文件 - 备份文件完整性验证 - 自动清理过期备份
练习2:日志分析工具
开发一个日志分析工具,功能包括: - 解析不同格式的日志文件 - 统计访问量、错误率等指标 - 生成分析报告 - 支持大文件流式处理
练习3:文件同步工具
实现一个简单的文件同步工具: - 比较源目录和目标目录的差异 - 同步新增、修改、删除的文件 - 处理文件冲突 - 支持排除规则
练习4:配置管理系统
构建一个配置管理系统: - 支持多种配置格式(JSON、YAML、INI) - 配置文件热重载 - 配置验证和默认值 - 环境变量覆盖
练习5:文件上传组件
开发一个完整的文件上传组件: - 支持拖拽上传 - 文件预览和进度显示 - 断点续传 - 图片自动压缩和缩略图生成
9.10 扩展阅读
PHP官方文档
相关技术
- SPL文件处理类
- 流(Streams)处理
- 文件系统监控
- 分布式文件系统
安全资源
- OWASP文件上传安全指南
- 文件系统安全最佳实践
- 路径遍历攻击防护
下一章预告:第十章将介绍《数据库操作》,包括PDO使用、SQL查询构建、事务处理、连接池管理等内容。 “`