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 }