Skip to content

Commit

Permalink
TemporaryDirectory fails to cleanup read-only files (Windows, py<3.8)
Browse files Browse the repository at this point in the history
On python < 3.8, TemporaryDirectory fails when cleaning up dirs
containing read-only files on Windows.
  • Loading branch information
dairiki committed Feb 3, 2022
1 parent f47a9e7 commit 6a953eb
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 1 deletion.
53 changes: 53 additions & 0 deletions lektor/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
import stat
import sys
import tempfile
from functools import partial
from itertools import chain


def _ensure_tree_writeable(path: str) -> None:
"""Attempt to ensure that all files in the tree rooted at path are writeable."""
dirscans = []

def fix_mode(path, statfunc):
try:
# paranoia regarding symlink attacks
current_mode = statfunc(follow_symlinks=False).st_mode
if not stat.S_ISLNK(current_mode):
isdir = stat.S_ISDIR(current_mode)
fixed_mode = current_mode | (0o700 if isdir else 0o200)
if current_mode != fixed_mode:
os.chmod(path, fixed_mode)
if isdir:
dirscans.append(os.scandir(path))
except FileNotFoundError:
pass

fix_mode(path, partial(os.stat, path))
for entry in chain.from_iterable(dirscans):
fix_mode(entry.path, entry.stat)


class FixedTemporaryDirectory(tempfile.TemporaryDirectory):
"""A version of tempfile.TemporaryDirectory that works if dir contains read-only files.
On python < 3.8 under Windows, if any read-only files are created
in a TemporaryDirectory, TemporaryDirectory will throw an
exception when it tries to remove them on cleanup. See
https://bugs.python.org/issue26660
This can create issues, e.g., with temporary git repositories since
git creates read-only files in its object store.
"""

def cleanup(self) -> None:
_ensure_tree_writeable(self.name)
super().cleanup()


if sys.version_info >= (3, 8):
TemporaryDirectory = tempfile.TemporaryDirectory
else:
TemporaryDirectory = FixedTemporaryDirectory
2 changes: 1 addition & 1 deletion lektor/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from subprocess import DEVNULL
from subprocess import PIPE
from subprocess import STDOUT
from tempfile import TemporaryDirectory
from typing import Any
from typing import Callable
from typing import ContextManager
Expand All @@ -31,6 +30,7 @@

from werkzeug import urls

from lektor.compat import TemporaryDirectory
from lektor.exception import LektorException
from lektor.utils import locate_executable
from lektor.utils import portable_popen
Expand Down
32 changes: 32 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
from pathlib import Path

import pytest

from lektor.compat import _ensure_tree_writeable
from lektor.compat import FixedTemporaryDirectory
from lektor.compat import TemporaryDirectory


def test_ensure_tree_writeable(tmp_path):
topdir = tmp_path / "topdir"
subdir = topdir / "subdir"
regfile = subdir / "regfile"
subdir.mkdir(parents=True)
regfile.touch(mode=0)
subdir.chmod(0)
topdir.chmod(0)

_ensure_tree_writeable(topdir)

for p in topdir, subdir, regfile:
assert os.access(p, os.W_OK)


@pytest.mark.parametrize("tmpdir_class", [FixedTemporaryDirectory, TemporaryDirectory])
def test_TemporaryDirectory(tmpdir_class):
with tmpdir_class() as tmpdir:
file = Path(tmpdir, "test-file")
file.touch(mode=0)
os.chmod(tmpdir, 0)
assert not os.path.exists(tmpdir)

0 comments on commit 6a953eb

Please sign in to comment.