diff options
Diffstat (limited to 'pkg/filecollector/file_collector.go')
-rw-r--r-- | pkg/filecollector/file_collector.go | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/pkg/filecollector/file_collector.go b/pkg/filecollector/file_collector.go new file mode 100644 index 0000000..8547bb7 --- /dev/null +++ b/pkg/filecollector/file_collector.go @@ -0,0 +1,210 @@ +package filecollector + +import ( + "archive/tar" + "context" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" + + git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "github.com/go-git/go-git/v5/plumbing/format/index" +) + +type Handler interface { + WriteFile(path string, fi fs.FileInfo, linkName string, f io.Reader) error +} + +type TarCollector struct { + TarWriter *tar.Writer + UID int + GID int + DstDir string +} + +func (tc TarCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error { + // create a new dir/file header + header, err := tar.FileInfoHeader(fi, linkName) + if err != nil { + return err + } + + // update the name to correctly reflect the desired destination when untaring + header.Name = path.Join(tc.DstDir, fpath) + header.Mode = int64(fi.Mode()) + header.ModTime = fi.ModTime() + header.Uid = tc.UID + header.Gid = tc.GID + + // write the header + if err := tc.TarWriter.WriteHeader(header); err != nil { + return err + } + + // this is a symlink no reader provided + if f == nil { + return nil + } + + // copy file data into tar writer + if _, err := io.Copy(tc.TarWriter, f); err != nil { + return err + } + return nil +} + +type CopyCollector struct { + DstDir string +} + +func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error { + fdestpath := filepath.Join(cc.DstDir, fpath) + if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil { + return err + } + if f == nil { + return os.Symlink(linkName, fdestpath) + } + df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode()) + if err != nil { + return err + } + defer df.Close() + if _, err := io.Copy(df, f); err != nil { + return err + } + return nil +} + +type FileCollector struct { + Ignorer gitignore.Matcher + SrcPath string + SrcPrefix string + Fs Fs + Handler Handler +} + +type Fs interface { + Walk(root string, fn filepath.WalkFunc) error + OpenGitIndex(path string) (*index.Index, error) + Open(path string) (io.ReadCloser, error) + Readlink(path string) (string, error) +} + +type DefaultFs struct { +} + +func (*DefaultFs) Walk(root string, fn filepath.WalkFunc) error { + return filepath.Walk(root, fn) +} + +func (*DefaultFs) OpenGitIndex(path string) (*index.Index, error) { + r, err := git.PlainOpen(path) + if err != nil { + return nil, err + } + i, err := r.Storer.Index() + if err != nil { + return nil, err + } + return i, nil +} + +func (*DefaultFs) Open(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +func (*DefaultFs) Readlink(path string) (string, error) { + return os.Readlink(path) +} + +//nolint:gocyclo +func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc { + i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...))) + return func(file string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if ctx != nil { + select { + case <-ctx.Done(): + return fmt.Errorf("copy cancelled") + default: + } + } + + sansPrefix := strings.TrimPrefix(file, fc.SrcPrefix) + split := strings.Split(sansPrefix, string(filepath.Separator)) + // The root folders should be skipped, submodules only have the last path component set to "." by filepath.Walk + if fi.IsDir() && len(split) > 0 && split[len(split)-1] == "." { + return nil + } + var entry *index.Entry + if i != nil { + entry, err = i.Entry(strings.Join(split[len(submodulePath):], "/")) + } else { + err = index.ErrEntryNotFound + } + if err != nil && fc.Ignorer != nil && fc.Ignorer.Match(split, fi.IsDir()) { + if fi.IsDir() { + if i != nil { + ms, err := i.Glob(strings.Join(append(split[len(submodulePath):], "**"), "/")) + if err != nil || len(ms) == 0 { + return filepath.SkipDir + } + } else { + return filepath.SkipDir + } + } else { + return nil + } + } + if err == nil && entry.Mode == filemode.Submodule { + err = fc.Fs.Walk(file, fc.CollectFiles(ctx, split)) + if err != nil { + return err + } + return filepath.SkipDir + } + path := filepath.ToSlash(sansPrefix) + + // return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update) + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + linkName, err := fc.Fs.Readlink(file) + if err != nil { + return fmt.Errorf("unable to readlink '%s': %w", file, err) + } + return fc.Handler.WriteFile(path, fi, linkName, nil) + } else if !fi.Mode().IsRegular() { + return nil + } + + // open file + f, err := fc.Fs.Open(file) + if err != nil { + return err + } + defer f.Close() + + if ctx != nil { + // make io.Copy cancellable by closing the file + cpctx, cpfinish := context.WithCancel(ctx) + defer cpfinish() + go func() { + select { + case <-cpctx.Done(): + case <-ctx.Done(): + f.Close() + } + }() + } + + return fc.Handler.WriteFile(path, fi, "", f) + } +} |