git-lfs/test/cmd/lfstest-gitserver.go

420 lines
8.5 KiB
Go

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"
"regexp"
"strings"
"sync"
)
var (
repoDir string
largeObjects = newLfsStorage()
server *httptest.Server
serveBatch = true
)
func main() {
repoDir = os.Getenv("LFSTEST_DIR")
mux := http.NewServeMux()
server = httptest.NewServer(mux)
stopch := make(chan bool)
mux.HandleFunc("/startbatch", func(w http.ResponseWriter, r *http.Request) {
serveBatch = true
})
mux.HandleFunc("/stopbatch", func(w http.ResponseWriter, r *http.Request) {
serveBatch = false
})
mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
stopch <- true
})
mux.HandleFunc("/storage/", storageHandler)
mux.HandleFunc("/redirect307/", redirect307Handler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/info/lfs") {
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("git server 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) {
repo, err := repoFromLfsUrl(r.URL.Path)
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(500)
return
}
log.Printf("git lfs %s %s repo: %s\n", r.Method, r.URL, repo)
w.Header().Set("Content-Type", "application/vnd.git-lfs+json")
switch r.Method {
case "POST":
if strings.HasSuffix(r.URL.String(), "batch") {
lfsBatchHandler(w, r, repo)
} else {
lfsPostHandler(w, r, repo)
}
case "GET":
lfsGetHandler(w, r, repo)
default:
w.WriteHeader(405)
}
}
func lfsUrl(repo, oid string) string {
return server.URL + "/storage/" + oid + "?r=" + repo
}
func lfsPostHandler(w http.ResponseWriter, r *http.Request, repo string) {
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{
Oid: obj.Oid,
Size: obj.Size,
Links: map[string]lfsLink{
"upload": lfsLink{
Href: lfsUrl(repo, obj.Oid),
Header: map[string]string{},
},
},
}
if testingChunkedTransferEncoding(r) {
res.Links["upload"].Header["Transfer-Encoding"] = "chunked"
}
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, repo string) {
parts := strings.Split(r.URL.Path, "/")
oid := parts[len(parts)-1]
by, ok := largeObjects.Get(repo, oid)
if !ok {
w.WriteHeader(404)
return
}
obj := &lfsObject{
Oid: oid,
Size: int64(len(by)),
Links: map[string]lfsLink{
"download": lfsLink{
Href: lfsUrl(repo, 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)
}
func lfsBatchHandler(w http.ResponseWriter, r *http.Request, repo string) {
if !serveBatch {
w.WriteHeader(404)
return
}
type batchReq struct {
Operation string `json:"operation"`
Objects []lfsObject `json:"objects"`
}
buf := &bytes.Buffer{}
tee := io.TeeReader(r.Body, buf)
var objs batchReq
err := json.NewDecoder(tee).Decode(&objs)
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
log.Println("REQUEST")
log.Println(buf.String())
if err != nil {
log.Fatal(err)
}
res := []lfsObject{}
testingChunked := testingChunkedTransferEncoding(r)
for _, obj := range objs.Objects {
o := lfsObject{
Oid: obj.Oid,
Size: obj.Size,
Links: map[string]lfsLink{
"upload": lfsLink{
Href: lfsUrl(repo, obj.Oid),
Header: map[string]string{},
},
},
}
if testingChunked {
o.Links["upload"].Header["Transfer-Encoding"] = "chunked"
}
res = append(res, o)
}
ores := map[string][]lfsObject{"objects": res}
by, err := json.Marshal(ores)
if err != nil {
log.Fatal(err)
}
log.Println("RESPONSE: 200")
log.Println(string(by))
w.WriteHeader(200)
w.Write(by)
}
// handles any /storage/{oid} requests
func storageHandler(w http.ResponseWriter, r *http.Request) {
repo := r.URL.Query().Get("r")
log.Printf("storage %s %s repo: %s\n", r.Method, r.URL, repo)
switch r.Method {
case "PUT":
if testingChunkedTransferEncoding(r) {
valid := false
for _, value := range r.TransferEncoding {
if value == "chunked" {
valid = true
break
}
}
if !valid {
log.Fatal("Chunked transfer encoding expected")
}
}
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.Set(repo, oid, buf.Bytes())
case "GET":
parts := strings.Split(r.URL.Path, "/")
oid := parts[len(parts)-1]
if by, ok := largeObjects.Get(repo, oid); ok {
w.Write(by)
return
}
w.WriteHeader(404)
default:
w.WriteHeader(405)
}
}
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)
}
func redirect307Handler(w http.ResponseWriter, r *http.Request) {
// Send a redirect to info/lfs
// Make it either absolute or relative depending on subpath
parts := strings.Split(r.URL.Path, "/")
// first element is always blank since rooted
var redirectTo string
if parts[2] == "rel" {
redirectTo = "/" + strings.Join(parts[3:], "/")
} else if parts[2] == "abs" {
redirectTo = server.URL + "/" + strings.Join(parts[3:], "/")
} else {
log.Fatal(fmt.Errorf("Invalid URL for redirect: %v", r.URL))
w.WriteHeader(404)
return
}
w.Header().Set("Location", redirectTo)
w.WriteHeader(307)
}
func testingChunkedTransferEncoding(r *http.Request) bool {
return strings.HasPrefix(r.URL.String(), "/test-chunked-transfer-encoding")
}
var lfsUrlRE = regexp.MustCompile(`\A/?([^/]+)/info/lfs`)
func repoFromLfsUrl(urlpath string) (string, error) {
matches := lfsUrlRE.FindStringSubmatch(urlpath)
if len(matches) != 2 {
return "", fmt.Errorf("LFS url '%s' does not match %v", urlpath, lfsUrlRE)
}
repo := matches[1]
if strings.HasSuffix(repo, ".git") {
return repo[0 : len(repo)-4], nil
}
return repo, nil
}
type lfsStorage struct {
objects map[string]map[string][]byte
mutex *sync.Mutex
}
func (s *lfsStorage) Get(repo, oid string) ([]byte, bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
repoObjects, ok := s.objects[repo]
if !ok {
return nil, ok
}
by, ok := repoObjects[oid]
return by, ok
}
func (s *lfsStorage) Set(repo, oid string, by []byte) {
s.mutex.Lock()
defer s.mutex.Unlock()
repoObjects, ok := s.objects[repo]
if !ok {
repoObjects = make(map[string][]byte)
s.objects[repo] = repoObjects
}
repoObjects[oid] = by
}
func newLfsStorage() *lfsStorage {
return &lfsStorage{
objects: make(map[string]map[string][]byte),
mutex: &sync.Mutex{},
}
}