{"record":{"id":"42f7665b4e3f40ea","repo":"BookStackApp/BookStack","slug":"failed-to-delete-file-at-itempath","errorCode":null,"errorMessage":"Failed to delete file at \"{$itemPath}\"","messagePattern":"Failed to delete file at \"(.+?)\"","errorType":"exception","errorClass":"ThemeModuleException","httpStatus":null,"severity":"error","filePath":"app/Theming/ThemeModuleManager.php","lineNumber":81,"sourceCode":"        $module = $this->loadFromFolder($folderName);\n        if (!$module) {\n            throw new ThemeModuleException(\"Failed to load module from zip file after extraction\");\n        }\n\n        return $module;\n    }\n\n    protected function deleteDirectoryRecursively(string $path): void\n    {\n        $items = array_diff(scandir($path), ['.', '..']);\n        foreach ($items as $item) {\n            $itemPath = $path . DIRECTORY_SEPARATOR . $item;\n            if (is_dir($itemPath)) {\n                $this->deleteDirectoryRecursively($itemPath);\n            } else {\n                $deleted = unlink($itemPath);\n                if (!$deleted) {\n                    throw new ThemeModuleException(\"Failed to delete file at \\\"{$itemPath}\\\"\");\n                }\n            }\n        }\n        rmdir($path);\n    }\n\n    public function load(): array\n    {\n        if ($this->loadedModules !== null) {\n            return $this->loadedModules;\n        }\n\n        if (!is_dir($this->modulesFolderPath)) {\n            return [];\n        }\n\n        $subFolders = array_filter(scandir($this->modulesFolderPath), function ($item) {\n            return $item !== '.' && $item !== '..' && is_dir($this->modulesFolderPath . DIRECTORY_SEPARATOR . $item);","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Theming/ThemeModuleManager.php#L63-L99","documentation":"ThemeModuleManager::deleteDirectoryRecursively throws ThemeModuleException when unlink() fails to remove a file while recursively deleting a module folder. It surfaces filesystem-level problems (permissions, stale handles) during module folder cleanup.","triggerScenarios":"Called by deleteModuleFolder, addFromZip (cleanup after failed extraction), or recursively by itself, when a file at $itemPath cannot be unlinked — e.g. owned by another user, read-only, or held open by a process.","commonSituations":"Module folders created by a different OS user (root vs www-data) or by a previous deployment; read-only mounts; open file handles on the files; immutable/locked attributes; concurrent processes using the folder.","solutions":["Fix ownership/permissions so the web-server user can delete the files (e.g. chown -R www-data:www-data <module folder>)","Check that the path isn't on a read-only mount and no process holds the files open","Stop conflicting workers/queues that may be reading the module during deletion, then retry","Manually remove the folder, then retry the operation in the app"],"exampleFix":"# diagnose then fix\nls -la <path-to-module-folder>\nsudo chown -R www-data:www-data <path-to-module-folder>\nsudo rm -rf <path-to-module-folder>  # manual fallback before retrying in-app","handlingStrategy":"try-catch","validationCode":"function folderIsDeletable(string $path): bool {\n    if (!is_dir($path)) return false;\n    $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS));\n    foreach ($it as $f) { if (!$f->isWritable()) return false; }\n    return is_writable(dirname($path));\n}\n// if (!folderIsDeletable($moduleFolderPath)) { fix perms first }","typeGuard":"function canUnlink(string $filePath): bool {\n    return is_writable($filePath) && is_writable(dirname($filePath));\n}","tryCatchPattern":"try {\n    $manager->deleteModuleFolder($folderName);\n} catch (ThemeModuleException $e) {\n    if (str_starts_with($e->getMessage(), 'Failed to delete file at')) {\n        Log::error('Module cleanup failed', ['detail' => $e->getMessage()]);\n        // fix permissions or delete manually, then retry\n    }\n}","preventionTips":["Keep module folders owned by the same user the app runs as (e.g. www-data)","Avoid creating module files as root during provisioning/CLI tasks","Don't hold open handles on module files during delete/uninstall operations","Check for read-only mounts and immutable flags before deploying module directories"],"tags":["filesystem","permissions","unlink","theming","bookstack"],"backgroundTag":"file-delete-permission-denied","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}