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}