mirror of https://github.com/goproxyio/goproxy
parent
3d49ac1020
commit
b259869435
@ -1,21 +1,20 @@
|
||||
.PHONY: build generate image clean test
|
||||
.PHONY: build image clean test
|
||||
|
||||
export GO111MODULE=on
|
||||
|
||||
all: build
|
||||
|
||||
build: generate
|
||||
build: tidy
|
||||
@go build -o bin/goproxy -ldflags "-s -w" .
|
||||
|
||||
generate:
|
||||
@go generate
|
||||
tidy:
|
||||
@go mod tidy
|
||||
|
||||
image:
|
||||
@docker build -t goproxy/goproxy .
|
||||
|
||||
test: generate
|
||||
@go test -v `(go list ./... | grep "pkg/proxy")`
|
||||
test: tidy
|
||||
@go test -v ./...
|
||||
|
||||
clean:
|
||||
@git clean -f -d -X
|
||||
|
@ -1,44 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PKG="${PWD}/internal"
|
||||
GOROOT="$(go env GOROOT)"
|
||||
GOVERSION=`go version | awk -F ' ' '{print $3}'|grep '1.12'`
|
||||
GTGO12=0
|
||||
echo "ENV GOLANG VER: $GOVERSION"
|
||||
|
||||
if [ "$GOVERSION" != "" ]
|
||||
then
|
||||
GTGO12=1
|
||||
echo "use 1.12 mode"
|
||||
else
|
||||
echo "use 1.11 mode"
|
||||
fi
|
||||
|
||||
mkdir -p "${PKG}"
|
||||
cp -r "${GOROOT}/src/cmd/go/internal/"* "${PKG}"
|
||||
|
||||
cp -r "${GOROOT}/src/cmd/internal/browser" "${PKG}"
|
||||
cp -r "${GOROOT}/src/cmd/internal/buildid" "${PKG}"
|
||||
cp -r "${GOROOT}/src/cmd/internal/objabi" "${PKG}"
|
||||
cp -r "${GOROOT}/src/cmd/internal/test2json" "${PKG}"
|
||||
|
||||
cp -r "${GOROOT}/src/internal/singleflight" "${PKG}"
|
||||
cp -r "${GOROOT}/src/internal/testenv" "${PKG}"
|
||||
|
||||
if [ "$GTGO12" = "1" ]
|
||||
then
|
||||
cp -r "${GOROOT}/src/internal/xcoff" "${PKG}"
|
||||
cp -r "${GOROOT}/src/internal/goroot" "${PKG}"
|
||||
cp -r "${GOROOT}/src/cmd/internal/sys" "${PKG}"
|
||||
fi
|
||||
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/cmd\/go\/internal/github.com\/goproxyio\/goproxy\/internal/g' {} +
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/cmd\/internal/github.com\/goproxyio\/goproxy\/internal/g' {} +
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/singleflight/github.com\/goproxyio\/goproxy\/internal\/singleflight/g' {} +
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/testenv/github.com\/goproxyio\/goproxy\/internal\/testenv/g' {} +
|
||||
|
||||
if [ "$GTGO12" = "1" ]
|
||||
then
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/goroot/github.com\/goproxyio\/goproxy\/internal\/goroot/g' {} +
|
||||
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/xcoff/github.com\/goproxyio\/goproxy\/internal\/xcoff/g' {} +
|
||||
fi
|
@ -1,3 +1,5 @@
|
||||
module github.com/goproxyio/goproxy
|
||||
|
||||
go 1.12
|
||||
|
||||
require golang.org/x/mod v0.1.0
|
||||
|
@ -0,0 +1,8 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
@ -1,94 +1,194 @@
|
||||
//go:generate ./build/generate.sh
|
||||
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Usage:
|
||||
//
|
||||
// goproxy [-listen [host]:port] [-cacheDir /tmp]
|
||||
//
|
||||
// goproxy serves the Go module proxy HTTP protocol at the given address (default 0.0.0.0:8081).
|
||||
// It invokes the local go command to answer requests and therefore reuses
|
||||
// the current GOPATH's module download cache and configuration (GOPROXY, GOSUMDB, and so on).
|
||||
//
|
||||
// While the proxy is running, setting GOPROXY=http://host:port will instruct the go command to use it.
|
||||
// Note that the module proxy cannot share a GOPATH with its own clients or else fetches will deadlock.
|
||||
// (The client will lock the entry as “being downloaded” before sending the request to the proxy,
|
||||
// which will then wait for the apparently-in-progress download to finish.)
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/goproxyio/goproxy/pkg/proxy"
|
||||
"github.com/goproxyio/goproxy/proxy"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
)
|
||||
|
||||
var downloadRoot string
|
||||
|
||||
const listExpire = 5 * time.Minute
|
||||
|
||||
var listen string
|
||||
var cacheDir string
|
||||
|
||||
func init() {
|
||||
log.SetOutput(os.Stdout)
|
||||
flag.StringVar(&cacheDir, "cacheDir", "", "go modules cache dir")
|
||||
flag.StringVar(&listen, "listen", "0.0.0.0:8081", "service listen address")
|
||||
flag.Parse()
|
||||
isGitValid := checkGitVersion()
|
||||
if !isGitValid {
|
||||
log.Fatal("Error in git version, please check your git installed in local, must be great 2.0")
|
||||
}
|
||||
}
|
||||
|
||||
func checkGitVersion() bool {
|
||||
var err error
|
||||
var ret []byte
|
||||
cmd := exec.Command("git", "version")
|
||||
if ret, err = cmd.Output(); err != nil {
|
||||
return false
|
||||
if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
|
||||
os.Setenv("GIT_TERMINAL_PROMPT", "0")
|
||||
}
|
||||
if strings.HasPrefix(string(ret), "git version 2") {
|
||||
return true
|
||||
|
||||
if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
|
||||
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
errCh := make(chan error)
|
||||
|
||||
log.Printf("goproxy: %s inited. listen on %s\n", time.Now().Format("2006-01-02 15:04:05"), listen)
|
||||
|
||||
if cacheDir == "" {
|
||||
cacheDir = "/go"
|
||||
gpEnv := os.Getenv("GOPATH")
|
||||
if gpEnv != "" {
|
||||
gp := filepath.SplitList(gpEnv)
|
||||
if gp[0] != "" {
|
||||
cacheDir = gp[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
fullCacheDir := filepath.Join(cacheDir, "pkg", "mod", "cache", "download")
|
||||
if _, err := os.Stat(fullCacheDir); os.IsNotExist(err) {
|
||||
log.Printf("goproxy: cache dir %s is not exist. To create it.\n", fullCacheDir)
|
||||
if err := os.MkdirAll(fullCacheDir, 0755); err != nil {
|
||||
log.Fatalf("goproxy: make cache dir failed: %s", err)
|
||||
}
|
||||
}
|
||||
server := http.Server{
|
||||
Addr: listen,
|
||||
Handler: proxy.NewProxy(cacheDir),
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
signCh := make(chan os.Signal)
|
||||
signal.Notify(signCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
log.SetPrefix("goproxy.io: ")
|
||||
log.SetFlags(0)
|
||||
// TODO flags
|
||||
var env struct {
|
||||
GOPATH string
|
||||
}
|
||||
if err := goJSON(&env, "go", "env", "-json", "GOPATH"); err != nil {
|
||||
log.Fatal(err)
|
||||
case sign := <-signCh:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(ctx)
|
||||
log.Printf("goproxy: Server gracefully %s", sign)
|
||||
}
|
||||
list := filepath.SplitList(env.GOPATH)
|
||||
if len(list) == 0 || list[0] == "" {
|
||||
log.Fatalf("missing $GOPATH")
|
||||
}
|
||||
downloadRoot = filepath.Join(list[0], "pkg/mod/cache/download")
|
||||
|
||||
if cacheDir != "" {
|
||||
downloadRoot = filepath.Join(cacheDir, "pkg/mod/cache/download")
|
||||
os.Setenv("GOPATH", cacheDir)
|
||||
}
|
||||
|
||||
log.Fatal(http.ListenAndServe(listen, &logger{proxy.NewServer(new(ops))}))
|
||||
}
|
||||
|
||||
// goJSON runs the go command and parses its JSON output into dst.
|
||||
func goJSON(dst interface{}, command ...string) error {
|
||||
cmd := exec.Command(command[0], command[1:]...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s:\n%s%s", strings.Join(command, " "), stderr.String(), stdout.String())
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), dst); err != nil {
|
||||
return fmt.Errorf("%s: reading json: %v", strings.Join(command, " "), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A logger is an http.Handler that logs traffic to standard error.
|
||||
type logger struct {
|
||||
h http.Handler
|
||||
}
|
||||
type responseLogger struct {
|
||||
code int
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (r *responseLogger) WriteHeader(code int) {
|
||||
r.code = code
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(os.Stderr, "------ --- %s\n", r.URL)
|
||||
start := time.Now()
|
||||
rl := &responseLogger{code: 200, ResponseWriter: w}
|
||||
l.h.ServeHTTP(rl, r)
|
||||
fmt.Fprintf(os.Stderr, "%.3fs %d %s\n", time.Since(start).Seconds(), rl.code, r.URL)
|
||||
}
|
||||
|
||||
// An ops is a proxy.ServerOps implementation.
|
||||
type ops struct{}
|
||||
|
||||
func (*ops) NewContext(r *http.Request) (context.Context, error) {
|
||||
return context.Background(), nil
|
||||
}
|
||||
func (*ops) List(ctx context.Context, path string) (proxy.File, error) {
|
||||
escMod, err := module.EscapePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file := filepath.Join(downloadRoot, escMod+"/@v/listproxy")
|
||||
if info, err := os.Stat(file); err == nil && time.Since(info.ModTime()) < listExpire {
|
||||
return os.Open(file)
|
||||
}
|
||||
var list struct {
|
||||
Path string
|
||||
Versions []string
|
||||
}
|
||||
if err := goJSON(&list, "go", "list", "-m", "-json", "-versions", path+"@latest"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list.Path != path {
|
||||
return nil, fmt.Errorf("go list -m: asked for %s but got %s", path, list.Path)
|
||||
}
|
||||
data := []byte(strings.Join(list.Versions, "\n") + "\n")
|
||||
if len(data) == 1 {
|
||||
data = nil
|
||||
}
|
||||
ioutil.WriteFile(file, data, 0666)
|
||||
return os.Open(file)
|
||||
}
|
||||
func (*ops) Latest(ctx context.Context, path string) (proxy.File, error) {
|
||||
d, err := download(module.Version{Path: path, Version: "latest"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(d.Info)
|
||||
}
|
||||
func (*ops) Info(ctx context.Context, m module.Version) (proxy.File, error) {
|
||||
d, err := download(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(d.Info)
|
||||
}
|
||||
func (*ops) GoMod(ctx context.Context, m module.Version) (proxy.File, error) {
|
||||
d, err := download(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(d.GoMod)
|
||||
}
|
||||
func (*ops) Zip(ctx context.Context, m module.Version) (proxy.File, error) {
|
||||
d, err := download(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(d.Zip)
|
||||
}
|
||||
|
||||
type downloadInfo struct {
|
||||
Path string
|
||||
Version string
|
||||
Info string
|
||||
GoMod string
|
||||
Zip string
|
||||
Dir string
|
||||
Sum string
|
||||
GoModSum string
|
||||
}
|
||||
|
||||
func download(m module.Version) (*downloadInfo, error) {
|
||||
d := new(downloadInfo)
|
||||
return d, goJSON(d, "go", "mod", "download", "-json", m.String())
|
||||
}
|
||||
|
@ -1,199 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goproxyio/goproxy/internal/cfg"
|
||||
"github.com/goproxyio/goproxy/internal/modfetch"
|
||||
"github.com/goproxyio/goproxy/internal/modfetch/codehost"
|
||||
"github.com/goproxyio/goproxy/internal/modload"
|
||||
"github.com/goproxyio/goproxy/internal/module"
|
||||
)
|
||||
|
||||
var cacheDir string
|
||||
var innerHandle http.Handler
|
||||
|
||||
type modInfo struct {
|
||||
module.Version
|
||||
suf string
|
||||
}
|
||||
|
||||
func setupEnv(basedir string) {
|
||||
modfetch.QuietLookup = true // just to hide modfetch/cache.go#127
|
||||
modfetch.PkgMod = filepath.Join(basedir, "pkg", "mod")
|
||||
codehost.WorkRoot = filepath.Join(modfetch.PkgMod, "cache", "vcs")
|
||||
cfg.CmdName = "mod download" // just to hide modfetch/fetch.go#L87
|
||||
}
|
||||
|
||||
func NewProxy(cache string) http.Handler {
|
||||
setupEnv(cache)
|
||||
|
||||
cacheDir = filepath.Join(modfetch.PkgMod, "cache", "download")
|
||||
innerHandle = http.FileServer(http.Dir(cacheDir))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("goproxy: %s request %s\n", r.RemoteAddr, r.URL.Path)
|
||||
info, err := parseModInfoFromUrl(r.URL.Path)
|
||||
if err != nil {
|
||||
innerHandle.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
switch suf := info.suf; suf {
|
||||
case ".info", ".mod", ".zip":
|
||||
{
|
||||
if _, err := os.Stat(filepath.Join(cacheDir, r.URL.Path)); err == nil {
|
||||
// cache files exist on disk
|
||||
innerHandle.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
realMod, err := getQuery(info.Version.Path, info.Version.Version)
|
||||
if err != nil {
|
||||
errLogger.Printf("goproxy: lookup %s@%s get err %s", info.Path, info.Version.Version, err)
|
||||
ReturnNotFound(w, err)
|
||||
return
|
||||
}
|
||||
if realMod.Path != info.Version.Path {
|
||||
log.Printf("goproxy: mod %s@%s may have subpath, just return to make client recurse", info.Path, info.Version.Version)
|
||||
ReturnSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
switch suf {
|
||||
case ".info":
|
||||
{
|
||||
if revInfo, err := modfetch.Stat(realMod.Path, realMod.Version); err != nil {
|
||||
// use Stat instead of InfoFile, because when query-version is master, no infoFile here, maybe bug of go
|
||||
// TODO(hxzhao527): check whether InfoFile have a bug?
|
||||
errLogger.Printf("goproxy: fetch info %s@%s get err %s", info.Path, info.Version.Version, err)
|
||||
ReturnNotFound(w, err)
|
||||
} else {
|
||||
ReturnJsonData(w, revInfo)
|
||||
}
|
||||
}
|
||||
case ".mod":
|
||||
{
|
||||
if modFile, err := modfetch.GoModFile(realMod.Path, realMod.Version); err != nil {
|
||||
errLogger.Printf("goproxy: fetch modfile %s@%s get err %s", info.Path, info.Version.Version, err)
|
||||
ReturnNotFound(w, err)
|
||||
} else {
|
||||
http.ServeFile(w, r, modFile)
|
||||
}
|
||||
}
|
||||
case ".zip":
|
||||
{
|
||||
mod := module.Version{Path: realMod.Path, Version: realMod.Version}
|
||||
if zipFile, err := modfetch.DownloadZip(mod); err != nil {
|
||||
errLogger.Printf("goproxy: download zip %s@%s get err %s", info.Path, info.Version.Version, err)
|
||||
ReturnNotFound(w, err)
|
||||
} else {
|
||||
http.ServeFile(w, r, zipFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
case "/@v/list", "/@latest":
|
||||
{
|
||||
repo, err := modfetch.Lookup(info.Path)
|
||||
if err != nil {
|
||||
ReturnNotFound(w, err)
|
||||
return
|
||||
}
|
||||
switch suf {
|
||||
case "/@v/list":
|
||||
modPath := strings.Trim(strings.TrimSuffix(r.URL.Path, "/@v/list"), "/")
|
||||
modPath, err := module.DecodePath(modPath)
|
||||
if err != nil {
|
||||
ReturnNotFound(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
modload.LoadBuildList()
|
||||
mods := modload.ListModules([]string{modPath + "@latest"}, false, true)
|
||||
|
||||
data := []byte(strings.Join(mods[0].Versions, "\n") + "\n")
|
||||
if len(data) == 1 {
|
||||
data = nil
|
||||
}
|
||||
w.Write(data)
|
||||
return
|
||||
case "/@latest":
|
||||
rev, err := repo.Stat("latest")
|
||||
if err != nil {
|
||||
ReturnNotFound(w, err)
|
||||
return
|
||||
}
|
||||
ReturnJsonData(w, rev)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseModInfoFromUrl(url string) (*modInfo, error) {
|
||||
|
||||
var modPath, modVersion, suf string
|
||||
var err error
|
||||
switch {
|
||||
case strings.HasSuffix(url, "/@v/list"):
|
||||
// /golang.org/x/net/@v/list
|
||||
suf = "/@v/list"
|
||||
modVersion = ""
|
||||
modPath = strings.Trim(strings.TrimSuffix(url, suf), "/")
|
||||
case strings.HasSuffix(url, "/@latest"):
|
||||
// /golang.org/x/@latest
|
||||
suf = "/@latest"
|
||||
modVersion = "latest"
|
||||
modPath = strings.Trim(strings.TrimSuffix(url, suf), "/")
|
||||
case strings.HasSuffix(url, ".info"), strings.HasSuffix(url, ".mod"), strings.HasSuffix(url, ".zip"):
|
||||
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.info
|
||||
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.mod
|
||||
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.zip
|
||||
suf = path.Ext(url)
|
||||
tmp := strings.Split(url, "/@v/")
|
||||
if len(tmp) != 2 {
|
||||
return nil, fmt.Errorf("bad module path:%s", url)
|
||||
}
|
||||
modPath = strings.Trim(tmp[0], "/")
|
||||
modVersion = strings.TrimSuffix(tmp[1], suf)
|
||||
|
||||
modVersion, err = module.DecodeVersion(modVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("bad module path:%s", url)
|
||||
}
|
||||
// decode path & version, next proxy and source need
|
||||
modPath, err = module.DecodePath(modPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &modInfo{module.Version{Path: modPath, Version: modVersion}, suf}, nil
|
||||
}
|
||||
|
||||
// getQuery evaluates the given package path, version pair
|
||||
// to determine the underlying module version being requested.
|
||||
// If forceModulePath is set, getQuery must interpret path
|
||||
// as a module path.
|
||||
func getQuery(path, vers string) (module.Version, error) {
|
||||
|
||||
// First choice is always to assume path is a module path.
|
||||
// If that works out, we're done.
|
||||
info, err := modload.Query(path, vers, modload.Allowed)
|
||||
if err == nil {
|
||||
return module.Version{Path: path, Version: info.Version}, nil
|
||||
}
|
||||
|
||||
// Otherwise, try a package path.
|
||||
m, _, err := modload.QueryPackage(path, vers, modload.Allowed)
|
||||
return m, err
|
||||
}
|
@ -1,338 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goproxyio/goproxy/internal/modfetch"
|
||||
"github.com/goproxyio/goproxy/internal/module"
|
||||
"github.com/goproxyio/goproxy/internal/testenv"
|
||||
)
|
||||
|
||||
var _handle http.Handler
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
tmpdir, err := ioutil.TempDir("", "goproxy-test-")
|
||||
if err != nil {
|
||||
log.Fatalf("init tmpdir failed: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
_handle = NewProxy(tmpdir)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
var _modInfoTests = []struct {
|
||||
path string
|
||||
query string // query
|
||||
version string // want
|
||||
latest bool
|
||||
time time.Time
|
||||
gomod string
|
||||
zip []string
|
||||
}{
|
||||
{
|
||||
path: "gopkg.in/check.v1",
|
||||
query: "v0.0.0-20161208181325-20d25e280405",
|
||||
version: "v0.0.0-20161208181325-20d25e280405",
|
||||
time: time.Date(2016, 12, 8, 18, 13, 25, 0, time.UTC),
|
||||
gomod: "module gopkg.in/check.v1\n",
|
||||
zip: []string{
|
||||
".gitignore",
|
||||
".travis.yml",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"TODO",
|
||||
"benchmark.go",
|
||||
"benchmark_test.go",
|
||||
"bootstrap_test.go",
|
||||
"check.go",
|
||||
"check_test.go",
|
||||
"checkers.go",
|
||||
"checkers_test.go",
|
||||
"export_test.go",
|
||||
"fixture_test.go",
|
||||
"foundation_test.go",
|
||||
"helpers.go",
|
||||
"helpers_test.go",
|
||||
"printer.go",
|
||||
"printer_test.go",
|
||||
"reporter.go",
|
||||
"reporter_test.go",
|
||||
"run.go",
|
||||
"run_test.go",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "github.com/PuerkitoBio/goquery",
|
||||
query: "v0.0.0-20181014175806-2af3d16e2bb8",
|
||||
version: "v0.0.0-20181014175806-2af3d16e2bb8",
|
||||
time: time.Date(2018, 10, 14, 17, 58, 6, 0, time.UTC),
|
||||
gomod: "module github.com/PuerkitoBio/goquery\n",
|
||||
zip: []string{
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".travis.yml",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"array.go",
|
||||
"array_test.go",
|
||||
"bench/v0.1.0",
|
||||
"bench/v0.1.1",
|
||||
"bench/v0.1.1-v0.2.1-go1.1rc1.svg",
|
||||
"bench/v0.2.0",
|
||||
"bench/v0.2.0-v0.2.1-go1.1rc1.svg",
|
||||
"bench/v0.2.1-go1.1rc1",
|
||||
"bench/v0.3.0",
|
||||
"bench/v0.3.2-go1.2",
|
||||
"bench/v0.3.2-go1.2-take2",
|
||||
"bench/v0.3.2-go1.2rc1",
|
||||
"bench/v1.0.0-go1.7",
|
||||
"bench/v1.0.1a-go1.7",
|
||||
"bench/v1.0.1b-go1.7",
|
||||
"bench/v1.0.1c-go1.7",
|
||||
"bench_array_test.go",
|
||||
"bench_example_test.go",
|
||||
"bench_expand_test.go",
|
||||
"bench_filter_test.go",
|
||||
"bench_iteration_test.go",
|
||||
"bench_property_test.go",
|
||||
"bench_query_test.go",
|
||||
"bench_traversal_test.go",
|
||||
"doc.go",
|
||||
"doc/tips.md",
|
||||
"example_test.go",
|
||||
"expand.go",
|
||||
"expand_test.go",
|
||||
"filter.go",
|
||||
"filter_test.go",
|
||||
"iteration.go",
|
||||
"iteration_test.go",
|
||||
"manipulation.go",
|
||||
"manipulation_test.go",
|
||||
"misc/git/pre-commit",
|
||||
"property.go",
|
||||
"property_test.go",
|
||||
"query.go",
|
||||
"query_test.go",
|
||||
"testdata/gotesting.html",
|
||||
"testdata/gowiki.html",
|
||||
"testdata/metalreview.html",
|
||||
"testdata/page.html",
|
||||
"testdata/page2.html",
|
||||
"testdata/page3.html",
|
||||
"traversal.go",
|
||||
"traversal_test.go",
|
||||
"type.go",
|
||||
"type_test.go",
|
||||
"utilities.go",
|
||||
"utilities_test.go",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "github.com/rsc/vgotest1",
|
||||
query: "v0.0.0-20180219223237-a08abb797a67",
|
||||
version: "v0.0.0-20180219223237-a08abb797a67",
|
||||
latest: true,
|
||||
time: time.Date(2018, 02, 19, 22, 32, 37, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
path: "github.com/hxzhao527/legacytest",
|
||||
query: "master",
|
||||
version: "v2.0.1+incompatible",
|
||||
time: time.Date(2018, 07, 17, 16, 42, 53, 0, time.UTC),
|
||||
gomod: "module github.com/hxzhao527/legacytest\n",
|
||||
zip: []string{
|
||||
"x.go",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "github.com/micro/go-api/resolver",
|
||||
query: "v0.5.0",
|
||||
version: "v0.5.0",
|
||||
gomod: "module github.com/micro/go-api\n",
|
||||
},
|
||||
}
|
||||
|
||||
var _modListTests = []struct {
|
||||
path string
|
||||
versions []string
|
||||
}{
|
||||
{
|
||||
path: "github.com/rsc/vgotest1",
|
||||
versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0", "v2.0.0+incompatible"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestFetchInfo(t *testing.T) {
|
||||
testenv.MustHaveExternalNetwork(t)
|
||||
|
||||
for _, mod := range _modInfoTests {
|
||||
req := buildRequest(mod.path, mod.query, ".info")
|
||||
|
||||
rr, err := basicCheck(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// check return data
|
||||
info := new(modfetch.RevInfo)
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
|
||||
t.Errorf("package info is not recognized")
|
||||
continue
|
||||
}
|
||||
if mod.version != info.Version {
|
||||
t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
|
||||
}
|
||||
if !mod.time.Equal(info.Time) {
|
||||
t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func TestFetchModFile(t *testing.T) {
|
||||
testenv.MustHaveExternalNetwork(t)
|
||||
|
||||
for _, mod := range _modInfoTests {
|
||||
if len(mod.gomod) == 0 {
|
||||
continue
|
||||
}
|
||||
req := buildRequest(mod.path, mod.query, ".mod")
|
||||
|
||||
rr, err := basicCheck(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if data := rr.Body.String(); data != mod.gomod {
|
||||
t.Errorf("repo.GoMod(%q) = %q, want %q", mod.version, data, mod.gomod)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestFetchZip(t *testing.T) {
|
||||
testenv.MustHaveExternalNetwork(t)
|
||||
|
||||
for _, mod := range _modInfoTests {
|
||||
if len(mod.zip) == 0 {
|
||||
continue
|
||||
}
|
||||
req := buildRequest(mod.path, mod.query, ".zip")
|
||||
|
||||
rr, err := basicCheck(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
prefix := mod.path + "@" + mod.version + "/"
|
||||
var names []string
|
||||
|
||||
data := rr.Body.Bytes() // ??
|
||||
z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
t.Errorf("open %s's zip failed: %v", mod.path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range z.File {
|
||||
if !strings.HasPrefix(file.Name, prefix) {
|
||||
t.Errorf("zip entry %v does not start with prefix %v", file.Name, prefix)
|
||||
continue
|
||||
}
|
||||
names = append(names, file.Name[len(prefix):])
|
||||
}
|
||||
if !reflect.DeepEqual(names, mod.zip) {
|
||||
t.Errorf("zip = %v\nwant %v\n", names, mod.zip)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLatest(t *testing.T) {
|
||||
testenv.MustHaveExternalNetwork(t)
|
||||
|
||||
for _, mod := range _modInfoTests {
|
||||
if !mod.latest {
|
||||
continue
|
||||
}
|
||||
req := buildRequest(mod.path, "latest", "")
|
||||
|
||||
rr, err := basicCheck(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
info := new(modfetch.RevInfo)
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
|
||||
t.Errorf("package info is not recognized")
|
||||
continue
|
||||
}
|
||||
if mod.version != info.Version {
|
||||
t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
|
||||
}
|
||||
if !mod.time.Equal(info.Time) {
|
||||
t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
|
||||
for _, mod := range _modListTests {
|
||||
req := buildRequest(mod.path, "", "")
|
||||
|
||||
rr, err := basicCheck(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
modfetch.SortVersions(mod.versions)
|
||||
|
||||
if data := rr.Body.String(); strings.Join(mod.versions, "\n") != data {
|
||||
t.Errorf("list not well,\n expected: %v\n, got: %v", mod.versions, strings.Split(data, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func buildRequest(modPath, modVersion string, ext string) *http.Request {
|
||||
modPath, _ = module.EncodePath(modPath)
|
||||
modVersion, _ = module.EncodeVersion(modVersion)
|
||||
url := "/" + modPath
|
||||
switch modVersion {
|
||||
case "":
|
||||
url += "/@v/list"
|
||||
case "latest":
|
||||
url += "/@latest"
|
||||
default:
|
||||
url = url + "/@v/" + modVersion + ext
|
||||
}
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
return req
|
||||
}
|
||||
|
||||
func basicCheck(req *http.Request) (*httptest.ResponseRecorder, error) {
|
||||
rr := httptest.NewRecorder()
|
||||
_handle.ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
return nil, fmt.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
return rr, nil
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var errLogger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
|
||||
func ReturnInternalServerError(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
msg := fmt.Sprintf("%v", err)
|
||||
errLogger.Printf("goproxy: %s\n", msg)
|
||||
_, _ = w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func ReturnBadRequest(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
msg := fmt.Sprintf("%v", err)
|
||||
errLogger.Printf("goproxy: %s\n", msg)
|
||||
_, _ = w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func ReturnNotFound(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
msg := fmt.Sprintf("%v", err)
|
||||
errLogger.Printf("goproxy: %s\n", msg)
|
||||
_, _ = w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func ReturnSuccess(w http.ResponseWriter, data []byte) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func ReturnJsonData(w http.ResponseWriter, data interface{}) {
|
||||
js, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
ReturnNotFound(w, err)
|
||||
} else {
|
||||
ReturnSuccess(w, js)
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Package proxy implements the HTTP protocols for serving a Go module proxy.
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
)
|
||||
|
||||
// A ServerOps provides the external operations
|
||||
// (accessing module information and so on) needed by the Server.
|
||||
type ServerOps interface {
|
||||
// NewContext returns the context to use for the request r.
|
||||
NewContext(r *http.Request) (context.Context, error)
|
||||
// List, Latest, Info, GoMod, and Zip all return a File to be sent to a client.
|
||||
// The File will be closed after its contents are sent.
|
||||
// In the case of an error, if the error satisfies errors.Is(err, os.ErrNotFound),
|
||||
// the server responds with an HTTP 404 error;
|
||||
// otherwise it responds with an HTTP 500 error.
|
||||
// List returns a list of tagged versions of the module identified by path.
|
||||
// The versions should all be canonical semantic versions
|
||||
// and formatted in a text listing, one per line.
|
||||
// Pseudo-versions derived from untagged commits should be omitted.
|
||||
// The go command exposes this list in 'go list -m -versions' output
|
||||
// and also uses it to resolve wildcards like 'go get m@v1.2'.
|
||||
List(ctx context.Context, path string) (File, error)
|
||||
// Latest returns an info file for the latest known version of the module identified by path.
|
||||
// The go command uses this for 'go get m' or 'go get m@latest'
|
||||
// but only after finding no suitable version among the ones returned by List.
|
||||
// Typically, Latest should return a pseudo-version for the latest known commit.
|
||||
Latest(ctx context.Context, path string) (File, error)
|
||||
// Info opens and returns the module version's info file.
|
||||
// The requested version can be a canonical semantic version
|
||||
// but can also be an arbitrary version reference, like "master".
|
||||
//
|
||||
// The metadata in the returned file should be a JSON object corresponding
|
||||
// to the Go type
|
||||
//
|
||||
// type Info struct {
|
||||
// Version string
|
||||
// Time time.Time
|
||||
// }
|
||||
//
|
||||
// where the version is the resolved canonical semantic version
|
||||
// and the time is the commit or publication time of that version
|
||||
// (for use with go list -m).
|
||||
// The NewInfo function can be used to construct an info File.
|
||||
//
|
||||
// Proxies should obtain the module version information by
|
||||
// executing 'go mod download -json' and caching the file
|
||||
// listed in the Info field.
|
||||
Info(ctx context.Context, m module.Version) (File, error)
|
||||
// GoMod opens and returns the module's go.mod file.
|
||||
// The requested version is a canonical semantic version.
|
||||
//
|
||||
// Proxies should obtain the module version information by
|
||||
// executing 'go mod download -json' and caching the file
|
||||
// listed in the GoMod field.
|
||||
GoMod(ctx context.Context, m module.Version) (File, error)
|
||||
// Zip opens and returns the module's zip file.
|
||||
// The requested version is a canonical semantic version.
|
||||
//
|
||||
// Proxies should obtain the module version information by
|
||||
// executing 'go mod download -json' and caching the file
|
||||
// listed in the Zip field.
|
||||
Zip(ctx context.Context, m module.Version) (File, error)
|
||||
}
|
||||
|
||||
// A File is a file to be served, typically an *os.File or the result of calling MemFile or NewInfo.
|
||||
// The modification time is the only necessary field in the Stat result.
|
||||
type File interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
Stat() (os.FileInfo, error)
|
||||
}
|
||||
|
||||
// A Server is the proxy HTTP server,
|
||||
// which implements http.Handler and should be invoked
|
||||
// to serve the paths listed in ServerPaths.
|
||||
//
|
||||
// The server assumes that the requests are made to the root of the URL space,
|
||||
// so it should typically be registered using:
|
||||
//
|
||||
// srv := proxy.NewServer(ops)
|
||||
// http.Handle("/", srv)
|
||||
//
|
||||
// To register a server at a subdirectory of the URL space, wrap the server in http.StripPrefix:
|
||||
//
|
||||
// srv := proxy.NewServer(ops)
|
||||
// http.Handle("/proxy/", http.StripPrefix("/proxy", srv))
|
||||
//
|
||||
// All recognized requests to the server contain the substring "/@v/" in the URL.
|
||||
// The server will respond with an http.StatusBadRequest (400) error to unrecognized requests.
|
||||
type Server struct {
|
||||
ops ServerOps
|
||||
}
|
||||
|
||||
// NewServer returns a new Server using the given operations.
|
||||
func NewServer(ops ServerOps) *Server {
|
||||
return &Server{ops: ops}
|
||||
}
|
||||
|
||||
// ServeHTTP is the server's implementation of http.Handler.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, err := s.ops.NewContext(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
i := strings.Index(r.URL.Path, "/@")
|
||||
if i < 0 {
|
||||
http.Error(w, "no such path", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
modPath, err := module.UnescapePath(strings.TrimPrefix(r.URL.Path[:i], "/"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
what := r.URL.Path[i+len("/@"):]
|
||||
const (
|
||||
contentTypeJSON = "application/json"
|
||||
contentTypeText = "text/plain; charset=UTF-8"
|
||||
contentTypeBinary = "application/octet-stream"
|
||||
)
|
||||
var ctype string
|
||||
var f File
|
||||
var openErr error
|
||||
switch what {
|
||||
case "latest":
|
||||
ctype = contentTypeJSON
|
||||
f, openErr = s.ops.Latest(ctx, modPath)
|
||||
case "v/list":
|
||||
ctype = contentTypeText
|
||||
f, openErr = s.ops.List(ctx, modPath)
|
||||
default:
|
||||
what = strings.TrimPrefix(what, "v/")
|
||||
ext := path.Ext(what)
|
||||
vers, err := module.UnescapeVersion(strings.TrimSuffix(what, ext))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
m := module.Version{Path: modPath, Version: vers}
|
||||
if vers == "latest" {
|
||||
// The go command handles "go get m@latest" by fetching /m/@v/latest, not latest.info.
|
||||
// We should never see requests for "latest.info" and so on, so avoid confusion
|
||||
// by disallowing it early.
|
||||
http.Error(w, "version latest is disallowed", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// All requests require canonical versions except for info,
|
||||
// which accepts any revision identifier known to the underlying storage.
|
||||
if ext != ".info" && vers != module.CanonicalVersion(vers) {
|
||||
http.Error(w, "version "+vers+" is not in canonical form", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch ext {
|
||||
case ".info":
|
||||
ctype = "application/json"
|
||||
f, openErr = s.ops.Info(ctx, m)
|
||||
case ".mod":
|
||||
ctype = "text/plain; charset=UTF-8"
|
||||
f, openErr = s.ops.GoMod(ctx, m)
|
||||
case ".zip":
|
||||
ctype = "application/octet-stream"
|
||||
f, openErr = s.ops.Zip(ctx, m)
|
||||
default:
|
||||
http.Error(w, "request not recognized", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
if openErr != nil {
|
||||
code := http.StatusNotFound
|
||||
http.Error(w, "not found", code)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
http.Error(w, "unexpected directory", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
http.ServeContent(w, r, what, info.ModTime(), f)
|
||||
}
|
||||
|
||||
// MemFile returns an File containing the given in-memory content and modification time.
|
||||
func MemFile(data []byte, t time.Time) File {
|
||||
return &memFile{bytes.NewReader(data), memStat{t, int64(len(data))}}
|
||||
}
|
||||
|
||||
type memFile struct {
|
||||
*bytes.Reader
|
||||
stat memStat
|
||||
}
|
||||
|
||||
func (f *memFile) Close() error { return nil }
|
||||
func (f *memFile) Stat() (os.FileInfo, error) { return &f.stat, nil }
|
||||
func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { return nil, os.ErrInvalid }
|
||||
|
||||
type memStat struct {
|
||||
t time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
func (s *memStat) Name() string { return "memfile" }
|
||||
func (s *memStat) Size() int64 { return s.size }
|
||||
func (s *memStat) Mode() os.FileMode { return 0444 }
|
||||
func (s *memStat) ModTime() time.Time { return s.t }
|
||||
func (s *memStat) IsDir() bool { return false }
|
||||
func (s *memStat) Sys() interface{} { return nil }
|
||||
|
||||
// NewInfo returns a formatted info file for the given version, time pair.
|
||||
// The version should be a canonical semantic version.
|
||||
func NewInfo(version string, t time.Time) File {
|
||||
var info = struct {
|
||||
Version string
|
||||
Time time.Time
|
||||
}{version, t}
|
||||
js, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
// json.Marshal only fails for bad types; there are no bad types in info.
|
||||
panic("unexpected json.Marshal failure")
|
||||
}
|
||||
return MemFile(js, t)
|
||||
}
|
Loading…
Reference in new issue