2015-05-15 16:52:42 +00:00
|
|
|
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"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
repoDir string
|
2015-07-06 18:32:14 +00:00
|
|
|
largeObjects = newLfsStorage()
|
2015-05-15 16:52:42 +00:00
|
|
|
server *httptest.Server
|
2015-06-02 15:54:03 +00:00
|
|
|
serveBatch = true
|
2015-05-15 16:52:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
2015-05-18 19:44:22 +00:00
|
|
|
repoDir = os.Getenv("LFSTEST_DIR")
|
2015-05-15 16:52:42 +00:00
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
server = httptest.NewServer(mux)
|
|
|
|
stopch := make(chan bool)
|
|
|
|
|
2015-06-02 15:54:03 +00:00
|
|
|
mux.HandleFunc("/startbatch", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
serveBatch = true
|
|
|
|
})
|
|
|
|
|
|
|
|
mux.HandleFunc("/stopbatch", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
serveBatch = false
|
|
|
|
})
|
|
|
|
|
2015-05-15 16:52:42 +00:00
|
|
|
mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
stopch <- true
|
|
|
|
})
|
|
|
|
|
|
|
|
mux.HandleFunc("/storage/", storageHandler)
|
2015-06-29 11:57:50 +00:00
|
|
|
mux.HandleFunc("/redirect307/", redirect307Handler)
|
2015-05-15 16:52:42 +00:00
|
|
|
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
|
2015-05-18 23:15:32 +00:00
|
|
|
log.Println("git server done")
|
2015-05-15 16:52:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
2015-05-19 21:18:14 +00:00
|
|
|
switch r.Method {
|
|
|
|
case "POST":
|
2015-05-21 20:51:02 +00:00
|
|
|
if strings.HasSuffix(r.URL.String(), "batch") {
|
|
|
|
lfsBatchHandler(w, r)
|
|
|
|
} else {
|
|
|
|
lfsPostHandler(w, r)
|
|
|
|
}
|
2015-05-19 21:18:14 +00:00
|
|
|
case "GET":
|
2015-05-15 16:52:42 +00:00
|
|
|
lfsGetHandler(w, r)
|
2015-05-19 21:18:14 +00:00
|
|
|
default:
|
|
|
|
w.WriteHeader(405)
|
2015-05-15 16:52:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-06 18:32:14 +00:00
|
|
|
func lfsUrl(oid string) string {
|
|
|
|
return server.URL + "/storage/" + oid
|
|
|
|
}
|
|
|
|
|
2015-05-15 16:52:42 +00:00
|
|
|
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{
|
2015-05-21 18:11:25 +00:00
|
|
|
Oid: obj.Oid,
|
|
|
|
Size: obj.Size,
|
2015-05-15 16:52:42 +00:00
|
|
|
Links: map[string]lfsLink{
|
|
|
|
"upload": lfsLink{
|
2015-07-06 18:32:14 +00:00
|
|
|
Href: lfsUrl(obj.Oid),
|
2015-06-12 15:19:53 +00:00
|
|
|
Header: map[string]string{},
|
2015-05-15 16:52:42 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2015-06-12 15:19:53 +00:00
|
|
|
if testingChunkedTransferEncoding(r) {
|
|
|
|
res.Links["upload"].Header["Transfer-Encoding"] = "chunked"
|
|
|
|
}
|
|
|
|
|
2015-05-15 16:52:42 +00:00
|
|
|
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]
|
|
|
|
|
2015-07-06 18:32:14 +00:00
|
|
|
by, ok := largeObjects.Get(oid)
|
2015-05-19 21:18:14 +00:00
|
|
|
if !ok {
|
|
|
|
w.WriteHeader(404)
|
|
|
|
return
|
|
|
|
}
|
2015-05-15 16:52:42 +00:00
|
|
|
|
2015-05-19 21:18:14 +00:00
|
|
|
obj := &lfsObject{
|
|
|
|
Oid: oid,
|
|
|
|
Size: int64(len(by)),
|
|
|
|
Links: map[string]lfsLink{
|
|
|
|
"download": lfsLink{
|
2015-07-06 18:32:14 +00:00
|
|
|
Href: lfsUrl(oid),
|
2015-05-19 21:18:14 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2015-05-15 16:52:42 +00:00
|
|
|
|
2015-05-19 21:18:14 +00:00
|
|
|
by, err := json.Marshal(obj)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2015-05-15 16:52:42 +00:00
|
|
|
}
|
|
|
|
|
2015-05-19 21:18:14 +00:00
|
|
|
log.Println("RESPONSE: 200")
|
|
|
|
log.Println(string(by))
|
|
|
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write(by)
|
2015-05-15 16:52:42 +00:00
|
|
|
}
|
|
|
|
|
2015-05-21 20:51:02 +00:00
|
|
|
func lfsBatchHandler(w http.ResponseWriter, r *http.Request) {
|
2015-06-02 15:54:03 +00:00
|
|
|
if !serveBatch {
|
|
|
|
w.WriteHeader(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-06-29 16:56:57 +00:00
|
|
|
type batchReq struct {
|
|
|
|
Operation string `json:"operation"`
|
|
|
|
Objects []lfsObject `json:"objects"`
|
|
|
|
}
|
|
|
|
|
2015-05-21 20:51:02 +00:00
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
tee := io.TeeReader(r.Body, buf)
|
2015-06-29 16:56:57 +00:00
|
|
|
var objs batchReq
|
2015-05-21 20:51:02 +00:00
|
|
|
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{}
|
2015-06-12 15:19:53 +00:00
|
|
|
testingChunked := testingChunkedTransferEncoding(r)
|
2015-06-29 16:56:57 +00:00
|
|
|
for _, obj := range objs.Objects {
|
2015-05-21 20:51:02 +00:00
|
|
|
o := lfsObject{
|
|
|
|
Oid: obj.Oid,
|
|
|
|
Size: obj.Size,
|
|
|
|
Links: map[string]lfsLink{
|
|
|
|
"upload": lfsLink{
|
2015-07-06 18:32:14 +00:00
|
|
|
Href: lfsUrl(obj.Oid),
|
2015-06-12 15:19:53 +00:00
|
|
|
Header: map[string]string{},
|
2015-05-21 20:51:02 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2015-06-12 15:19:53 +00:00
|
|
|
if testingChunked {
|
|
|
|
o.Links["upload"].Header["Transfer-Encoding"] = "chunked"
|
|
|
|
}
|
|
|
|
|
2015-05-21 20:51:02 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2015-05-15 16:52:42 +00:00
|
|
|
// 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":
|
2015-06-12 15:19:53 +00:00
|
|
|
if testingChunkedTransferEncoding(r) {
|
|
|
|
valid := false
|
|
|
|
for _, value := range r.TransferEncoding {
|
|
|
|
if value == "chunked" {
|
|
|
|
valid = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
log.Fatal("Chunked transfer encoding expected")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-15 16:52:42 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-07-06 18:32:14 +00:00
|
|
|
largeObjects.Set(oid, buf.Bytes())
|
2015-05-15 16:52:42 +00:00
|
|
|
|
|
|
|
case "GET":
|
|
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
|
|
oid := parts[len(parts)-1]
|
|
|
|
|
2015-07-06 18:32:14 +00:00
|
|
|
if by, ok := largeObjects.Get(oid); ok {
|
2015-05-15 16:52:42 +00:00
|
|
|
w.Write(by)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(404)
|
|
|
|
default:
|
2015-05-19 21:18:14 +00:00
|
|
|
w.WriteHeader(405)
|
2015-05-15 16:52:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2015-06-29 11:57:50 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2015-06-12 15:19:53 +00:00
|
|
|
|
|
|
|
func testingChunkedTransferEncoding(r *http.Request) bool {
|
|
|
|
return strings.HasPrefix(r.URL.String(), "/test-chunked-transfer-encoding")
|
|
|
|
}
|
2015-07-06 18:32:14 +00:00
|
|
|
|
|
|
|
type lfsStorage struct {
|
|
|
|
objects map[string][]byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *lfsStorage) Get(oid string) ([]byte, bool) {
|
|
|
|
by, ok := s.objects[oid]
|
|
|
|
return by, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *lfsStorage) Set(oid string, by []byte) {
|
|
|
|
s.objects[oid] = by
|
|
|
|
}
|
|
|
|
|
|
|
|
func newLfsStorage() *lfsStorage {
|
|
|
|
return &lfsStorage{
|
|
|
|
objects: make(map[string][]byte),
|
|
|
|
}
|
|
|
|
}
|