mirror of
https://github.com/moby/buildkit.git
synced 2026-08-04 14:50:21 +00:00
Brings in fix for incorrect timestamps for parent destination directories. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
399 lines
9.4 KiB
Go
399 lines
9.4 KiB
Go
package file
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/continuity/fs"
|
|
"github.com/docker/docker/pkg/idtools"
|
|
"github.com/moby/buildkit/snapshot"
|
|
"github.com/moby/buildkit/solver/llbsolver/ops/fileoptypes"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/system"
|
|
"github.com/pkg/errors"
|
|
copy "github.com/tonistiigi/fsutil/copy"
|
|
)
|
|
|
|
func timestampToTime(ts int64) *time.Time {
|
|
if ts == -1 {
|
|
return nil
|
|
}
|
|
tm := time.Unix(ts/1e9, ts%1e9)
|
|
return &tm
|
|
}
|
|
|
|
func mkdir(d string, action *pb.FileActionMkDir, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
|
|
defer func() {
|
|
var osErr *os.PathError
|
|
if errors.As(err, &osErr) {
|
|
osErr.Path = strings.TrimPrefix(osErr.Path, d)
|
|
}
|
|
}()
|
|
|
|
p, err := fs.RootPath(d, action.Path)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
ch, err := mapUserToChowner(user, idmap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if action.MakeParents {
|
|
if _, err := copy.MkdirAll(p, os.FileMode(action.Mode)&0777, ch, timestampToTime(action.Timestamp)); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := os.Mkdir(p, os.FileMode(action.Mode)&0777); err != nil {
|
|
if errors.Is(err, os.ErrExist) {
|
|
return nil
|
|
}
|
|
return errors.WithStack(err)
|
|
}
|
|
if err := copy.Chown(p, nil, ch); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
if err := copy.Utimes(p, timestampToTime(action.Timestamp)); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mkfile(d string, action *pb.FileActionMkFile, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
|
|
defer func() {
|
|
var osErr *os.PathError
|
|
if errors.As(err, &osErr) {
|
|
// remove system root from error path if present
|
|
osErr.Path = strings.TrimPrefix(osErr.Path, d)
|
|
}
|
|
}()
|
|
|
|
p, err := fs.RootPath(d, filepath.Join("/", action.Path))
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
ch, err := mapUserToChowner(user, idmap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.WriteFile(p, action.Data, os.FileMode(action.Mode)&0777); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if err := copy.Chown(p, nil, ch); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if err := copy.Utimes(p, timestampToTime(action.Timestamp)); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rm(d string, action *pb.FileActionRm) (err error) {
|
|
defer func() {
|
|
var osErr *os.PathError
|
|
if errors.As(err, &osErr) {
|
|
// remove system root from error path if present
|
|
osErr.Path = strings.TrimPrefix(osErr.Path, d)
|
|
}
|
|
}()
|
|
|
|
if action.AllowWildcard {
|
|
src, err := cleanPath(action.Path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "cleaning path")
|
|
}
|
|
m, err := copy.ResolveWildcards(d, src, false)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
for _, s := range m {
|
|
if err := rmPath(d, s, action.AllowNotFound); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return rmPath(d, action.Path, action.AllowNotFound)
|
|
}
|
|
|
|
func rmPath(root, src string, allowNotFound bool) error {
|
|
src = filepath.Clean(src)
|
|
dir, base := filepath.Split(src)
|
|
if base == "" {
|
|
return errors.New("rmPath: invalid empty path")
|
|
}
|
|
dir, err := fs.RootPath(root, filepath.Join("/", dir))
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
p := filepath.Join(dir, base)
|
|
|
|
if !allowNotFound {
|
|
_, err := os.Stat(p)
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return errors.WithStack(os.RemoveAll(p))
|
|
}
|
|
|
|
func docopy(ctx context.Context, src, dest string, action *pb.FileActionCopy, u *copy.User, idmap *idtools.IdentityMapping) (err error) {
|
|
srcPath, err := cleanPath(action.Src)
|
|
if err != nil {
|
|
return errors.Wrap(err, "cleaning source path")
|
|
}
|
|
destPath, err := cleanPath(action.Dest)
|
|
if err != nil {
|
|
return errors.Wrap(err, "cleaning destination path")
|
|
}
|
|
if !action.CreateDestPath {
|
|
p, err := fs.RootPath(dest, filepath.Join("/", action.Dest))
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
if _, err := os.Lstat(filepath.Dir(p)); err != nil {
|
|
return errors.Wrapf(err, "failed to stat %s", action.Dest)
|
|
}
|
|
}
|
|
|
|
xattrErrorHandler := func(dst, src, key string, err error) error {
|
|
log.Println(err)
|
|
return nil
|
|
}
|
|
|
|
ch, err := mapUserToChowner(u, idmap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opt := []copy.Opt{
|
|
func(ci *copy.CopyInfo) {
|
|
ci.IncludePatterns = action.IncludePatterns
|
|
ci.ExcludePatterns = action.ExcludePatterns
|
|
ci.Chown = ch
|
|
ci.Utime = timestampToTime(action.Timestamp)
|
|
if action.ModeStr != "" {
|
|
ci.ModeStr = action.ModeStr
|
|
} else if m := int(action.Mode); m != -1 {
|
|
ci.Mode = &m
|
|
}
|
|
ci.CopyDirContents = action.DirCopyContents
|
|
ci.FollowLinks = action.FollowSymlink
|
|
ci.AlwaysReplaceExistingDestPaths = action.AlwaysReplaceExistingDestPaths
|
|
},
|
|
copy.WithXAttrErrorHandler(xattrErrorHandler),
|
|
}
|
|
|
|
defer func() {
|
|
var osErr *os.PathError
|
|
if errors.As(err, &osErr) {
|
|
// remove system root from error path if present
|
|
osErr.Path = strings.TrimPrefix(osErr.Path, src)
|
|
osErr.Path = strings.TrimPrefix(osErr.Path, dest)
|
|
}
|
|
}()
|
|
|
|
var m []string
|
|
if !action.AllowWildcard {
|
|
m = []string{srcPath}
|
|
} else {
|
|
var err error
|
|
m, err = copy.ResolveWildcards(src, srcPath, action.FollowSymlink)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if len(m) == 0 {
|
|
if action.AllowEmptyWildcard {
|
|
return nil
|
|
}
|
|
return errors.Errorf("%s not found", srcPath)
|
|
}
|
|
}
|
|
|
|
for _, s := range m {
|
|
if action.AttemptUnpackDockerCompatibility {
|
|
if ok, err := unpack(src, s, dest, destPath, ch, timestampToTime(action.Timestamp), idmap); err != nil {
|
|
return errors.WithStack(err)
|
|
} else if ok {
|
|
continue
|
|
}
|
|
}
|
|
if err := copy.Copy(ctx, src, s, dest, destPath, opt...); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewFileOpBackend returns a new file operation backend. The executor is currently only used for Windows,
|
|
// and it is used to construct the readUserFn field set in the returned Backend.
|
|
func NewFileOpBackend(readUser ReadUserCallback) (*Backend, error) {
|
|
if readUser == nil {
|
|
return nil, errors.New("readUser callback must be provided")
|
|
}
|
|
return &Backend{
|
|
readUser: readUser,
|
|
}, nil
|
|
}
|
|
|
|
type ReadUserCallback func(chopt *pb.ChownOpt, mu, mg snapshot.Mountable) (*copy.User, error)
|
|
|
|
type Backend struct {
|
|
readUser ReadUserCallback
|
|
}
|
|
|
|
func (fb *Backend) Mkdir(ctx context.Context, m, user, group fileoptypes.Mount, action *pb.FileActionMkDir) error {
|
|
mnt, ok := m.(*Mount)
|
|
if !ok {
|
|
return errors.Errorf("invalid mount type %T", m)
|
|
}
|
|
|
|
lm := snapshot.LocalMounter(mnt.m)
|
|
dir, err := lm.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lm.Unmount()
|
|
|
|
u, err := fb.readUserWrapper(action.Owner, user, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return mkdir(dir, action, u, mnt.m.IdentityMapping())
|
|
}
|
|
|
|
func (fb *Backend) Mkfile(ctx context.Context, m, user, group fileoptypes.Mount, action *pb.FileActionMkFile) error {
|
|
mnt, ok := m.(*Mount)
|
|
if !ok {
|
|
return errors.Errorf("invalid mount type %T", m)
|
|
}
|
|
|
|
lm := snapshot.LocalMounter(mnt.m)
|
|
dir, err := lm.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lm.Unmount()
|
|
|
|
u, err := fb.readUserWrapper(action.Owner, user, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return mkfile(dir, action, u, mnt.m.IdentityMapping())
|
|
}
|
|
|
|
func (fb *Backend) Rm(ctx context.Context, m fileoptypes.Mount, action *pb.FileActionRm) error {
|
|
mnt, ok := m.(*Mount)
|
|
if !ok {
|
|
return errors.Errorf("invalid mount type %T", m)
|
|
}
|
|
|
|
lm := snapshot.LocalMounter(mnt.m)
|
|
dir, err := lm.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lm.Unmount()
|
|
|
|
return rm(dir, action)
|
|
}
|
|
|
|
func (fb *Backend) Copy(ctx context.Context, m1, m2, user, group fileoptypes.Mount, action *pb.FileActionCopy) error {
|
|
mnt1, ok := m1.(*Mount)
|
|
if !ok {
|
|
return errors.Errorf("invalid mount type %T", m1)
|
|
}
|
|
mnt2, ok := m2.(*Mount)
|
|
if !ok {
|
|
return errors.Errorf("invalid mount type %T", m2)
|
|
}
|
|
|
|
lm := snapshot.LocalMounter(mnt1.m)
|
|
src, err := lm.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lm.Unmount()
|
|
|
|
lm2 := snapshot.LocalMounter(mnt2.m)
|
|
dest, err := lm2.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lm2.Unmount()
|
|
|
|
u, err := fb.readUserWrapper(action.Owner, user, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return docopy(ctx, src, dest, action, u, mnt2.m.IdentityMapping())
|
|
}
|
|
|
|
func (fb *Backend) readUserWrapper(owner *pb.ChownOpt, user, group fileoptypes.Mount) (*copy.User, error) {
|
|
var userMountable, groupMountable snapshot.Mountable
|
|
if user != nil {
|
|
usr, ok := user.(*Mount)
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid mount type %T", user)
|
|
}
|
|
userMountable = usr.Mountable()
|
|
}
|
|
|
|
if group != nil {
|
|
grp, ok := group.(*Mount)
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid mount type %T", group)
|
|
}
|
|
groupMountable = grp.Mountable()
|
|
}
|
|
|
|
// We don't check the mountables for nil here. Depending on the ChownOpt value,
|
|
// one of them may be nil. Allow the readUser function to handle this.
|
|
u, err := fb.readUser(owner, userMountable, groupMountable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func cleanPath(s string) (string, error) {
|
|
s, err := system.CheckSystemDriveAndRemoveDriveLetter(s, runtime.GOOS)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "removing drive letter")
|
|
}
|
|
s = filepath.FromSlash(s)
|
|
s2 := filepath.Join("/", s)
|
|
if strings.HasSuffix(s, string(filepath.Separator)+".") {
|
|
if s2 != string(filepath.Separator) {
|
|
s2 += string(filepath.Separator)
|
|
}
|
|
s2 += "."
|
|
} else if strings.HasSuffix(s, string(filepath.Separator)) && s2 != string(filepath.Separator) {
|
|
s2 += string(filepath.Separator)
|
|
}
|
|
return s2, nil
|
|
}
|