{"record":{"id":"350a847df0ea6604","repo":"getgrav/grav","slug":"unable-to-s-s","errorCode":null,"errorMessage":"Unable to %s: %s","messagePattern":"Unable to (.+?): (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Filesystem/Folder.php","lineNumber":430,"sourceCode":"     * @param  bool   $include_target\n     * @return bool\n     * @throws RuntimeException\n     */\n    public static function delete($target, $include_target = true)\n    {\n        if (!is_dir($target)) {\n            return false;\n        }\n\n        $failure = null;\n        $success = self::doDelete($target, $include_target, $failure);\n\n        if (!$success) {\n            // Prefer the precise reason captured at the first failing path\n            // (which file/dir, the OS error, and any owner/process mismatch).\n            // Fall back to the last PHP error, then to a generic message.\n            if (null !== $failure) {\n                throw new RuntimeException($failure);\n            }\n\n            $error = error_get_last();\n\n            throw new RuntimeException($error['message'] ?? 'Unknown error');\n        }\n\n        // Make sure that the change will be detected when caching.\n        if ($include_target) {\n            @touch(dirname($target));\n        } else {\n            @touch($target);\n        }\n\n        return $success;\n    }\n\n    /**","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Filesystem/Folder.php#L412-L448","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the message: it names the failing path, the OS error, and any owner-vs-process mismatch — fix that exact path first","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)","Loosen permissions only as needed: find cache -type d -exec chmod 755 {} + and files 644, avoiding 0777","On NFS/Windows, ensure no process holds files open and the mount permits the operating user to unlink"],"exampleFix":"# before — cron runs as root, files owned by www-data\n0 3 * * * php /var/www/grav/bin/grav clear-cache  # throws 'Unable to delete file: ... (Permission denied)'\n\n# after — run as the web user\n0 3 * * * sudo -u www-data php /var/www/grav/bin/grav clear-cache\n# or fix ownership once:\nchown -R www-data:www-data /var/www/grav/{cache,logs,tmp,images}","handlingStrategy":"try-catch","validationCode":"// pre-flight: can this process remove everything under the target?\n$iterator = new RecursiveIteratorIterator(\n    new RecursiveDirectoryIterator($target, FilesystemIterator::SKIP_DOTS),\n    RecursiveIteratorIterator::SELF_FIRST\n);\nforeach ($iterator as $file) {\n    if (!is_writable($file->getPathname())) {\n        throw new RuntimeException('Not removable (perms): ' . $file->getPathname());\n        break;\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    Folder::delete($dir);\n} catch (RuntimeException $e) {\n    // message names the path, OS reason, and owner/process mismatch — log it whole\n    $log->warning('Cache purge failed: ' . $e->getMessage());\n    // safe to retry once after chown/chmod, otherwise report\n}","preventionTips":["Run CLI tasks (clear-cache, scheduler) as the same user the web server runs as (sudo -u www-data)","chown the writable tree (cache/, logs/, tmp/, images/, backup/) to the web user at deploy time","Monitor backup/cron output so the first 'Permission denied' is fixed before files accumulate"],"tags":["php","filesystem","permissions","file-deletion","ownership"],"backgroundTag":"permission-denied-file-delete","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}