github.com/x-oss-byte/git-lfs@v2.5.2+incompatible/t/cmd/lfstest-customadapter.go (about) 1 // +build testtools 2 3 package main 4 5 import ( 6 "bufio" 7 "encoding/json" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/git-lfs/git-lfs/config" 18 "github.com/git-lfs/git-lfs/lfsapi" 19 "github.com/git-lfs/git-lfs/tools" 20 ) 21 22 var cfg = config.New() 23 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 28 func main() { 29 scanner := bufio.NewScanner(os.Stdin) 30 writer := bufio.NewWriter(os.Stdout) 31 errWriter := bufio.NewWriter(os.Stderr) 32 apiClient, err := lfsapi.NewClient(cfg) 33 if err != nil { 34 writeToStderr("Error creating api client: "+err.Error(), errWriter) 35 os.Exit(1) 36 } 37 38 for scanner.Scan() { 39 line := scanner.Text() 40 var req request 41 if err := json.Unmarshal([]byte(line), &req); err != nil { 42 writeToStderr(fmt.Sprintf("Unable to parse request: %v\n", line), errWriter) 43 continue 44 } 45 46 switch req.Event { 47 case "init": 48 writeToStderr(fmt.Sprintf("Initialised test custom adapter for %s\n", req.Operation), errWriter) 49 resp := &initResponse{} 50 sendResponse(resp, writer, errWriter) 51 case "download": 52 writeToStderr(fmt.Sprintf("Received download request for %s\n", req.Oid), errWriter) 53 performDownload(apiClient, req.Oid, req.Size, req.Action, writer, errWriter) 54 case "upload": 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) 57 case "terminate": 58 writeToStderr("Terminating test custom adapter gracefully.\n", errWriter) 59 break 60 } 61 } 62 63 } 64 65 func writeToStderr(msg string, errWriter *bufio.Writer) { 66 if !strings.HasSuffix(msg, "\n") { 67 msg = msg + "\n" 68 } 69 errWriter.WriteString(msg) 70 errWriter.Flush() 71 } 72 73 func sendResponse(r interface{}, writer, errWriter *bufio.Writer) error { 74 b, err := json.Marshal(r) 75 if err != nil { 76 return err 77 } 78 // Line oriented JSON 79 b = append(b, '\n') 80 _, err = writer.Write(b) 81 if err != nil { 82 return err 83 } 84 writer.Flush() 85 writeToStderr(fmt.Sprintf("Sent message %v", string(b)), errWriter) 86 return nil 87 } 88 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) 92 if err != nil { 93 writeToStderr(fmt.Sprintf("Unable to send transfer error: %v\n", err), errWriter) 94 } 95 } 96 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) 100 if err != nil { 101 writeToStderr(fmt.Sprintf("Unable to send progress update: %v\n", err), errWriter) 102 } 103 } 104 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) 109 if err != nil { 110 sendTransferError(oid, 2, err.Error(), writer, errWriter) 111 return 112 } 113 114 for k := range a.Header { 115 req.Header.Set(k, a.Header[k]) 116 } 117 118 res, err := apiClient.DoWithAuth("origin", req) 119 if err != nil { 120 sendTransferError(oid, res.StatusCode, err.Error(), writer, errWriter) 121 return 122 } 123 defer res.Body.Close() 124 125 dlFile, err := ioutil.TempFile("", "lfscustomdl") 126 if err != nil { 127 sendTransferError(oid, 3, err.Error(), writer, errWriter) 128 return 129 } 130 defer dlFile.Close() 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) 135 return nil 136 } 137 _, err = tools.CopyWithCallback(dlFile, res.Body, res.ContentLength, cb) 138 if err != nil { 139 sendTransferError(oid, 4, fmt.Sprintf("cannot write data to tempfile %q: %v", dlfilename, err), writer, errWriter) 140 os.Remove(dlfilename) 141 return 142 } 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) 146 return 147 } 148 149 // completed 150 complete := &transferResponse{"complete", oid, dlfilename, nil} 151 err = sendResponse(complete, writer, errWriter) 152 if err != nil { 153 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter) 154 } 155 } 156 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) 161 if err != nil { 162 sendTransferError(oid, 2, err.Error(), writer, errWriter) 163 return 164 } 165 166 for k := range a.Header { 167 req.Header.Set(k, a.Header[k]) 168 } 169 170 if len(req.Header.Get("Content-Type")) == 0 { 171 req.Header.Set("Content-Type", "application/octet-stream") 172 } 173 174 if req.Header.Get("Transfer-Encoding") == "chunked" { 175 req.TransferEncoding = []string{"chunked"} 176 } else { 177 req.Header.Set("Content-Length", strconv.FormatInt(size, 10)) 178 } 179 180 req.ContentLength = size 181 182 f, err := os.OpenFile(fromPath, os.O_RDONLY, 0644) 183 if err != nil { 184 sendTransferError(oid, 3, fmt.Sprintf("Cannot read data from %q: %v", fromPath, err), writer, errWriter) 185 return 186 } 187 defer f.Close() 188 189 // Turn callback into progress messages 190 cb := func(totalSize int64, readSoFar int64, readSinceLast int) error { 191 sendProgress(oid, readSoFar, readSinceLast, writer, errWriter) 192 return nil 193 } 194 req.Body = tools.NewBodyWithCallback(f, size, cb) 195 196 res, err := apiClient.DoWithAuth("origin", req) 197 if err != nil { 198 sendTransferError(oid, res.StatusCode, fmt.Sprintf("Error uploading data for %s: %v", oid, err), writer, errWriter) 199 return 200 } 201 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) 206 return 207 } 208 209 io.Copy(ioutil.Discard, res.Body) 210 res.Body.Close() 211 212 // completed 213 complete := &transferResponse{"complete", oid, "", nil} 214 err = sendResponse(complete, writer, errWriter) 215 if err != nil { 216 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter) 217 } 218 219 } 220 221 // Structs reimplemented so closer to a real external implementation 222 type header struct { 223 Key string `json:"key"` 224 Value string `json:"value"` 225 } 226 type action struct { 227 Href string `json:"href"` 228 Header map[string]string `json:"header,omitempty"` 229 ExpiresAt time.Time `json:"expires_at,omitempty"` 230 } 231 type transferError struct { 232 Code int `json:"code"` 233 Message string `json:"message"` 234 } 235 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"` 246 } 247 248 type initResponse struct { 249 Error *transferError `json:"error,omitempty"` 250 } 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"` 256 } 257 type progressResponse struct { 258 Event string `json:"event"` 259 Oid string `json:"oid"` 260 BytesSoFar int64 `json:"bytesSoFar"` 261 BytesSinceLast int `json:"bytesSinceLast"` 262 }