BookStackApp/BookStack · error · ThemeModuleException
Failed to delete file at "{$itemPath}"
Error message
Failed to delete file at "{$itemPath}" What it means
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.
Source
Thrown at app/Theming/ThemeModuleManager.php:81
$module = $this->loadFromFolder($folderName);
if (!$module) {
throw new ThemeModuleException("Failed to load module from zip file after extraction");
}
return $module;
}
protected function deleteDirectoryRecursively(string $path): void
{
$items = array_diff(scandir($path), ['.', '..']);
foreach ($items as $item) {
$itemPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($itemPath)) {
$this->deleteDirectoryRecursively($itemPath);
} else {
$deleted = unlink($itemPath);
if (!$deleted) {
throw new ThemeModuleException("Failed to delete file at \"{$itemPath}\"");
}
}
}
rmdir($path);
}
public function load(): array
{
if ($this->loadedModules !== null) {
return $this->loadedModules;
}
if (!is_dir($this->modulesFolderPath)) {
return [];
}
$subFolders = array_filter(scandir($this->modulesFolderPath), function ($item) {
return $item !== '.' && $item !== '..' && is_dir($this->modulesFolderPath . DIRECTORY_SEPARATOR . $item);View on GitHub (pinned to 18f8469a1c)
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
Example fix
# diagnose then fix ls -la <path-to-module-folder> sudo chown -R www-data:www-data <path-to-module-folder> sudo rm -rf <path-to-module-folder> # manual fallback before retrying in-app
Defensive patterns
Strategy: try-catch
Validate before calling
function folderIsDeletable(string $path): bool {
if (!is_dir($path)) return false;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS));
foreach ($it as $f) { if (!$f->isWritable()) return false; }
return is_writable(dirname($path));
}
// if (!folderIsDeletable($moduleFolderPath)) { fix perms first } Type guard
function canUnlink(string $filePath): bool {
return is_writable($filePath) && is_writable(dirname($filePath));
} Try / catch
try {
$manager->deleteModuleFolder($folderName);
} catch (ThemeModuleException $e) {
if (str_starts_with($e->getMessage(), 'Failed to delete file at')) {
Log::error('Module cleanup failed', ['detail' => $e->getMessage()]);
// fix permissions or delete manually, then retry
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to load extract files from module ZIP with error: {$e
- errors.chapter_not_found
- errors.page_not_found
- User does not have permission to create a chapter within the
- User does not have permission to create a page within the ne
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/42f7665b4e3f40ea.
Report an issue: GitHub.