Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Move command #24

Merged
merged 2 commits into from
Jul 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions magefile.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build mage
// +build mage

package main
Expand Down
2 changes: 1 addition & 1 deletion shx/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
type CopyOption int

const (
// CopyNoOverwrite does not overwrite existing files in the destination
CopyDefault CopyOption = iota
// CopyNoOverwrite does not overwrite existing files in the destination
CopyNoOverwrite
CopyRecursive
)
Expand Down
2 changes: 2 additions & 0 deletions shx/copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ func TestCopy_CopyNoOverwrite(t *testing.T) {
}

func assertFile(t *testing.T, f string) {
t.Helper()

gotContents, err := ioutil.ReadFile(f)
require.NoErrorf(t, err, "could not read file %s", f)

Expand Down
97 changes: 97 additions & 0 deletions shx/move.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package shx

import (
"fmt"
"log"
"os"
"path/filepath"
)

type MoveOption int

const (
MoveDefault MoveOption = iota
// MoveNoOverwrite does not overwrite existing files in the destination
MoveNoOverwrite
MoveRecursive
)

// Move a file or directory with the specified set of MoveOption.
// The source may use globbing, which is resolved with filepath.Glob.
func Move(src string, dest string, opts ...MoveOption) error {
items, err := filepath.Glob(src)
if err != nil {
return err
}

if len(items) == 0 {
return fmt.Errorf("no such file or directory '%s'", src)
}

var combinedOpts MoveOption
for _, opt := range opts {
combinedOpts |= opt
}

// Check if the destination exists, e.g. if we are moving to /tmp/foo, /tmp should already exist
if _, err := os.Stat(filepath.Dir(dest)); err != nil {
return err
}

for _, item := range items {
err := moveFileOrDirectory(item, dest, combinedOpts)
if err != nil {
return err
}
}

return nil
}

func moveFileOrDirectory(src string, dest string, opts MoveOption) error {
// If the destination is a directory that exists,
// move into the directory.
destInfo, err := os.Stat(dest)
if err == nil && destInfo.IsDir() {
dest = filepath.Join(dest, filepath.Base(src))
}

return move(src, dest, opts)
}

func move(src string, dest string, opts MoveOption) error {
destExists := true
destInfo, err := os.Stat(dest)
if err != nil {
if os.IsNotExist(err) {
destExists = false
} else {
return err
}
}

overwrite := opts&MoveNoOverwrite != MoveNoOverwrite
if destExists {
if overwrite {
// Do not try to rename a file to an existing directory (mimics mv behavior)
if destInfo.IsDir() {
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if !srcInfo.IsDir() {
return fmt.Errorf("rename %s to %s: not a directory", src, dest)
}
}

os.RemoveAll(dest)
} else {
// Do not overwrite, skip
log.Printf("%s not overwritten\n", dest)
return nil
}
}

log.Printf("%s -> %s\n", src, dest)
return os.Rename(src, dest)
}
18 changes: 18 additions & 0 deletions shx/move_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package shx_test

import "github.com/carolynvs/magex/shx"

func ExampleMove() {
// Move a file from the current directory into TEMP
shx.Move("a.txt", "/tmp")

// Move matching files in the current directory into TEMP
shx.Move("*.txt", "/tmp")

// Overwrite a file
shx.Move("/tmp/a.txt", "/tmp/b.txt")

// Move the contents of a directory into TEMP
// Do not overwrite existing files
shx.Move("a/*", "/tmp", shx.MoveNoOverwrite)
}
205 changes: 205 additions & 0 deletions shx/move_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package shx

import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func resetTestdata(t *testing.T) {
err := exec.Command("git", "checkout", "testdata").Run()
require.NoError(t, err, "error resetting the testdata directory")
}

func TestMove(t *testing.T) {
t.Run("recursively move directory into empty dest dir", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/a", tmp, MoveRecursive)
require.NoError(t, err, "Move into empty directory failed")

assert.DirExists(t, filepath.Join(tmp, "a"))
assertFile(t, filepath.Join(tmp, "a/a1.txt"))
assertFile(t, filepath.Join(tmp, "a/a2.txt"))
assert.DirExists(t, filepath.Join(tmp, "a/ab"))
assertFile(t, filepath.Join(tmp, "a/ab/ab1.txt"))
assertFile(t, filepath.Join(tmp, "a/ab/ab2.txt"))
})

t.Run("recursively move directory into populated dest dir", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

require.NoError(t, os.MkdirAll(filepath.Join(tmp, "a"), 0700))
require.NoError(t, os.WriteFile(filepath.Join(tmp, "a/a1.txt"), []byte("a lot of extra data that should be overwritten"), 0600))

err = Move("testdata/copy/a", tmp, MoveRecursive)
require.NoError(t, err, "Move into directory with same directory name")

assert.DirExists(t, filepath.Join(tmp, "a"))
assertFile(t, filepath.Join(tmp, "a/a1.txt"))
assertFile(t, filepath.Join(tmp, "a/a2.txt"))
assert.DirExists(t, filepath.Join(tmp, "a/ab"))
assertFile(t, filepath.Join(tmp, "a/ab/ab1.txt"))
assertFile(t, filepath.Join(tmp, "a/ab/ab2.txt"))
})

