1package lfs23import (4 "bytes"5 "context"6 "encoding/json"7 "errors"8 "fmt"9 "net/http"1011 "charm.land/log/v2"12 "github.com/charmbracelet/soft-serve/pkg/ssrf"13)1415// httpClient is a Git LFS client to communicate with a LFS source API.16type httpClient struct {17 client *http.Client18 endpoint Endpoint19 transfers map[string]TransferAdapter20}2122var _ Client = (*httpClient)(nil)2324// newHTTPClient returns a new Git LFS client.25func newHTTPClient(endpoint Endpoint) *httpClient {26 client := ssrf.NewSecureClient()27 return &httpClient{28 client: client,29 endpoint: endpoint,30 transfers: map[string]TransferAdapter{31 TransferBasic: &BasicTransferAdapter{client},32 },33 }34}3536// Download implements Client.37func (c *httpClient) Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error {38 return c.performOperation(ctx, objects, callback, nil)39}4041// Upload implements Client.42func (c *httpClient) Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error {43 return c.performOperation(ctx, objects, nil, callback)44}4546func (c *httpClient) transferNames() []string {47 names := make([]string, len(c.transfers))48 i := 049 for name := range c.transfers {50 names[i] = name51 i++52 }53 return names54}5556// batch performs a batch request to the LFS server.57func (c *httpClient) batch(ctx context.Context, operation string, objects []Pointer) (*BatchResponse, error) {58 logger := log.FromContext(ctx).WithPrefix("lfs")59 url := fmt.Sprintf("%s/objects/batch", c.endpoint.String())6061 // TODO: support ref62 request := &BatchRequest{operation, c.transferNames(), nil, objects, HashAlgorithmSHA256}6364 payload := new(bytes.Buffer)65 err := json.NewEncoder(payload).Encode(request)66 if err != nil {67 logger.Errorf("Error encoding json: %v", err)68 return nil, err69 }7071 logger.Debugf("Calling: %s", url)7273 req, err := http.NewRequestWithContext(ctx, "POST", url, payload)74 if err != nil {75 logger.Errorf("Error creating request: %v", err)76 return nil, err77 }78 req.Header.Set("Content-type", MediaType)79 req.Header.Set("Accept", MediaType)8081 res, err := c.client.Do(req)82 if err != nil {83 select {84 case <-ctx.Done():85 return nil, ctx.Err()86 default:87 }88 logger.Errorf("Error while processing request: %v", err)89 return nil, err90 }91 defer res.Body.Close() //nolint: errcheck9293 if res.StatusCode != http.StatusOK {94 return nil, fmt.Errorf("unexpected server response: %s", res.Status)95 }9697 var response BatchResponse98 err = json.NewDecoder(res.Body).Decode(&response)99 if err != nil {100 logger.Errorf("Error decoding json: %v", err)101 return nil, err102 }103104 if len(response.Transfer) == 0 {105 response.Transfer = TransferBasic106 }107108 return &response, nil109}110111func (c *httpClient) performOperation(ctx context.Context, objects []Pointer, dc DownloadCallback, uc UploadCallback) error {112 logger := log.FromContext(ctx).WithPrefix("lfs")113 if len(objects) == 0 {114 return nil115 }116117 operation := OperationDownload118 if uc != nil {119 operation = OperationUpload120 }121122 result, err := c.batch(ctx, operation, objects)123 if err != nil {124 return err125 }126127 transferAdapter, ok := c.transfers[result.Transfer]128 if !ok {129 return fmt.Errorf("TransferAdapter not found: %s", result.Transfer)130 }131132 for _, object := range result.Objects {133 if object.Error != nil {134 objectError := errors.New(object.Error.Message)135 logger.Debugf("Error on object %v: %v", object.Pointer, objectError)136 if uc != nil {137 if _, err := uc(object.Pointer, objectError); err != nil {138 return err139 }140 } else {141 if err := dc(object.Pointer, nil, objectError); err != nil {142 return err143 }144 }145 continue146 }147148 if uc != nil {149 if len(object.Actions) == 0 {150 logger.Debugf("%v already present on server", object.Pointer)151 continue152 }153154 link, ok := object.Actions[ActionUpload]155 if !ok {156 logger.Debugf("%+v", object)157 return errors.New("missing action 'upload'")158 }159160 content, err := uc(object.Pointer, nil)161 if err != nil {162 return err163 }164165 err = transferAdapter.Upload(ctx, object.Pointer, content, link)166167 content.Close() //nolint: errcheck168169 if err != nil {170 return err171 }172173 link, ok = object.Actions[ActionVerify]174 if ok {175 if err := transferAdapter.Verify(ctx, object.Pointer, link); err != nil {176 return err177 }178 }179 } else {180 link, ok := object.Actions[ActionDownload]181 if !ok {182 logger.Debugf("%+v", object)183 return errors.New("missing action 'download'")184 }185186 content, err := transferAdapter.Download(ctx, object.Pointer, link)187 if err != nil {188 return err189 }190191 if err := dc(object.Pointer, content, nil); err != nil {192 return err193 }194 }195 }196197 return nil198}