Skip to content

Commit

Permalink
Merge pull request #24 from carolynvs/move
Browse files Browse the repository at this point in the history
Add Move command
  • Loading branch information
carolynvs committed Jul 17, 2022
2 parents 614c2d9 + 74fa12f commit cae1473
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 := 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)
})
}
}

0 comments on commit cae1473

Please sign in to comment.