直接用file_get_contents+file_put_contents会丢失原文件权限、属主、时间戳等元数据,导致web服务器无法读取;正确做法是先写临时文件,再rename原子替换,并用chmod/chown/touch同步元数据。

用 file_get_contents + file_put_contents 会丢权限
直接读写文件内容时,file_put_contents 默认以当前用户 umask 创建新文件,完全不管原文件的权限、所有者、SELinux 上下文或 ACL。哪怕你用 FILE_APPEND 或 LOCK_EX,也只影响写入行为,不继承元数据。
典型现象:原文件是 0644,属主 www-data,执行替换后变成 0600,属主变成运行脚本的用户(比如 ubuntu),Web 服务器直接无法读取。
- 别用
file_put_contents($path, $new_content)直接覆盖 - 别依赖
chmod()补救 —— 你不一定有权限改回原属主,且可能漏掉 sticky bit、ACL 等 - 如果文件在 NFS 或容器卷上,
chown()可能被静默忽略,但权限变更仍会失败
安全替换的核心:原子写 + 元数据复制
正确做法是先写入临时文件(同目录下),再用 rename() 原子替换,最后把原文件的权限、属主、时间戳等拷过去。Linux/macOS 下 rename() 是原子的,不会出现“半新半旧”状态。
关键点在于:临时文件必须和原文件在同一个文件系统(否则 rename() 会退化为 copy+unlink,失去原子性);复制元数据要用 stat() + chmod() + chown() + touch()。
- 用
tempnam(dirname($path), basename($path) . '.tmp.')生成同目录临时路径 - 写完后立即
chmod($temp_path, $original_stat['mode'] & 0777)—— 注意stat()['mode']是十进制带文件类型位,要 & 0777 提纯 - 用
chown($temp_path, $original_stat['uid'])和chgrp($temp_path, $original_stat['gid'])复制属主(需脚本有相应权限) - 用
touch($temp_path, $original_stat['mtime'], $original_stat['atime'])同步时间戳
一行命令做不到,但封装成函数很稳
PHP 没有内置“保留权限的文本替换”,所以得自己封装。下面这个函数能处理绝大多数场景,包括符号链接目标文件(lstat() 替代 stat())、跳过只读文件检查(先 chmod() 再操作):
function replace_file_content(string $path, string $new_content): bool
{
if (!is_file($path)) {
return false;
}
$stat = lstat($path);
$temp = tempnam(dirname($path), '.' . basename($path) . '.tmp.');
if ($temp === false || file_put_contents($temp, $new_content) === false) {
return false;
}
// 复制权限、属主、时间戳
chmod($temp, $stat['mode'] & 0777);
chown($temp, $stat['uid']);
chgrp($temp, $stat['gid']);
touch($temp, $stat['mtime'], $stat['atime']);
// 原子替换
return rename($temp, $path);
}
注意:chown()/chgrp() 在大多数 Web 环境下会被禁用(safe_mode 已废弃,但 open_basedir 或容器限制仍存在),此时函数会失败 —— 这不是 bug,是系统策略在起作用,得靠部署层解决(比如让 PHP 进程以目标属主运行)。
更严格的场景:需要保留 ACL 或扩展属性
如果文件启用了 ACL(getfacl 可见)或 SELinux 上下文(ls -Z),上面的函数无能为力。chmod/chown 不动这些,PHP 也没提供对应扩展函数。
这时候只能调用系统命令(前提是允许 shell_exec):
- ACL:用
getfacl $path | setfacl --set-file=- $temp_path - SELinux:用
ls -Z $path解析上下文,再chcon -u USER -r ROLE -t TYPE $temp_path - 务必校验命令是否成功,失败时应
unlink($temp_path)并报错,不能留脏临时文件
这类操作已经超出 PHP 标准能力边界,本质是运维配置问题 —— 文件权限不该由运行时 PHP 来兜底,而应在部署阶段通过模板、Ansible 或 CI/CD 流水线固化。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











