Skip to content

Commit

Permalink
Add Move command
Browse files Browse the repository at this point in the history
This tries to replicate the mv command using mostly the same logic from
the existing shx.Copy command.

Signed-off-by: Carolyn Van Slyck <me@carolynvanslyck.com>
  • Loading branch information
carolynvs committed Jul 16, 2022
1 parent 9c6f785 commit dbd6e17
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 1 deletion.
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 := ioutil.TempDir("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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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 := ioutil.TempDir("", "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)
})
}
}

0 comments on commit dbd6e17

Please sign in to comment.