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  }