github.com/nektos/act@v0.2.63/pkg/runner/action_cache.go (about)

     1  package runner
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"crypto/rand"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/fs"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	git "github.com/go-git/go-git/v5"
    17  	config "github.com/go-git/go-git/v5/config"
    18  	"github.com/go-git/go-git/v5/plumbing"
    19  	"github.com/go-git/go-git/v5/plumbing/object"
    20  	"github.com/go-git/go-git/v5/plumbing/transport"
    21  	"github.com/go-git/go-git/v5/plumbing/transport/http"
    22  )
    23  
    24  type ActionCache interface {
    25  	Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error)
    26  	GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error)
    27  }
    28  
    29  type GoGitActionCache struct {
    30  	Path string
    31  }
    32  
    33  func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
    34  	gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
    35  	gogitrepo, err := git.PlainInit(gitPath, true)
    36  	if errors.Is(err, git.ErrRepositoryAlreadyExists) {
    37  		gogitrepo, err = git.PlainOpen(gitPath)
    38  	}
    39  	if err != nil {
    40  		return "", err
    41  	}
    42  	tmpBranch := make([]byte, 12)
    43  	if _, err := rand.Read(tmpBranch); err != nil {
    44  		return "", err
    45  	}
    46  	branchName := hex.EncodeToString(tmpBranch)
    47  
    48  	var auth transport.AuthMethod
    49  	if token != "" {
    50  		auth = &http.BasicAuth{
    51  			Username: "token",
    52  			Password: token,
    53  		}
    54  	}
    55  	remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{
    56  		Name: "anonymous",
    57  		URLs: []string{
    58  			url,
    59  		},
    60  	})
    61  	if err != nil {
    62  		return "", err
    63  	}
    64  	defer func() {
    65  		_ = gogitrepo.DeleteBranch(branchName)
    66  	}()
    67  	if err := remote.FetchContext(ctx, &git.FetchOptions{
    68  		RefSpecs: []config.RefSpec{
    69  			config.RefSpec(ref + ":" + branchName),
    70  		},
    71  		Auth:  auth,
    72  		Force: true,
    73  	}); err != nil {
    74  		return "", err
    75  	}
    76  	hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName))
    77  	if err != nil {
    78  		return "", err
    79  	}
    80  	return hash.String(), nil
    81  }
    82  
    83  type GitFileInfo struct {
    84  	name    string
    85  	size    int64
    86  	modTime time.Time
    87  	isDir   bool
    88  	mode    fs.FileMode
    89  }
    90  
    91  // IsDir implements fs.FileInfo.
    92  func (g *GitFileInfo) IsDir() bool {
    93  	return g.isDir
    94  }
    95  
    96  // ModTime implements fs.FileInfo.
    97  func (g *GitFileInfo) ModTime() time.Time {
    98  	return g.modTime
    99  }
   100  
   101  // Mode implements fs.FileInfo.
   102  func (g *GitFileInfo) Mode() fs.FileMode {
   103  	return g.mode
   104  }
   105  
   106  // Name implements fs.FileInfo.
   107  func (g *GitFileInfo) Name() string {
   108  	return g.name
   109  }
   110  
   111  // Size implements fs.FileInfo.
   112  func (g *GitFileInfo) Size() int64 {
   113  	return g.size
   114  }
   115  
   116  // Sys implements fs.FileInfo.
   117  func (g *GitFileInfo) Sys() any {
   118  	return nil
   119  }
   120  
   121  func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
   122  	gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
   123  	gogitrepo, err := git.PlainOpen(gitPath)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha))
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	t, err := commit.Tree()
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	files, err := commit.Files()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	rpipe, wpipe := io.Pipe()
   140  	// Interrupt io.Copy using ctx
   141  	ch := make(chan int, 1)
   142  	go func() {
   143  		select {
   144  		case <-ctx.Done():
   145  			wpipe.CloseWithError(ctx.Err())
   146  		case <-ch:
   147  		}
   148  	}()
   149  	go func() {
   150  		defer wpipe.Close()
   151  		defer close(ch)
   152  		tw := tar.NewWriter(wpipe)
   153  		cleanIncludePrefix := path.Clean(includePrefix)
   154  		wpipe.CloseWithError(files.ForEach(func(f *object.File) error {
   155  			return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, f.Name, f)
   156  		}))
   157  	}()
   158  	return rpipe, err
   159  }
   160  
   161  func actionCacheCopyFileOrDir(ctx context.Context, cleanIncludePrefix string, t *object.Tree, tw *tar.Writer, origin string, f *object.File) error {
   162  	if err := ctx.Err(); err != nil {
   163  		return err
   164  	}
   165  	name := origin
   166  	if strings.HasPrefix(name, cleanIncludePrefix+"/") {
   167  		name = name[len(cleanIncludePrefix)+1:]
   168  	} else if cleanIncludePrefix != "." && name != cleanIncludePrefix {
   169  		return nil
   170  	}
   171  	fmode, err := f.Mode.ToOSFileMode()
   172  	if err != nil {
   173  		return err
   174  	}
   175  	if fmode&fs.ModeSymlink == fs.ModeSymlink {
   176  		content, err := f.Contents()
   177  		if err != nil {
   178  			return err
   179  		}
   180  
   181  		destPath := path.Join(path.Dir(f.Name), content)
   182  
   183  		subtree, err := t.Tree(destPath)
   184  		if err == nil {
   185  			return subtree.Files().ForEach(func(ft *object.File) error {
   186  				return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin+strings.TrimPrefix(ft.Name, f.Name), f)
   187  			})
   188  		}
   189  
   190  		f, err := t.File(destPath)
   191  		if err != nil {
   192  			return fmt.Errorf("%s (%s): %w", destPath, origin, err)
   193  		}
   194  		return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin, f)
   195  	}
   196  	header, err := tar.FileInfoHeader(&GitFileInfo{
   197  		name: name,
   198  		mode: fmode,
   199  		size: f.Size,
   200  	}, "")
   201  	if err != nil {
   202  		return err
   203  	}
   204  	err = tw.WriteHeader(header)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	reader, err := f.Reader()
   209  	if err != nil {
   210  		return err
   211  	}
   212  	_, err = io.Copy(tw, reader)
   213  	return err
   214  }