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, "requiresRoot": true,
"rollbackSupport": true, "rollbackSupport": true,
"estimatedTime": "2-3 minutes", "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", "instructions": "Run the installer script with --version flag",
"prerequisites": ["systemd", "root access"], "prerequisites": ["systemd", "root access"],
"steps": [ "steps": [
@ -1030,7 +1030,7 @@ Kick off an update using the download URL returned by the release metadata. Puls
POST /api/updates/apply POST /api/updates/apply
Content-Type: application/json 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. 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:** **1. Download binary:**
```bash ```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 -o /usr/local/bin/pulse-sensor-proxy
chmod 0755 /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 // Download URL - tarball with version in filename
url := fmt.Sprintf("https://github.com/rcourtman/Pulse/releases/download/%s/pulse-linux-%s", version, arch) 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 // Create temp file
tmpDir, err := os.MkdirTemp("", "pulse-rollback-*") tmpDir, err := os.MkdirTemp("", "pulse-rollback-*")
@ -387,11 +388,10 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
return "", err return "", err
} }
localName := fmt.Sprintf("pulse-linux-%s", arch) tarballPath := filepath.Join(tmpDir, tarballName)
binaryPath := filepath.Join(tmpDir, localName)
// Download binary // Download tarball
cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", binaryPath, url) cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", tarballPath, url)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
os.RemoveAll(tmpDir) os.RemoveAll(tmpDir)
return "", fmt.Errorf("download failed: %w", err) return "", fmt.Errorf("download failed: %w", err)
@ -399,7 +399,7 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
// Download checksum // Download checksum
checksumURL := url + ".sha256" checksumURL := url + ".sha256"
checksumPath := binaryPath + ".sha256" checksumPath := tarballPath + ".sha256"
cmd = exec.CommandContext(ctx, "curl", "-fsSL", "-o", checksumPath, checksumURL) cmd = exec.CommandContext(ctx, "curl", "-fsSL", "-o", checksumPath, checksumURL)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
os.RemoveAll(tmpDir) os.RemoveAll(tmpDir)
@ -418,10 +418,10 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
return "", fmt.Errorf("checksum file was empty") return "", fmt.Errorf("checksum file was empty")
} }
file, err := os.Open(binaryPath) file, err := os.Open(tarballPath)
if err != nil { if err != nil {
os.RemoveAll(tmpDir) 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() defer file.Close()
@ -434,16 +434,35 @@ func (a *InstallShAdapter) downloadBinary(ctx context.Context, version string) (
if !strings.EqualFold(actualHash, expectedHash[0]) { if !strings.EqualFold(actualHash, expectedHash[0]) {
os.RemoveAll(tmpDir) os.RemoveAll(tmpDir)
return "", fmt.Errorf("checksum verification failed for %s", localName) return "", fmt.Errorf("checksum verification failed for %s", tarballName)
} }
_ = os.Remove(checksumPath) _ = 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) 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 return binaryPath, nil
} }