t.Run("move glob", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/a/*.txt", tmp)
require.NoError(t, err, "Move into empty directory failed")

assertFile(t, filepath.Join(tmp, "a1.txt"))
assertFile(t, filepath.Join(tmp, "a2.txt"))
assert.NoDirExists(t, filepath.Join(tmp, "a"))
assert.NoDirExists(t, filepath.Join(tmp, "ab"))
})

t.Run("missing parent dir", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/a", filepath.Join(tmp, "missing-parent/dir"))
require.Error(t, err)
})

t.Run("missing src", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/missing-src", tmp)
require.Error(t, err)
require.Contains(t, err.Error(), "no such file or directory")
})

t.Run("recursively move directory to new name", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

dest := filepath.Join(tmp, "dest")
err = Move("testdata/copy/a", dest, MoveRecursive)
require.NoError(t, err, "Move into empty directory failed")

assert.DirExists(t, dest)
assertFile(t, filepath.Join(dest, "a1.txt"))
assertFile(t, filepath.Join(dest, "a2.txt"))
assert.DirExists(t, filepath.Join(dest, "ab"))
assertFile(t, filepath.Join(dest, "ab/ab1.txt"))
assertFile(t, filepath.Join(dest, "ab/ab2.txt"))
})

t.Run("recursively merge dest dir", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/partial-dest", tmp, MoveRecursive)
require.NoError(t, err, "Move partial destination failed")

err = Move("testdata/copy/a", tmp, MoveRecursive)
require.NoError(t, err, "Merge into non-empty destination failed")

assert.DirExists(t, filepath.Join(tmp, "a"))
assertFile(t, filepath.Join(tmp, "a/a1.txt"))
assertFile(t, filepath.Join(tmp, "a/a2.txt"))
assert.DirExists(t, filepath.Join(tmp, "a/ab"))
assertFile(t, filepath.Join(tmp, "a/ab/ab1.txt"))
assertFile(t, filepath.Join(tmp, "a/ab/ab2.txt"))
})

t.Run("move file into empty directory", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/a/a1.txt", tmp)
require.NoError(t, err, "Move file failed")

assertFile(t, filepath.Join(tmp, "a1.txt"))
})

t.Run("overwrite directory should fail", func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/directory-conflict/a", tmp, MoveRecursive)
require.NoError(t, err, "Setup failed")

err = Move("testdata/copy/a/*", filepath.Join(tmp, "a"))
require.Error(t, err, "Overwrite directory should have failed")
})
}

func TestMove_MoveNoOverwrite(t *testing.T) {
testcases := []struct {
name string
opts MoveOption
wantContents string
}{
{name: "overwrite", opts: MoveDefault, wantContents: "a2.txt"},
{name: "no overwrite", opts: MoveNoOverwrite, wantContents: "a1.txt"},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
defer resetTestdata(t)

// Make the temp directory on the same physical drive, os.Rename doesn't work across drives and /tmp may be on another drive
tmp, err := os.MkdirTemp("testdata", "magex")
require.NoError(t, err, "could not create temp directory for test")
defer os.RemoveAll(tmp)

err = Move("testdata/copy/a/a1.txt", tmp)
require.NoError(t, err, "Move a1.txt failed")

err = Move("testdata/copy/a/a2.txt", filepath.Join(tmp, "a1.txt"), tc.opts)
require.NoError(t, err, "Overwrite failed")

gotContents, err := ioutil.ReadFile(filepath.Join(tmp, "a1.txt"))
require.NoError(t, err, "could not read file")
assert.Equal(t, tc.wantContents, string(gotContents), "invalid contents, want: %s, got: %s", tc.wantContents, gotContents)
})
}
}