mirror of https://github.com/docker/buildx.git
Merge pull request #2745 from crazy-max/detect-sudo
config: fix file/folder ownership
This commit is contained in:
commit
746eadd16e
|
@ -151,11 +151,11 @@ func toRepoOnly(in string) (string, error) {
|
|||
return strings.Join(out, ","), nil
|
||||
}
|
||||
|
||||
func Build(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) {
|
||||
return BuildWithResultHandler(ctx, nodes, opts, docker, configDir, w, nil)
|
||||
func Build(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, cfg *confutil.Config, w progress.Writer) (resp map[string]*client.SolveResponse, err error) {
|
||||
return BuildWithResultHandler(ctx, nodes, opts, docker, cfg, w, nil)
|
||||
}
|
||||
|
||||
func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) {
|
||||
func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, cfg *confutil.Config, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) {
|
||||
if len(nodes) == 0 {
|
||||
return nil, errors.Errorf("driver required for build")
|
||||
}
|
||||
|
@ -234,12 +234,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
|
|||
return nil, err
|
||||
}
|
||||
localOpt := opt
|
||||
so, release, err := toSolveOpt(ctx, np.Node(), multiDriver, &localOpt, gatewayOpts, configDir, w, docker)
|
||||
so, release, err := toSolveOpt(ctx, np.Node(), multiDriver, &localOpt, gatewayOpts, cfg, w, docker)
|
||||
opts[k] = localOpt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := saveLocalState(so, k, opt, np.Node(), configDir); err != nil {
|
||||
if err := saveLocalState(so, k, opt, np.Node(), cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addGitAttrs(so)
|
||||
|
|
|
@ -5,10 +5,11 @@ import (
|
|||
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/localstate"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/moby/buildkit/client"
|
||||
)
|
||||
|
||||
func saveLocalState(so *client.SolveOpt, target string, opts Options, node builder.Node, configDir string) error {
|
||||
func saveLocalState(so *client.SolveOpt, target string, opts Options, node builder.Node, cfg *confutil.Config) error {
|
||||
var err error
|
||||
if so.Ref == "" {
|
||||
return nil
|
||||
|
@ -30,7 +31,7 @@ func saveLocalState(so *client.SolveOpt, target string, opts Options, node build
|
|||
if lp == "" && dp == "" {
|
||||
return nil
|
||||
}
|
||||
l, err := localstate.New(configDir)
|
||||
l, err := localstate.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
"github.com/tonistiigi/fsutil"
|
||||
)
|
||||
|
||||
func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, configDir string, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) {
|
||||
func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) {
|
||||
nodeDriver := node.Driver
|
||||
defers := make([]func(), 0, 2)
|
||||
releaseF := func() {
|
||||
|
@ -271,7 +271,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
|
|||
|
||||
// add node identifier to shared key if one was specified
|
||||
if so.SharedKey != "" {
|
||||
so.SharedKey += ":" + confutil.TryNodeIdentifier(configDir)
|
||||
so.SharedKey += ":" + cfg.TryNodeIdentifier()
|
||||
}
|
||||
|
||||
if opt.Pull {
|
||||
|
|
|
@ -439,7 +439,7 @@ func Create(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts Cre
|
|||
if buildkitdConfigFile == "" {
|
||||
// if buildkit daemon config is not provided, check if the default one
|
||||
// is available and use it
|
||||
if f, ok := confutil.DefaultConfigFile(dockerCli); ok {
|
||||
if f, ok := confutil.NewConfig(dockerCli).BuildKitConfigFile(); ok {
|
||||
buildkitdConfigFile = f
|
||||
}
|
||||
}
|
||||
|
@ -584,7 +584,7 @@ func Leave(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts Leav
|
|||
return err
|
||||
}
|
||||
|
||||
ls, err := localstate.New(confutil.ConfigDir(dockerCli))
|
||||
ls, err := localstate.New(confutil.NewConfig(dockerCli))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -265,7 +265,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
|||
}
|
||||
|
||||
done := timeBuildCommand(mp, attributes)
|
||||
resp, retErr := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), printer)
|
||||
resp, retErr := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), printer)
|
||||
if err := printer.Wait(); retErr == nil {
|
||||
retErr = err
|
||||
}
|
||||
|
@ -470,7 +470,7 @@ func saveLocalStateGroup(dockerCli command.Cli, in bakeOptions, targets []string
|
|||
refs = append(refs, b.Ref)
|
||||
bo[k] = b
|
||||
}
|
||||
l, err := localstate.New(confutil.ConfigDir(dockerCli))
|
||||
l, err := localstate.New(confutil.NewConfig(dockerCli))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -621,7 +621,7 @@ func bakeMetricAttributes(dockerCli command.Cli, driverType, url, cmdContext str
|
|||
commandNameAttribute.String("bake"),
|
||||
attribute.Stringer(string(commandOptionsHash), &bakeOptionsHash{
|
||||
bakeOptions: options,
|
||||
configDir: confutil.ConfigDir(dockerCli),
|
||||
cfg: confutil.NewConfig(dockerCli),
|
||||
url: url,
|
||||
cmdContext: cmdContext,
|
||||
targets: targets,
|
||||
|
@ -633,7 +633,7 @@ func bakeMetricAttributes(dockerCli command.Cli, driverType, url, cmdContext str
|
|||
|
||||
type bakeOptionsHash struct {
|
||||
*bakeOptions
|
||||
configDir string
|
||||
cfg *confutil.Config
|
||||
url string
|
||||
cmdContext string
|
||||
targets []string
|
||||
|
@ -657,7 +657,7 @@ func (o *bakeOptionsHash) String() string {
|
|||
|
||||
joinedFiles := strings.Join(files, ",")
|
||||
joinedTargets := strings.Join(targets, ",")
|
||||
salt := confutil.TryNodeIdentifier(o.configDir)
|
||||
salt := o.cfg.TryNodeIdentifier()
|
||||
|
||||
h := sha256.New()
|
||||
for _, s := range []string{url, cmdContext, joinedFiles, joinedTargets, salt} {
|
||||
|
|
|
@ -238,7 +238,7 @@ func buildMetricAttributes(dockerCli command.Cli, driverType string, options *bu
|
|||
commandNameAttribute.String("build"),
|
||||
attribute.Stringer(string(commandOptionsHash), &buildOptionsHash{
|
||||
buildOptions: options,
|
||||
configDir: confutil.ConfigDir(dockerCli),
|
||||
cfg: confutil.NewConfig(dockerCli),
|
||||
}),
|
||||
driverNameAttribute.String(options.builder),
|
||||
driverTypeAttribute.String(driverType),
|
||||
|
@ -250,7 +250,7 @@ func buildMetricAttributes(dockerCli command.Cli, driverType string, options *bu
|
|||
// the fmt.Stringer interface.
|
||||
type buildOptionsHash struct {
|
||||
*buildOptions
|
||||
configDir string
|
||||
cfg *confutil.Config
|
||||
result string
|
||||
resultOnce sync.Once
|
||||
}
|
||||
|
@ -267,7 +267,7 @@ func (o *buildOptionsHash) String() string {
|
|||
if contextPath != "-" && osutil.IsLocalDir(contextPath) {
|
||||
contextPath = osutil.ToAbs(contextPath)
|
||||
}
|
||||
salt := confutil.TryNodeIdentifier(o.configDir)
|
||||
salt := o.cfg.TryNodeIdentifier()
|
||||
|
||||
h := sha256.New()
|
||||
for _, s := range []string{target, contextPath, dockerfile, salt} {
|
||||
|
|
|
@ -214,7 +214,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.No
|
|||
if generateResult {
|
||||
var mu sync.Mutex
|
||||
var idx int
|
||||
resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) {
|
||||
resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if res == nil || driverIndex < idx {
|
||||
|
@ -222,7 +222,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.No
|
|||
}
|
||||
})
|
||||
} else {
|
||||
resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress)
|
||||
resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), progress)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, res, err
|
||||
|
|
|
@ -258,7 +258,7 @@ func prepareRootDir(dockerCli command.Cli, config *serverConfig) (string, error)
|
|||
}
|
||||
|
||||
func rootDataDir(dockerCli command.Cli) string {
|
||||
return filepath.Join(confutil.ConfigDir(dockerCli), "controller")
|
||||
return filepath.Join(confutil.NewConfig(dockerCli).Dir(), "controller")
|
||||
}
|
||||
|
||||
func newBuildxClientAndCheck(ctx context.Context, addr string) (*Client, error) {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -147,6 +147,7 @@ require (
|
|||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -439,6 +439,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0 h1:H9++AiQUqjwrOMA/DOpWhxWp3JLyyT+MN4sRPbMmwoY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
@ -42,18 +42,18 @@ type StateGroup struct {
|
|||
}
|
||||
|
||||
type LocalState struct {
|
||||
root string
|
||||
cfg *confutil.Config
|
||||
}
|
||||
|
||||
func New(root string) (*LocalState, error) {
|
||||
if root == "" {
|
||||
return nil, errors.Errorf("root dir empty")
|
||||
func New(cfg *confutil.Config) (*LocalState, error) {
|
||||
if cfg.Dir() == "" {
|
||||
return nil, errors.Errorf("config dir empty")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, refsDir), 0700); err != nil {
|
||||
if err := cfg.MkdirAll(refsDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LocalState{
|
||||
root: root,
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ func (ls *LocalState) ReadRef(builderName, nodeName, id string) (*State, error)
|
|||
if err := ls.validate(builderName, nodeName, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dt, err := os.ReadFile(filepath.Join(ls.root, refsDir, builderName, nodeName, id))
|
||||
dt, err := os.ReadFile(filepath.Join(ls.cfg.Dir(), refsDir, builderName, nodeName, id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -76,19 +76,19 @@ func (ls *LocalState) SaveRef(builderName, nodeName, id string, st State) error
|
|||
if err := ls.validate(builderName, nodeName, id); err != nil {
|
||||
return err
|
||||
}
|
||||
refDir := filepath.Join(ls.root, refsDir, builderName, nodeName)
|
||||
if err := os.MkdirAll(refDir, 0700); err != nil {
|
||||
refDir := filepath.Join(refsDir, builderName, nodeName)
|
||||
if err := ls.cfg.MkdirAll(refDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
dt, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutils.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600)
|
||||
return ls.cfg.AtomicWriteFile(filepath.Join(refDir, id), dt, 0644)
|
||||
}
|
||||
|
||||
func (ls *LocalState) ReadGroup(id string) (*StateGroup, error) {
|
||||
dt, err := os.ReadFile(filepath.Join(ls.root, refsDir, groupDir, id))
|
||||
dt, err := os.ReadFile(filepath.Join(ls.cfg.Dir(), refsDir, groupDir, id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -100,15 +100,15 @@ func (ls *LocalState) ReadGroup(id string) (*StateGroup, error) {
|
|||
}
|
||||
|
||||
func (ls *LocalState) SaveGroup(id string, stg StateGroup) error {
|
||||
refDir := filepath.Join(ls.root, refsDir, groupDir)
|
||||
if err := os.MkdirAll(refDir, 0700); err != nil {
|
||||
refDir := filepath.Join(refsDir, groupDir)
|
||||
if err := ls.cfg.MkdirAll(refDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
dt, err := json.Marshal(stg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutils.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600)
|
||||
return ls.cfg.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600)
|
||||
}
|
||||
|
||||
func (ls *LocalState) RemoveBuilder(builderName string) error {
|
||||
|
@ -116,7 +116,7 @@ func (ls *LocalState) RemoveBuilder(builderName string) error {
|
|||
return errors.Errorf("builder name empty")
|
||||
}
|
||||
|
||||
dir := filepath.Join(ls.root, refsDir, builderName)
|
||||
dir := filepath.Join(ls.cfg.Dir(), refsDir, builderName)
|
||||
if _, err := os.Lstat(dir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
|
@ -147,7 +147,7 @@ func (ls *LocalState) RemoveBuilderNode(builderName string, nodeName string) err
|
|||
return errors.Errorf("node name empty")
|
||||
}
|
||||
|
||||
dir := filepath.Join(ls.root, refsDir, builderName, nodeName)
|
||||
dir := filepath.Join(ls.cfg.Dir(), refsDir, builderName, nodeName)
|
||||
if _, err := os.Lstat(dir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
|
@ -208,7 +208,7 @@ func (ls *LocalState) removeGroup(id string) error {
|
|||
if id == "" {
|
||||
return errors.Errorf("group ref empty")
|
||||
}
|
||||
f := filepath.Join(ls.root, refsDir, groupDir, id)
|
||||
f := filepath.Join(ls.cfg.Dir(), refsDir, groupDir, id)
|
||||
if _, err := os.Lstat(f); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -39,10 +40,10 @@ func newls(t *testing.T) *LocalState {
|
|||
t.Helper()
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
l, err := New(tmpdir)
|
||||
l, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir)))
|
||||
require.NoError(t, err)
|
||||
require.DirExists(t, filepath.Join(tmpdir, refsDir))
|
||||
require.Equal(t, tmpdir, l.root)
|
||||
require.Equal(t, tmpdir, l.cfg.Dir())
|
||||
|
||||
require.NoError(t, l.SaveRef(testBuilderName, testNodeName, testStateRefID, testStateRef))
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/docker/buildx/localstate"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -20,25 +20,25 @@ const (
|
|||
activityDir = "activity"
|
||||
)
|
||||
|
||||
func New(root string) (*Store, error) {
|
||||
if err := os.MkdirAll(filepath.Join(root, instanceDir), 0700); err != nil {
|
||||
func New(cfg *confutil.Config) (*Store, error) {
|
||||
if err := cfg.MkdirAll(instanceDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, defaultsDir), 0700); err != nil {
|
||||
if err := cfg.MkdirAll(defaultsDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, activityDir), 0700); err != nil {
|
||||
if err := cfg.MkdirAll(activityDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{root: root}, nil
|
||||
return &Store{cfg: cfg}, nil
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
root string
|
||||
cfg *confutil.Config
|
||||
}
|
||||
|
||||
func (s *Store) Txn() (*Txn, func(), error) {
|
||||
l := flock.New(filepath.Join(s.root, ".lock"))
|
||||
l := flock.New(filepath.Join(s.cfg.Dir(), ".lock"))
|
||||
if err := l.Lock(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ type Txn struct {
|
|||
}
|
||||
|
||||
func (t *Txn) List() ([]*NodeGroup, error) {
|
||||
pp := filepath.Join(t.s.root, instanceDir)
|
||||
pp := filepath.Join(t.s.cfg.Dir(), instanceDir)
|
||||
fis, err := os.ReadDir(pp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -84,7 +84,7 @@ func (t *Txn) NodeGroupByName(name string) (*NodeGroup, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.root, instanceDir, name))
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), instanceDir, name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ func (t *Txn) Save(ng *NodeGroup) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutils.AtomicWriteFile(filepath.Join(t.s.root, instanceDir, name), dt, 0600)
|
||||
return t.s.cfg.AtomicWriteFile(filepath.Join(instanceDir, name), dt, 0600)
|
||||
}
|
||||
|
||||
func (t *Txn) Remove(name string) error {
|
||||
|
@ -121,14 +121,14 @@ func (t *Txn) Remove(name string) error {
|
|||
if err := t.RemoveLastActivity(name); err != nil {
|
||||
return err
|
||||
}
|
||||
ls, err := localstate.New(t.s.root)
|
||||
ls, err := localstate.New(t.s.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ls.RemoveBuilder(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(filepath.Join(t.s.root, instanceDir, name))
|
||||
return os.RemoveAll(filepath.Join(t.s.cfg.Dir(), instanceDir, name))
|
||||
}
|
||||
|
||||
func (t *Txn) SetCurrent(key, name string, global, def bool) error {
|
||||
|
@ -141,28 +141,28 @@ func (t *Txn) SetCurrent(key, name string, global, def bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutils.AtomicWriteFile(filepath.Join(t.s.root, "current"), dt, 0600); err != nil {
|
||||
if err := t.s.cfg.AtomicWriteFile("current", dt, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := toHash(key)
|
||||
|
||||
if def {
|
||||
if err := ioutils.AtomicWriteFile(filepath.Join(t.s.root, defaultsDir, h), []byte(name), 0600); err != nil {
|
||||
if err := t.s.cfg.AtomicWriteFile(filepath.Join(defaultsDir, h), []byte(name), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
os.RemoveAll(filepath.Join(t.s.root, defaultsDir, h)) // ignore error
|
||||
os.RemoveAll(filepath.Join(t.s.cfg.Dir(), defaultsDir, h)) // ignore error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Txn) UpdateLastActivity(ng *NodeGroup) error {
|
||||
return ioutils.AtomicWriteFile(filepath.Join(t.s.root, activityDir, ng.Name), []byte(time.Now().UTC().Format(time.RFC3339)), 0600)
|
||||
return t.s.cfg.AtomicWriteFile(filepath.Join(activityDir, ng.Name), []byte(time.Now().UTC().Format(time.RFC3339)), 0600)
|
||||
}
|
||||
|
||||
func (t *Txn) GetLastActivity(ng *NodeGroup) (la time.Time, _ error) {
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.root, activityDir, ng.Name))
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), activityDir, ng.Name))
|
||||
if err != nil {
|
||||
if os.IsNotExist(errors.Cause(err)) {
|
||||
return la, nil
|
||||
|
@ -177,7 +177,7 @@ func (t *Txn) RemoveLastActivity(name string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(filepath.Join(t.s.root, activityDir, name))
|
||||
return os.RemoveAll(filepath.Join(t.s.cfg.Dir(), activityDir, name))
|
||||
}
|
||||
|
||||
func (t *Txn) reset(key string) error {
|
||||
|
@ -185,11 +185,11 @@ func (t *Txn) reset(key string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutils.AtomicWriteFile(filepath.Join(t.s.root, "current"), dt, 0600)
|
||||
return t.s.cfg.AtomicWriteFile("current", dt, 0600)
|
||||
}
|
||||
|
||||
func (t *Txn) Current(key string) (*NodeGroup, error) {
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.root, "current"))
|
||||
dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), "current"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
|
@ -220,7 +220,7 @@ func (t *Txn) Current(key string) (*NodeGroup, error) {
|
|||
|
||||
h := toHash(key)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(t.s.root, defaultsDir, h))
|
||||
dt, err = os.ReadFile(filepath.Join(t.s.cfg.Dir(), defaultsDir, h))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.reset(key)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -15,7 +16,7 @@ func TestEmptyStartup(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
s, err := New(tmpdir)
|
||||
s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir)))
|
||||
require.NoError(t, err)
|
||||
|
||||
txn, release, err := s.Txn()
|
||||
|
@ -33,7 +34,7 @@ func TestNodeLocking(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
s, err := New(tmpdir)
|
||||
s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir)))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, release, err := s.Txn()
|
||||
|
@ -68,7 +69,7 @@ func TestNodeManagement(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
s, err := New(tmpdir)
|
||||
s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir)))
|
||||
require.NoError(t, err)
|
||||
|
||||
txn, release, err := s.Txn()
|
||||
|
@ -240,7 +241,7 @@ func TestNodeInvalidName(t *testing.T) {
|
|||
t.Parallel()
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
s, err := New(tmpdir)
|
||||
s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir)))
|
||||
require.NoError(t, err)
|
||||
|
||||
txn, release, err := s.Txn()
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
// GetStore returns current builder instance store
|
||||
func GetStore(dockerCli command.Cli) (*store.Txn, func(), error) {
|
||||
s, err := store.New(confutil.ConfigDir(dockerCli))
|
||||
s, err := store.New(confutil.NewConfig(dockerCli))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/containerd/platforms"
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/buildx/localstate"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/gitutil"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/frontend/subrequests/lint"
|
||||
|
@ -167,7 +168,7 @@ COPY --from=base /etc/bar /bar
|
|||
err = json.Unmarshal(dt, &md)
|
||||
require.NoError(t, err)
|
||||
|
||||
ls, err := localstate.New(buildxConfig(sb))
|
||||
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
|
||||
require.NoError(t, err)
|
||||
|
||||
refParts := strings.Split(md.BuildRef, "/")
|
||||
|
@ -209,7 +210,7 @@ COPY --from=base /etc/bar /bar
|
|||
err = json.Unmarshal(dt, &md)
|
||||
require.NoError(t, err)
|
||||
|
||||
ls, err := localstate.New(buildxConfig(sb))
|
||||
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
|
||||
require.NoError(t, err)
|
||||
|
||||
refParts := strings.Split(md.BuildRef, "/")
|
||||
|
@ -261,7 +262,7 @@ COPY foo /foo
|
|||
err = json.Unmarshal(dt, &md)
|
||||
require.NoError(t, err)
|
||||
|
||||
ls, err := localstate.New(buildxConfig(sb))
|
||||
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
|
||||
require.NoError(t, err)
|
||||
|
||||
refParts := strings.Split(md.BuildRef, "/")
|
||||
|
|
|
@ -1,39 +1,143 @@
|
|||
package confutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
fs "github.com/tonistiigi/fsutil/copy"
|
||||
)
|
||||
|
||||
// ConfigDir will look for correct configuration store path;
|
||||
// if `$BUILDX_CONFIG` is set - use it, otherwise use parent directory
|
||||
// of Docker config file (i.e. `${DOCKER_CONFIG}/buildx`)
|
||||
func ConfigDir(dockerCli command.Cli) string {
|
||||
if buildxConfig := os.Getenv("BUILDX_CONFIG"); buildxConfig != "" {
|
||||
logrus.Debugf("using config store %q based in \"$BUILDX_CONFIG\" environment variable", buildxConfig)
|
||||
return buildxConfig
|
||||
}
|
||||
const defaultBuildKitConfigFile = "buildkitd.default.toml"
|
||||
|
||||
buildxConfig := filepath.Join(filepath.Dir(dockerCli.ConfigFile().Filename), "buildx")
|
||||
logrus.Debugf("using default config store %q", buildxConfig)
|
||||
return buildxConfig
|
||||
type Config struct {
|
||||
dir string
|
||||
chowner *chowner
|
||||
}
|
||||
|
||||
// DefaultConfigFile returns the default BuildKit configuration file path
|
||||
func DefaultConfigFile(dockerCli command.Cli) (string, bool) {
|
||||
f := path.Join(ConfigDir(dockerCli), "buildkitd.default.toml")
|
||||
type chowner struct {
|
||||
uid int
|
||||
gid int
|
||||
}
|
||||
|
||||
type ConfigOption func(*configOptions)
|
||||
|
||||
type configOptions struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func WithDir(dir string) ConfigOption {
|
||||
return func(o *configOptions) {
|
||||
o.dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig(dockerCli command.Cli, opts ...ConfigOption) *Config {
|
||||
co := configOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&co)
|
||||
}
|
||||
|
||||
configDir := co.dir
|
||||
if configDir == "" {
|
||||
configDir = os.Getenv("BUILDX_CONFIG")
|
||||
if configDir == "" {
|
||||
configDir = filepath.Join(filepath.Dir(dockerCli.ConfigFile().Filename), "buildx")
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
dir: configDir,
|
||||
chowner: sudoer(configDir),
|
||||
}
|
||||
}
|
||||
|
||||
// Dir will look for correct configuration store path;
|
||||
// if `$BUILDX_CONFIG` is set - use it, otherwise use parent directory
|
||||
// of Docker config file (i.e. `${DOCKER_CONFIG}/buildx`)
|
||||
func (c *Config) Dir() string {
|
||||
return c.dir
|
||||
}
|
||||
|
||||
// BuildKitConfigFile returns the default BuildKit configuration file path
|
||||
func (c *Config) BuildKitConfigFile() (string, bool) {
|
||||
f := filepath.Join(c.dir, defaultBuildKitConfigFile)
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return f, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// MkdirAll creates a directory and all necessary parents within the config dir.
|
||||
func (c *Config) MkdirAll(dir string, perm os.FileMode) error {
|
||||
var chown fs.Chowner
|
||||
if c.chowner != nil {
|
||||
chown = func(user *fs.User) (*fs.User, error) {
|
||||
return &fs.User{UID: c.chowner.uid, GID: c.chowner.gid}, nil
|
||||
}
|
||||
}
|
||||
d := filepath.Join(c.dir, dir)
|
||||
st, err := os.Stat(d)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fs.MkdirAll(d, perm, chown, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
// if directory already exists, fix the owner if necessary
|
||||
if c.chowner == nil {
|
||||
return nil
|
||||
}
|
||||
currentOwner := fileOwner(st)
|
||||
if currentOwner != nil && (currentOwner.uid != c.chowner.uid || currentOwner.gid != c.chowner.gid) {
|
||||
return os.Chown(d, c.chowner.uid, c.chowner.gid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AtomicWriteFile writes data to a file within the config dir atomically
|
||||
func (c *Config) AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f := filepath.Join(c.dir, filename)
|
||||
if err := ioutils.AtomicWriteFile(f, data, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.chowner == nil {
|
||||
return nil
|
||||
}
|
||||
return os.Chown(f, c.chowner.uid, c.chowner.gid)
|
||||
}
|
||||
|
||||
var nodeIdentifierMu sync.Mutex
|
||||
|
||||
func (c *Config) TryNodeIdentifier() (out string) {
|
||||
nodeIdentifierMu.Lock()
|
||||
defer nodeIdentifierMu.Unlock()
|
||||
sessionFilename := ".buildNodeID"
|
||||
sessionFilepath := filepath.Join(c.Dir(), sessionFilename)
|
||||
if _, err := os.Lstat(sessionFilepath); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return out
|
||||
}
|
||||
if err := c.AtomicWriteFile(sessionFilename, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
dt, err := os.ReadFile(sessionFilepath)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadConfigTree loads BuildKit config toml tree
|
||||
func LoadConfigTree(fp string) (*toml.Tree, error) {
|
||||
f, err := os.Open(fp)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sudoer returns the user that invoked the current process with sudo only if
|
||||
// sudo HOME env matches the home directory of the user that ran sudo and is
|
||||
// part of configDir.
|
||||
func sudoer(configDir string) *chowner {
|
||||
if _, ok := os.LookupEnv("SUDO_COMMAND"); !ok {
|
||||
return nil
|
||||
}
|
||||
suidenv := os.Getenv("SUDO_UID") // https://www.sudo.ws/docs/man/sudo.man/#SUDO_UID
|
||||
sgidenv := os.Getenv("SUDO_GID") // https://www.sudo.ws/docs/man/sudo.man/#SUDO_GID
|
||||
if suidenv == "" || sgidenv == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := user.LookupId(suidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
suid, err := strconv.Atoi(suidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
sgid, err := strconv.Atoi(sgidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" || u.HomeDir != home {
|
||||
return nil
|
||||
}
|
||||
if ok, _ := isSubPath(home, configDir); !ok {
|
||||
return nil
|
||||
}
|
||||
return &chowner{uid: suid, gid: sgid}
|
||||
}
|
||||
|
||||
func fileOwner(fi os.FileInfo) *chowner {
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
return &chowner{uid: int(st.Uid), gid: int(st.Gid)}
|
||||
}
|
||||
|
||||
func isSubPath(basePath, subPath string) (bool, error) {
|
||||
rel, err := filepath.Rel(basePath, subPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !strings.HasPrefix(rel, "..") && rel != ".", nil
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSubPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
basePath string
|
||||
subPath string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "SubPath is a direct subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user/docs",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SubPath is the same as basePath",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SubPath is not a subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/otheruser",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SubPath is a nested subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user/docs/reports",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SubPath is a sibling directory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user2",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ok, err := isSubPath(tt.basePath, tt.subPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, ok)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package confutil
|
||||
|
||||
import "os"
|
||||
|
||||
func sudoer(_ string) *chowner {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileOwner(_ os.FileInfo) *chowner {
|
||||
return nil
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package confutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var nodeIdentifierMu sync.Mutex
|
||||
|
||||
func TryNodeIdentifier(configDir string) (out string) {
|
||||
nodeIdentifierMu.Lock()
|
||||
defer nodeIdentifierMu.Unlock()
|
||||
sessionFile := filepath.Join(configDir, ".buildNodeID")
|
||||
if _, err := os.Lstat(sessionFile); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return out
|
||||
}
|
||||
if err := os.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dt, err := os.ReadFile(sessionFile)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
syntax: glob
|
||||
bench*.out*
|
||||
cmode
|
||||
coverage.out
|
||||
coverage.txt
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.23
|
||||
ARG XX_VERSION=1.5.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS base
|
||||
RUN apk add --no-cache git
|
||||
COPY --from=xx / /
|
||||
WORKDIR /src
|
||||
|
||||
FROM base AS build
|
||||
ARG TARGETPLATFORM
|
||||
RUN --mount=target=. --mount=target=/go/pkg/mod,type=cache \
|
||||
--mount=target=/root/.cache,type=cache \
|
||||
xx-go build ./...
|
||||
|
||||
FROM base AS test
|
||||
ARG TESTFLAGS
|
||||
RUN --mount=target=. --mount=target=/go/pkg/mod,type=cache \
|
||||
--mount=target=/root/.cache,type=cache \
|
||||
xx-go test -v -coverprofile=/tmp/coverage.txt -covermode=atomic ${TESTFLAGS} ./...
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test /tmp/coverage.txt /coverage-root.txt
|
||||
|
||||
FROM build
|
|
@ -0,0 +1,22 @@
|
|||
Copyright © 2016-2018, Dave Chapeskie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,26 @@
|
|||
Mode
|
||||
========
|
||||
|
||||
This is a fork of [hg.sr.ht/~dchapes/mode](https://hg.sr.ht/~dchapes/mode) with minimal patches and basic CI.
|
||||
|
||||
[Mode](https://hg.sr.ht/~dchapes/mode)
|
||||
is a [Go](http://golang.org/) package that provides
|
||||
a native Go implementation of BSD's
|
||||
[`setmode`](https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3)
|
||||
and `getmode` which can be used to modify the mode bits of
|
||||
an [`os.FileMode`](https://golang.org/pkg/os#FileMode) value
|
||||
based on a symbolic value as described by the
|
||||
Unix [`chmod`](https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1) command.
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/hg.sr.ht/~dchapes/mode.svg)](https://pkg.go.dev/hg.sr.ht/~dchapes/mode)
|
||||
|
||||
Online package documentation is available via
|
||||
[pkg.go.dev](https://pkg.go.dev/hg.sr.ht/~dchapes/mode).
|
||||
|
||||
To install:
|
||||
|
||||
go get hg.sr.ht/~dchapes/mode
|
||||
|
||||
or `go build` any Go code that imports it:
|
||||
|
||||
import "hg.sr.ht/~dchapes/mode"
|
|
@ -0,0 +1,76 @@
|
|||
package mode
|
||||
|
||||
import "os"
|
||||
|
||||
type modet uint16
|
||||
|
||||
// Although many of these can be found in the syscall package
|
||||
// we don't use those to avoid the dependency, add some more
|
||||
// values, use non-exported Go names, and use octal for better clarity.
|
||||
//
|
||||
// Note that Go only uses the the nine least significant bits as "Unix
|
||||
// permission bits" (os.ModePerm == 0777). We use chmod(1)'s octal
|
||||
// definitions that include three further bits: isUID, isGID, and
|
||||
// isTXT (07000). Go has os.ModeSetuid=1<<23, os.ModeSetgid=1<<22,
|
||||
// and os.ModeSticy=1<<20 for these. We do this so that absolute
|
||||
// octal values can include those bits as defined by chmod(1).
|
||||
const (
|
||||
//ifDir = 040000 // directory
|
||||
isUID = 04000 // set user id on execution
|
||||
isGID = 02000 // set group id on execution
|
||||
isTXT = 01000 // sticky bit
|
||||
iRWXU = 00700 // RWX mask for owner
|
||||
iRUser = 00400 // R for owner
|
||||
iWUser = 00200 // W for owner
|
||||
iXUser = 00100 // X for owner
|
||||
iRWXG = 00070 // RWX mask for group
|
||||
iRGroup = 00040 // R for group
|
||||
iWGroup = 00020 // W for group
|
||||
iXGroup = 00010 // X for group
|
||||
iRWXO = 00007 // RWX mask for other
|
||||
iROther = 00004 // R for other
|
||||
iWOther = 00002 // W for other
|
||||
iXOther = 00001 // X for other
|
||||
|
||||
standardBits = isUID | isGID | iRWXU | iRWXG | iRWXO
|
||||
|
||||
// os.FileMode bits we touch
|
||||
fmBits = os.ModeSetuid | os.ModeSetgid | os.ModeSticky | os.ModePerm
|
||||
)
|
||||
|
||||
func fileModeToBits(fm os.FileMode) modet {
|
||||
m := modet(fm.Perm())
|
||||
/*
|
||||
if fm&os.ModeSetuid != 0 {
|
||||
m |= isUID
|
||||
}
|
||||
if fm&os.ModeSetgid != 0 {
|
||||
m |= isGID
|
||||
}
|
||||
if fm&os.ModeSticky != 0 {
|
||||
m |= isTXT
|
||||
}
|
||||
*/
|
||||
m |= modet(fm & (os.ModeSetuid | os.ModeSetgid) >> 12)
|
||||
m |= modet(fm & os.ModeSticky >> 11)
|
||||
return m
|
||||
}
|
||||
|
||||
func bitsToFileMode(old os.FileMode, m modet) os.FileMode {
|
||||
fm := old &^ fmBits
|
||||
fm |= os.FileMode(m) & os.ModePerm
|
||||
/*
|
||||
if m&isUID != 0 {
|
||||
fm |= os.ModeSetuid
|
||||
}
|
||||
if m&isGID != 0 {
|
||||
fm |= os.ModeSetgid
|
||||
}
|
||||
if m&isTXT != 0 {
|
||||
fm |= os.ModeSticky
|
||||
}
|
||||
*/
|
||||
fm |= os.FileMode(m&(isUID|isGID)) << 12
|
||||
fm |= os.FileMode(m&isTXT) << 11
|
||||
return fm
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
variable "GO_VERSION" {
|
||||
default = null
|
||||
}
|
||||
|
||||
group "default" {
|
||||
targets = ["build"]
|
||||
}
|
||||
|
||||
target "build" {
|
||||
args = {
|
||||
GO_VERSION = GO_VERSION
|
||||
}
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "test" {
|
||||
inherits = ["build"]
|
||||
target = "test"
|
||||
}
|
||||
|
||||
target "cross" {
|
||||
inherits = ["build"]
|
||||
platforms = ["linux/amd64", "linux/386", "linux/arm64", "linux/arm", "linux/ppc64le", "linux/s390x", "darwin/amd64", "darwin/arm64", "windows/amd64", "windows/arm64", "freebsd/amd64", "freebsd/arm64"]
|
||||
}
|
|
@ -0,0 +1,546 @@
|
|||
/*
|
||||
|
||||
Parts of this file are a heavily modified C to Go
|
||||
translation of BSD's /usr/src/lib/libc/gen/setmode.c
|
||||
that contains the following copyright notice:
|
||||
|
||||
* Copyright (c) 1989, 1993, 1994
|
||||
* The Regents of the University of California. All rights reserved.
|
||||
*
|
||||
* This code is derived from software contributed to Berkeley by
|
||||
* Dave Borman at Cray Research, Inc.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 4. Neither the name of the University nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
// Package mode provides a native Go implementation of BSD's setmode and getmode
|
||||
// which can be used to modify the mode bits of an os.FileMode value based on
|
||||
// a symbolic value as described by the Unix chmod command.
|
||||
//
|
||||
// For a full description of the mode string see chmod(1).
|
||||
// Some examples include:
|
||||
//
|
||||
// 644 make a file readable by anyone and writable by the owner
|
||||
// only.
|
||||
//
|
||||
// go-w deny write permission to group and others.
|
||||
//
|
||||
// =rw,+X set the read and write permissions to the usual defaults,
|
||||
// but retain any execute permissions that are currently set.
|
||||
//
|
||||
// +X make a directory or file searchable/executable by everyone
|
||||
// if it is already searchable/executable by anyone.
|
||||
//
|
||||
// 755
|
||||
// u=rwx,go=rx
|
||||
// u=rwx,go=u-w make a file readable/executable by everyone and writable by
|
||||
// the owner only.
|
||||
//
|
||||
// go= clear all mode bits for group and others.
|
||||
//
|
||||
// go=u-w set the group bits equal to the user bits, but clear the
|
||||
// group write bit.
|
||||
//
|
||||
// See Also:
|
||||
//
|
||||
// setmode(3): https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3
|
||||
// chmod(1): https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1
|
||||
package mode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Set is a set of changes to apply to an os.FileMode.
|
||||
// Changes include setting or clearing specific bits, copying bits from one
|
||||
// user class to another (e.g. "u=go" sets the user permissions to a copy of
|
||||
// the group and other permsissions), etc.
|
||||
type Set struct {
|
||||
cmds []bitcmd
|
||||
}
|
||||
|
||||
type bitcmd struct {
|
||||
cmd byte
|
||||
cmd2 byte
|
||||
bits modet
|
||||
}
|
||||
|
||||
const (
|
||||
cmd2Clear byte = 1 << iota
|
||||
cmd2Set
|
||||
cmd2GBits
|
||||
cmd2OBits
|
||||
cmd2UBits
|
||||
)
|
||||
|
||||
func (c bitcmd) String() string {
|
||||
c2 := ""
|
||||
if c.cmd2 != 0 {
|
||||
c2 = " cmd2:"
|
||||
if c.cmd2&cmd2Clear != 0 {
|
||||
c2 += " CLR"
|
||||
}
|
||||
if c.cmd2&cmd2Set != 0 {
|
||||
c2 += " SET"
|
||||
}
|
||||
if c.cmd2&cmd2UBits != 0 {
|
||||
c2 += " UBITS"
|
||||
}
|
||||
if c.cmd2&cmd2GBits != 0 {
|
||||
c2 += " GBITS"
|
||||
}
|
||||
if c.cmd2&cmd2OBits != 0 {
|
||||
c2 += " OBITS"
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("cmd: %q bits %#05o%s", c.cmd, c.bits, c2)
|
||||
}
|
||||
|
||||
// The String method will likely only be useful when testing.
|
||||
func (s Set) String() string {
|
||||
var buf strings.Builder
|
||||
buf.Grow(21*len(s.cmds) + 10)
|
||||
_, _ = buf.WriteString("set: {\n")
|
||||
for _, c := range s.cmds {
|
||||
_, _ = buf.WriteString(c.String())
|
||||
_ = buf.WriteByte('\n')
|
||||
}
|
||||
_, _ = buf.WriteString("}")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ErrSyntax indicates an argument does not represent a valid mode.
|
||||
var ErrSyntax = errors.New("invalid syntax")
|
||||
|
||||
// Apply changes the provided os.FileMode based on the given umask and
|
||||
// absolute or symbolic mode value.
|
||||
//
|
||||
// Apply is a convience to calling ParseWithUmask followed by Apply.
|
||||
// Since it needs to parse the mode value string on each call it
|
||||
// should only be used when mode value string will not be reapplied.
|
||||
func Apply(s string, perm os.FileMode, umask uint) (os.FileMode, error) {
|
||||
set, err := ParseWithUmask(s, umask)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return set.Apply(perm), nil
|
||||
}
|
||||
|
||||
// Parse takes an absolute (octal) or symbolic mode value,
|
||||
// as described in chmod(1), as an argument and returns
|
||||
// the set of bit operations representing the mode value
|
||||
// that can be applied to specific os.FileMode values.
|
||||
//
|
||||
// Same as ParseWithUmask(s, 0).
|
||||
func Parse(s string) (Set, error) {
|
||||
return ParseWithUmask(s, 0)
|
||||
}
|
||||
|
||||
// TODO(dchapes): A Set.Parse method that reuses existing memory.
|
||||
|
||||
// TODO(dchapes): Only call syscall.Umask when abosolutely necessary and
|
||||
// provide a Set method to query if set is umask dependant (and perhaps
|
||||
// the umask that was in effect when parsed).
|
||||
|
||||
// ParseWithUmask is like Parse but uses the provided
|
||||
// file creation mask instead of calling syscall.Umask.
|
||||
func ParseWithUmask(s string, umask uint) (Set, error) {
|
||||
var m Set
|
||||
if s == "" {
|
||||
return m, ErrSyntax
|
||||
}
|
||||
|
||||
// If an absolute number, get it and return;
|
||||
// disallow non-octal digits or illegal bits.
|
||||
if d := s[0]; '0' <= d && d <= '9' {
|
||||
v, err := strconv.ParseInt(s, 8, 16)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
if v&^(standardBits|isTXT) != 0 {
|
||||
return m, ErrSyntax
|
||||
}
|
||||
// We know this takes exactly two bitcmds.
|
||||
m.cmds = make([]bitcmd, 0, 2)
|
||||
m.addcmd('=', standardBits|isTXT, modet(v), 0)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Get a copy of the mask for the permissions that are mask relative.
|
||||
// Flip the bits, we want what's not set.
|
||||
var mask modet = ^modet(umask)
|
||||
|
||||
// Pre-allocate room for several commands.
|
||||
//m.cmds = make([]bitcmd, 0, 8)
|
||||
|
||||
// Build list of bitcmd structs to set/clear/copy bits as described by
|
||||
// each clause of the symbolic mode.
|
||||
equalOpDone := false
|
||||
for {
|
||||
// First, find out which bits might be modified.
|
||||
var who modet
|
||||
whoLoop:
|
||||
for {
|
||||
if len(s) == 0 {
|
||||
return Set{}, ErrSyntax
|
||||
}
|
||||
switch s[0] {
|
||||
case 'a':
|
||||
who |= standardBits
|
||||
case 'u':
|
||||
who |= isUID | iRWXU
|
||||
case 'g':
|
||||
who |= isGID | iRWXG
|
||||
case 'o':
|
||||
who |= iRWXO
|
||||
default:
|
||||
break whoLoop
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
var op byte
|
||||
getop:
|
||||
op, s = s[0], s[1:]
|
||||
switch op {
|
||||
case '+', '-':
|
||||
// Nothing.
|
||||
case '=':
|
||||
equalOpDone = false
|
||||
default:
|
||||
return Set{}, ErrSyntax
|
||||
}
|
||||
|
||||
who &^= isTXT
|
||||
permLoop:
|
||||
for perm, permX := modet(0), modet(0); ; s = s[1:] {
|
||||
var b byte
|
||||
if len(s) > 0 {
|
||||
b = s[0]
|
||||
}
|
||||
switch b {
|
||||
case 'r':
|
||||
perm |= iRUser | iRGroup | iROther
|
||||
case 's':
|
||||
// If only "other" bits ignore set-id.
|
||||
if who == 0 || who&^iRWXO != 0 {
|
||||
perm |= isUID | isGID
|
||||
}
|
||||
case 't':
|
||||
// If only "other bits ignore sticky.
|
||||
if who == 0 || who&^iRWXO != 0 {
|
||||
who |= isTXT
|
||||
perm |= isTXT
|
||||
}
|
||||
case 'w':
|
||||
perm |= iWUser | iWGroup | iWOther
|
||||
case 'X':
|
||||
if op == '+' {
|
||||
permX = iXUser | iXGroup | iXOther
|
||||
}
|
||||
case 'x':
|
||||
perm |= iXUser | iXGroup | iXOther
|
||||
case 'u', 'g', 'o':
|
||||
// Whenever we hit 'u', 'g', or 'o', we have
|
||||
// to flush out any partial mode that we have,
|
||||
// and then do the copying of the mode bits.
|
||||
if perm != 0 {
|
||||
m.addcmd(op, who, perm, mask)
|
||||
perm = 0
|
||||
}
|
||||
if op == '=' {
|
||||
equalOpDone = true
|
||||
}
|
||||
if permX != 0 {
|
||||
m.addcmd('X', who, permX, mask)
|
||||
permX = 0
|
||||
}
|
||||
m.addcmd(b, who, modet(op), mask)
|
||||
default:
|
||||
// Add any permissions that we haven't alread done.
|
||||
if perm != 0 || op == '=' && !equalOpDone {
|
||||
if op == '=' {
|
||||
equalOpDone = true
|
||||
}
|
||||
m.addcmd(op, who, perm, mask)
|
||||
//perm = 0
|
||||
}
|
||||
if permX != 0 {
|
||||
m.addcmd('X', who, permX, mask)
|
||||
//permX = 0
|
||||
}
|
||||
break permLoop
|
||||
}
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
break
|
||||
}
|
||||
if s[0] != ',' {
|
||||
goto getop
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
m.compress()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Apply returns the os.FileMode after applying the set of changes.
|
||||
func (s Set) Apply(perm os.FileMode) os.FileMode {
|
||||
omode := fileModeToBits(perm)
|
||||
newmode := omode
|
||||
|
||||
// When copying the user, group or other bits around, we "know"
|
||||
// where the bits are in the mode so that we can do shifts to
|
||||
// copy them around. If we don't use shifts, it gets real
|
||||
// grundgy with lots of single bit checks and bit sets.
|
||||
common := func(c bitcmd, value modet) {
|
||||
if c.cmd2&cmd2Clear != 0 {
|
||||
var clrval modet
|
||||
if c.cmd2&cmd2Set != 0 {
|
||||
clrval = iRWXO
|
||||
} else {
|
||||
clrval = value
|
||||
}
|
||||
if c.cmd2&cmd2UBits != 0 {
|
||||
newmode &^= clrval << 6 & c.bits
|
||||
}
|
||||
if c.cmd2&cmd2GBits != 0 {
|
||||
newmode &^= clrval << 3 & c.bits
|
||||
}
|
||||
if c.cmd2&cmd2OBits != 0 {
|
||||
newmode &^= clrval & c.bits
|
||||
}
|
||||
}
|
||||
if c.cmd2&cmd2Set != 0 {
|
||||
if c.cmd2&cmd2UBits != 0 {
|
||||
newmode |= value << 6 & c.bits
|
||||
}
|
||||
if c.cmd2&cmd2GBits != 0 {
|
||||
newmode |= value << 3 & c.bits
|
||||
}
|
||||
if c.cmd2&cmd2OBits != 0 {
|
||||
newmode |= value & c.bits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range s.cmds {
|
||||
switch c.cmd {
|
||||
case 'u':
|
||||
common(c, newmode&iRWXU>>6)
|
||||
case 'g':
|
||||
common(c, newmode&iRWXG>>3)
|
||||
case 'o':
|
||||
common(c, newmode&iRWXO)
|
||||
|
||||
case '+':
|
||||
newmode |= c.bits
|
||||
case '-':
|
||||
newmode &^= c.bits
|
||||
|
||||
case 'X':
|
||||
if omode&(iXUser|iXGroup|iXOther) != 0 || perm.IsDir() {
|
||||
newmode |= c.bits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bitsToFileMode(perm, newmode)
|
||||
}
|
||||
|
||||
// Chmod is a convience routine that applies the changes in
|
||||
// Set to the named file. To avoid some race conditions,
|
||||
// it opens the file and uses os.File.Stat and
|
||||
// os.File.Chmod rather than os.Stat and os.Chmod if possible.
|
||||
func (s *Set) Chmod(name string) (old, new os.FileMode, err error) {
|
||||
if f, err := os.Open(name); err == nil { // nolint: vetshadow
|
||||
defer f.Close() // nolint: errcheck
|
||||
return s.ChmodFile(f)
|
||||
}
|
||||
// Fallback to os.Stat and os.Chmod if we
|
||||
// don't have permission to open the file.
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
old = fi.Mode()
|
||||
new = s.Apply(old)
|
||||
if new != old {
|
||||
err = os.Chmod(name, new)
|
||||
}
|
||||
return old, new, err
|
||||
|
||||
}
|
||||
|
||||
// ChmodFile is a convience routine that applies
|
||||
// the changes in Set to the open file f.
|
||||
func (s *Set) ChmodFile(f *os.File) (old, new os.FileMode, err error) {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
old = fi.Mode()
|
||||
new = s.Apply(old)
|
||||
if new != old {
|
||||
err = f.Chmod(new)
|
||||
}
|
||||
return old, new, err
|
||||
}
|
||||
|
||||
func (s *Set) addcmd(op byte, who, oparg, mask modet) {
|
||||
c := bitcmd{}
|
||||
switch op {
|
||||
case '=':
|
||||
c.cmd = '-'
|
||||
if who != 0 {
|
||||
c.bits = who
|
||||
} else {
|
||||
c.bits = standardBits
|
||||
}
|
||||
|
||||
s.cmds = append(s.cmds, c)
|
||||
//c = bitcmd{} // reset, not actually needed
|
||||
op = '+'
|
||||
fallthrough
|
||||
case '+', '-', 'X':
|
||||
c.cmd = op
|
||||
if who != 0 {
|
||||
c.bits = who & oparg
|
||||
} else {
|
||||
c.bits = mask & oparg
|
||||
}
|
||||
|
||||
case 'u', 'g', 'o':
|
||||
c.cmd = op
|
||||
if who != 0 {
|
||||
if who&iRUser != 0 {
|
||||
c.cmd2 |= cmd2UBits
|
||||
}
|
||||
if who&iRGroup != 0 {
|
||||
c.cmd2 |= cmd2GBits
|
||||
}
|
||||
if who&iROther != 0 {
|
||||
c.cmd2 |= cmd2OBits
|
||||
}
|
||||
c.bits = ^modet(0)
|
||||
} else {
|
||||
c.cmd2 = cmd2UBits | cmd2GBits | cmd2OBits
|
||||
c.bits = mask
|
||||
}
|
||||
|
||||
switch oparg {
|
||||
case '+':
|
||||
c.cmd2 |= cmd2Set
|
||||
case '-':
|
||||
c.cmd2 |= cmd2Clear
|
||||
case '=':
|
||||
c.cmd2 |= cmd2Set | cmd2Clear
|
||||
}
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
s.cmds = append(s.cmds, c)
|
||||
}
|
||||
|
||||
// compress by compacting consecutive '+', '-' and 'X'
|
||||
// commands into at most 3 commands, one of each. The 'u',
|
||||
// 'g' and 'o' commands continue to be separate. They could
|
||||
// probably be compacted, but it's not worth the effort.
|
||||
func (s *Set) compress() {
|
||||
//log.Println("before:", *m)
|
||||
//log.Println("Start compress:")
|
||||
j := 0
|
||||
for i := 0; i < len(s.cmds); i++ {
|
||||
c := s.cmds[i]
|
||||
//log.Println(" read", i, c)
|
||||
if strings.IndexByte("+-X", c.cmd) < 0 {
|
||||
// Copy over any 'u', 'g', and 'o' commands.
|
||||
if i != j {
|
||||
s.cmds[j] = c
|
||||
}
|
||||
//log.Println(" wrote", j, "from", i)
|
||||
j++
|
||||
continue
|
||||
}
|
||||
var setbits, clrbits, Xbits modet
|
||||
for ; i < len(s.cmds); i++ {
|
||||
c = s.cmds[i]
|
||||
//log.Println(" scan", i, c)
|
||||
switch c.cmd {
|
||||
case '-':
|
||||
clrbits |= c.bits
|
||||
setbits &^= c.bits
|
||||
Xbits &^= c.bits
|
||||
continue
|
||||
case '+':
|
||||
setbits |= c.bits
|
||||
clrbits &^= c.bits
|
||||
Xbits &^= c.bits
|
||||
continue
|
||||
case 'X':
|
||||
Xbits |= c.bits &^ setbits
|
||||
continue
|
||||
default:
|
||||
i--
|
||||
}
|
||||
break
|
||||
}
|
||||
if clrbits != 0 {
|
||||
s.cmds[j].cmd = '-'
|
||||
s.cmds[j].cmd2 = 0
|
||||
s.cmds[j].bits = clrbits
|
||||
//log.Println(" wrote", j, "clrbits")
|
||||
j++
|
||||
}
|
||||
if setbits != 0 {
|
||||
s.cmds[j].cmd = '+'
|
||||
s.cmds[j].cmd2 = 0
|
||||
s.cmds[j].bits = setbits
|
||||
//log.Println(" wrote", j, "setbits")
|
||||
j++
|
||||
}
|
||||
if Xbits != 0 {
|
||||
s.cmds[j].cmd = 'X'
|
||||
s.cmds[j].cmd2 = 0
|
||||
s.cmds[j].bits = Xbits
|
||||
//log.Println(" wrote", j, "Xbits")
|
||||
j++
|
||||
}
|
||||
}
|
||||
/*
|
||||
if len(m.cmds) != j {
|
||||
log.Println("compressed", len(m.cmds), "down to", j)
|
||||
}
|
||||
*/
|
||||
s.cmds = s.cmds[:j]
|
||||
//log.Println("after:", *m)
|
||||
}
|
|
@ -0,0 +1,691 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/pkg/errors"
|
||||
mode "github.com/tonistiigi/dchapes-mode"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
)
|
||||
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
buffer := make([]byte, 32*1024)
|
||||
return &buffer
|
||||
},
|
||||
}
|
||||
|
||||
func rootPath(root, p string, followLinks bool) (string, error) {
|
||||
p = filepath.Join("/", p)
|
||||
if p == "/" {
|
||||
return root, nil
|
||||
}
|
||||
if followLinks {
|
||||
return fs.RootPath(root, p)
|
||||
}
|
||||
d, f := filepath.Split(p)
|
||||
ppath, err := fs.RootPath(root, d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(ppath, f), nil
|
||||
}
|
||||
|
||||
func ResolveWildcards(root, src string, followLinks bool) ([]string, error) {
|
||||
d1, d2 := splitWildcards(src)
|
||||
if d2 != "" {
|
||||
p, err := rootPath(root, d1, followLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches, err := resolveWildcards(p, d2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, m := range matches {
|
||||
p, err := rel(root, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches[i] = p
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
return []string{d1}, nil
|
||||
}
|
||||
|
||||
// Copy copies files using `cp -a` semantics.
|
||||
// Copy is likely unsafe to be used in non-containerized environments.
|
||||
func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) error {
|
||||
var ci CopyInfo
|
||||
for _, o := range opts {
|
||||
o(&ci)
|
||||
}
|
||||
ensureDstPath := dst
|
||||
if d, f := filepath.Split(dst); f != "" && f != "." {
|
||||
ensureDstPath = d
|
||||
}
|
||||
if ensureDstPath != "" {
|
||||
ensureDstPath, err := fs.RootPath(dstRoot, ensureDstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := MkdirAll(ensureDstPath, 0755, ci.Chown, ci.Utime); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var modeSet *mode.Set
|
||||
if ci.ModeStr != "" {
|
||||
ms, err := mode.ParseWithUmask(ci.ModeStr, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modeSet = &ms
|
||||
}
|
||||
|
||||
dst, err := fs.RootPath(dstRoot, filepath.Clean(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := newCopier(dstRoot, ci.Chown, ci.Utime, ci.Mode, modeSet, ci.XAttrErrorHandler, ci.IncludePatterns, ci.ExcludePatterns, ci.AlwaysReplaceExistingDestPaths, ci.ChangeFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcs := []string{src}
|
||||
|
||||
if ci.AllowWildcards {
|
||||
matches, err := ResolveWildcards(srcRoot, src, ci.FollowLinks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return errors.Errorf("no matches found: %s", src)
|
||||
}
|
||||
srcs = matches
|
||||
}
|
||||
|
||||
for _, src := range srcs {
|
||||
srcFollowed, err := rootPath(srcRoot, src, ci.FollowLinks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst, err := c.prepareTargetDir(srcFollowed, src, dst, ci.CopyDirContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.copy(ctx, srcFollowed, "", dst, false, patternmatcher.MatchInfo{}, patternmatcher.MatchInfo{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) prepareTargetDir(srcFollowed, src, destPath string, copyDirContents bool) (string, error) {
|
||||
fiSrc, err := os.Lstat(srcFollowed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fiDest, err := os.Stat(destPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return "", errors.Wrap(err, "failed to lstat destination path")
|
||||
}
|
||||
}
|
||||
|
||||
if (!copyDirContents && fiSrc.IsDir() && fiDest != nil) || (!fiSrc.IsDir() && fiDest != nil && fiDest.IsDir()) {
|
||||
destPath = filepath.Join(destPath, filepath.Base(src))
|
||||
}
|
||||
|
||||
target := filepath.Dir(destPath)
|
||||
|
||||
if copyDirContents && fiSrc.IsDir() && fiDest == nil {
|
||||
target = destPath
|
||||
}
|
||||
if err := MkdirAll(target, 0755, c.chown, c.utime); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return destPath, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
UID, GID int
|
||||
SID string
|
||||
}
|
||||
|
||||
type Chowner func(*User) (*User, error)
|
||||
|
||||
type XAttrErrorHandler func(dst, src, xattrKey string, err error) error
|
||||
|
||||
type CopyInfo struct {
|
||||
Chown Chowner
|
||||
Utime *time.Time
|
||||
AllowWildcards bool
|
||||
Mode *int
|
||||
// ModeStr is mode in non-octal format. Overrides Mode if non-empty.
|
||||
ModeStr string
|
||||
XAttrErrorHandler XAttrErrorHandler
|
||||
CopyDirContents bool
|
||||
FollowLinks bool
|
||||
// Include only files/dirs matching at least one of these patterns
|
||||
IncludePatterns []string
|
||||
// Exclude files/dir matching any of these patterns (even if they match an include pattern)
|
||||
ExcludePatterns []string
|
||||
// If true, any source path that overwrite existing destination paths will always replace
|
||||
// the existing destination path, even if they are of different types (e.g. a directory will
|
||||
// replace any existing symlink or file)
|
||||
AlwaysReplaceExistingDestPaths bool
|
||||
ChangeFunc fsutil.ChangeFunc
|
||||
}
|
||||
|
||||
type Opt func(*CopyInfo)
|
||||
|
||||
func WithCopyInfo(ci CopyInfo) func(*CopyInfo) {
|
||||
return func(c *CopyInfo) {
|
||||
*c = ci
|
||||
}
|
||||
}
|
||||
|
||||
func WithChown(uid, gid int) Opt {
|
||||
return func(ci *CopyInfo) {
|
||||
ci.Chown = func(*User) (*User, error) {
|
||||
return &User{UID: uid, GID: gid}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AllowWildcards(ci *CopyInfo) {
|
||||
ci.AllowWildcards = true
|
||||
}
|
||||
|
||||
func WithXAttrErrorHandler(h XAttrErrorHandler) Opt {
|
||||
return func(ci *CopyInfo) {
|
||||
ci.XAttrErrorHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
func AllowXAttrErrors(ci *CopyInfo) {
|
||||
h := func(string, string, string, error) error {
|
||||
return nil
|
||||
}
|
||||
WithXAttrErrorHandler(h)(ci)
|
||||
}
|
||||
|
||||
func WithIncludePattern(includePattern string) Opt {
|
||||
return func(ci *CopyInfo) {
|
||||
ci.IncludePatterns = append(ci.IncludePatterns, includePattern)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExcludePattern(excludePattern string) Opt {
|
||||
return func(ci *CopyInfo) {
|
||||
ci.ExcludePatterns = append(ci.ExcludePatterns, excludePattern)
|
||||
}
|
||||
}
|
||||
|
||||
func WithChangeNotifier(fn fsutil.ChangeFunc) Opt {
|
||||
return func(ci *CopyInfo) {
|
||||
ci.ChangeFunc = fn
|
||||
}
|
||||
}
|
||||
|
||||
type copier struct {
|
||||
chown Chowner
|
||||
utime *time.Time
|
||||
mode *int
|
||||
modeSet *mode.Set
|
||||
inodes map[uint64]string
|
||||
xattrErrorHandler XAttrErrorHandler
|
||||
includePatternMatcher *patternmatcher.PatternMatcher
|
||||
excludePatternMatcher *patternmatcher.PatternMatcher
|
||||
parentDirs []parentDir
|
||||
changefn fsutil.ChangeFunc
|
||||
root string
|
||||
alwaysReplaceExistingDestPaths bool
|
||||
}
|
||||
|
||||
type parentDir struct {
|
||||
srcPath string
|
||||
dstPath string
|
||||
copied bool
|
||||
}
|
||||
|
||||
func newCopier(root string, chown Chowner, tm *time.Time, mode *int, modeSet *mode.Set, xeh XAttrErrorHandler, includePatterns, excludePatterns []string, alwaysReplaceExistingDestPaths bool, changeFunc fsutil.ChangeFunc) (*copier, error) {
|
||||
if xeh == nil {
|
||||
xeh = func(dst, src, key string, err error) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var includePatternMatcher *patternmatcher.PatternMatcher
|
||||
if len(includePatterns) != 0 {
|
||||
var err error
|
||||
includePatternMatcher, err = patternmatcher.New(includePatterns)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid includepatterns: %s", includePatterns)
|
||||
}
|
||||
}
|
||||
|
||||
var excludePatternMatcher *patternmatcher.PatternMatcher
|
||||
if len(excludePatterns) != 0 {
|
||||
var err error
|
||||
excludePatternMatcher, err = patternmatcher.New(excludePatterns)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid excludepatterns: %s", excludePatterns)
|
||||
}
|
||||
}
|
||||
|
||||
return &copier{
|
||||
root: root,
|
||||
inodes: map[uint64]string{},
|
||||
chown: chown,
|
||||
utime: tm,
|
||||
xattrErrorHandler: xeh,
|
||||
mode: mode,
|
||||
modeSet: modeSet,
|
||||
includePatternMatcher: includePatternMatcher,
|
||||
excludePatternMatcher: excludePatternMatcher,
|
||||
changefn: changeFunc,
|
||||
alwaysReplaceExistingDestPaths: alwaysReplaceExistingDestPaths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dest is always clean
|
||||
func (c *copier) copy(ctx context.Context, src, srcComponents, target string, overwriteTargetMetadata bool, parentIncludeMatchInfo, parentExcludeMatchInfo patternmatcher.MatchInfo) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
targetFi, err := os.Lstat(target)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
|
||||
include := true
|
||||
var (
|
||||
includeMatchInfo patternmatcher.MatchInfo
|
||||
excludeMatchInfo patternmatcher.MatchInfo
|
||||
)
|
||||
if srcComponents != "" {
|
||||
matchesIncludePattern := false
|
||||
matchesExcludePattern := false
|
||||
matchesIncludePattern, includeMatchInfo, err = c.include(srcComponents, fi, parentIncludeMatchInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
include = matchesIncludePattern
|
||||
|
||||
matchesExcludePattern, excludeMatchInfo, err = c.exclude(srcComponents, fi, parentExcludeMatchInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matchesExcludePattern {
|
||||
include = false
|
||||
}
|
||||
}
|
||||
|
||||
if include {
|
||||
if err := c.removeTargetIfNeeded(src, target, fi, targetFi); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.createParentDirs(src, srcComponents, target, overwriteTargetMetadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
if !include {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ensureEmptyFileTarget(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
copyFileInfo := include
|
||||
restoreFileTimestamp := false
|
||||
notify := true
|
||||
|
||||
switch {
|
||||
case fi.IsDir():
|
||||
if created, err := c.copyDirectory(
|
||||
ctx, src, srcComponents, target, fi, overwriteTargetMetadata,
|
||||
include, includeMatchInfo, excludeMatchInfo,
|
||||
); err != nil {
|
||||
return err
|
||||
} else if !overwriteTargetMetadata {
|
||||
// if we aren't supposed to overwrite existing target metadata,
|
||||
// then we only need to copy the new file info if we newly created
|
||||
// it, or restore the previous file timestamp if not
|
||||
copyFileInfo = created
|
||||
restoreFileTimestamp = !created
|
||||
}
|
||||
notify = false
|
||||
case (fi.Mode() & os.ModeType) == 0:
|
||||
link, err := getLinkSource(target, fi, c.inodes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get hardlink")
|
||||
}
|
||||
if link != "" {
|
||||
if err := os.Link(link, target); err != nil {
|
||||
return errors.Wrap(err, "failed to create hard link")
|
||||
}
|
||||
} else if err := copyFile(src, target); err != nil {
|
||||
return errors.Wrap(err, "failed to copy files")
|
||||
}
|
||||
case (fi.Mode() & os.ModeSymlink) == os.ModeSymlink:
|
||||
link, err := os.Readlink(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read link: %s", src)
|
||||
}
|
||||
if err := os.Symlink(link, target); err != nil {
|
||||
return errors.Wrapf(err, "failed to create symlink: %s", target)
|
||||
}
|
||||
case (fi.Mode() & os.ModeDevice) == os.ModeDevice,
|
||||
(fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe,
|
||||
(fi.Mode() & os.ModeSocket) == os.ModeSocket:
|
||||
if err := copyDevice(target, fi); err != nil {
|
||||
return errors.Wrapf(err, "failed to create device")
|
||||
}
|
||||
}
|
||||
|
||||
if copyFileInfo {
|
||||
if err := c.copyFileInfo(fi, src, target); err != nil {
|
||||
return errors.Wrap(err, "failed to copy file info")
|
||||
}
|
||||
|
||||
if err := copyXAttrs(target, src, c.xattrErrorHandler); err != nil {
|
||||
return errors.Wrap(err, "failed to copy xattrs")
|
||||
}
|
||||
} else if restoreFileTimestamp && targetFi != nil {
|
||||
if err := c.copyFileTimestamp(fi, target); err != nil {
|
||||
return errors.Wrap(err, "failed to restore file timestamp")
|
||||
}
|
||||
}
|
||||
if notify {
|
||||
if err := c.notifyChange(target, fi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) notifyChange(target string, fi os.FileInfo) error {
|
||||
if c.changefn != nil {
|
||||
if err := c.changefn(fsutil.ChangeKindAdd, path.Clean(strings.TrimPrefix(target, c.root)), fi, nil); err != nil {
|
||||
return errors.Wrap(err, "failed to notify file change")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) include(path string, fi os.FileInfo, parentIncludeMatchInfo patternmatcher.MatchInfo) (bool, patternmatcher.MatchInfo, error) {
|
||||
if c.includePatternMatcher == nil {
|
||||
return true, patternmatcher.MatchInfo{}, nil
|
||||
}
|
||||
|
||||
m, matchInfo, err := c.includePatternMatcher.MatchesUsingParentResults(path, parentIncludeMatchInfo)
|
||||
if err != nil {
|
||||
return false, matchInfo, errors.Wrap(err, "failed to match includepatterns")
|
||||
}
|
||||
return m, matchInfo, nil
|
||||
}
|
||||
|
||||
func (c *copier) exclude(path string, fi os.FileInfo, parentExcludeMatchInfo patternmatcher.MatchInfo) (bool, patternmatcher.MatchInfo, error) {
|
||||
if c.excludePatternMatcher == nil {
|
||||
return false, patternmatcher.MatchInfo{}, nil
|
||||
}
|
||||
|
||||
m, matchInfo, err := c.excludePatternMatcher.MatchesUsingParentResults(path, parentExcludeMatchInfo)
|
||||
if err != nil {
|
||||
return false, matchInfo, errors.Wrap(err, "failed to match excludepatterns")
|
||||
}
|
||||
return m, matchInfo, nil
|
||||
}
|
||||
|
||||
func (c *copier) removeTargetIfNeeded(src, target string, srcFi, targetFi os.FileInfo) error {
|
||||
if !c.alwaysReplaceExistingDestPaths {
|
||||
return nil
|
||||
}
|
||||
if targetFi == nil {
|
||||
// already doesn't exist
|
||||
return nil
|
||||
}
|
||||
if srcFi.IsDir() && targetFi.IsDir() {
|
||||
// directories are merged, not replaced
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(target)
|
||||
}
|
||||
|
||||
// Delayed creation of parent directories when a file or dir matches an include
|
||||
// pattern.
|
||||
func (c *copier) createParentDirs(src, srcComponents, target string, overwriteTargetMetadata bool) error {
|
||||
for i, parentDir := range c.parentDirs {
|
||||
if parentDir.copied {
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err := os.Stat(parentDir.srcPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.Errorf("%s is not a directory", parentDir.srcPath)
|
||||
}
|
||||
|
||||
created, err := copyDirectoryOnly(parentDir.srcPath, parentDir.dstPath, fi, overwriteTargetMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if created {
|
||||
if err := c.copyFileInfo(fi, parentDir.srcPath, parentDir.dstPath); err != nil {
|
||||
return errors.Wrap(err, "failed to copy file info")
|
||||
}
|
||||
|
||||
if err := copyXAttrs(parentDir.dstPath, parentDir.srcPath, c.xattrErrorHandler); err != nil {
|
||||
return errors.Wrap(err, "failed to copy xattrs")
|
||||
}
|
||||
}
|
||||
|
||||
c.parentDirs[i].copied = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) copyDirectory(
|
||||
ctx context.Context,
|
||||
src string,
|
||||
srcComponents string,
|
||||
dst string,
|
||||
stat os.FileInfo,
|
||||
overwriteTargetMetadata bool,
|
||||
include bool,
|
||||
includeMatchInfo patternmatcher.MatchInfo,
|
||||
excludeMatchInfo patternmatcher.MatchInfo,
|
||||
) (bool, error) {
|
||||
if !stat.IsDir() {
|
||||
return false, errors.Errorf("source is not directory")
|
||||
}
|
||||
|
||||
created := false
|
||||
|
||||
parentDir := parentDir{
|
||||
srcPath: src,
|
||||
dstPath: dst,
|
||||
}
|
||||
|
||||
// If this directory passed include/exclude matching directly, go ahead
|
||||
// and create the directory. Otherwise, delay to handle include
|
||||
// patterns like a/*/c where we do not want to create a/b until we
|
||||
// encounter a/b/c.
|
||||
if include {
|
||||
var err error
|
||||
created, err = copyDirectoryOnly(src, dst, stat, overwriteTargetMetadata)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
if created || overwriteTargetMetadata {
|
||||
if err := c.notifyChange(dst, stat); err != nil {
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
parentDir.copied = true
|
||||
}
|
||||
|
||||
c.parentDirs = append(c.parentDirs, parentDir)
|
||||
|
||||
defer func() {
|
||||
c.parentDirs = c.parentDirs[:len(c.parentDirs)-1]
|
||||
}()
|
||||
|
||||
fis, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to read %s", src)
|
||||
}
|
||||
|
||||
for _, fi := range fis {
|
||||
if err := c.copy(
|
||||
ctx,
|
||||
filepath.Join(src, fi.Name()), filepath.Join(srcComponents, fi.Name()),
|
||||
filepath.Join(dst, fi.Name()),
|
||||
true, includeMatchInfo, excludeMatchInfo,
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func copyDirectoryOnly(src, dst string, stat os.FileInfo, overwriteTargetMetadata bool) (bool, error) {
|
||||
if st, err := os.Lstat(dst); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
if err := os.Mkdir(dst, stat.Mode()); err != nil {
|
||||
return false, errors.Wrapf(err, "failed to mkdir %s", dst)
|
||||
}
|
||||
return true, nil
|
||||
} else if !st.IsDir() {
|
||||
return false, errors.Errorf("cannot copy to non-directory: %s", dst)
|
||||
} else if overwriteTargetMetadata {
|
||||
if err := os.Chmod(dst, stat.Mode()); err != nil {
|
||||
return false, errors.Wrapf(err, "failed to chmod on %s", dst)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ensureEmptyFileTarget(dst string) error {
|
||||
fi, err := os.Lstat(dst)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to lstat file target")
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return errors.Errorf("cannot replace to directory %s with file", dst)
|
||||
}
|
||||
return os.Remove(dst)
|
||||
}
|
||||
|
||||
func containsWildcards(name string) bool {
|
||||
isWindows := runtime.GOOS == "windows"
|
||||
for i := 0; i < len(name); i++ {
|
||||
ch := name[i]
|
||||
if ch == '\\' && !isWindows {
|
||||
i++
|
||||
} else if ch == '*' || ch == '?' || ch == '[' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitWildcards(p string) (d1, d2 string) {
|
||||
parts := strings.Split(filepath.Join(p), string(filepath.Separator))
|
||||
var p1, p2 []string
|
||||
var found bool
|
||||
for _, p := range parts {
|
||||
if !found && containsWildcards(p) {
|
||||
found = true
|
||||
}
|
||||
if p == "" {
|
||||
p = "/"
|
||||
}
|
||||
if !found {
|
||||
p1 = append(p1, p)
|
||||
} else {
|
||||
p2 = append(p2, p)
|
||||
}
|
||||
}
|
||||
return filepath.Join(p1...), filepath.Join(p2...)
|
||||
}
|
||||
|
||||
func resolveWildcards(basePath, comp string) ([]string, error) {
|
||||
var out []string
|
||||
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := rel(basePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
if match, _ := filepath.Match(comp, rel); !match {
|
||||
return nil
|
||||
}
|
||||
out = append(out, path)
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// rel makes a path relative to base path. Same as `filepath.Rel` but can also
|
||||
// handle UUID paths in windows.
|
||||
func rel(basepath, targpath string) (string, error) {
|
||||
// filepath.Rel can't handle UUID paths in windows
|
||||
if runtime.GOOS == "windows" {
|
||||
pfx := basepath + `\`
|
||||
if strings.HasPrefix(targpath, pfx) {
|
||||
p := strings.TrimPrefix(targpath, pfx)
|
||||
if p == "" {
|
||||
p = "."
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return filepath.Rel(basepath, targpath)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
if err := unix.Clonefileat(unix.AT_FDCWD, source, unix.AT_FDCWD, target, unix.CLONE_NOFOLLOW); err != nil {
|
||||
if err != unix.EINVAL && err != unix.EXDEV {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open source %s", source)
|
||||
}
|
||||
defer src.Close()
|
||||
tgt, err := os.Create(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open target %s", target)
|
||||
}
|
||||
defer tgt.Close()
|
||||
|
||||
return copyFileContent(tgt, src)
|
||||
}
|
||||
|
||||
func copyFileContent(dst, src *os.File) error {
|
||||
buf := bufferPool.Get().(*[]byte)
|
||||
_, err := io.CopyBuffer(dst, src, *buf)
|
||||
bufferPool.Put(buf)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func mknod(dst string, mode uint32, rDev int) error {
|
||||
return unix.Mknod(dst, uint32(mode), rDev)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open source %s", source)
|
||||
}
|
||||
defer src.Close()
|
||||
tgt, err := os.Create(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open target %s", target)
|
||||
}
|
||||
defer tgt.Close()
|
||||
|
||||
return copyFileContent(tgt, src)
|
||||
}
|
||||
|
||||
func copyFileContent(dst, src *os.File) error {
|
||||
buf := bufferPool.Get().(*[]byte)
|
||||
_, err := io.CopyBuffer(dst, src, *buf)
|
||||
bufferPool.Put(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func mknod(dst string, mode uint32, rDev int) error {
|
||||
return unix.Mknod(dst, uint32(mode), uint64(rDev))
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getUIDGID(fi os.FileInfo) (uid, gid int) {
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
return int(st.Uid), int(st.Gid)
|
||||
}
|
||||
|
||||
func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error {
|
||||
chown := c.chown
|
||||
uid, gid := getUIDGID(fi)
|
||||
old := &User{UID: uid, GID: gid}
|
||||
if chown == nil {
|
||||
chown = func(u *User) (*User, error) {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
if err := Chown(name, old, chown); err != nil {
|
||||
return errors.Wrapf(err, "failed to chown %s", name)
|
||||
}
|
||||
|
||||
m := fi.Mode()
|
||||
if c.modeSet != nil {
|
||||
m = c.modeSet.Apply(m)
|
||||
} else if c.mode != nil {
|
||||
m = os.FileMode(*c.mode).Perm()
|
||||
if *c.mode&syscall.S_ISGID != 0 {
|
||||
m |= os.ModeSetgid
|
||||
}
|
||||
if *c.mode&syscall.S_ISUID != 0 {
|
||||
m |= os.ModeSetuid
|
||||
}
|
||||
if *c.mode&syscall.S_ISVTX != 0 {
|
||||
m |= os.ModeSticky
|
||||
}
|
||||
}
|
||||
if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
|
||||
if err := os.Chmod(name, m); err != nil {
|
||||
return errors.Wrapf(err, "failed to chmod %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.copyFileTimestamp(fi, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error {
|
||||
if c.utime != nil {
|
||||
return Utimes(name, c.utime)
|
||||
}
|
||||
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
timespec := []unix.Timespec{unix.Timespec(StatAtime(st)), unix.Timespec(StatMtime(st))}
|
||||
if err := unix.UtimesNanoAt(unix.AT_FDCWD, name, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return errors.Wrapf(err, "failed to utime %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open source %s", source)
|
||||
}
|
||||
defer src.Close()
|
||||
tgt, err := os.Create(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open target %s", target)
|
||||
}
|
||||
defer tgt.Close()
|
||||
|
||||
return copyFileContent(tgt, src)
|
||||
}
|
||||
|
||||
func copyFileContent(dst, src *os.File) error {
|
||||
st, err := src.Stat()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to stat source")
|
||||
}
|
||||
|
||||
var written int64
|
||||
size := st.Size()
|
||||
first := true
|
||||
|
||||
for written < size {
|
||||
var desired int
|
||||
if size-written > math.MaxInt32 {
|
||||
desired = int(math.MaxInt32)
|
||||
} else {
|
||||
desired = int(size - written)
|
||||
}
|
||||
|
||||
n, err := unix.CopyFileRange(int(src.Fd()), nil, int(dst.Fd()), nil, desired, 0)
|
||||
if err != nil {
|
||||
// matches go/src/internal/poll/copy_file_range_linux.go
|
||||
if (err != unix.ENOSYS && err != unix.EXDEV && err != unix.EPERM && err != syscall.EIO && err != unix.EOPNOTSUPP && err != syscall.EINVAL) || !first {
|
||||
return errors.Wrap(err, "copy file range failed")
|
||||
}
|
||||
|
||||
buf := bufferPool.Get().(*[]byte)
|
||||
_, err = io.CopyBuffer(dst, src, *buf)
|
||||
bufferPool.Put(buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "userspace copy failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
first = false
|
||||
written += int64(n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mknod(dst string, mode uint32, rDev int) error {
|
||||
return unix.Mknod(dst, uint32(mode), rDev)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/containerd/continuity/sysx"
|
||||
)
|
||||
|
||||
// copyXAttrs requires xeh to be non-nil
|
||||
func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error {
|
||||
xattrKeys, err := sysx.LListxattr(src)
|
||||
if err != nil {
|
||||
return xeh(dst, src, "", errors.Wrapf(err, "failed to list xattrs on %s", src))
|
||||
}
|
||||
for _, xattr := range xattrKeys {
|
||||
data, err := sysx.LGetxattr(src, xattr)
|
||||
if err != nil {
|
||||
return xeh(dst, src, xattr, errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src))
|
||||
}
|
||||
if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil {
|
||||
return xeh(dst, src, xattr, errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDevice(dst string, fi os.FileInfo) error {
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return errors.New("unsupported stat type")
|
||||
}
|
||||
var rDev int
|
||||
if fi.Mode()&os.ModeDevice == os.ModeDevice || fi.Mode()&os.ModeCharDevice == os.ModeCharDevice {
|
||||
rDev = int(st.Rdev)
|
||||
}
|
||||
mode := st.Mode
|
||||
mode &^= syscall.S_IFSOCK // socket copied as stub
|
||||
return mknod(dst, uint32(mode), rDev)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//go:build solaris || darwin || freebsd
|
||||
// +build solaris darwin freebsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getUIDGID(fi os.FileInfo) (uid, gid int) {
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
return int(st.Uid), int(st.Gid)
|
||||
}
|
||||
|
||||
func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error {
|
||||
chown := c.chown
|
||||
uid, gid := getUIDGID(fi)
|
||||
old := &User{UID: uid, GID: gid}
|
||||
if chown == nil {
|
||||
chown = func(u *User) (*User, error) {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
if err := Chown(name, old, chown); err != nil {
|
||||
return errors.Wrapf(err, "failed to chown %s", name)
|
||||
}
|
||||
|
||||
m := fi.Mode()
|
||||
if c.modeSet != nil {
|
||||
m = c.modeSet.Apply(m)
|
||||
} else if c.mode != nil {
|
||||
m = os.FileMode(*c.mode).Perm()
|
||||
if *c.mode&syscall.S_ISGID != 0 {
|
||||
m |= os.ModeSetgid
|
||||
}
|
||||
if *c.mode&syscall.S_ISUID != 0 {
|
||||
m |= os.ModeSetuid
|
||||
}
|
||||
if *c.mode&syscall.S_ISVTX != 0 {
|
||||
m |= os.ModeSticky
|
||||
}
|
||||
}
|
||||
if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
|
||||
if err := os.Chmod(name, m); err != nil {
|
||||
return errors.Wrapf(err, "failed to chmod %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.copyFileTimestamp(fi, name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error {
|
||||
if c.utime != nil {
|
||||
return Utimes(name, c.utime)
|
||||
}
|
||||
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
timespec := []unix.Timespec{unix.Timespec(StatAtime(st)), unix.Timespec(StatMtime(st))}
|
||||
if err := unix.UtimesNanoAt(unix.AT_FDCWD, name, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return errors.Wrapf(err, "failed to utime %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
seTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege"
|
||||
)
|
||||
|
||||
func getUIDGID(fi os.FileInfo) (uid, gid int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func getFileSecurityInfo(name string) (*windows.SID, *windows.ACL, error) {
|
||||
secInfo, err := windows.GetNamedSecurityInfo(
|
||||
name, windows.SE_FILE_OBJECT,
|
||||
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "fetching security info")
|
||||
}
|
||||
sid, _, err := secInfo.Owner()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "fetching owner SID")
|
||||
}
|
||||
dacl, _, err := secInfo.DACL()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "fetching dacl")
|
||||
}
|
||||
return sid, dacl, nil
|
||||
}
|
||||
|
||||
func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error {
|
||||
if c.modeSet != nil {
|
||||
return errors.Errorf("non-octal mode not supported on windows")
|
||||
}
|
||||
|
||||
if err := os.Chmod(name, fi.Mode()); err != nil {
|
||||
return errors.Wrapf(err, "failed to chmod %s", name)
|
||||
}
|
||||
|
||||
sid, dacl, err := getFileSecurityInfo(src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting file info")
|
||||
}
|
||||
|
||||
if c.chown != nil {
|
||||
// Use the defined chowner.
|
||||
usr := &User{SID: sid.String()}
|
||||
if err := Chown(name, usr, c.chown); err != nil {
|
||||
return errors.Wrapf(err, "failed to chown %s", name)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
// Copy file ownership and ACL from the source file.
|
||||
// We need SeRestorePrivilege and SeTakeOwnershipPrivilege in order
|
||||
// to restore security info on a file, especially if we're trying to
|
||||
// apply security info which includes SIDs not necessarily present on
|
||||
// the host.
|
||||
privileges := []string{winio.SeRestorePrivilege, seTakeOwnershipPrivilege}
|
||||
if err := winio.EnableProcessPrivileges(privileges); err != nil {
|
||||
return err
|
||||
}
|
||||
defer winio.DisableProcessPrivileges(privileges)
|
||||
|
||||
if err := windows.SetNamedSecurityInfo(
|
||||
name, windows.SE_FILE_OBJECT,
|
||||
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
|
||||
sid, nil, dacl, nil); err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.copyFileTimestamp(fi, name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error {
|
||||
if c.utime != nil {
|
||||
return Utimes(name, c.utime)
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
if err := os.Chtimes(name, fi.ModTime(), fi.ModTime()); err != nil {
|
||||
return errors.Wrap(err, "changing mtime")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open source %s", source)
|
||||
}
|
||||
defer src.Close()
|
||||
tgt, err := os.Create(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open target %s", target)
|
||||
}
|
||||
defer tgt.Close()
|
||||
|
||||
return copyFileContent(tgt, src)
|
||||
}
|
||||
|
||||
func copyFileContent(dst, src *os.File) error {
|
||||
buf := bufferPool.Get().(*[]byte)
|
||||
_, err := io.CopyBuffer(dst, src, *buf)
|
||||
bufferPool.Put(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDevice(dst string, fi os.FileInfo) error {
|
||||
return errors.New("device copy not supported")
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
// GetLinkInfo returns an identifier representing the node a hardlink is pointing
|
||||
// to. If the file is not hard linked then 0 will be returned.
|
||||
func GetLinkInfo(fi os.FileInfo) (uint64, bool) {
|
||||
return getLinkInfo(fi)
|
||||
}
|
||||
|
||||
// getLinkSource returns a path for the given name and
|
||||
// file info to its link source in the provided inode
|
||||
// map. If the given file name is not in the map and
|
||||
// has other links, it is added to the inode map
|
||||
// to be a source for other link locations.
|
||||
func getLinkSource(name string, fi os.FileInfo, inodes map[uint64]string) (string, error) {
|
||||
inode, isHardlink := getLinkInfo(fi)
|
||||
if !isHardlink {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
path, ok := inodes[inode]
|
||||
if !ok {
|
||||
inodes[inode] = name
|
||||
}
|
||||
return path, nil
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getLinkInfo(fi os.FileInfo) (uint64, bool) {
|
||||
s, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return uint64(s.Ino), !fi.IsDir() && s.Nlink > 1
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
func getLinkInfo(fi os.FileInfo) (uint64, bool) {
|
||||
return 0, false
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MkdirAll is forked os.MkdirAll
|
||||
func MkdirAll(path string, perm os.FileMode, user Chowner, tm *time.Time) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent.
|
||||
err = MkdirAll(fixRootDirectory(path[:j-1]), perm, user, tm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = os.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Chown(path, nil, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Utimes(path, tm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func fixRootDirectory(p string) string {
|
||||
return p
|
||||
}
|
||||
|
||||
func Utimes(p string, tm *time.Time) error {
|
||||
if tm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ts, err := unix.TimeToTimespec(*tm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timespec := []unix.Timespec{ts, ts}
|
||||
if err := unix.UtimesNanoAt(unix.AT_FDCWD, p, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return errors.Wrapf(err, "failed to utime %s", p)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Chown(p string, old *User, fn Chowner) error {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
user, err := fn(old)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if user != nil {
|
||||
if err := os.Lchown(p, user.UID, user.GID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
containerAdministratorSidString = "S-1-5-93-2-1"
|
||||
)
|
||||
|
||||
func fixRootDirectory(p string) string {
|
||||
if len(p) == len(`\\?\c:`) {
|
||||
if os.IsPathSeparator(p[0]) && os.IsPathSeparator(p[1]) && p[2] == '?' && os.IsPathSeparator(p[3]) && p[5] == ':' {
|
||||
return p + `\`
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func Utimes(p string, tm *time.Time) error {
|
||||
info, err := os.Lstat(p)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching file info")
|
||||
}
|
||||
if tm != nil && info.Mode()&os.ModeSymlink == 0 {
|
||||
if err := os.Chtimes(p, *tm, *tm); err != nil {
|
||||
return errors.Wrap(err, "changing times")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Chown(p string, old *User, fn Chowner) error {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
user, err := fn(old)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
var userSIDstring string
|
||||
if user != nil && user.SID != "" {
|
||||
userSIDstring = user.SID
|
||||
}
|
||||
if userSIDstring == "" {
|
||||
userSIDstring = containerAdministratorSidString
|
||||
|
||||
}
|
||||
|
||||
sidPtr, err := syscall.UTF16PtrFromString(userSIDstring)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "converting to utf16 ptr")
|
||||
}
|
||||
var userSID *windows.SID
|
||||
if err := windows.ConvertStringSidToSid(sidPtr, &userSID); err != nil {
|
||||
return errors.Wrap(err, "converting to windows SID")
|
||||
}
|
||||
var dacl *windows.ACL
|
||||
newEntries := []windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
AccessPermissions: windows.GENERIC_ALL,
|
||||
AccessMode: windows.GRANT_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(userSID),
|
||||
},
|
||||
},
|
||||
}
|
||||
newAcl, err := windows.ACLFromEntries(newEntries, dacl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding acls: %w", err)
|
||||
}
|
||||
|
||||
// Copy file ownership and ACL
|
||||
// We need SeRestorePrivilege and SeTakeOwnershipPrivilege in order
|
||||
// to restore security info on a file, especially if we're trying to
|
||||
// apply security info which includes SIDs not necessarily present on
|
||||
// the host.
|
||||
privileges := []string{winio.SeRestorePrivilege, seTakeOwnershipPrivilege}
|
||||
err = winio.RunWithPrivileges(privileges, func() error {
|
||||
if err := windows.SetNamedSecurityInfo(
|
||||
p, windows.SE_FILE_OBJECT,
|
||||
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
|
||||
userSID, nil, newAcl, nil); err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// +build darwin freebsd netbsd openbsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Returns the last-accessed time
|
||||
func StatAtime(st *syscall.Stat_t) syscall.Timespec {
|
||||
return st.Atimespec
|
||||
}
|
||||
|
||||
// Returns the last-modified time
|
||||
func StatMtime(st *syscall.Stat_t) syscall.Timespec {
|
||||
return st.Mtimespec
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//go:build dragonfly || linux || solaris
|
||||
// +build dragonfly linux solaris
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Returns the last-accessed time
|
||||
func StatAtime(st *syscall.Stat_t) syscall.Timespec {
|
||||
return st.Atim
|
||||
}
|
||||
|
||||
// Returns the last-modified time
|
||||
func StatMtime(st *syscall.Stat_t) syscall.Timespec {
|
||||
return st.Mtim
|
||||
}
|
|
@ -710,9 +710,13 @@ github.com/theupdateframework/notary/tuf/data
|
|||
github.com/theupdateframework/notary/tuf/signed
|
||||
github.com/theupdateframework/notary/tuf/utils
|
||||
github.com/theupdateframework/notary/tuf/validation
|
||||
# github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205
|
||||
## explicit; go 1.21
|
||||
github.com/tonistiigi/dchapes-mode
|
||||
# github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0
|
||||
## explicit; go 1.21
|
||||
github.com/tonistiigi/fsutil
|
||||
github.com/tonistiigi/fsutil/copy
|
||||
github.com/tonistiigi/fsutil/types
|
||||
# github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
|
||||
## explicit; go 1.16
|
||||
|
|
Loading…
Reference in New Issue