diff options
Diffstat (limited to 'pkg/runner/action_cache.go')
-rw-r--r-- | pkg/runner/action_cache.go | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/pkg/runner/action_cache.go b/pkg/runner/action_cache.go new file mode 100644 index 0000000..da4e651 --- /dev/null +++ b/pkg/runner/action_cache.go @@ -0,0 +1,181 @@ +package runner + +import ( + "archive/tar" + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "path" + "strings" + + git "github.com/go-git/go-git/v5" + config "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +type ActionCache interface { + Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) + GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) +} + +type GoGitActionCache struct { + Path string +} + +func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { + gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") + gogitrepo, err := git.PlainInit(gitPath, true) + if errors.Is(err, git.ErrRepositoryAlreadyExists) { + gogitrepo, err = git.PlainOpen(gitPath) + } + if err != nil { + return "", err + } + tmpBranch := make([]byte, 12) + if _, err := rand.Read(tmpBranch); err != nil { + return "", err + } + branchName := hex.EncodeToString(tmpBranch) + var refSpec config.RefSpec + spec := config.RefSpec(ref + ":" + branchName) + tagOrSha := false + if spec.IsExactSHA1() { + refSpec = spec + } else if strings.HasPrefix(ref, "refs/") { + refSpec = config.RefSpec(ref + ":refs/heads/" + branchName) + } else { + tagOrSha = true + refSpec = config.RefSpec("refs/*/" + ref + ":refs/heads/*/" + branchName) + } + var auth transport.AuthMethod + if token != "" { + auth = &http.BasicAuth{ + Username: "token", + Password: token, + } + } + remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{ + Name: "anonymous", + URLs: []string{ + url, + }, + }) + if err != nil { + return "", err + } + defer func() { + if refs, err := gogitrepo.References(); err == nil { + _ = refs.ForEach(func(r *plumbing.Reference) error { + if strings.Contains(r.Name().String(), branchName) { + return gogitrepo.DeleteBranch(r.Name().String()) + } + return nil + }) + } + }() + if err := remote.FetchContext(ctx, &git.FetchOptions{ + RefSpecs: []config.RefSpec{ + refSpec, + }, + Auth: auth, + Force: true, + }); err != nil { + if tagOrSha && errors.Is(err, git.NoErrAlreadyUpToDate) { + return "", fmt.Errorf("couldn't find remote ref \"%s\"", ref) + } + return "", err + } + if tagOrSha { + for _, prefix := range []string{"refs/heads/tags/", "refs/heads/heads/"} { + hash, err := gogitrepo.ResolveRevision(plumbing.Revision(prefix + branchName)) + if err == nil { + return hash.String(), nil + } + } + } + hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName)) + if err != nil { + return "", err + } + return hash.String(), nil +} + +func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { + gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") + gogitrepo, err := git.PlainOpen(gitPath) + if err != nil { + return nil, err + } + commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha)) + if err != nil { + return nil, err + } + files, err := commit.Files() + if err != nil { + return nil, err + } + rpipe, wpipe := io.Pipe() + // Interrupt io.Copy using ctx + ch := make(chan int, 1) + go func() { + select { + case <-ctx.Done(): + wpipe.CloseWithError(ctx.Err()) + case <-ch: + } + }() + go func() { + defer wpipe.Close() + defer close(ch) + tw := tar.NewWriter(wpipe) + cleanIncludePrefix := path.Clean(includePrefix) + wpipe.CloseWithError(files.ForEach(func(f *object.File) error { + if err := ctx.Err(); err != nil { + return err + } + name := f.Name + if strings.HasPrefix(name, cleanIncludePrefix+"/") { + name = name[len(cleanIncludePrefix)+1:] + } else if cleanIncludePrefix != "." && name != cleanIncludePrefix { + return nil + } + fmode, err := f.Mode.ToOSFileMode() + if err != nil { + return err + } + if fmode&fs.ModeSymlink == fs.ModeSymlink { + content, err := f.Contents() + if err != nil { + return err + } + return tw.WriteHeader(&tar.Header{ + Name: name, + Mode: int64(fmode), + Linkname: content, + }) + } + err = tw.WriteHeader(&tar.Header{ + Name: name, + Mode: int64(fmode), + Size: f.Size, + }) + if err != nil { + return err + } + reader, err := f.Reader() + if err != nil { + return err + } + _, err = io.Copy(tw, reader) + return err + })) + }() + return rpipe, err +} |