春江暮客

春江暮客的个人学习分享网站

用 Python 实测目录写权限:定位 Permission denied、只读挂载与路径错误

2026-10-19 技术
用 Python 实测目录写权限:定位 Permission denied、只读挂载与路径错误

批处理任务计算完才发现输出目录报 Permission denied;换成管理员运行又“好了”,但定时任务依旧失败。本文用 Python 标准库在目标目录实际创建、写入并删除一个小文件,确认当前运行身份此刻能否完成这条写入路径。

这适合初级到中级读者,用于导出、备份和 Hugo 构建前的诊断。如果还需要估算大型产物的空间预算,可配合磁盘空间预检使用。

1. 方法:用实际操作验证写入路径

os.access() 官方文档提醒,权限检查通过后,真实 I/O 仍可能失败。我们的探测直接尝试写入,并记录 errno;它只反映当前账号、当前环境和检查时刻的结果。

顺序是:解析已有目录 → 创建随机命名文件 → 写入 12 字节 → flush → fsync → 关闭并删除。它不会修改已有业务文件,也不会创建缺失目录;它会真实写入临时文件,请对可信、允许测试的目录运行。目录符号链接会解析到实际目标。

2. 准备环境

需要 Linux、Python 3.9 或更新版本,以及运行入口示例所需的 Bash。无需 pip 包,以下示例已在 Linux、Python 3.11.4、普通用户账号下验证。

  1. 新建演示目录;重复练习时换一个新名字:
python3 --version
mkdir write-probe-demo
cd write-probe-demo
mkdir output
  1. 把下一节保存为当前目录中的 write_probe.py,也可下载完整脚本。后续命令都在这个演示目录执行。

3. 实现:返回 JSON 和退出码

"""Probe a trusted existing directory by creating a tiny temporary file. Linux."""
import argparse
import errno
import json
import os
from pathlib import Path
import tempfile


def probe(directory):
    report = {
        "path": os.path.abspath(directory),
        "euid": os.geteuid(),
        "ok": False,
        "stage": "resolve",
    }
    try:
        target = Path(directory).resolve(strict=True)
        report["path"] = str(target)
        report["stage"] = "create"
        with tempfile.NamedTemporaryFile(
            mode="w+b", dir=target, prefix=".write-probe-"
        ) as handle:
            report["stage"] = "write"
            handle.write(b"write-probe\n")
            report["stage"] = "flush"
            handle.flush()
            report["stage"] = "fsync"
            os.fsync(handle.fileno())
            report["stage"] = "cleanup"
        report.update(ok=True, stage="done")
    except (OSError, RuntimeError) as exc:
        number = getattr(exc, "errno", None)
        report.update(
            error=type(exc).__name__,
            errno=number,
            code=errno.errorcode.get(number, "PATH_ERROR"),
            message=str(exc),
        )
    return report


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("directory", help="existing trusted output directory")
    args = parser.parse_args(argv)
    report = probe(args.directory)
    print(json.dumps(report, ensure_ascii=True))
    return 0 if report["ok"] else 1


if __name__ == "__main__":
    raise SystemExit(main())

NamedTemporaryFile 在指定目录创建可见的临时文件,并在正常关闭时删除。with 也会在普通异常时尝试清理;SIGKILL 或掉电可能留下 .write-probe- 文件。

path 是解析后的路径,euid 是实际执行文件操作的有效用户 ID。stage 记录最近开始的步骤;正常完成为 done。若异常退出时清理又失败,清理异常可能覆盖原始异常,因此仍需结合 message 排查。JSON 统一写到 stdout;命令行用法错误由 argparse 写到 stderr。

退出码 含义 后续处理
0 写入、同步与正常清理完成 继续尝试启动任务
1 路径或实际文件操作失败 查看 code、stage、message
2 缺少参数或未知选项 修正调用方式

4. 运行与验证

  1. 查看帮助,检查 output,再立即查看退出码:
python3 write_probe.py --help
python3 write_probe.py output
printf 'exit=%s\n' "$?"

成功时 JSON 包含 "ok": true 和 "stage": "done",随后输出 exit=0;绝对路径和 euid 随机器变化。失败时会返回 "ok": false,并包含如 "code": "EACCES" 的错误信息。

  1. 用一次性测试目录验证正常写入、清理、缺失路径、普通文件路径和无写权限的目录:
python3 - <<'PY_CHECK'
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile

script = str(Path("write_probe.py").resolve())

def check(path, status, code=None):
    result = subprocess.run([sys.executable, script, str(path)],
                            capture_output=True, text=True, check=False)
    report = json.loads(result.stdout)
    assert result.returncode == status, report
    assert report["ok"] is (status == 0), report
    if code:
        assert report["code"] == code, report
    return report

