diff --git a/tests/cmd/lfstest-gitserver.go b/tests/cmd/lfstest-gitserver.go new file mode 100644 index 00000000..643b923f --- /dev/null +++ b/tests/cmd/lfstest-gitserver.go @@ -0,0 +1,242 @@ +package main + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/textproto" + "os" + "os/exec" + "path/filepath" + "strings" +) + +var ( + repoDir string + largeObjects = make(map[string][]byte) + server *httptest.Server +) + +func main() { + // should be run from project root + wd, err := os.Getwd() + if err != nil { + log.Fatalln(err) + } + + repoDir = filepath.Join(wd, "tests", "remote") + + mux := http.NewServeMux() + server = httptest.NewServer(mux) + stopch := make(chan bool) + + mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { + stopch <- true + }) + + mux.HandleFunc("/storage/", storageHandler) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "/info/lfs") { + log.Printf("git lfs %s %s\n", r.Method, r.URL) + lfsHandler(w, r) + return + } + + log.Printf("git http-backend %s %s\n", r.Method, r.URL) + gitHandler(w, r) + }) + + urlname := os.Getenv("LFSTEST_URL") + if len(urlname) == 0 { + urlname = "lfstest-gitserver" + } + + file, err := os.Create(urlname) + if err != nil { + log.Fatalln(err) + } + + file.Write([]byte(server.URL)) + file.Close() + log.Println(server.URL) + + defer func() { + os.RemoveAll(urlname) + }() + + <-stopch + log.Println("done") +} + +type lfsObject struct { + Oid string `json:"oid,omitempty"` + Size int64 `json:"size,omitempty"` + Links map[string]lfsLink `json:"_links,omitempty"` +} + +type lfsLink struct { + Href string `json:"href"` + Header map[string]string `json:"header,omitempty"` +} + +// handles any requests with "{name}.server.git/info/lfs" in the path +func lfsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.git-lfs+json") + if r.Method == "POST" { + lfsPostHandler(w, r) + } else { + lfsGetHandler(w, r) + } +} + +func lfsPostHandler(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + tee := io.TeeReader(r.Body, buf) + obj := &lfsObject{} + err := json.NewDecoder(tee).Decode(obj) + io.Copy(ioutil.Discard, r.Body) + r.Body.Close() + + log.Println("REQUEST") + log.Println(buf.String()) + log.Printf("OID: %s\n", obj.Oid) + log.Printf("Size: %d\n", obj.Size) + + if err != nil { + log.Fatal(err) + } + + res := &lfsObject{ + Links: map[string]lfsLink{ + "upload": lfsLink{ + Href: server.URL + "/storage/" + obj.Oid, + }, + }, + } + + by, err := json.Marshal(res) + if err != nil { + log.Fatal(err) + } + + log.Println("RESPONSE: 202") + log.Println(string(by)) + + w.WriteHeader(202) + w.Write(by) +} + +func lfsGetHandler(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + oid := parts[len(parts)-1] + + if by, ok := largeObjects[oid]; ok { + obj := &lfsObject{ + Oid: oid, + Size: int64(len(by)), + Links: map[string]lfsLink{ + "download": lfsLink{ + Href: server.URL + "/storage/" + oid, + }, + }, + } + + by, err := json.Marshal(obj) + if err != nil { + log.Fatal(err) + } + + log.Println("RESPONSE: 200") + log.Println(string(by)) + + w.WriteHeader(200) + w.Write(by) + + return + } + + w.WriteHeader(404) +} + +// handles any /storage/{oid} requests +func storageHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("storage %s %s\n", r.Method, r.URL) + switch r.Method { + case "PUT": + hash := sha256.New() + buf := &bytes.Buffer{} + io.Copy(io.MultiWriter(hash, buf), r.Body) + oid := hex.EncodeToString(hash.Sum(nil)) + if !strings.HasSuffix(r.URL.Path, "/"+oid) { + w.WriteHeader(403) + return + } + + largeObjects[oid] = buf.Bytes() + + case "GET": + parts := strings.Split(r.URL.Path, "/") + oid := parts[len(parts)-1] + + if by, ok := largeObjects[oid]; ok { + w.Write(by) + return + } + + w.WriteHeader(404) + default: + w.WriteHeader(500) + } +} + +func gitHandler(w http.ResponseWriter, r *http.Request) { + defer func() { + io.Copy(ioutil.Discard, r.Body) + r.Body.Close() + }() + + cmd := exec.Command("git", "http-backend") + cmd.Env = []string{ + fmt.Sprintf("GIT_PROJECT_ROOT=%s", repoDir), + fmt.Sprintf("GIT_HTTP_EXPORT_ALL="), + fmt.Sprintf("PATH_INFO=%s", r.URL.Path), + fmt.Sprintf("QUERY_STRING=%s", r.URL.RawQuery), + fmt.Sprintf("REQUEST_METHOD=%s", r.Method), + fmt.Sprintf("CONTENT_TYPE=%s", r.Header.Get("Content-Type")), + } + + buffer := &bytes.Buffer{} + cmd.Stdin = r.Body + cmd.Stdout = buffer + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + log.Fatal(err) + } + + text := textproto.NewReader(bufio.NewReader(buffer)) + + code, _, _ := text.ReadCodeLine(-1) + + if code != 0 { + w.WriteHeader(code) + } + + headers, _ := text.ReadMIMEHeader() + head := w.Header() + for key, values := range headers { + for _, value := range values { + head.Add(key, value) + } + } + + io.Copy(w, text.R) +} diff --git a/tests/test-happy-path.sh b/tests/test-happy-path.sh index fcf68e3c..f676f845 100755 --- a/tests/test-happy-path.sh +++ b/tests/test-happy-path.sh @@ -2,6 +2,19 @@ # this should run from the git-lfs project root. set -e +wait_for_file() { + local filename=$1 + for ((n=30; n>0; n--)); do + if [ -s $filename ]; then + return 0 + fi + + sleep 0.5 + done + + return 1 +} + # cleanup rm -rf "tests/remote" rm -rf "tests/local" @@ -13,6 +26,12 @@ echo "compile git-lfs" script/bootstrap GITLFS="`pwd`/bin/git-lfs" +echo "spin up test server" +LFSTEST_URL=tests/remote/url go run tests/cmd/lfstest-gitserver.go & +wait_for_file tests/remote/url +GITSERVER=$(cat tests/remote/url) +echo $GITSERVER + echo "set up 'remote' git repository" REPONAME="$(basename "$0")" REPODIR="`pwd`/tests/remote/$REPONAME.git" @@ -26,11 +45,17 @@ echo "set up 'local' test directory with git clone" cd $ROOTDIR TESTDIR="$(mktemp -d "`pwd`/tests/local/XXXXXX")" cd $TESTDIR +git clone $GITSERVER/$REPONAME repo +cd repo +echo "start the test" out=$($GITLFS track "*.dat") echo "$out" echo "$out" | grep "dat" -echo "ok" +# only run if this test complete rm -rf $TESTDIR + +# run after the entire test run regardless of success or failure +curl $GITSERVER/shutdown