commands/helpers/file_archiver.go (225 lines of code) (raw):
package helpers
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/sirupsen/logrus"
)
type fileArchiver struct {
Paths []string `long:"path" description:"Add paths to archive"`
Exclude []string `long:"exclude" description:"Exclude paths from the archive"`
Untracked bool `long:"untracked" description:"Add git untracked files"`
Verbose bool `long:"verbose" description:"Detailed information"`
wd string
files map[string]os.FileInfo
excluded map[string]int64
}
func (c *fileArchiver) isChanged(modTime time.Time) bool {
for _, info := range c.files {
if modTime.Before(info.ModTime()) {
return true
}
}
return false
}
func (c *fileArchiver) isFileChanged(fileName string) bool {
ai, err := os.Stat(fileName)
if ai != nil {
if !c.isChanged(ai.ModTime()) {
return false
}
} else if !os.IsNotExist(err) {
logrus.Warningln(err)
}
return true
}
func (c *fileArchiver) sortedFiles() []string {
files := make([]string, len(c.files))
i := 0
for file := range c.files {
files[i] = file
i++
}
sort.Strings(files)
return files
}
func (c *fileArchiver) process(match string) bool {
var absolute, relative string
var err error
absolute, err = filepath.Abs(match)
if err == nil {
// Let's try to find a real relative path to an absolute from working directory
relative, err = filepath.Rel(c.wd, absolute)
}
if err == nil {
// Process path only if it lives in our build directory
if !strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
excluded, rule := c.isExcluded(relative)
if excluded {
c.exclude(rule)
return false
}
err = c.add(relative)
} else {
err = errors.New("not supported: outside build directory")
}
}
if err == nil {
return true
}
if os.IsNotExist(err) {
// We hide the error that file doesn't exist
return false
}
logrus.Warningf("%s: %v", match, err)
return false
}
func (c *fileArchiver) isExcluded(path string) (bool, string) {
// Both path and pattern need to be normalized with filepath.ToSlash().
// Matching will fail with Windows machines using "\\" path separators and patterns with "/" path separators
path = filepath.ToSlash(path)
for _, pattern := range c.Exclude {
relPattern, err := c.findRelativePathInProject(pattern)
if err != nil {
logrus.Warningf("isExcluded: %v", err.Error())
return false, ""
}
relPattern = filepath.ToSlash(relPattern)
excluded, err := doublestar.Match(relPattern, path)
if err == nil && excluded {
return true, pattern
}
}
return false, ""
}
func (c *fileArchiver) exclude(rule string) {
c.excluded[rule]++
}
func (c *fileArchiver) add(path string) error {
// Always use slashes
path = filepath.ToSlash(path)
// Check if file exist
info, err := os.Lstat(path)
if err == nil {
c.files[path] = info
}
return err
}
func (c *fileArchiver) processPaths() {
for _, path := range c.Paths {
c.processPath(path)
}
}
func (c *fileArchiver) processPath(path string) {
if path == "" {
logrus.Warningf("No matching files. Path is empty.")
return
}
rel, err := c.findRelativePathInProject(path)
if err != nil {
// Do not fail job when a file is invalid or not found.
logrus.Warningf("processPath: %v", err.Error())
return
}
matches, err := doublestar.FilepathGlob(rel)
if err != nil {
logrus.Warningf("%s: %v", path, err)
return
}
found := 0
for _, match := range matches {
err := filepath.Walk(match, func(path string, info os.FileInfo, err error) error {
if c.process(path) {
found++
}
return nil
})
if err != nil {
logrus.Warningln("Walking", match, err)
}
}
if found == 0 {
logrus.Warningf(
"%s: no matching files. Ensure that the artifact path is relative to the working directory (%s)",
path,
c.wd,
)
} else {
logrus.Infof("%s: found %d matching artifact files and directories", path, found)
}
}
func (c *fileArchiver) findRelativePathInProject(path string) (string, error) {
slashPath := filepath.ToSlash(path)
if filepath.Clean(slashPath) == filepath.Clean(c.wd) {
return ".", nil
}
base, patt := slashPath, ""
// check if path contains a glob pattern
if strings.ContainsAny(slashPath, "*?[{") {
base, patt = doublestar.SplitPattern(slashPath)
}
abs, err := filepath.Abs(base)
if err != nil {
return "", fmt.Errorf("could not resolve artifact absolute path %s: %v", path, err)
}
rel, err := filepath.Rel(c.wd, abs)
if err != nil {
return "", fmt.Errorf("could not resolve artifact relative path %s: %v", path, err)
}
// If fully resolved relative path begins with ".." it is not a subpath of our working directory
if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
return "", fmt.Errorf("artifact path is not a subpath of project directory: %s", path)
}
// Relative path is needed now that our fsys "root" is at the working directory
rel = filepath.Join(rel, patt)
rel = filepath.FromSlash(rel)
return rel, nil
}
func (c *fileArchiver) processUntracked() {
if !c.Untracked {
return
}
found := 0
var output bytes.Buffer
cmd := exec.Command("git", "ls-files", "-o", "-z")
cmd.Env = os.Environ()
cmd.Stdout = &output
cmd.Stderr = os.Stderr
logrus.Debugln("Executing command:", strings.Join(cmd.Args, " "))
err := cmd.Run()
if err != nil {
logrus.Warningf("untracked: %v", err)
return
}
reader := bufio.NewReader(&output)
for {
line, err := reader.ReadString(0)
if err == io.EOF {
break
} else if err != nil {
logrus.Warningln(err)
break
}
if c.process(line[:len(line)-1]) {
found++
}
}
if found == 0 {
logrus.Warningf("untracked: no files")
} else {
logrus.Infof("untracked: found %d files", found)
}
}
func (c *fileArchiver) enumerate() error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
c.wd = wd
c.files = make(map[string]os.FileInfo)
c.excluded = make(map[string]int64)
c.processPaths()
c.processUntracked()
for path, count := range c.excluded {
logrus.Infof("%s: excluded %d files", path, count)
}
return nil
}