getgrav/grav · error · RuntimeException

Unable to %s: %s

Error message

Unable to %s: %s

What it means

Folder::delete() recursively removes a directory via doDelete(), which suppresses per-path PHP errors but records the first failing operation. When deletion fails, the precise reason captured by deleteFailure() is thrown: 'Unable to delete file|remove directory|remove symlink: /path (OS reason)' plus an ownership hint like [owned by "www-data", but this process runs as "cli"] when POSIX data shows a mismatch. The message exists to pinpoint exactly which path failed and why, instead of a bare 'Permission denied'.

Source

Thrown at system/src/Grav/Common/Filesystem/Folder.php:430

     * @param  bool   $include_target
     * @return bool
     * @throws RuntimeException
     */
    public static function delete($target, $include_target = true)
    {
        if (!is_dir($target)) {
            return false;
        }

        $failure = null;
        $success = self::doDelete($target, $include_target, $failure);

        if (!$success) {
            // Prefer the precise reason captured at the first failing path
            // (which file/dir, the OS error, and any owner/process mismatch).
            // Fall back to the last PHP error, then to a generic message.
            if (null !== $failure) {
                throw new RuntimeException($failure);
            }

            $error = error_get_last();

            throw new RuntimeException($error['message'] ?? 'Unknown error');
        }

        // Make sure that the change will be detected when caching.
        if ($include_target) {
            @touch(dirname($target));
        } else {
            @touch($target);
        }

        return $success;
    }

    /**

View on GitHub (pinned to 6040efed04)

Solutions

  1. Read the message: it names the failing path, the OS error, and any owner-vs-process mismatch — fix that exact path first
  2. Align ownership: chown -R <webuser>:<webgroup> cache/ logs/ images/ tmp/ backup/ (or run the CLI as the same user, e.g. sudo -u www-data bin/grav clear-cache)
  3. Loosen permissions only as needed: find cache -type d -exec chmod 755 {} + and files 644, avoiding 0777
  4. On NFS/Windows, ensure no process holds files open and the mount permits the operating user to unlink

Example fix

# before — cron runs as root, files owned by www-data
0 3 * * * php /var/www/grav/bin/grav clear-cache  # throws 'Unable to delete file: ... (Permission denied)'

# after — run as the web user
0 3 * * * sudo -u www-data php /var/www/grav/bin/grav clear-cache
# or fix ownership once:
chown -R www-data:www-data /var/www/grav/{cache,logs,tmp,images}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: can this process remove everything under the target?
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($target, FilesystemIterator::SKIP_DOTS),
    RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
    if (!is_writable($file->getPathname())) {
        throw new RuntimeException('Not removable (perms): ' . $file->getPathname());
        break;
    }
}

Try / catch

try {
    Folder::delete($dir);
} catch (RuntimeException $e) {
    // message names the path, OS reason, and owner/process mismatch — log it whole
    $log->warning('Cache purge failed: ' . $e->getMessage());
    // safe to retry once after chown/chmod, otherwise report
}

Prevention

When it happens

Trigger: Running bin/grav clear-cache or scheduled tasks as a different user than the web server that owns cache//logs/ files; a read-only or full filesystem; files held open/locked (common on Windows or NFS); a symlink whose unlink fails; safe_mode-style permission bits (0444 dirs, immutable flags) preventing rmdir/unlink.

Common situations: Cache dirs created by www-data but cleared from cron as root or vice versa; containerized deployments with mismatched UID/GID between PHP-FPM and CLI; migrated sites preserving old owner IDs; NFS/SMB mounts with root-squash semantics.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/350a847df0ea6604. Report an issue: GitHub.