Leantime/leantime · error · Exception
notification.plugin_cant_remove
Error message
notification.plugin_cant_remove
What it means
During marketplace plugin install/update, the code first removes an existing installation: if app/Plugins/{Folder} is a directory and Laravel's File::deleteDirectory() returns false, it throws the translated 'Could not remove existing plugin'. The marketplace download already succeeded at this point; the failure is purely a filesystem deletion failure on the previous plugin version's directory.
Source
Thrown at app/Domain/Plugins/Services/Plugins.php:725
$filename = $response->header('Content-Disposition');
$filename = substr($filename, strpos($filename, 'filename=') + 9);
$foldername = Str::studly(basename($filename, '.zip'));
$filename = Str::finish($foldername, '.zip');
if (
! file_put_contents(
$temporaryFile = Str::finish(sys_get_temp_dir(), '/').$filename,
$response->body()
)
) {
throw new \Exception(__('notification.plugin_cant_download'));
}
if (
is_dir($pluginDir = "{$this->pluginDirectory}{$foldername}")
&& ! File::deleteDirectory($pluginDir)
) {
throw new \Exception(__('notification.plugin_cant_remove'));
}
if (! mkdir($pluginDir) && ! is_dir($pluginDir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $pluginDir));
}
$zip = new \ZipArchive;
match ($zip->open($temporaryFile)) {
\ZipArchive::ER_EXISTS => throw new \Exception(__('notification.plugin_zip_exists')),
\ZipArchive::ER_INCONS => throw new \Exception(__('notification.plugin_zip_inconsistent')),
\ZipArchive::ER_INVAL => throw new \Exception(__('notification.plugin_zip_invalid_arg')),
\ZipArchive::ER_MEMORY => throw new \Exception(__('notification.plugin_zip_malloc')),
\ZipArchive::ER_NOENT => throw new \Exception(__('notification.plugin_zip_no_file')),
\ZipArchive::ER_NOZIP => throw new \Exception(__('notification.plugin_zip_not_zip')),
\ZipArchive::ER_OPEN => throw new \Exception(__('notification.plugin_zip_cant_open')),
\ZipArchive::ER_READ => throw new \Exception(__('notification.plugin_zip_read_err')),
\ZipArchive::ER_SEEK => throw new \Exception(__('notification.plugin_zip_seek_err')),View on GitHub (pinned to 9a9f49f100)
Solutions
- Fix ownership: chown -R <webuser>: app/Plugins/<Folder> (and the parent), ensure the directory itself is writable (chmod u+w).
- Remove the stale directory manually: rm -rf app/Plugins/<Folder>, then retry the install from the marketplace UI.
- Ensure the app/Plugins mount is writable (remove ':ro' from the docker volume).
- Disable the plugin in Leantime before updating so no process holds files open.
Defensive patterns
Strategy: validation
Validate before calling
// before updating, confirm we can actually remove the previous install
$dir = $pluginDirectory.$foldername;
if (is_dir($dir) && ! is_writable($dir) && ! chmod($dir, 0755)) {
throw new RuntimeException("Cannot remove previous install at {$dir}: fix ownership first");
} Try / catch
try {
$plugins->installMarketplacePlugin($plugin, $version);
} catch (\Exception $e) {
if (str_contains($e->getMessage(), 'remove')) {
// previous install is stuck: surface an actionable ops message
return back()->with('error', "Remove app/Plugins/{$plugin->identifier} manually (permissions), then retry");
}
throw $e;
} Prevention
- Keep app/Plugins owned by the web server user at all times.
- Disable a plugin before updating it.
- Never create plugin files as root (run artisan plugin commands as the web user).
- Keep the app/Plugins volume mounted read-write.
When it happens
Trigger: installMarketplacePlugin() for a plugin whose folder already exists under app/Plugins AND deletion fails: files owned by a different user than the PHP process (e.g. created by root, web server runs as www-data); the app/Plugins volume mounted read-only in a container; restrictive directory permissions (directory not writable, so child files cannot be unlinked); an NFS/bind mount with broken delete semantics.
Common situations: Plugin folders created by a CLI/artisan run as root, then updated from the web UI; docker-compose volume for app/Plugins mounted ':ro'; permissions tightened after initial install.
Related errors
- Directory "%s" was not created
- notifications.plugin_install_cant_find_composer
- notification.plugin_cant_download
- notification.plugin_zip_no_file
- notification.plugin_zip_cant_open
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/af2f6194ca3cbffc.
Report an issue: GitHub.