soft-serve

git clone git://git.lin.moe/fork/soft-serve.git

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