initial commit
This commit is contained in:
230
pkg/api/api.go
Normal file
230
pkg/api/api.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/sixt/gomodproxy/pkg/store"
|
||||
"github.com/sixt/gomodproxy/pkg/vcs"
|
||||
)
|
||||
|
||||
type logger = func(v ...interface{})
|
||||
|
||||
type api struct {
|
||||
log logger
|
||||
vcsPaths []vcsPath
|
||||
stores []store.Store
|
||||
}
|
||||
|
||||
type vcsPath struct {
|
||||
prefix string
|
||||
vcs func(module string) vcs.VCS
|
||||
}
|
||||
|
||||
type Option func(*api)
|
||||
|
||||
var (
|
||||
apiList = regexp.MustCompile(`^/(?P<module>.*)/@v/list$`)
|
||||
apiInfo = regexp.MustCompile(`^/(?P<module>.*)/@v/(?P<version>.*).info$`)
|
||||
apiMod = regexp.MustCompile(`^/(?P<module>.*)/@v/(?P<version>.*).mod$`)
|
||||
apiZip = regexp.MustCompile(`^/(?P<module>.*)/@v/(?P<version>.*).zip$`)
|
||||
)
|
||||
|
||||
func New(options ...Option) http.Handler {
|
||||
api := &api{log: func(...interface{}) {}}
|
||||
for _, opt := range options {
|
||||
opt(api)
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
func Log(log logger) Option { return func(api *api) { api.log = log } }
|
||||
|
||||
func Git(prefix string, auth string) Option {
|
||||
a := vcs.Key(auth)
|
||||
if creds := strings.SplitN(auth, ":", 2); len(creds) == 2 {
|
||||
a = vcs.Password(creds[0], creds[1])
|
||||
}
|
||||
return func(api *api) {
|
||||
api.vcsPaths = append(api.vcsPaths, vcsPath{
|
||||
prefix: prefix,
|
||||
vcs: func(module string) vcs.VCS {
|
||||
return vcs.NewGit(api.log, module, a)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Memory() Option {
|
||||
return func(api *api) {
|
||||
api.stores = append(api.stores, store.Memory())
|
||||
}
|
||||
}
|
||||
|
||||
func CacheDir(dir string) Option {
|
||||
return func(api *api) {
|
||||
api.stores = append(api.stores, store.Disk(dir))
|
||||
}
|
||||
}
|
||||
|
||||
func decodeBangs(s string) string {
|
||||
buf := []rune{}
|
||||
bang := false
|
||||
for _, r := range []rune(s) {
|
||||
if bang {
|
||||
bang = false
|
||||
buf = append(buf, unicode.ToUpper(r))
|
||||
continue
|
||||
}
|
||||
if r == '!' {
|
||||
bang = true
|
||||
continue
|
||||
}
|
||||
buf = append(buf, r)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (api *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
defer func() { api.log("api.ServeHTTP", "method", r.Method, "url", r.URL, "time", time.Since(now)) }()
|
||||
|
||||
for _, route := range []struct {
|
||||
regexp *regexp.Regexp
|
||||
handler func(w http.ResponseWriter, r *http.Request, module, version string)
|
||||
}{
|
||||
{apiList, api.list},
|
||||
{apiInfo, api.info},
|
||||
{apiMod, api.mod},
|
||||
{apiZip, api.zip},
|
||||
} {
|
||||
if m := route.regexp.FindStringSubmatch(r.URL.Path); m != nil {
|
||||
module, version := m[1], ""
|
||||
if len(m) > 2 {
|
||||
version = m[2]
|
||||
}
|
||||
module = decodeBangs(module)
|
||||
route.handler(w, r, module, version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
func (api *api) vcs(ctx context.Context, module string) vcs.VCS {
|
||||
for _, path := range api.vcsPaths {
|
||||
if strings.HasPrefix(module, path.prefix) {
|
||||
return path.vcs(module)
|
||||
}
|
||||
}
|
||||
return vcs.NewGit(api.log, module, vcs.NoAuth())
|
||||
}
|
||||
|
||||
func (api *api) module(ctx context.Context, module string, version vcs.Version) ([]byte, time.Time, error) {
|
||||
for _, store := range api.stores {
|
||||
if snapshot, err := store.Get(ctx, module, version); err == nil {
|
||||
return snapshot.Data, snapshot.Timestamp, nil
|
||||
}
|
||||
}
|
||||
|
||||
timestamp, err := api.vcs(ctx, module).Timestamp(ctx, version)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
zr, err := api.vcs(ctx, module).Zip(ctx, version)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
if _, err := io.Copy(b, zr); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
for i := len(api.stores) - 1; i >= 0; i-- {
|
||||
err := api.stores[i].Put(ctx, store.Snapshot{
|
||||
Module: module,
|
||||
Version: version,
|
||||
Timestamp: timestamp,
|
||||
Data: b.Bytes(),
|
||||
})
|
||||
if err != nil {
|
||||
api.log("api.module.Put", "module", module, "version", version, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return b.Bytes(), timestamp, nil
|
||||
}
|
||||
|
||||
func (api *api) list(w http.ResponseWriter, r *http.Request, module, version string) {
|
||||
api.log("api.list", "module", module)
|
||||
list, err := api.vcs(r.Context(), module).List(r.Context())
|
||||
if err != nil {
|
||||
api.log("api.list", "module", module, "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range list {
|
||||
fmt.Fprintln(w, string(v))
|
||||
}
|
||||
}
|
||||
|
||||
func (api *api) info(w http.ResponseWriter, r *http.Request, module, version string) {
|
||||
api.log("api.info", "module", module, "version", version)
|
||||
_, t, err := api.module(r.Context(), module, vcs.Version(version))
|
||||
|
||||
if err != nil {
|
||||
api.log("api.info", "module", module, "version", version, "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Version string
|
||||
Time time.Time
|
||||
}{version, t})
|
||||
}
|
||||
|
||||
func (api *api) mod(w http.ResponseWriter, r *http.Request, module, version string) {
|
||||
api.log("api.mod", "module", module, "version", version)
|
||||
b, _, err := api.module(r.Context(), module, vcs.Version(version))
|
||||
if err == nil {
|
||||
if zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))); err == nil {
|
||||
for _, f := range zr.File {
|
||||
if f.Name == filepath.Join(module+"@"+string(version), "go.mod") {
|
||||
if r, err := f.Open(); err == nil {
|
||||
defer r.Close()
|
||||
io.Copy(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("module %s\n", module)))
|
||||
}
|
||||
|
||||
func (api *api) zip(w http.ResponseWriter, r *http.Request, module, version string) {
|
||||
api.log("api.zip", "module", module, "version", version)
|
||||
b, _, err := api.module(r.Context(), module, vcs.Version(version))
|
||||
if err != nil {
|
||||
api.log("api.zip", "module", module, "version", version, "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
io.Copy(w, bytes.NewReader(b))
|
||||
}
|
||||
49
pkg/store/disk.go
Normal file
49
pkg/store/disk.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sixt/gomodproxy/pkg/vcs"
|
||||
)
|
||||
|
||||
type disk string
|
||||
|
||||
func Disk(dir string) Store { return disk(dir) }
|
||||
|
||||
func (d disk) Put(ctx context.Context, snapshot Snapshot) error {
|
||||
dir := string(d)
|
||||
timeFile := filepath.Join(dir, snapshot.Key()+".time")
|
||||
zipFile := filepath.Join(dir, snapshot.Key()+".zip")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(timeFile), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := snapshot.Timestamp.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(timeFile, t, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(zipFile, snapshot.Data, 0644)
|
||||
}
|
||||
|
||||
func (d disk) Get(ctx context.Context, module string, version vcs.Version) (Snapshot, error) {
|
||||
dir := string(d)
|
||||
s := Snapshot{Module: module, Version: version}
|
||||
t, err := ioutil.ReadFile(filepath.Join(dir, s.Key()+".time"))
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if err := s.Timestamp.UnmarshalText(t); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
s.Data, err = ioutil.ReadFile(filepath.Join(dir, s.Key()+".zip"))
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (d disk) Close() error { return nil }
|
||||
41
pkg/store/mem.go
Normal file
41
pkg/store/mem.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/sixt/gomodproxy/pkg/vcs"
|
||||
)
|
||||
|
||||
type memory struct {
|
||||
sync.Mutex
|
||||
cache []Snapshot
|
||||
}
|
||||
|
||||
func Memory() Store { return &memory{} }
|
||||
|
||||
func (m *memory) Put(ctx context.Context, snapshot Snapshot) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
for _, item := range m.cache {
|
||||
if item.Module == snapshot.Module && item.Version == snapshot.Version {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
m.cache = append(m.cache, snapshot)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memory) Get(ctx context.Context, module string, version vcs.Version) (Snapshot, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
for _, snapshot := range m.cache {
|
||||
if snapshot.Module == module && snapshot.Version == version {
|
||||
return snapshot, nil
|
||||
}
|
||||
}
|
||||
return Snapshot{}, errors.New("not found")
|
||||
}
|
||||
|
||||
func (m *memory) Close() error { return nil }
|
||||
25
pkg/store/store.go
Normal file
25
pkg/store/store.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sixt/gomodproxy/pkg/vcs"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Put(ctx context.Context, snapshot Snapshot) error
|
||||
Get(ctx context.Context, module string, version vcs.Version) (Snapshot, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Module string
|
||||
Version vcs.Version
|
||||
Timestamp time.Time
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (s Snapshot) Key() string {
|
||||
return s.Module + "@" + string(s.Version)
|
||||
}
|
||||
221
pkg/vcs/git.go
Normal file
221
pkg/vcs/git.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/config"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
|
||||
"gopkg.in/src-d/go-git.v4/storage/memory"
|
||||
)
|
||||
|
||||
const remoteName = "origin"
|
||||
|
||||
type gitVCS struct {
|
||||
log logger
|
||||
module string
|
||||
auth Auth
|
||||
}
|
||||
|
||||
func NewGit(l logger, module string, auth Auth) VCS {
|
||||
return &gitVCS{log: l, module: module, auth: auth}
|
||||
}
|
||||
|
||||
func (g *gitVCS) List(ctx context.Context) ([]Version, error) {
|
||||
g.log("gitVCS.List", "module", g.module)
|
||||
repo, err := g.repo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remote, err := repo.Remote(remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := g.authMethod()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs, err := remote.List(&git.ListOptions{Auth: auth})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []Version{}
|
||||
masterHash := ""
|
||||
for _, ref := range refs {
|
||||
name := ref.Name()
|
||||
if name == plumbing.Master {
|
||||
masterHash = ref.Hash().String()
|
||||
} else if name.IsTag() && strings.HasPrefix(name.String(), "refs/tags/v") {
|
||||
list = append(list, Version(strings.TrimPrefix(name.String(), "refs/tags/")))
|
||||
}
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
if masterHash == "" {
|
||||
return nil, errors.New("no tags and no master branch found")
|
||||
}
|
||||
short := masterHash[:12]
|
||||
t, err := g.Timestamp(ctx, Version("v0.0.0-20060102150405-"+short))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = []Version{Version(fmt.Sprintf("v0.0.0-%s-%s", t.Format("20060102150405"), short))}
|
||||
}
|
||||
|
||||
g.log("gitVCS.List", "module", g.module, "list", list)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (g *gitVCS) Timestamp(ctx context.Context, version Version) (time.Time, error) {
|
||||
g.log("gitVCS.Timestamp", "module", g.module, "version", version)
|
||||
ci, err := g.commit(ctx, version)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
g.log("gitVCS.Timestamp", "module", g.module, "version", version, "timestamp", ci.Committer.When)
|
||||
return ci.Committer.When, nil
|
||||
}
|
||||
|
||||
func isVendoredPackage(name string) bool {
|
||||
var i int
|
||||
if strings.HasPrefix(name, "vendor/") {
|
||||
i += len("vendor/")
|
||||
} else if j := strings.Index(name, "/vendor/"); j >= 0 {
|
||||
i += len("/vendor/")
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(name[i:], "/")
|
||||
}
|
||||
|
||||
func (g *gitVCS) Zip(ctx context.Context, version Version) (io.ReadCloser, error) {
|
||||
g.log("gitVCS.Zip", "module", g.module, "version", version)
|
||||
ci, err := g.commit(ctx, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := ci.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
zw := zip.NewWriter(b)
|
||||
tree.Files().ForEach(func(f *object.File) error {
|
||||
// go mod strips vendored directories from the zip, and we do the same
|
||||
// to match the checksums in the go.sum
|
||||
if isVendoredPackage(f.Name) {
|
||||
return nil
|
||||
}
|
||||
w, err := zw.Create(filepath.Join(g.module+"@"+string(version), f.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err := f.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
io.Copy(w, r)
|
||||
return nil
|
||||
})
|
||||
zw.Close()
|
||||
return ioutil.NopCloser(bytes.NewBuffer(b.Bytes())), nil
|
||||
}
|
||||
|
||||
func (g *gitVCS) repo(ctx context.Context) (*git.Repository, error) {
|
||||
repo, err := git.Init(memory.NewStorage(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schema := "https://"
|
||||
if g.auth.Key != "" {
|
||||
schema = "ssh://"
|
||||
}
|
||||
repoRoot := g.module
|
||||
if meta, err := MetaImports(ctx, g.module); err == nil {
|
||||
repoRoot = meta
|
||||
}
|
||||
_, err = repo.CreateRemote(&config.RemoteConfig{
|
||||
Name: remoteName,
|
||||
URLs: []string{schema + repoRoot + ".git"},
|
||||
})
|
||||
return repo, err
|
||||
}
|
||||
|
||||
func (g *gitVCS) commit(ctx context.Context, version Version) (*object.Commit, error) {
|
||||
repo, err := g.repo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth, err := g.authMethod()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = repo.FetchContext(ctx, &git.FetchOptions{
|
||||
RemoteName: remoteName,
|
||||
Auth: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version = Version(strings.TrimSuffix(string(version), "+incompatible"))
|
||||
hash := version.Hash()
|
||||
if version.IsSemVer() {
|
||||
tags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags.ForEach(func(t *plumbing.Reference) error {
|
||||
if t.Name().String() == "refs/tags/"+string(version) {
|
||||
hash = t.Hash().String()
|
||||
annotated, err := repo.TagObject(t.Hash())
|
||||
if err == nil {
|
||||
hash = annotated.Target.String()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
commits, err := repo.CommitObjects()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits.ForEach(func(ci *object.Commit) error {
|
||||
if strings.HasPrefix(ci.Hash.String(), version.Hash()) {
|
||||
hash = ci.Hash.String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
g.log("gitVCS.commit", "module", g.module, "version", version, "hash", hash)
|
||||
return repo.CommitObject(plumbing.NewHash(hash))
|
||||
}
|
||||
|
||||
func (g *gitVCS) authMethod() (transport.AuthMethod, error) {
|
||||
if g.auth.Key != "" {
|
||||
return ssh.NewPublicKeysFromFile("git", g.auth.Key, "")
|
||||
} else if g.auth.Username != "" {
|
||||
return &http.BasicAuth{Username: g.auth.Username, Password: g.auth.Password}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
90
pkg/vcs/vcs.go
Normal file
90
pkg/vcs/vcs.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logger = func(v ...interface{})
|
||||
|
||||
type Version string
|
||||
|
||||
var reSemVer = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
|
||||
|
||||
func (v Version) IsSemVer() bool { return reSemVer.MatchString(string(v)) }
|
||||
func (v Version) Hash() string {
|
||||
fields := strings.Split(string(v), "-")
|
||||
if len(fields) != 3 {
|
||||
return ""
|
||||
}
|
||||
return fields[2]
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
Timestamp(ctx context.Context, version Version) (time.Time, error)
|
||||
Zip(ctx context.Context, version Version) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
type VCS interface {
|
||||
List(ctx context.Context) ([]Version, error)
|
||||
Module
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Username string
|
||||
Password string
|
||||
Key string
|
||||
}
|
||||
|
||||
func NoAuth() Auth { return Auth{} }
|
||||
func Password(username, password string) Auth { return Auth{Username: username, Password: password} }
|
||||
func Key(key string) Auth { return Auth{Key: key} }
|
||||
|
||||
func MetaImports(ctx context.Context, module string) (string, error) {
|
||||
if strings.HasPrefix(module, "github.com/") || strings.HasPrefix(module, "bitbucket.org/") {
|
||||
return module, nil
|
||||
}
|
||||
// TODO: use context
|
||||
res, err := http.Get("https://" + module + "?go-get=1")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
html := struct {
|
||||
HTML string `xml:"html"`
|
||||
Head struct {
|
||||
Meta []struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Content string `xml:"content,attr"`
|
||||
} `xml:"meta"`
|
||||
} `xml:"head"`
|
||||
}{}
|
||||
dec := xml.NewDecoder(res.Body)
|
||||
dec.Strict = false
|
||||
dec.AutoClose = xml.HTMLAutoClose
|
||||
dec.Entity = xml.HTMLEntity
|
||||
if err := dec.Decode(&html); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, meta := range html.Head.Meta {
|
||||
if meta.Name == "go-import" {
|
||||
if f := strings.Fields(meta.Content); len(f) == 3 {
|
||||
if f[0] != module {
|
||||
return "", errors.New("prefix does not match the module")
|
||||
}
|
||||
url := f[2]
|
||||
if i := strings.Index(url, "://"); i >= 0 {
|
||||
url = url[i+3:]
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("go-import meta tag not found")
|
||||
}
|
||||
Reference in New Issue
Block a user