isBackupRateLimited()) { return false; } $backupSettings = $this->server->service->product->settings()->where('key', 'backups')->first(); $backupLimit = $backupSettings?->value ?? 0; // If limit is 0 or not set, allow unlimited backups if ($backupLimit <= 0) { return true; } $proxmox = new Proxmox; $currentBackups = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ 'content' => 'backup', 'vmid' => $this->server->vm_id, ])->json()['data'] ?? []; $currentBackupCount = count($currentBackups); return $currentBackupCount < $backupLimit; } public function backup() { $this->validate([ 'name' => 'required|string|max:255', 'type' => 'required|in:snapshot,suspend,stop', 'compression' => 'required|in:gzip,lzo,zstd,none', ]); if ($this->isBackupRateLimited()) { $remainingSeconds = $this->secondsUntilRateLimitExpires(); $this->notify( sprintf( 'You must wait %s before creating another backup.', $this->formatDurationForHumans($remainingSeconds) ), 'error' ); return; } // Check backup limit $backupSettings = $this->server->service->product->settings()->where('key', 'backups')->first(); $backupLimit = $backupSettings?->value ?? 0; if ($backupLimit > 0) { $proxmox = new Proxmox; $currentBackups = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ 'content' => 'backup', 'vmid' => $this->server->vm_id, ])->json()['data'] ?? []; $currentBackupCount = count($currentBackups); if ($currentBackupCount >= $backupLimit) { $this->notify("Backup limit reached. You can only create {$backupLimit} backup(s). Please delete an existing backup before creating a new one.", 'error'); return; } } $proxmox = new Proxmox; try { $proxmox->request('/nodes/' . $this->server->node->name . '/vzdump', 'post', data: [ 'vmid' => $this->server->vm_id, 'storage' => $this->server->node->backup_location, 'mode' => $this->type, 'compress' => $this->compression == 'none' ? (int) false : $this->compression, 'notes-template' => $this->name, ]); } catch (\Throwable $e) { $this->notify('Failed to start backup: ' . $e->getMessage(), 'error'); return; } Cache::put( $this->getBackupRateLimitCacheKey(), now()->timestamp, now()->addMinutes(self::BACKUP_RATE_LIMIT_MINUTES + 1) ); $this->notify('Backup started', 'success'); $this->visible = false; $this->reset([ 'name', 'type', 'compression', ]); } public function render() { return view('proxmox::backups.create'); } public function pollEligibility(): void { // Intentionally empty. Polling triggers Livewire to re-render, // which re-evaluates canCreateBackup (rate limit + plan limits). } private function isBackupRateLimited(): bool { $lastTimestamp = Cache::get($this->getBackupRateLimitCacheKey()); if (!$lastTimestamp) { return false; } $elapsed = now()->timestamp - (int) $lastTimestamp; if ($elapsed >= self::BACKUP_RATE_LIMIT_MINUTES * 60) { Cache::forget($this->getBackupRateLimitCacheKey()); return false; } return true; } private function secondsUntilRateLimitExpires(): int { if (!$this->isBackupRateLimited()) { return 0; } $lastTimestamp = (int) Cache::get($this->getBackupRateLimitCacheKey()); $elapsed = now()->timestamp - $lastTimestamp; return max(0, (self::BACKUP_RATE_LIMIT_MINUTES * 60) - $elapsed); } private function getBackupRateLimitCacheKey(): string { return 'proxmox-backup-last-create-' . $this->server->id; } private function formatDurationForHumans(int $seconds): string { if ($seconds <= 0) { return 'a moment'; } $minutes = intdiv($seconds, 60); $remainingSeconds = $seconds % 60; if ($minutes > 0 && $remainingSeconds > 0) { return sprintf('%d minute(s) and %d second(s)', $minutes, $remainingSeconds); } if ($minutes > 0) { return sprintf('%d minute(s)', $minutes); } return sprintf('%d second(s)', $remainingSeconds); } }