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

  1. Fix ownership/permissions so the web-server user can delete the files (e.g. chown -R www-data:www-data <module folder>)
  2. Check that the path isn't on a read-only mount and no process holds the files open
  3. Stop conflicting workers/queues that may be reading the module during deletion, then retry
  4. 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

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


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/42f7665b4e3f40ea. Report an issue: GitHub.