Leantime/leantime · error · Exception
notification.plugin_zip_no_file
Error message
notification.plugin_zip_no_file
What it means
The match maps ZipArchive::ER_NOENT to 'Zip: No such file'. open() was handed a path to a temp file that no longer exists. In this flow file_put_contents() succeeded moments earlier (Plugins.php:712-719), so between the write and the open the file vanished or the constructed path is wrong.
Source
Thrown at app/Domain/Plugins/Services/Plugins.php:739
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')),
default => throw new \Exception(__('notification.plugin_zip_unknown_err')),
true => null,
};
if (! $zip->extractTo($pluginDir)) {
throw new \Exception(__('notification.plugin_zip_cant_extract'));
}
$zip->close();
unlink($temporaryFile);
// read the composer.json content from the plugin phar file
$pluginModel = $this->createPluginFromComposer($foldername, $plugin->license);View on GitHub (pinned to 9a9f49f100)
Solutions
- Immediately after a failed install, check whether /tmp/{Folder}.zip exists to confirm the vanish vs a bad path.
- Disable or slow down tmp-cleaning daemons/cron for the install window.
- Point sys_temp_dir to a private directory no janitor touches and retry.
- Log $temporaryFile right before open() to rule out Content-Disposition parsing producing a different name than expected.
Defensive patterns
Strategy: validation
Validate before calling
// re-check between write and open
declare(ticks = 0);
$tempFile = Str::finish(sys_get_temp_dir(), '/').$filename;
if (! is_file($tempFile) || filesize($tempFile) !== strlen($response->body())) {
throw new RuntimeException('Temp archive vanished or short after write - check tmp cleaners');
} Prevention
- Disable aggressive /tmp janitors (systemd-tmpfiles, cron cleaners) or point sys_temp_dir to a private directory.
- Avoid concurrent installs of the same plugin (they share the temp filename).
- Log the temp path used so vanish-vs-bad-path is distinguishable.
When it happens
Trigger: installMarketplacePlugin() where /tmp/{Folder}.zip is deleted between write and open: aggressive tmp cleaners (systemd-tmpfiles, docker sidecar janitors, hosting cron jobs purging /tmp), a filename parsed from Content-Disposition that resolves differently than expected, or open_basedir affecting path resolution.
Common situations: Hardened images running periodic /tmp cleanup with short retention; shared hosts purging /tmp every few minutes; concurrent installs racing on the same filename.
Related errors
- notification.plugin_cant_download
- notification.plugin_zip_seek_err
- notifications.plugin_install_cant_find_composer
- notification.plugin_cant_remove
- Directory "%s" was not created
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/9f42c844f325e7af.
Report an issue: GitHub.