Allow read-only access to file cache (when needed) (#29693)

* Allow read-only access to file cache (when needed)
* Tweaked and added unit tests
* Skip test_cache_init_entry_fails for windows
This commit is contained in:
Tamara Dahlgren 2022-05-11 16:25:06 -07:00 committed by GitHub
parent 82b916be36
commit 1b254d19c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 47 additions and 2 deletions

View file

@ -5,10 +5,13 @@
"""Test Spack's FileCache.""" """Test Spack's FileCache."""
import os import os
import sys
import pytest import pytest
from spack.util.file_cache import FileCache import llnl.util.filesystem as fs
from spack.util.file_cache import CacheError, FileCache
@pytest.fixture() @pytest.fixture()
@ -55,3 +58,39 @@ def test_write_and_remove_cache_file(file_cache):
# After removal both the file and the lock file should not exist # After removal both the file and the lock file should not exist
assert not os.path.exists(file_cache.cache_path('test.yaml')) assert not os.path.exists(file_cache.cache_path('test.yaml'))
assert not os.path.exists(file_cache._lock_path('test.yaml')) assert not os.path.exists(file_cache._lock_path('test.yaml'))
@pytest.mark.skipif(sys.platform == 'win32',
reason="Not supported on Windows (yet)")
def test_cache_init_entry_fails(file_cache):
"""Test init_entry failures."""
relpath = fs.join_path('test-dir', 'read-only-file.txt')
cachefile = file_cache.cache_path(relpath)
fs.touchp(cachefile)
# Ensure directory causes exception
with pytest.raises(CacheError, match='not a file'):
file_cache.init_entry(os.path.dirname(relpath))
# Ensure non-readable file causes exception
os.chmod(cachefile, 0o200)
with pytest.raises(CacheError, match='Cannot access cache file'):
file_cache.init_entry(relpath)
# Ensure read-only parent causes exception
relpath = fs.join_path('test-dir', 'another-file.txxt')
cachefile = file_cache.cache_path(relpath)
os.chmod(os.path.dirname(cachefile), 0o400)
with pytest.raises(CacheError, match='Cannot access cache dir'):
file_cache.init_entry(relpath)
def test_cache_write_readonly_cache_fails(file_cache):
"""Test writing a read-only cached file."""
filename = 'read-only-file.txt'
path = file_cache.cache_path(filename)
fs.touch(path)
os.chmod(path, 0o400)
with pytest.raises(CacheError, match='Insufficient permissions to write'):
file_cache.write_transaction(filename)

View file

@ -81,7 +81,7 @@ def init_entry(self, key):
if not os.path.isfile(cache_path): if not os.path.isfile(cache_path):
raise CacheError("Cache file is not a file: %s" % cache_path) raise CacheError("Cache file is not a file: %s" % cache_path)
if not os.access(cache_path, os.R_OK | os.W_OK): if not os.access(cache_path, os.R_OK):
raise CacheError("Cannot access cache file: %s" % cache_path) raise CacheError("Cannot access cache file: %s" % cache_path)
else: else:
# if the file is hierarchical, make parent directories # if the file is hierarchical, make parent directories
@ -118,6 +118,12 @@ def write_transaction(self, key):
moves the file into place on top of the old file atomically. moves the file into place on top of the old file atomically.
""" """
filename = self.cache_path(key)
if os.path.exists(filename) and not os.access(filename, os.W_OK):
raise CacheError(
"Insufficient permissions to write to file cache at {0}"
.format(filename))
# TODO: this nested context manager adds a lot of complexity and # TODO: this nested context manager adds a lot of complexity and
# TODO: is pretty hard to reason about in llnl.util.lock. At some # TODO: is pretty hard to reason about in llnl.util.lock. At some
# TODO: point we should just replace it with functions and simplify # TODO: point we should just replace it with functions and simplify