diff --git a/fsspec/implementations/cached.py b/fsspec/implementations/cached.py index c140f8d61..08b4ad579 100644 --- a/fsspec/implementations/cached.py +++ b/fsspec/implementations/cached.py @@ -76,6 +76,7 @@ def __init__( same_names: bool | None = None, compression=None, cache_mapper: AbstractCacheMapper | None = None, + cache_storage_mode=None, **kwargs, ): """ @@ -118,6 +119,13 @@ def __init__( cache_mapper: AbstractCacheMapper (optional) The object use to map from original filenames to cached filenames. Only one of this and ``same_names`` should be specified. + cache_storage_mode: int (optional) + Permission mode used when creating the cache storage directory, + e.g. ``0o700``. The default of ``None`` leaves the directory at + the system default (governed by the umask), which is the existing + behaviour. Pass an octal mode such as ``0o700`` to keep the cache + directory, and so the cached data files within it, readable by the + owner only. """ super().__init__(**kwargs) if fs is None and target_protocol is None: @@ -137,7 +145,8 @@ def __init__( storage = [cache_storage] else: storage = cache_storage - os.makedirs(storage[-1], exist_ok=True) + self.cache_storage_mode = cache_storage_mode + self._makedirs(storage[-1]) self.storage = storage self.kwargs = target_options or {} self.cache_check = cache_check @@ -184,8 +193,14 @@ def _remove_tempdir(tempdir): except Exception: pass + def _makedirs(self, path): + if self.cache_storage_mode is None: + os.makedirs(path, exist_ok=True) + else: + os.makedirs(path, exist_ok=True, mode=self.cache_storage_mode) + def _mkcache(self): - os.makedirs(self.storage[-1], exist_ok=True) + self._makedirs(self.storage[-1]) def cache_size(self): """Return size of cache in bytes. @@ -452,6 +467,7 @@ def __getattribute__(self, item): "isdir", "_check_file", "_check_cache", + "_makedirs", "_mkcache", "clear_cache", "clear_expired_cache", @@ -815,7 +831,7 @@ def __init__(self, **kwargs): super().__init__(**kw) for storage in self.storage: if not os.path.exists(storage): - os.makedirs(storage, exist_ok=True) + self._makedirs(storage) def _check_file(self, path): self._check_cache() diff --git a/fsspec/implementations/tests/test_cached.py b/fsspec/implementations/tests/test_cached.py index 3a4727902..794eea061 100644 --- a/fsspec/implementations/tests/test_cached.py +++ b/fsspec/implementations/tests/test_cached.py @@ -172,6 +172,27 @@ def test_constructor_kwargs(tmpdir): ) +@pytest.mark.skipif(win, reason="POSIX file permissions") +@pytest.mark.parametrize("protocol", ["filecache", "simplecache", "blockcache"]) +def test_cache_storage_mode(tmp_path, protocol): + import stat + + cache = tmp_path / "cache" # does not exist yet, fsspec must create it + old = os.umask(0o022) + try: + fsspec.filesystem( + protocol, + target_protocol="file", + cache_storage=str(cache), + cache_storage_mode=0o700, + ) + finally: + os.umask(old) + + mode = stat.S_IMODE(os.stat(cache).st_mode) + assert mode & (stat.S_IRWXG | stat.S_IRWXO) == 0, oct(mode) + + def test_idempotent(): import pickle