| package main
|
|
|
| import (
|
| "crypto/sha1"
|
| "crypto/sha256"
|
| "encoding/hex"
|
| "flag"
|
| "fmt"
|
| "hash"
|
| "io"
|
| "net/http"
|
| "os"
|
| "path/filepath"
|
| "strings"
|
| "time"
|
| )
|
|
|
|
|
|
|
|
|
| func main() {
|
| url := flag.String("url", "", "URL to download smartctl.exe from (required)")
|
| out := flag.String("out", "", "Destination path for smartctl.exe (required)")
|
| sha := flag.String("sha", "", "Optional SHA1/SHA256 checksum for integrity validation")
|
| force := flag.Bool("force", false, "Force re-download even if destination exists")
|
| flag.Parse()
|
|
|
| if *url == "" || *out == "" {
|
| fatalf("-url and -out are required")
|
| }
|
|
|
| if !*force {
|
| if info, err := os.Stat(*out); err == nil && info.Size() > 0 {
|
| fmt.Println("smartctl.exe already present, skipping download")
|
| return
|
| }
|
| }
|
|
|
| if err := downloadFile(*url, *out, *sha); err != nil {
|
| fatalf("download failed: %v", err)
|
| }
|
| }
|
|
|
| func downloadFile(url, dest, shaHex string) error {
|
|
|
| if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
| return fmt.Errorf("create dir: %w", err)
|
| }
|
|
|
|
|
| client := &http.Client{Timeout: 60 * time.Second}
|
| req, err := http.NewRequest(http.MethodGet, url, nil)
|
| if err != nil {
|
| return fmt.Errorf("new request: %w", err)
|
| }
|
| req.Header.Set("User-Agent", "beszel-fetchsmartctl/1.0")
|
|
|
| resp, err := client.Do(req)
|
| if err != nil {
|
| return fmt.Errorf("http get: %w", err)
|
| }
|
| defer resp.Body.Close()
|
|
|
| if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| return fmt.Errorf("unexpected HTTP status: %s", resp.Status)
|
| }
|
|
|
| tmp := dest + ".tmp"
|
| f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
| if err != nil {
|
| return fmt.Errorf("open tmp: %w", err)
|
| }
|
|
|
|
|
| var hasher hash.Hash
|
| if shaHex := strings.TrimSpace(shaHex); shaHex != "" {
|
| cleanSha := strings.ToLower(strings.ReplaceAll(shaHex, " ", ""))
|
| switch len(cleanSha) {
|
| case 40:
|
| hasher = sha1.New()
|
| case 64:
|
| hasher = sha256.New()
|
| default:
|
| f.Close()
|
| os.Remove(tmp)
|
| return fmt.Errorf("unsupported hash length: %d (expected 40 for SHA1 or 64 for SHA256)", len(cleanSha))
|
| }
|
| }
|
|
|
| var mw io.Writer = f
|
| if hasher != nil {
|
| mw = io.MultiWriter(f, hasher)
|
| }
|
| if _, err := io.Copy(mw, resp.Body); err != nil {
|
| f.Close()
|
| os.Remove(tmp)
|
| return fmt.Errorf("write tmp: %w", err)
|
| }
|
| if err := f.Close(); err != nil {
|
| os.Remove(tmp)
|
| return fmt.Errorf("close tmp: %w", err)
|
| }
|
|
|
| if hasher != nil && shaHex != "" {
|
| cleanSha := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(shaHex), " ", ""))
|
| got := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
| if got != cleanSha {
|
| os.Remove(tmp)
|
| return fmt.Errorf("hash mismatch: got %s want %s", got, cleanSha)
|
| }
|
| }
|
|
|
|
|
| if err := os.Chmod(tmp, 0o755); err != nil {
|
| os.Remove(tmp)
|
| return fmt.Errorf("chmod: %w", err)
|
| }
|
| if err := os.Rename(tmp, dest); err != nil {
|
| os.Remove(tmp)
|
| return fmt.Errorf("rename: %w", err)
|
| }
|
|
|
| fmt.Println("smartctl.exe downloaded to", dest)
|
| return nil
|
| }
|
|
|
| func fatalf(format string, a ...any) {
|
| fmt.Fprintf(os.Stderr, format+"\n", a...)
|
| os.Exit(1)
|
| }
|
|
|