mirror of
https://github.com/dutchcoders/transfer.sh.git
synced 2024-12-13 06:10:18 +01:00
cb6e5cb0c7
* use dep for vendoring * lets encrypt * moved web to transfer.sh-web repo * single command install * added first tests
356 lines
8.5 KiB
Go
356 lines
8.5 KiB
Go
// Copyright 2013 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 or at
|
|
// https://developers.google.com/open-source/licenses/bsd.
|
|
|
|
// +build !appengine
|
|
|
|
package gosrc
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func init() {
|
|
addService(&service{
|
|
pattern: regexp.MustCompile(`^(?P<repo>(?:[a-z0-9.\-]+\.)+[a-z0-9.\-]+(?::[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(?P<vcs>bzr|git|hg|svn)(?P<dir>/[A-Za-z0-9_.\-/]*)?$`),
|
|
prefix: "",
|
|
get: getVCSDir,
|
|
})
|
|
getVCSDirFn = getVCSDir
|
|
}
|
|
|
|
const (
|
|
lsRemoteTimeout = 5 * time.Minute
|
|
cloneTimeout = 10 * time.Minute
|
|
fetchTimeout = 5 * time.Minute
|
|
checkoutTimeout = 1 * time.Minute
|
|
)
|
|
|
|
// Store temporary data in this directory.
|
|
var TempDir = filepath.Join(os.TempDir(), "gddo")
|
|
|
|
type urlTemplates struct {
|
|
re *regexp.Regexp
|
|
fileBrowse string
|
|
project string
|
|
line string
|
|
}
|
|
|
|
var vcsServices = []*urlTemplates{
|
|
{
|
|
regexp.MustCompile(`^git\.gitorious\.org/(?P<repo>[^/]+/[^/]+)$`),
|
|
"https://gitorious.org/{repo}/blobs/{tag}/{dir}{0}",
|
|
"https://gitorious.org/{repo}",
|
|
"%s#line%d",
|
|
},
|
|
{
|
|
regexp.MustCompile(`^git\.oschina\.net/(?P<repo>[^/]+/[^/]+)$`),
|
|
"http://git.oschina.net/{repo}/blob/{tag}/{dir}{0}",
|
|
"http://git.oschina.net/{repo}",
|
|
"%s#L%d",
|
|
},
|
|
{
|
|
regexp.MustCompile(`^(?P<r1>[^.]+)\.googlesource.com/(?P<r2>[^./]+)$`),
|
|
"https://{r1}.googlesource.com/{r2}/+/{tag}/{dir}{0}",
|
|
"https://{r1}.googlesource.com/{r2}/+/{tag}",
|
|
"%s#%d",
|
|
},
|
|
{
|
|
regexp.MustCompile(`^gitcafe.com/(?P<repo>[^/]+/.[^/]+)$`),
|
|
"https://gitcafe.com/{repo}/tree/{tag}/{dir}{0}",
|
|
"https://gitcafe.com/{repo}",
|
|
"",
|
|
},
|
|
}
|
|
|
|
// lookupURLTemplate finds an expand() template, match map and line number
|
|
// format for well known repositories.
|
|
func lookupURLTemplate(repo, dir, tag string) (*urlTemplates, map[string]string) {
|
|
if strings.HasPrefix(dir, "/") {
|
|
dir = dir[1:] + "/"
|
|
}
|
|
for _, t := range vcsServices {
|
|
if m := t.re.FindStringSubmatch(repo); m != nil {
|
|
match := map[string]string{
|
|
"dir": dir,
|
|
"tag": tag,
|
|
}
|
|
for i, name := range t.re.SubexpNames() {
|
|
if name != "" {
|
|
match[name] = m[i]
|
|
}
|
|
}
|
|
return t, match
|
|
}
|
|
}
|
|
return &urlTemplates{}, nil
|
|
}
|
|
|
|
type vcsCmd struct {
|
|
schemes []string
|
|
download func(schemes []string, clonePath, repo, savedEtag string) (tag, etag string, err error)
|
|
}
|
|
|
|
var vcsCmds = map[string]*vcsCmd{
|
|
"git": {
|
|
schemes: []string{"http", "https", "ssh", "git"},
|
|
download: downloadGit,
|
|
},
|
|
"svn": {
|
|
schemes: []string{"http", "https", "svn"},
|
|
download: downloadSVN,
|
|
},
|
|
}
|
|
|
|
var lsremoteRe = regexp.MustCompile(`(?m)^([0-9a-f]{40})\s+refs/(?:tags|heads)/(.+)$`)
|
|
|
|
func downloadGit(schemes []string, clonePath, repo, savedEtag string) (string, string, error) {
|
|
var p []byte
|
|
var scheme string
|
|
for i := range schemes {
|
|
cmd := exec.Command("git", "ls-remote", "--heads", "--tags", schemes[i]+"://"+clonePath)
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
var err error
|
|
p, err = outputWithTimeout(cmd, lsRemoteTimeout)
|
|
if err == nil {
|
|
scheme = schemes[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if scheme == "" {
|
|
return "", "", NotFoundError{Message: "VCS not found"}
|
|
}
|
|
|
|
tags := make(map[string]string)
|
|
for _, m := range lsremoteRe.FindAllSubmatch(p, -1) {
|
|
tags[string(m[2])] = string(m[1])
|
|
}
|
|
|
|
tag, commit, err := bestTag(tags, "master")
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
etag := scheme + "-" + commit
|
|
|
|
if etag == savedEtag {
|
|
return "", "", NotModifiedError{}
|
|
}
|
|
|
|
dir := filepath.Join(TempDir, repo+".git")
|
|
p, err = ioutil.ReadFile(filepath.Join(dir, ".git", "HEAD"))
|
|
switch {
|
|
case err != nil:
|
|
if err := os.MkdirAll(dir, 0777); err != nil {
|
|
return "", "", err
|
|
}
|
|
cmd := exec.Command("git", "clone", scheme+"://"+clonePath, dir)
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
if err := runWithTimeout(cmd, cloneTimeout); err != nil {
|
|
return "", "", err
|
|
}
|
|
case string(bytes.TrimRight(p, "\n")) == commit:
|
|
return tag, etag, nil
|
|
default:
|
|
cmd := exec.Command("git", "fetch")
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
cmd.Dir = dir
|
|
if err := runWithTimeout(cmd, fetchTimeout); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("git", "checkout", "--detach", "--force", commit)
|
|
cmd.Dir = dir
|
|
if err := runWithTimeout(cmd, checkoutTimeout); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return tag, etag, nil
|
|
}
|
|
|
|
func downloadSVN(schemes []string, clonePath, repo, savedEtag string) (string, string, error) {
|
|
var scheme string
|
|
var revno string
|
|
for i := range schemes {
|
|
var err error
|
|
revno, err = getSVNRevision(schemes[i] + "://" + clonePath)
|
|
if err == nil {
|
|
scheme = schemes[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if scheme == "" {
|
|
return "", "", NotFoundError{Message: "VCS not found"}
|
|
}
|
|
|
|
etag := scheme + "-" + revno
|
|
if etag == savedEtag {
|
|
return "", "", NotModifiedError{}
|
|
}
|
|
|
|
dir := filepath.Join(TempDir, repo+".svn")
|
|
localRevno, err := getSVNRevision(dir)
|
|
switch {
|
|
case err != nil:
|
|
log.Printf("err: %v", err)
|
|
if err := os.MkdirAll(dir, 0777); err != nil {
|
|
return "", "", err
|
|
}
|
|
cmd := exec.Command("svn", "checkout", scheme+"://"+clonePath, "-r", revno, dir)
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
if err := runWithTimeout(cmd, cloneTimeout); err != nil {
|
|
return "", "", err
|
|
}
|
|
case localRevno != revno:
|
|
cmd := exec.Command("svn", "update", "-r", revno)
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
cmd.Dir = dir
|
|
if err := runWithTimeout(cmd, fetchTimeout); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
return "", etag, nil
|
|
}
|
|
|
|
var svnrevRe = regexp.MustCompile(`(?m)^Last Changed Rev: ([0-9]+)$`)
|
|
|
|
func getSVNRevision(target string) (string, error) {
|
|
cmd := exec.Command("svn", "info", target)
|
|
log.Println(strings.Join(cmd.Args, " "))
|
|
out, err := outputWithTimeout(cmd, lsRemoteTimeout)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
match := svnrevRe.FindStringSubmatch(string(out))
|
|
if match != nil {
|
|
return match[1], nil
|
|
}
|
|
return "", NotFoundError{Message: "Last changed revision not found"}
|
|
}
|
|
|
|
func getVCSDir(client *http.Client, match map[string]string, etagSaved string) (*Directory, error) {
|
|
cmd := vcsCmds[match["vcs"]]
|
|
if cmd == nil {
|
|
return nil, NotFoundError{Message: expand("VCS not supported: {vcs}", match)}
|
|
}
|
|
|
|
scheme := match["scheme"]
|
|
if scheme == "" {
|
|
i := strings.Index(etagSaved, "-")
|
|
if i > 0 {
|
|
scheme = etagSaved[:i]
|
|
}
|
|
}
|
|
|
|
schemes := cmd.schemes
|
|
if scheme != "" {
|
|
for i := range cmd.schemes {
|
|
if cmd.schemes[i] == scheme {
|
|
schemes = cmd.schemes[i : i+1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
clonePath, ok := match["clonePath"]
|
|
if !ok {
|
|
// clonePath may be unset if we're being called via the generic repo.vcs/dir regexp matcher.
|
|
// In that case, set it to the repo value.
|
|
clonePath = match["repo"]
|
|
}
|
|
|
|
// Download and checkout.
|
|
|
|
tag, etag, err := cmd.download(schemes, clonePath, match["repo"], etagSaved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Find source location.
|
|
|
|
template, urlMatch := lookupURLTemplate(match["repo"], match["dir"], tag)
|
|
|
|
// Slurp source files.
|
|
|
|
d := filepath.Join(TempDir, filepath.FromSlash(expand("{repo}.{vcs}", match)), filepath.FromSlash(match["dir"]))
|
|
f, err := os.Open(d)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = NotFoundError{Message: err.Error()}
|
|
}
|
|
return nil, err
|
|
}
|
|
fis, err := f.Readdir(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var files []*File
|
|
var subdirs []string
|
|
for _, fi := range fis {
|
|
switch {
|
|
case fi.IsDir():
|
|
if isValidPathElement(fi.Name()) {
|
|
subdirs = append(subdirs, fi.Name())
|
|
}
|
|
case isDocFile(fi.Name()):
|
|
b, err := ioutil.ReadFile(filepath.Join(d, fi.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files = append(files, &File{
|
|
Name: fi.Name(),
|
|
BrowseURL: expand(template.fileBrowse, urlMatch, fi.Name()),
|
|
Data: b,
|
|
})
|
|
}
|
|
}
|
|
|
|
return &Directory{
|
|
LineFmt: template.line,
|
|
ProjectRoot: expand("{repo}.{vcs}", match),
|
|
ProjectName: path.Base(match["repo"]),
|
|
ProjectURL: expand(template.project, urlMatch),
|
|
BrowseURL: "",
|
|
Etag: etag,
|
|
VCS: match["vcs"],
|
|
Subdirectories: subdirs,
|
|
Files: files,
|
|
}, nil
|
|
}
|
|
|
|
func runWithTimeout(cmd *exec.Cmd, timeout time.Duration) error {
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
t := time.AfterFunc(timeout, func() { cmd.Process.Kill() })
|
|
defer t.Stop()
|
|
return cmd.Wait()
|
|
}
|
|
|
|
func outputWithTimeout(cmd *exec.Cmd, timeout time.Duration) ([]byte, error) {
|
|
if cmd.Stdout != nil {
|
|
return nil, errors.New("exec: Stdout already set")
|
|
}
|
|
var b bytes.Buffer
|
|
cmd.Stdout = &b
|
|
err := runWithTimeout(cmd, timeout)
|
|
return b.Bytes(), err
|
|
}
|