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}