github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/test/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/progress" 20 "github.com/git-lfs/git-lfs/tools" 21 ) 22 23 var cfg = config.New() 24 25 // This test custom adapter just acts as a bridge for uploads/downloads 26 // in order to demonstrate & test the custom transfer adapter protocols 27 // All we actually do is relay the requests back to the normal storage URLs 28 // of our test server for simplicity, but this proves the principle 29 func main() { 30 scanner := bufio.NewScanner(os.Stdin) 31 writer := bufio.NewWriter(os.Stdout) 32 errWriter := bufio.NewWriter(os.Stderr) 33 apiClient, err := lfsapi.NewClient(cfg.Os, cfg.Git) 34 if err != nil { 35 writeToStderr("Error creating api client: "+err.Error(), errWriter) 36 os.Exit(1) 37 } 38 39 for scanner.Scan() { 40 line := scanner.Text() 41 var req request 42 if err := json.Unmarshal([]byte(line), &req); err != nil { 43 writeToStderr(fmt.Sprintf("Unable to parse request: %v\n", line), errWriter) 44 continue 45 } 46 47 switch req.Event { 48 case "init": 49 writeToStderr(fmt.Sprintf("Initialised test custom adapter for %s\n", req.Operation), errWriter) 50 resp := &initResponse{} 51 sendResponse(resp, writer, errWriter) 52 case "download": 53 writeToStderr(fmt.Sprintf("Received download request for %s\n", req.Oid), errWriter) 54 performDownload(apiClient, req.Oid, req.Size, req.Action, writer, errWriter) 55 case "upload": 56 writeToStderr(fmt.Sprintf("Received upload request for %s\n", req.Oid), errWriter) 57 performUpload(apiClient, req.Oid, req.Size, req.Action, req.Path, writer, errWriter) 58 case "terminate": 59 writeToStderr("Terminating test custom adapter gracefully.\n", errWriter) 60 break 61 } 62 } 63 64 } 65 66 func writeToStderr(msg string, errWriter *bufio.Writer) { 67 if !strings.HasSuffix(msg, "\n") { 68 msg = msg + "\n" 69 } 70 errWriter.WriteString(msg) 71 errWriter.Flush() 72 } 73 74 func sendResponse(r interface{}, writer, errWriter *bufio.Writer) error { 75 b, err := json.Marshal(r) 76 if err != nil { 77 return err 78 } 79 // Line oriented JSON 80 b = append(b, '\n') 81 _, err = writer.Write(b) 82 if err != nil { 83 return err 84 } 85 writer.Flush() 86 writeToStderr(fmt.Sprintf("Sent message %v", string(b)), errWriter) 87 return nil 88 } 89 90 func sendTransferError(oid string, code int, message string, writer, errWriter *bufio.Writer) { 91 resp := &transferResponse{"complete", oid, "", &transferError{code, message}} 92 err := sendResponse(resp, writer, errWriter) 93 if err != nil { 94 writeToStderr(fmt.Sprintf("Unable to send transfer error: %v\n", err), errWriter) 95 } 96 } 97 98 func sendProgress(oid string, bytesSoFar int64, bytesSinceLast int, writer, errWriter *bufio.Writer) { 99 resp := &progressResponse{"progress", oid, bytesSoFar, bytesSinceLast} 100 err := sendResponse(resp, writer, errWriter) 101 if err != nil { 102 writeToStderr(fmt.Sprintf("Unable to send progress update: %v\n", err), errWriter) 103 } 104 } 105 106 func performDownload(apiClient *lfsapi.Client, oid string, size int64, a *action, writer, errWriter *bufio.Writer) { 107 // We just use the URLs we're given, so we're just a proxy for the direct method 108 // but this is enough to test intermediate custom adapters 109 req, err := http.NewRequest("GET", a.Href, nil) 110 if err != nil { 111 sendTransferError(oid, 2, err.Error(), writer, errWriter) 112 return 113 } 114 115 for k := range a.Header { 116 req.Header.Set(k, a.Header[k]) 117 } 118 119 res, err := apiClient.DoWithAuth("origin", req) 120 if err != nil { 121 sendTransferError(oid, res.StatusCode, err.Error(), writer, errWriter) 122 return 123 } 124 defer res.Body.Close() 125 126 dlFile, err := ioutil.TempFile("", "lfscustomdl") 127 if err != nil { 128 sendTransferError(oid, 3, err.Error(), writer, errWriter) 129 return 130 } 131 defer dlFile.Close() 132 dlfilename := dlFile.Name() 133 // Turn callback into progress messages 134 cb := func(totalSize int64, readSoFar int64, readSinceLast int) error { 135 sendProgress(oid, readSoFar, readSinceLast, writer, errWriter) 136 return nil 137 } 138 _, err = tools.CopyWithCallback(dlFile, res.Body, res.ContentLength, cb) 139 if err != nil { 140 sendTransferError(oid, 4, fmt.Sprintf("cannot write data to tempfile %q: %v", dlfilename, err), writer, errWriter) 141 os.Remove(dlfilename) 142 return 143 } 144 if err := dlFile.Close(); err != nil { 145 sendTransferError(oid, 5, fmt.Sprintf("can't close tempfile %q: %v", dlfilename, err), writer, errWriter) 146 os.Remove(dlfilename) 147 return 148 } 149 150 // completed 151 complete := &transferResponse{"complete", oid, dlfilename, nil} 152 err = sendResponse(complete, writer, errWriter) 153 if err != nil { 154 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter) 155 } 156 } 157 158 func performUpload(apiClient *lfsapi.Client, oid string, size int64, a *action, fromPath string, writer, errWriter *bufio.Writer) { 159 // We just use the URLs we're given, so we're just a proxy for the direct method 160 // but this is enough to test intermediate custom adapters 161 req, err := http.NewRequest("PUT", a.Href, nil) 162 if err != nil { 163 sendTransferError(oid, 2, err.Error(), writer, errWriter) 164 return 165 } 166 167 for k := range a.Header { 168 req.Header.Set(k, a.Header[k]) 169 } 170 171 if len(req.Header.Get("Content-Type")) == 0 { 172 req.Header.Set("Content-Type", "application/octet-stream") 173 } 174 175 if req.Header.Get("Transfer-Encoding") == "chunked" { 176 req.TransferEncoding = []string{"chunked"} 177 } else { 178 req.Header.Set("Content-Length", strconv.FormatInt(size, 10)) 179 } 180 181 req.ContentLength = size 182 183 f, err := os.OpenFile(fromPath, os.O_RDONLY, 0644) 184 if err != nil { 185 sendTransferError(oid, 3, fmt.Sprintf("Cannot read data from %q: %v", fromPath, err), writer, errWriter) 186 return 187 } 188 defer f.Close() 189 190 // Turn callback into progress messages 191 cb := func(totalSize int64, readSoFar int64, readSinceLast int) error { 192 sendProgress(oid, readSoFar, readSinceLast, writer, errWriter) 193 return nil 194 } 195 req.Body = progress.NewBodyWithCallback(f, size, cb) 196 197 res, err := apiClient.DoWithAuth("origin", req) 198 if err != nil { 199 sendTransferError(oid, res.StatusCode, fmt.Sprintf("Error uploading data for %s: %v", oid, err), writer, errWriter) 200 return 201 } 202 203 if res.StatusCode > 299 { 204 msg := fmt.Sprintf("Invalid status for %s %s: %d", 205 req.Method, strings.SplitN(req.URL.String(), "?", 2)[0], res.StatusCode) 206 sendTransferError(oid, res.StatusCode, msg, writer, errWriter) 207 return 208 } 209 210 io.Copy(ioutil.Discard, res.Body) 211 res.Body.Close() 212 213 // completed 214 complete := &transferResponse{"complete", oid, "", nil} 215 err = sendResponse(complete, writer, errWriter) 216 if err != nil { 217 writeToStderr(fmt.Sprintf("Unable to send completion message: %v\n", err), errWriter) 218 } 219 220 } 221 222 // Structs reimplemented so closer to a real external implementation 223 type header struct { 224 Key string `json:"key"` 225 Value string `json:"value"` 226 } 227 type action struct { 228 Href string `json:"href"` 229 Header map[string]string `json:"header,omitempty"` 230 ExpiresAt time.Time `json:"expires_at,omitempty"` 231 } 232 type transferError struct { 233 Code int `json:"code"` 234 Message string `json:"message"` 235 } 236 237 // Combined request struct which can accept anything 238 type request struct { 239 Event string `json:"event"` 240 Operation string `json:"operation"` 241 Concurrent bool `json:"concurrent"` 242 ConcurrentTransfers int `json:"concurrenttransfers"` 243 Oid string `json:"oid"` 244 Size int64 `json:"size"` 245 Path string `json:"path"` 246 Action *action `json:"action"` 247 } 248 249 type initResponse struct { 250 Error *transferError `json:"error,omitempty"` 251 } 252 type transferResponse struct { 253 Event string `json:"event"` 254 Oid string `json:"oid"` 255 Path string `json:"path,omitempty"` // always blank for upload 256 Error *transferError `json:"error,omitempty"` 257 } 258 type progressResponse struct { 259 Event string `json:"event"` 260 Oid string `json:"oid"` 261 BytesSoFar int64 `json:"bytesSoFar"` 262 BytesSinceLast int `json:"bytesSinceLast"` 263 }