go.mway.dev/x@v0.0.0-20240520034138-950aede9a3fb/net/http/get_file.go (about) 1 // Copyright (c) 2024 Matt Way 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to 5 // deal in the Software without restriction, including without limitation the 6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 // sell copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 // IN THE THE SOFTWARE. 20 21 // Package http provides http-related utilities and helpers. 22 package http 23 24 import ( 25 "context" 26 "io/fs" 27 "net/http" 28 "os" 29 "path/filepath" 30 31 "go.mway.dev/errors" 32 xos "go.mway.dev/x/os" 33 "golang.org/x/sys/unix" 34 ) 35 36 var ( 37 // ErrDestNotWritable is returned when a given destination is a directory 38 // that is not writable. 39 ErrDestNotWritable = errors.New("destination is not writable") 40 41 _clientDo = http.DefaultClient.Do 42 _osStat = os.Stat 43 _osMkdirAll = os.MkdirAll 44 _unixAccess = unix.Access 45 ) 46 47 // GetFile retrieves a file referenced by the given url and writes it to the 48 // given destination, returning any errors in the process. The resulting file 49 // may have been written despite returning a non-nil error. If dst does not 50 // exist, GetFile will make its parent directory if needed before writing; if 51 // dst exists and is a directory, GetFile will use the basename of the URL to 52 // inform the resulting filename. 53 func GetFile(url string, dst string) (err error) { 54 return GetFileContext(context.Background(), url, dst) 55 } 56 57 // GetFileContext retrieves a file referenced by the given url and writes it to 58 // the given destination, returning any errors in the process. The resulting 59 // file may have been written despite returning a non-nil error. If dst does 60 // not exist, GetFile will make its parent directory if needed before writing; 61 // if dst exists and is a directory, GetFile will use the basename of the URL 62 // to inform the resulting filename. 63 // 64 //nolint:gocyclo 65 func GetFileContext(ctx context.Context, url string, dst string) (err error) { 66 var ( 67 req *http.Request 68 resp *http.Response 69 ) 70 71 req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 72 if err != nil { 73 return err 74 } 75 76 resp, err = _clientDo(req) 77 if resp != nil && resp.Body != nil { 78 defer func() { 79 err = errors.Join(err, errors.Wrap( 80 resp.Body.Close(), 81 "failed to close response body", 82 )) 83 }() 84 } 85 if err != nil { 86 return err 87 } 88 89 var dstInfo fs.FileInfo 90 if dstInfo, err = _osStat(dst); err != nil { 91 if errors.Is(err, fs.ErrNotExist) { 92 err = _osMkdirAll(filepath.Dir(dst), 0o755) 93 } 94 } 95 96 switch { 97 case err != nil: 98 return err 99 case dstInfo != nil && dstInfo.IsDir(): 100 if base := filepath.Base(url); len(base) > 0 { 101 dst = filepath.Join(dst, base) 102 } 103 } 104 105 if parent := filepath.Dir(dst); !existsAndWritable(parent) { 106 return errors.Wrap(ErrDestNotWritable, parent) 107 } 108 109 _, err = xos.WriteReaderToFile(dst, resp.Body) 110 return err 111 } 112 113 func existsAndWritable(path string) bool { 114 return _unixAccess(path, unix.W_OK) == nil 115 }