Skip to content

Commit e1fad5d

Browse files
committed
modified: internal/service/site.go
1 parent 698758a commit e1fad5d

File tree

1 file changed

+176
-40
lines changed

1 file changed

+176
-40
lines changed

internal/service/site.go

Lines changed: 176 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"path/filepath"
2020
"regexp"
2121
"sort"
22+
"strconv"
2223
"strings"
2324
"time"
2425

@@ -50,6 +51,10 @@ const (
5051
defaultModSecurityModuleFile = "/usr/local/lsws/modules/mod_security.so"
5152
defaultLiteSpeedRepoScript = "https://repo.litespeed.sh"
5253
defaultRepoScriptTempPath = "/tmp/ols-cli-litespeed-repo.sh"
54+
defaultHTTPUserAgent = "ols-cli/0.1 (+https://github.com/ols/ols-cli)"
55+
defaultHTTPRetryAttempts = 5
56+
defaultHTTPRetryMinDelay = 2 * time.Second
57+
defaultHTTPRetryMaxDelay = 30 * time.Second
5358
wpArchiveURL = "https://wordpress.org/latest.tar.gz"
5459
wpArchiveSHA1URL = "https://wordpress.org/latest.tar.gz.sha1"
5560
wpCLIPharURL = "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
@@ -2854,23 +2859,113 @@ func packagesForPHPUpdate(phpVersion string) []string {
28542859
return []string{"lsphp" + phpVersion, "lsphp" + phpVersion + "-mysql"}
28552860
}
28562861

2857-
func downloadText(url string) (string, error) {
2858-
client := &http.Client{Timeout: 2 * time.Minute}
2859-
resp, err := client.Get(url)
2862+
func newHTTPGetRequest(url string) (*http.Request, error) {
2863+
req, err := http.NewRequest(http.MethodGet, url, nil)
28602864
if err != nil {
2861-
return "", apperr.Wrap(apperr.CodeCommand, "failed to download remote text", err)
2865+
return nil, apperr.Wrap(apperr.CodeValidation, "invalid download URL", err)
28622866
}
2863-
defer resp.Body.Close()
2867+
req.Header.Set("User-Agent", defaultHTTPUserAgent)
2868+
req.Header.Set("Accept", "*/*")
2869+
return req, nil
2870+
}
2871+
2872+
func isRetryableHTTPStatus(status int) bool {
2873+
return status == http.StatusTooManyRequests ||
2874+
status == http.StatusRequestTimeout ||
2875+
status == http.StatusBadGateway ||
2876+
status == http.StatusServiceUnavailable ||
2877+
status == http.StatusGatewayTimeout ||
2878+
status >= 500
2879+
}
28642880

2865-
if resp.StatusCode != http.StatusOK {
2866-
return "", apperr.New(apperr.CodeCommand, fmt.Sprintf("failed to download remote text: http %d", resp.StatusCode))
2881+
func parseRetryAfterDelay(header string) time.Duration {
2882+
raw := strings.TrimSpace(header)
2883+
if raw == "" {
2884+
return 0
2885+
}
2886+
if seconds, err := strconv.Atoi(raw); err == nil {
2887+
if seconds <= 0 {
2888+
return 0
2889+
}
2890+
return time.Duration(seconds) * time.Second
2891+
}
2892+
if t, err := http.ParseTime(raw); err == nil {
2893+
d := time.Until(t)
2894+
if d > 0 {
2895+
return d
2896+
}
28672897
}
2898+
return 0
2899+
}
28682900

2869-
b, err := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
2870-
if err != nil {
2871-
return "", apperr.Wrap(apperr.CodeCommand, "failed to read remote text", err)
2901+
func retryDelayForAttempt(attempt int, retryAfterHeader string) time.Duration {
2902+
if d := parseRetryAfterDelay(retryAfterHeader); d > 0 {
2903+
if d > defaultHTTPRetryMaxDelay {
2904+
return defaultHTTPRetryMaxDelay
2905+
}
2906+
return d
2907+
}
2908+
delay := defaultHTTPRetryMinDelay
2909+
for i := 1; i < attempt; i++ {
2910+
delay *= 2
2911+
if delay >= defaultHTTPRetryMaxDelay {
2912+
return defaultHTTPRetryMaxDelay
2913+
}
28722914
}
2873-
return strings.TrimSpace(string(b)), nil
2915+
if delay > defaultHTTPRetryMaxDelay {
2916+
return defaultHTTPRetryMaxDelay
2917+
}
2918+
return delay
2919+
}
2920+
2921+
func downloadText(url string) (string, error) {
2922+
client := &http.Client{Timeout: 2 * time.Minute}
2923+
var lastErr error
2924+
for attempt := 1; attempt <= defaultHTTPRetryAttempts; attempt++ {
2925+
req, err := newHTTPGetRequest(url)
2926+
if err != nil {
2927+
return "", err
2928+
}
2929+
resp, err := client.Do(req)
2930+
if err != nil {
2931+
lastErr = apperr.Wrap(apperr.CodeCommand, "failed to download remote text", err)
2932+
if attempt < defaultHTTPRetryAttempts {
2933+
time.Sleep(retryDelayForAttempt(attempt, ""))
2934+
continue
2935+
}
2936+
return "", lastErr
2937+
}
2938+
2939+
if resp.StatusCode != http.StatusOK {
2940+
statusCode := resp.StatusCode
2941+
retryAfter := resp.Header.Get("Retry-After")
2942+
retryable := isRetryableHTTPStatus(statusCode)
2943+
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
2944+
resp.Body.Close()
2945+
lastErr = apperr.New(apperr.CodeCommand, fmt.Sprintf("failed to download remote text: http %d", statusCode))
2946+
if retryable && attempt < defaultHTTPRetryAttempts {
2947+
time.Sleep(retryDelayForAttempt(attempt, retryAfter))
2948+
continue
2949+
}
2950+
return "", lastErr
2951+
}
2952+
2953+
b, err := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
2954+
resp.Body.Close()
2955+
if err != nil {
2956+
lastErr = apperr.Wrap(apperr.CodeCommand, "failed to read remote text", err)
2957+
if attempt < defaultHTTPRetryAttempts {
2958+
time.Sleep(retryDelayForAttempt(attempt, ""))
2959+
continue
2960+
}
2961+
return "", lastErr
2962+
}
2963+
return strings.TrimSpace(string(b)), nil
2964+
}
2965+
if lastErr != nil {
2966+
return "", lastErr
2967+
}
2968+
return "", apperr.New(apperr.CodeCommand, "failed to download remote text")
28742969
}
28752970

28762971
func parseExpectedHexDigest(raw string, minLen, maxLen int) (string, error) {
@@ -2915,43 +3010,84 @@ func computeFileSHA512(path string) (string, error) {
29153010

29163011
func downloadToFile(url, destPath string, mode os.FileMode) error {
29173012
client := &http.Client{Timeout: 2 * time.Minute}
2918-
resp, err := client.Get(url)
2919-
if err != nil {
2920-
return apperr.Wrap(apperr.CodeCommand, "failed to download file", err)
2921-
}
2922-
defer resp.Body.Close()
2923-
2924-
if resp.StatusCode != http.StatusOK {
2925-
return apperr.New(apperr.CodeCommand, fmt.Sprintf("failed to download file: http %d", resp.StatusCode))
2926-
}
29273013

29283014
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
29293015
return apperr.Wrap(apperr.CodeConfig, "failed to create destination directory", err)
29303016
}
29313017

2932-
tmp, err := os.CreateTemp(filepath.Dir(destPath), "ols-download-*")
2933-
if err != nil {
2934-
return apperr.Wrap(apperr.CodeConfig, "failed to create temporary download file", err)
2935-
}
2936-
tmpPath := tmp.Name()
2937-
defer func() {
2938-
_ = tmp.Close()
2939-
_ = os.Remove(tmpPath)
2940-
}()
3018+
var lastErr error
3019+
for attempt := 1; attempt <= defaultHTTPRetryAttempts; attempt++ {
3020+
req, err := newHTTPGetRequest(url)
3021+
if err != nil {
3022+
return err
3023+
}
3024+
resp, err := client.Do(req)
3025+
if err != nil {
3026+
lastErr = apperr.Wrap(apperr.CodeCommand, "failed to download file", err)
3027+
if attempt < defaultHTTPRetryAttempts {
3028+
time.Sleep(retryDelayForAttempt(attempt, ""))
3029+
continue
3030+
}
3031+
return lastErr
3032+
}
3033+
3034+
if resp.StatusCode != http.StatusOK {
3035+
statusCode := resp.StatusCode
3036+
retryAfter := resp.Header.Get("Retry-After")
3037+
retryable := isRetryableHTTPStatus(statusCode)
3038+
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
3039+
resp.Body.Close()
3040+
lastErr = apperr.New(apperr.CodeCommand, fmt.Sprintf("failed to download file: http %d", statusCode))
3041+
if retryable && attempt < defaultHTTPRetryAttempts {
3042+
time.Sleep(retryDelayForAttempt(attempt, retryAfter))
3043+
continue
3044+
}
3045+
return lastErr
3046+
}
29413047

2942-
if _, err := io.Copy(tmp, io.LimitReader(resp.Body, 256*1024*1024)); err != nil {
2943-
return apperr.Wrap(apperr.CodeConfig, "failed to write downloaded file", err)
2944-
}
2945-
if err := tmp.Close(); err != nil {
2946-
return apperr.Wrap(apperr.CodeConfig, "failed to finalize downloaded file", err)
2947-
}
2948-
if err := os.Chmod(tmpPath, mode); err != nil {
2949-
return apperr.Wrap(apperr.CodeConfig, "failed to set downloaded file permissions", err)
3048+
tmp, err := os.CreateTemp(filepath.Dir(destPath), "ols-download-*")
3049+
if err != nil {
3050+
resp.Body.Close()
3051+
return apperr.Wrap(apperr.CodeConfig, "failed to create temporary download file", err)
3052+
}
3053+
tmpPath := tmp.Name()
3054+
3055+
_, copyErr := io.Copy(tmp, io.LimitReader(resp.Body, 256*1024*1024))
3056+
resp.Body.Close()
3057+
closeErr := tmp.Close()
3058+
3059+
if copyErr != nil {
3060+
_ = os.Remove(tmpPath)
3061+
lastErr = apperr.Wrap(apperr.CodeConfig, "failed to write downloaded file", copyErr)
3062+
if attempt < defaultHTTPRetryAttempts {
3063+
time.Sleep(retryDelayForAttempt(attempt, ""))
3064+
continue
3065+
}
3066+
return lastErr
3067+
}
3068+
if closeErr != nil {
3069+
_ = os.Remove(tmpPath)
3070+
lastErr = apperr.Wrap(apperr.CodeConfig, "failed to finalize downloaded file", closeErr)
3071+
if attempt < defaultHTTPRetryAttempts {
3072+
time.Sleep(retryDelayForAttempt(attempt, ""))
3073+
continue
3074+
}
3075+
return lastErr
3076+
}
3077+
if err := os.Chmod(tmpPath, mode); err != nil {
3078+
_ = os.Remove(tmpPath)
3079+
return apperr.Wrap(apperr.CodeConfig, "failed to set downloaded file permissions", err)
3080+
}
3081+
if err := os.Rename(tmpPath, destPath); err != nil {
3082+
_ = os.Remove(tmpPath)
3083+
return apperr.Wrap(apperr.CodeConfig, "failed to move downloaded file into place", err)
3084+
}
3085+
return nil
29503086
}
2951-
if err := os.Rename(tmpPath, destPath); err != nil {
2952-
return apperr.Wrap(apperr.CodeConfig, "failed to move downloaded file into place", err)
3087+
if lastErr != nil {
3088+
return lastErr
29533089
}
2954-
return nil
3090+
return apperr.New(apperr.CodeCommand, "failed to download file")
29553091
}
29563092

29573093
func downloadFileWithSHA1Verification(url, checksumURL, destPath string) error {

0 commit comments

Comments
 (0)