-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
6 changed files
with
324 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
//go:build mage | ||
// +build mage | ||
|
||
package main | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |