@@ -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
28762971func parseExpectedHexDigest (raw string , minLen , maxLen int ) (string , error ) {
@@ -2915,43 +3010,84 @@ func computeFileSHA512(path string) (string, error) {
29153010
29163011func 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
29573093func downloadFileWithSHA1Verification (url , checksumURL , destPath string ) error {
0 commit comments