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  }