Fix critical rollback download URL bug and doc inconsistencies

Issues found during systematic audit after #642:

1. CRITICAL BUG - Rollback downloads were completely broken:
   - Code constructed: pulse-linux-amd64 (no version, no .tar.gz)
   - Actual asset name: pulse-v4.26.1-linux-amd64.tar.gz
   - This would cause 404 errors on all rollback attempts
   - Fixed: Construct correct tarball URL with version
   - Added: Extract tarball after download to get binary

2. TEMPERATURE_MONITORING.md referenced non-existent v4.27.0:
   - Changed to use /latest/download/ for future-proof docs

3. API.md example had wrong filename format:
   - Changed pulse-linux-amd64.tar.gz to pulse-v4.30.0-linux-amd64.tar.gz
   - Ensures example matches actual release asset naming

The rollback bug would have affected any user attempting to roll back
to a previous version via the UI or API.
This commit is contained in:
rcourtman 2025-11-06 14:25:32 +00:00
parent fd3a72606f
commit becda56897
3 changed files with 34 additions and 15 deletions

View file

@ -1014,7 +1014,7 @@ Response example (systemd deployment, v4.24.0+):
"requiresRoot": true,
"rollbackSupport": true,
"estimatedTime": "2-3 minutes",
"downloadUrl": "https://github.com/rcourtman/Pulse/releases/download/v4.30.0/pulse-linux-amd64.tar.gz",
"downloadUrl": "https://github.com/rcourtman/Pulse/releases/download/v4.30.0/pulse-v4.30.0-linux-amd64.tar.gz",
"instructions": "Run the installer script with --version flag",
"prerequisites": ["systemd", "root access"],
"steps": [
@ -1030,7 +1030,7 @@ Kick off an update using the download URL returned by the release metadata. Puls
POST /api/updates/apply
Content-Type: application/json
{ "downloadUrl": "https://github.com/rcourtman/Pulse/releases/download/v4.30.0/pulse-linux-amd64.tar.gz" }
{ "downloadUrl": "https://github.com/rcourtman/Pulse/releases/download/v4.30.0/pulse-v4.30.0-linux-amd64.tar.gz" }
```
Only deployments that can self-update (systemd, Proxmox VE appliance, AUR) will honour this call. Docker users should continue to pull a new image manually.

View file

@ -629,7 +629,7 @@ If you can't run the installer script, create the configuration manually:
**1. Download binary:**
```bash
curl -L https://github.com/rcourtman/Pulse/releases/download/v4.27.0/pulse-sensor-proxy-linux-amd64 \
curl -L https://github.com/rcourtman/Pulse/releases/latest/download/pulse-sensor-proxy-linux-amd64 \
-o /usr/local/bin/pulse-sensor-proxy
chmod 0755 /usr/local/bin/pulse-sensor-proxy
```

View file

@ -378,8 +378,9 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
}
}
// Download URL
url := fmt.Sprintf("https://github.com/rcourtman/Pulse/releases/download/%s/pulse-linux-%s", version, arch)
// Download URL - tarball with version in filename
tarballName := fmt.Sprintf("pulse-%s-linux-%s.tar.gz", version, arch)
url := fmt.Sprintf("https://github.com/rcourtman/Pulse/releases/download/%s/%s", version, tarballName)
// Create temp file
tmpDir, err := os.MkdirTemp("", "pulse-rollback-*")
@ -387,11 +388,10 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
return "", err
}
localName := fmt.Sprintf("pulse-linux-%s", arch)
binaryPath := filepath.Join(tmpDir, localName)
tarballPath := filepath.Join(tmpDir, tarballName)
// Download binary
cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", binaryPath, url)
// Download tarball
cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", tarballPath, url)
if err := cmd.Run(); err != nil {
os.RemoveAll(tmpDir)
return "", fmt.Errorf("download failed: %w", err)
@ -399,7 +399,7 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
// Download checksum
checksumURL := url + ".sha256"
checksumPath := binaryPath + ".sha256"
checksumPath := tarballPath + ".sha256"
cmd = exec.CommandContext(ctx, "curl", "-fsSL", "-o", checksumPath, checksumURL)
if err := cmd.Run(); err != nil {
os.RemoveAll(tmpDir)
@ -418,10 +418,10 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
return "", fmt.Errorf("checksum file was empty")
}
file, err := os.Open(binaryPath)
file, err := os.Open(tarballPath)
if err != nil {
os.RemoveAll(tmpDir)
return "", fmt.Errorf("failed to open downloaded binary: %w", err)
return "", fmt.Errorf("failed to open downloaded tarball: %w", err)
}
defer file.Close()
@ -434,16 +434,35 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
if !strings.EqualFold(actualHash, expectedHash[0]) {
os.RemoveAll(tmpDir)
return "", fmt.Errorf("checksum verification failed for %s", localName)
return "", fmt.Errorf("checksum verification failed for %s", tarballName)
}
_ = os.Remove(checksumPath)
if err := os.Chmod(binaryPath, 0755); err != nil {
// Extract tarball to get the binary
extractDir := filepath.Join(tmpDir, "extracted")
if err := os.MkdirAll(extractDir, 0755); err != nil {
os.RemoveAll(tmpDir)
return "", err
return "", fmt.Errorf("failed to create extract directory: %w", err)
}
// Extract: tar -xzf tarball -C extractDir
cmd = exec.CommandContext(ctx, "tar", "-xzf", tarballPath, "-C", extractDir)
if err := cmd.Run(); err != nil {
os.RemoveAll(tmpDir)
return "", fmt.Errorf("failed to extract tarball: %w", err)
}
// The binary is at extractDir/bin/pulse
binaryPath := filepath.Join(extractDir, "bin", "pulse")
if _, err := os.Stat(binaryPath); err != nil {
os.RemoveAll(tmpDir)
return "", fmt.Errorf("binary not found in tarball at expected path: %w", err)
}
// Remove tarball to save space
_ = os.Remove(tarballPath)
return binaryPath, nil
}