with tempfile.TemporaryDirectory(prefix="probe-test-", dir=".") as name:
    root = Path(name).resolve()
    writable = root / "writable"
    writable.mkdir()
    check(writable, 0)
    assert not list(writable.iterdir())
    check(root / "missing", 1, "ENOENT")
    regular = root / "regular.txt"
    regular.write_text("keep me", encoding="utf-8")
    check(regular, 1, "ENOTDIR")
    assert regular.read_text(encoding="utf-8") == "keep me"
    denied = root / "denied"
    denied.mkdir()
    denied.chmod(0o500)
    try:
        if os.geteuid() == 0:
            print("SKIP: run as a normal user to test EACCES")
        else:
            check(denied, 1, "EACCES")
    finally:
        denied.chmod(0o700)
print("PASS: creation, cleanup, missing path, file path, permission checks")
PY_CHECK

非 root 用户应看到最后的 PASS,且不出现 SKIP。代码只修改自己新建的测试目录权限,并在 finally 恢复;root 可能绕过普通权限位,所以跳过该分支。不要为了测试去修改真实业务目录。

  1. 模拟只读文件系统、空间不足、配额和同步错误,无需填满磁盘或更改挂载:
python3 - <<'PY_ERRORS'
import errno
from pathlib import Path
from unittest.mock import patch
from write_probe import probe

for code in ("EROFS", "ENOSPC", "EDQUOT"):
    number = getattr(errno, code)
    with patch("write_probe.tempfile.NamedTemporaryFile",
               side_effect=OSError(number, code)):
        report = probe("output")
    assert report["ok"] is False and report["code"] == code, report
with patch("write_probe.os.fsync", side_effect=OSError(errno.EIO, "I/O error")):
    report = probe("output")
assert report["code"] == "EIO" and report["stage"] == "fsync", report
assert not list(Path("output").glob(".write-probe-*"))
print("PASS: simulated disk errors and fsync failure cleanup")
PY_ERRORS

应看到 PASS: simulated disk errors and fsync failure cleanup。这是错误处理逻辑测试,不是实际只读挂载或配额测试;正常与权限失败路径则使用真实文件操作。

5. 接入任务入口

将下面内容保存为演示目录中的 run_demo.sh。只有探测成功才会打印 TASK STARTED:

#!/usr/bin/env bash
set -eu
cd -- "$(dirname -- "$0")"
if python3 write_probe.py output; then
  python3 -c 'print("TASK STARTED")'
else
  result=$?
  printf 'Write probe stopped the task (exit %s)\n' "$result" >&2
  exit "$result"
fi

执行:

bash run_demo.sh

接入真实任务时,将打印命令替换为导出或构建命令,使用任务实际输出目录。为便于排错,先创建并确认目标目录,再运行入口。定时任务必须使用同一账号、容器、挂载环境和解释器;工作目录与解释器的设置可参考定时任务路径排错。

6. 排错与边界

先以任务本身的身份检查账号和目录:

id
pwd
ls -ld -- . output

检查父目录是否允许该账号通过,以及输出目录是否允许创建文件。只对本应拥有写权限的账号或组授予必要权限;不要用全目录 chmod 777 或管理员运行掩盖原因。

下面的错误名来自 Python errno 官方文档。错误描述可能因系统语言不同而变化,应优先看 code。

错误 具体处理
ENOENT 核对 JSON 中的路径和工作目录,确认目标应存在后再创建。
ENOTDIR 传入目录路径,检查某一级路径是否误用了普通文件。
EACCES / EPERM 核对运行账号、目录及父目录权限;权限位正确时继续检查 ACL 和安全策略。
EROFS 确认任务所在环境的挂载是否只读;选择获准写入的卷,chmod 无法解除只读挂载。
ENOSPC / EDQUOT 分别检查可用空间、inode 和用户配额。
EIO 或 stage: fsync 失败 保留完整报错,检查存储状态与文件系统支持;不要把同步失败当作成功。
stage: cleanup 失败 检查目录权限是否在运行中变化,并根据报错中的确切文件名处理遗留探测文件。

新建文件成功不能证明可覆盖已有文件。 目标文件权限、粘滞位、不可变属性及原子替换还可能限制后续操作。需要替换 JSON 状态文件时,继续参考原子写入教程。

探测也不预留空间,不测试大文件容量或子目录创建。按 os.fsync() 文档先 flush 再同步,只能检验这次文件同步调用;本例没有同步父目录,不能据此推断掉电后的目录项持久性。网络文件系统上的调用可能阻塞,此脚本没有硬超时;即使探测通过,实际任务仍必须处理自己的 OSError。

7. 小结

在真正输出的目录中,以任务相同的运行身份做小型写入探测,可以比查看权限位更直接地定位故障。先验证成功和失败退出码,再接入任务入口;保留真实写入时的错误处理,避免把检查通过当作后续一定成功。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tcp tensorflow tkinter tls toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它