github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/pkg/store-client/client.go (about) 1 package storeclient 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "mime/multipart" 8 "net/http" 9 "net/textproto" 10 "os" 11 "time" 12 13 "github.com/benchkram/bob/bob/playbook" 14 progress2 "github.com/benchkram/bob/pkg/progress" 15 "github.com/benchkram/errz" 16 "github.com/pkg/errors" 17 "github.com/schollz/progressbar/v3" 18 19 "github.com/benchkram/bob/pkg/usererror" 20 ) 21 22 var ErrProjectNotFound = errors.New("project not found") 23 var ErrNotAuthorized = errors.New("not authorized") 24 25 func (c *c) UploadArtifact( 26 ctx context.Context, 27 projectName string, 28 artifactID string, 29 src io.Reader, 30 size int64, 31 ) (err error) { 32 defer errz.Recover(&err) 33 34 r, w := io.Pipe() 35 mpw := multipart.NewWriter(w) 36 37 bar := progressBar(ctx, size) 38 rb := progressbar.NewReader(src, bar) 39 40 go func() { 41 err0 := attachMimeHeader(mpw, "id", artifactID) 42 if err0 != nil { 43 _ = w.CloseWithError(err) 44 return 45 } 46 47 pw, err0 := mpw.CreateFormFile("file", artifactID+".bin") 48 if err0 != nil { 49 _ = w.CloseWithError(err) 50 return 51 } 52 53 tr := io.TeeReader(&rb, pw) 54 buf := make([]byte, 8192) 55 56 for { 57 _, err0 := tr.Read(buf) 58 if err0 == io.EOF { 59 _ = mpw.Close() 60 _ = w.Close() 61 break 62 } 63 if err0 != nil { 64 _ = w.CloseWithError(err) 65 } 66 } 67 }() 68 69 resp, err := c.clientWithResponses.UploadArtifactWithBodyWithResponse( 70 ctx, projectName, mpw.FormDataContentType(), r) 71 errz.Fatal(err) 72 73 if resp.StatusCode() != http.StatusOK { 74 err = errors.Errorf("request failed [status: %d, msg: %q]", resp.StatusCode(), resp.Body) 75 errz.Fatal(err) 76 } 77 78 return nil 79 } 80 81 func attachMimeHeader(w *multipart.Writer, key, value string) error { 82 h := make(textproto.MIMEHeader) 83 h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"`, key)) 84 h.Set("Content-Type", "text/plain") 85 86 p, err := w.CreatePart(h) 87 if err != nil { 88 return errors.Wrapf(err, "failed to create form field [%s]", key) 89 } 90 91 if _, err := p.Write([]byte(value)); err != nil { 92 return errors.Wrapf(err, "failed to write form field [%s]", key) 93 } 94 95 return nil 96 } 97 98 func (c *c) ListArtifacts(ctx context.Context, project string) (ids []string, err error) { 99 defer errz.Recover(&err) 100 101 res, err := c.clientWithResponses.GetProjectArtifactsWithResponse( 102 ctx, project) 103 errz.Fatal(err) 104 105 if res.StatusCode() == http.StatusNotFound { 106 errz.Fatal(usererror.Wrapm(ErrProjectNotFound, "upload to remote repository failed")) 107 } else if res.StatusCode() == http.StatusUnauthorized || res.StatusCode() == http.StatusForbidden { 108 errz.Fatal(usererror.Wrap(ErrNotAuthorized)) 109 } else if res.StatusCode() != http.StatusOK { 110 err = errors.Errorf("request failed [status: %d, msg: %q]", res.StatusCode(), res.Body) 111 errz.Fatal(err) 112 } 113 114 if res.JSON200 == nil { 115 errz.Fatal(errors.New("invalid response")) 116 } 117 118 return *res.JSON200, nil 119 } 120 121 func (c *c) GetArtifact(ctx context.Context, projectId string, artifactId string) (rc io.ReadCloser, size int64, err error) { 122 defer errz.Recover(&err) 123 124 res, err := c.clientWithResponses.GetProjectArtifactWithResponse( 125 ctx, projectId, artifactId) 126 errz.Fatal(err) 127 128 if res.StatusCode() == http.StatusNotFound { 129 errz.Fatal(usererror.Wrapm(ErrProjectNotFound, "upload to remote repository failed")) 130 } else if res.StatusCode() != http.StatusOK { 131 err = errors.Errorf("request failed [status: %d, msg: %q]", res.StatusCode(), res.Body) 132 errz.Fatal(err) 133 } 134 135 if res.JSON200 == nil { 136 errz.Fatal(errors.New("invalid response")) 137 } 138 139 req, err := http.NewRequest("GET", *res.JSON200.Location, nil) 140 errz.Fatal(err) 141 req = req.WithContext(ctx) 142 143 client := http.DefaultClient 144 res2, err := client.Do(req) 145 errz.Fatal(err) 146 147 if res2.StatusCode != http.StatusOK { 148 errz.Fatal(fmt.Errorf("invalid response")) 149 } 150 151 bar := progress(ctx, res2.ContentLength) 152 153 rb := progress2.NewReader(res2.Body, bar) 154 155 return &rb, res2.ContentLength, nil 156 } 157 158 func progress(ctx context.Context, size int64) *progress2.Progress { 159 getDescription := func(ctx context.Context, k playbook.TaskKey) string { 160 if v := ctx.Value(k); v != nil { 161 return v.(string) 162 } 163 return "" 164 } 165 description := getDescription(ctx, "description") 166 return progress2.NewProgress(size, description, time.Second) 167 } 168 169 func progressBar(ctx context.Context, size int64) *progressbar.ProgressBar { 170 getDescription := func(ctx context.Context, k playbook.TaskKey) string { 171 if v := ctx.Value(k); v != nil { 172 return v.(string) 173 } 174 return "" 175 } 176 description := getDescription(ctx, "description") 177 178 bar := progressbar.NewOptions64(size, 179 progressbar.OptionSetWriter(os.Stdout), 180 progressbar.OptionSetPredictTime(false), 181 progressbar.OptionShowCount(), 182 progressbar.OptionThrottle(100*time.Millisecond), 183 progressbar.OptionEnableColorCodes(true), 184 progressbar.OptionSetDescription(description), 185 progressbar.OptionOnCompletion(func() { 186 fmt.Fprint(os.Stdout, "\n") 187 }), 188 progressbar.OptionSetRenderBlankState(false), 189 progressbar.OptionSetTheme( 190 progressbar.Theme{ 191 Saucer: "", 192 SaucerHead: "", 193 SaucerPadding: "", 194 BarStart: "", 195 BarEnd: "", 196 }, 197 )) 198 return bar 199 }