dev-tools/mage/copy.go (131 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package mage
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
)
type SkipFn func(string) bool
// Copy copies a file or a directory (recursively) and preserves the permissions.
func Copy(src, dest string) error {
return CopyWithCheck(src, dest, nil)
}
func CopyWithCheck(src, dest string, skipFn SkipFn) error {
copy := &CopyTask{Source: src, Dest: dest, skipFn: skipFn}
return copy.Execute()
}
// CopyTask copies a file or directory (recursively) and preserves the permissions.
type CopyTask struct {
Source string // Source directory or file.
Dest string // Destination directory or file.
Mode os.FileMode // Mode to use for copied files. Defaults to preserve permissions.
DirMode os.FileMode // Mode to use for copied dirs. Defaults to preserve permissions.
Exclude []string // Exclude paths that match these regular expressions.
excludes []*regexp.Regexp // Compiled exclude regexes.
skipFn SkipFn // external check, returns true if we skip
}
// Execute executes the copy and returns an error of there is a failure.
func (t *CopyTask) Execute() error {
if err := t.init(); err != nil {
return fmt.Errorf("copy failed: %w", err)
}
info, err := os.Stat(t.Source)
if err != nil {
return fmt.Errorf("copy failed: cannot stat source file %v: %w", t.Source, err)
}
if err := t.recursiveCopy(t.Source, t.Dest, fs.FileInfoToDirEntry(info)); err != nil {
return fmt.Errorf("copy failed: %w", err)
}
return nil
}
func (t *CopyTask) init() error {
for _, excl := range t.Exclude {
re, err := regexp.Compile(excl)
if err != nil {
return fmt.Errorf("bad exclude pattern %v: %w", excl, err)
}
t.excludes = append(t.excludes, re)
}
return nil
}
func (t *CopyTask) isExcluded(src string) bool {
for _, excl := range t.excludes {
if match := excl.MatchString(filepath.ToSlash(src)); match {
return true
}
}
return false
}
func (t *CopyTask) isDestinationExcluded(dst string) bool {
if t.skipFn != nil {
return t.skipFn(dst)
}
return false
}
func (t *CopyTask) recursiveCopy(src, dest string, entry fs.DirEntry) error {
if entry.IsDir() {
return t.dirCopy(src, dest, entry)
}
return t.fileCopy(src, dest, entry)
}
func (t *CopyTask) fileCopy(src, dest string, entry fs.DirEntry) error {
if t.isExcluded(src) {
return nil
}
if t.isDestinationExcluded(dest) {
return nil
}
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
info, err := entry.Info()
if err != nil {
return fmt.Errorf("converting dir entry: %w", err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("failed to copy source file because it is not a regular file")
}
mode := t.Mode
if mode == 0 {
mode = info.Mode()
}
destFile, err := os.OpenFile(createDir(dest),
os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode&os.ModePerm)
if err != nil {
return err
}
defer destFile.Close()
if _, err = io.Copy(destFile, srcFile); err != nil {
return err
}
return destFile.Close()
}
func (t *CopyTask) dirCopy(src, dest string, entry fs.DirEntry) error {
if t.isExcluded(src) {
return nil
}
info, err := entry.Info()
if err != nil {
return fmt.Errorf("converting dir entry: %w", err)
}
mode := t.DirMode
if mode == 0 {
mode = info.Mode()
}
if err := os.MkdirAll(dest, mode&os.ModePerm); err != nil {
return fmt.Errorf("failed creating dirs: %w", err)
}
contents, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("failed to read dir %v: %w", src, err)
}
for _, entry := range contents {
srcFile := filepath.Join(src, entry.Name())
destFile := filepath.Join(dest, entry.Name())
if err = t.recursiveCopy(srcFile, destFile, entry); err != nil {
return fmt.Errorf("failed to copy %v to %v: %w", srcFile, destFile, err)
}
}
return nil
}