1package lfs23import (4 "bytes"5 "context"6 "encoding/json"7 "errors"8 "fmt"9 "net/http"1011 "github.com/charmbracelet/log/v2"12)1314// httpClient is a Git LFS client to communicate with a LFS source API.15type httpClient struct {16 client *http.Client17 endpoint Endpoint18 transfers map[string]TransferAdapter19}2021var _ Client = (*httpClient)(nil)2223// newHTTPClient returns a new Git LFS client.24func newHTTPClient(endpoint Endpoint) *httpClient {25 return &httpClient{26 client: http.DefaultClient,27 endpoint: endpoint,28 transfers: map[string]TransferAdapter{29 TransferBasic: &BasicTransferAdapter{http.DefaultClient},30 },31 }32}3334// Download implements Client.35func (c *httpClient) Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error {36 return c.performOperation(ctx, objects, callback, nil)37}3839// Upload implements Client.40func (c *httpClient) Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error {41 return c.performOperation(ctx, objects, nil, callback)42}4344func (c *httpClient) transferNames() []string {45 names := make([]string, len(c.transfers))46 i := 047 for name := range c.transfers {48 names[i] = name49 i++50 }51 return names52}5354// batch performs a batch request to the LFS server.55func (c *httpClient) batch(ctx context.Context, operation string, objects []Pointer) (*BatchResponse, error) {56 logger := log.FromContext(ctx).WithPrefix("lfs")57 url := fmt.Sprintf("%s/objects/batch", c.endpoint.String())5859 // TODO: support ref60 request := &BatchRequest{operation, c.transferNames(), nil, objects, HashAlgorithmSHA256}6162 payload := new(bytes.Buffer)63 err := json.NewEncoder(payload).Encode(request)64 if err != nil {65 logger.Errorf("Error encoding json: %v", err)66 return nil, err67 }6869 logger.Debugf("Calling: %s", url)7071 req, err := http.NewRequestWithContext(ctx, "POST", url, payload)72 if err != nil {73 logger.Errorf("Error creating request: %v", err)74 return nil, err75 }76 req.Header.Set("Content-type", MediaType)77 req.Header.Set("Accept", MediaType)7879 res, err := c.client.Do(req)80 if err != nil {81 select {82 case <-ctx.Done():83 return nil, ctx.Err()84 default:85 }86 logger.Errorf("Error while processing request: %v", err)87 return nil, err88 }89 defer res.Body.Close() // nolint: errcheck9091 if res.StatusCode != http.StatusOK {92 return nil, fmt.Errorf("Unexpected server response: %s", res.Status)93 }9495 var response BatchResponse96 err = json.NewDecoder(res.Body).Decode(&response)97 if err != nil {98 logger.Errorf("Error decoding json: %v", err)99 return nil, err100 }101102 if len(response.Transfer) == 0 {103 response.Transfer = TransferBasic104 }105106 return &response, nil107}108109func (c *httpClient) performOperation(ctx context.Context, objects []Pointer, dc DownloadCallback, uc UploadCallback) error {110 logger := log.FromContext(ctx).WithPrefix("lfs")111 if len(objects) == 0 {112 return nil113 }114115 operation := OperationDownload116 if uc != nil {117 operation = OperationUpload118 }119120 result, err := c.batch(ctx, operation, objects)121 if err != nil {122 return err123 }124125 transferAdapter, ok := c.transfers[result.Transfer]126 if !ok {127 return fmt.Errorf("TransferAdapter not found: %s", result.Transfer)128 }129130 for _, object := range result.Objects {131 if object.Error != nil {132 objectError := errors.New(object.Error.Message)133 logger.Debugf("Error on object %v: %v", object.Pointer, objectError)134 if uc != nil {135 if _, err := uc(object.Pointer, objectError); err != nil {136 return err137 }138 } else {139 if err := dc(object.Pointer, nil, objectError); err != nil {140 return err141 }142 }143 continue144 }145146 if uc != nil {147 if len(object.Actions) == 0 {148 logger.Debugf("%v already present on server", object.Pointer)149 continue150 }151152 link, ok := object.Actions[ActionUpload]153 if !ok {154 logger.Debugf("%+v", object)155 return errors.New("Missing action 'upload'")156 }157158 content, err := uc(object.Pointer, nil)159 if err != nil {160 return err161 }162163 err = transferAdapter.Upload(ctx, object.Pointer, content, link)164165 content.Close() // nolint: errcheck166167 if err != nil {168 return err169 }170171 link, ok = object.Actions[ActionVerify]172 if ok {173 if err := transferAdapter.Verify(ctx, object.Pointer, link); err != nil {174 return err175 }176 }177 } else {178 link, ok := object.Actions[ActionDownload]179 if !ok {180 logger.Debugf("%+v", object)181 return errors.New("Missing action 'download'")182 }183184 content, err := transferAdapter.Download(ctx, object.Pointer, link)185 if err != nil {186 return err187 }188189 if err := dc(object.Pointer, content, nil); err != nil {190 return err191 }192 }193 }194195 return nil196}