http_client.go

  1package lfs
  2
  3import (
  4	"bytes"
  5	"context"
  6	"encoding/json"
  7	"errors"
  8	"fmt"
  9	"net/http"
 10
 11	log "github.com/charmbracelet/log/v2"
 12)
 13
 14// httpClient is a Git LFS client to communicate with a LFS source API.
 15type httpClient struct {
 16	client    *http.Client
 17	endpoint  Endpoint
 18	transfers map[string]TransferAdapter
 19}
 20
 21var _ Client = (*httpClient)(nil)
 22
 23// 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}
 33
 34// Download implements Client.
 35func (c *httpClient) Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error {
 36	return c.performOperation(ctx, objects, callback, nil)
 37}
 38
 39// Upload implements Client.
 40func (c *httpClient) Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error {
 41	return c.performOperation(ctx, objects, nil, callback)
 42}
 43
 44func (c *httpClient) transferNames() []string {
 45	names := make([]string, len(c.transfers))
 46	i := 0
 47	for name := range c.transfers {
 48		names[i] = name
 49		i++
 50	}
 51	return names
 52}
 53
 54// 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())
 58
 59	// TODO: support ref
 60	request := &BatchRequest{operation, c.transferNames(), nil, objects, HashAlgorithmSHA256}
 61
 62	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, err
 67	}
 68
 69	logger.Debugf("Calling: %s", url)
 70
 71	req, err := http.NewRequestWithContext(ctx, "POST", url, payload)
 72	if err != nil {
 73		logger.Errorf("Error creating request: %v", err)
 74		return nil, err
 75	}
 76	req.Header.Set("Content-type", MediaType)
 77	req.Header.Set("Accept", MediaType)
 78
 79	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, err
 88	}
 89	defer res.Body.Close() 
 90
 91	if res.StatusCode != http.StatusOK {
 92		return nil, fmt.Errorf("Unexpected server response: %s", res.Status)
 93	}
 94
 95	var response BatchResponse
 96	err = json.NewDecoder(res.Body).Decode(&response)
 97	if err != nil {
 98		logger.Errorf("Error decoding json: %v", err)
 99		return nil, err
100	}
101
102	if len(response.Transfer) == 0 {
103		response.Transfer = TransferBasic
104	}
105
106	return &response, nil
107}
108
109func (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 nil
113	}
114
115	operation := OperationDownload
116	if uc != nil {
117		operation = OperationUpload
118	}
119
120	result, err := c.batch(ctx, operation, objects)
121	if err != nil {
122		return err
123	}
124
125	transferAdapter, ok := c.transfers[result.Transfer]
126	if !ok {
127		return fmt.Errorf("TransferAdapter not found: %s", result.Transfer)
128	}
129
130	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 err
137				}
138			} else {
139				if err := dc(object.Pointer, nil, objectError); err != nil {
140					return err
141				}
142			}
143			continue
144		}
145
146		if uc != nil {
147			if len(object.Actions) == 0 {
148				logger.Debugf("%v already present on server", object.Pointer)
149				continue
150			}
151
152			link, ok := object.Actions[ActionUpload]
153			if !ok {
154				logger.Debugf("%+v", object)
155				return errors.New("Missing action 'upload'")
156			}
157
158			content, err := uc(object.Pointer, nil)
159			if err != nil {
160				return err
161			}
162
163			err = transferAdapter.Upload(ctx, object.Pointer, content, link)
164
165			content.Close() 
166
167			if err != nil {
168				return err
169			}
170
171			link, ok = object.Actions[ActionVerify]
172			if ok {
173				if err := transferAdapter.Verify(ctx, object.Pointer, link); err != nil {
174					return err
175				}
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			}
183
184			content, err := transferAdapter.Download(ctx, object.Pointer, link)
185			if err != nil {
186				return err
187			}
188
189			if err := dc(object.Pointer, content, nil); err != nil {
190				return err
191			}
192		}
193	}
194
195	return nil
196}