17 "github.com/git-lfs/git-lfs/config"
18 "github.com/git-lfs/git-lfs/lfsapi"
19 "github.com/git-lfs/git-lfs/tools"
22 var cfg = config.New()
24 // This test custom adapter just acts as a bridge for uploads/downloads
25 // in order to demonstrate & test the custom transfer adapter protocols
26 // All we actually do is relay the requests back to the normal storage URLs
27 // of our test server for simplicity, but this proves the principle
29 scanner := bufio.NewScanner(os.Stdin)
30 writer := bufio.NewWriter(os.Stdout)
31 errWriter := bufio.NewWriter(os.Stderr)
32 apiClient, err := lfsapi.NewClient(cfg)
34 writeToStderr("Error creating api client: "+err.Error(), errWriter)
39 line := scanner.Text()
41 if err := json.Unmarshal([]byte(line), &req); err != nil {
42 writeToStderr(fmt.Sprintf("Unable to parse request: %v\n", line), errWriter)
48 writeToStderr(fmt.Sprintf("Initialised test custom adapter for %s\n", req.Operation), errWriter)
49 resp := &initResponse{}
50 sendResponse(resp, writer, errWriter)
52 writeToStderr(fmt.Sprintf("Received download request for %s\n", req.Oid), errWriter)
53 performDownload(apiClient, req.Oid, req.Size, req.Action, writer, errWriter)
55 writeToStderr(fmt.Sprintf("Received upload request for %s\n", req.Oid), errWriter)
56 performUpload(apiClient, req.Oid, req.Size, req.Action, req.Path, writer, errWriter)
58 writeToStderr("Terminating test custom adapter gracefully.\n", errWriter)
65 func writeToStderr(msg string, errWriter *bufio.Writer) {
66 if !strings.HasSuffix(msg, "\n") {
69 errWriter.WriteString(msg)
73 func sendResponse(r interface{}, writer, errWriter *bufio.Writer) error {
74 b, err := json.Marshal(r)
80 _, err = writer.Write(b)
85 writeToStderr(fmt.Sprintf("Sent message %v", string(b)), errWriter)
89 func sendTransferError(oid string, code int, message string, writer, errWriter *bufio.Writer) {
90 resp := &transferResponse{"complete", oid, "", &transferError{code, message}}
91 err := sendResponse(resp, writer, errWriter)
93 writeToStderr(fmt.Sprintf("Unable to send transfer error: %v\n", err), errWriter)
97 func sendProgress(oid string, bytesSoFar int64, bytesSinceLast int, writer, errWriter *bufio.Writer) {
98 resp := &progressResponse{"progress", oid, bytesSoFar, bytesSinceLast}
99 err := sendResponse(resp, writer, errWriter)
101 writeToStderr(fmt.Sprintf("Unable to send progress update: %v\n", err), errWriter)
105 func performDownload(apiClient *lfsapi.Client, oid string, size int64, a *action, writer, errWriter *bufio.Writer) {
106 // We just use the URLs we're given, so we're just a proxy for the direct method
107 // but this is enough to test intermediate custom adapters
108 req, err := http.NewRequest("GET", a.Href, nil)
110 sendTransferError(oid, 2, err.Error(), writer, errWriter)
114 for k := range a.Header {
115 req.Header.Set(k, a.Header[k])
118 res, err := apiClient.DoWithAuth("origin", req)
120 sendTransferError(oid, res.StatusCode, err.Error(), writer, errWriter)
123 defer res.Body.Close()
125 dlFile, err := ioutil.TempFile("", "lfscustomdl")
127 sendTransferError(oid, 3, err.Error(), writer, errWriter)
131 dlfilename := dlFile.Name()
132 // Turn callback into progress messages
133 cb := func(totalSize int64, readSoFar int64, readSinceLast int) error {
134 sendProgress(oid, readSoFar, readSinceLast, writer, errWriter)
137 _, err = tools.CopyWithCallback(dlFile, res.Body, res.ContentLength, cb)
139 sendTransferError(oid, 4, fmt.Sprintf("cannot write data to tempfile %q: %v", dlfilename, err), writer, errWriter)
140 os.Remove(dlfilename)
143 if err := dlFile.Close(); err != nil {
144 sendTransferError(oid, 5, fmt.Sprintf("can't close tempfile %q: %v", dlfilename, err), writer, errWriter)
145 os.Remove(dlfilename)
150 complete := &transferResponse{"complete", oid, dlfilename, nil}
151 err = sendResponse(complete, writer, errWriter)
153 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter)
157 func performUpload(apiClient *lfsapi.Client, oid string, size int64, a *action, fromPath string, writer, errWriter *bufio.Writer) {
158 // We just use the URLs we're given, so we're just a proxy for the direct method
159 // but this is enough to test intermediate custom adapters
160 req, err := http.NewRequest("PUT", a.Href, nil)
162 sendTransferError(oid, 2, err.Error(), writer, errWriter)
166 for k := range a.Header {
167 req.Header.Set(k, a.Header[k])
170 if len(req.Header.Get("Content-Type")) == 0 {
171 req.Header.Set("Content-Type", "application/octet-stream")
174 if req.Header.Get("Transfer-Encoding") == "chunked" {
175 req.TransferEncoding = []string{"chunked"}
177 req.Header.Set("Content-Length", strconv.FormatInt(size, 10))
180 req.ContentLength = size
182 f, err := os.OpenFile(fromPath, os.O_RDONLY, 0644)
184 sendTransferError(oid, 3, fmt.Sprintf("Cannot read data from %q: %v", fromPath, err), writer, errWriter)
189 // Turn callback into progress messages
190 cb := func(totalSize int64, readSoFar int64, readSinceLast int) error {
191 sendProgress(oid, readSoFar, readSinceLast, writer, errWriter)
194 req.Body = tools.NewBodyWithCallback(f, size, cb)
196 res, err := apiClient.DoWithAuth("origin", req)
198 sendTransferError(oid, res.StatusCode, fmt.Sprintf("Error uploading data for %s: %v", oid, err), writer, errWriter)
202 if res.StatusCode > 299 {
203 msg := fmt.Sprintf("Invalid status for %s %s: %d",
204 req.Method, strings.SplitN(req.URL.String(), "?", 2)[0], res.StatusCode)
205 sendTransferError(oid, res.StatusCode, msg, writer, errWriter)
209 io.Copy(ioutil.Discard, res.Body)
213 complete := &transferResponse{"complete", oid, "", nil}
214 err = sendResponse(complete, writer, errWriter)
216 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter)
221 // Structs reimplemented so closer to a real external implementation
223 Key string `json:"key"`
224 Value string `json:"value"`
227 Href string `json:"href"`
228 Header map[string]string `json:"header,omitempty"`
229 ExpiresAt time.Time `json:"expires_at,omitempty"`
231 type transferError struct {
232 Code int `json:"code"`
233 Message string `json:"message"`
236 // Combined request struct which can accept anything
237 type request struct {
238 Event string `json:"event"`
239 Operation string `json:"operation"`
240 Concurrent bool `json:"concurrent"`
241 ConcurrentTransfers int `json:"concurrenttransfers"`
242 Oid string `json:"oid"`
243 Size int64 `json:"size"`
244 Path string `json:"path"`
245 Action *action `json:"action"`
248 type initResponse struct {
249 Error *transferError `json:"error,omitempty"`
251 type transferResponse struct {
252 Event string `json:"event"`
253 Oid string `json:"oid"`
254 Path string `json:"path,omitempty"` // always blank for upload
255 Error *transferError `json:"error,omitempty"`
257 type progressResponse struct {
258 Event string `json:"event"`
259 Oid string `json:"oid"`
260 BytesSoFar int64 `json:"bytesSoFar"`
261 BytesSinceLast int `json:"bytesSinceLast"`