Source code for rivia.utils.fs

"""Filesystem utilities."""

from __future__ import annotations

import logging
import os
import tempfile
from pathlib import Path

logger = logging.getLogger(__name__)


[docs] def assert_path_writable(path: str | Path) -> None: """Raise ``PermissionError`` early if *path* cannot be written. Tests the actual filesystem operations that a write requires: - If the file **exists**: attempts a rename to a temporary name and back. On Windows this requires ``DELETE`` access on the source — the same permission GDAL needs to delete-and-rewrite the file. GIS applications (ArcGIS, QGIS, RASMapper) typically hold files open without ``FILE_SHARE_DELETE``, so the rename fails if any of them have the file open, even on network/SMB shares where ``CreateFileW`` checks are unreliable. - If the file **does not exist**: creates and immediately removes a zero-byte sentinel in the parent directory to verify write permission. Parameters ---------- path: Output file path to check. Raises ------ PermissionError If the path cannot be written, with a message identifying the file and suggesting the user close it in any open application. """ p = Path(path).resolve() if p.exists(): tmp = p.with_suffix(p.suffix + ".rivia_write_check") try: os.rename(p, tmp) except OSError as err: msg = ( f"Output file is locked by another application: {p}\n" "Close the file (e.g. in ArcGIS, QGIS, or RASMapper) and retry." ) logger.error(msg) raise PermissionError(msg) from err try: os.rename(tmp, p) except OSError: # Rename-back failed — restore from tmp so we don't lose the file. # Re-raise the original lock check error is not applicable here; # surface this as an unexpected OS error. raise else: parent = p.parent parent.mkdir(parents=True, exist_ok=True) try: fd, tmp_name = tempfile.mkstemp(dir=parent) os.close(fd) os.unlink(tmp_name) except OSError as err: msg = ( f"Cannot write to directory: {parent}\n" "Check that you have write permission to the output folder." ) logger.error(msg) raise PermissionError(msg) from err