Skip to content

Commit

Permalink
Add OnSymlink options: Shallow/Deep/Skip
Browse files Browse the repository at this point in the history
  • Loading branch information
otiai10 committed Mar 5, 2020
1 parent a1d6619 commit ec4d969
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 43 deletions.
41 changes: 26 additions & 15 deletions all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,39 @@ func TestCopy(t *testing.T) {
Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(0)
})

When(t, "source directory includes a dangling symbolic link, but with option to follow it", func(t *testing.T) {
err := Copy("testdata/case03", "testdata.copy/case03", Opts{FollowSymlink: func(string) bool { return true }})
Expect(t, err).Not().ToBe(nil)
})
When(t, "symlink with Opt.OnSymlink provided", func(t *testing.T) {
opt := Options{OnSymlink: func(string) SymlinkAction { return Deep }}
err := Copy("testdata/case03", "testdata.copy/case03.deep", opt)
Expect(t, err).ToBe(nil)
info, err := os.Lstat("testdata.copy/case03.deep/case01")
Expect(t, err).ToBe(nil)
Expect(t, info.Mode()&os.ModeSymlink).ToBe(os.FileMode(0))

When(t, "source directory includes a symbolic link, but with option not to follow it", func(t *testing.T) {
err := Copy("testdata/case06", "testdata.copy/case06-2", Opts{FollowSymlink: func(string) bool { return true }})
opt = Options{OnSymlink: func(string) SymlinkAction { return Shallow }}
err = Copy("testdata/case03", "testdata.copy/case03.shallow", opt)
Expect(t, err).ToBe(nil)
info, err = os.Lstat("testdata.copy/case03.shallow/case01")
Expect(t, err).ToBe(nil)
info, err := os.Lstat("testdata.copy/case06-2/README.md")
Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))

opt = Options{OnSymlink: func(string) SymlinkAction { return Skip }}
err = Copy("testdata/case03", "testdata.copy/case03.skip", opt)
Expect(t, err).ToBe(nil)
Expect(t, int(info.Mode()&os.ModeSymlink)).ToBe(0)
})
_, err = os.Stat("testdata.copy/case03.skip/case01")
Expect(t, os.IsNotExist(err)).ToBe(true)

When(t, "source directory includes a symbolic link, but with option to follow it", func(t *testing.T) {
err := Copy("testdata/case06", "testdata.copy/case06")
err = Copy("testdata/case03", "testdata.copy/case03.default")
Expect(t, err).ToBe(nil)
info, err = os.Lstat("testdata.copy/case03.default/case01")
Expect(t, err).ToBe(nil)
info, err := os.Lstat("testdata.copy/case06/README.md")
Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))

opt = Options{OnSymlink: nil}
err = Copy("testdata/case03", "testdata.copy/case03.not-specified", opt)
Expect(t, err).ToBe(nil)
Expect(t, info.Mode()&os.ModeSymlink).ToBe(os.ModeSymlink)
os.RemoveAll("testdata.copy/case06")
info, err = os.Lstat("testdata.copy/case03.not-specified/case01")
Expect(t, err).ToBe(nil)
Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))
})

When(t, "try to copy to an existing path", func(t *testing.T) {
Expand All @@ -103,7 +114,7 @@ func TestCopy(t *testing.T) {
})

When(t, "try to copy READ-not-allowed source", func(t *testing.T) {
err := Copy("testdata/case07", "testdata.copy/case07")
err := Copy("testdata/case06", "testdata.copy/case06")
Expect(t, err).Not().ToBe(nil)
})

Expand Down
64 changes: 37 additions & 27 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,25 @@ const (
tmpPermissionForDirectory = os.FileMode(0755)
)

type Opts struct {
// FollowSymlink is called with a source path it is found to be a symlink. If
// this function returns false, Copy copies the symlink itself. Else, Copy
// follows the link. If this field is not set, Copy never follows symlinks.
FollowSymlink func(path string) bool
}

// Copy copies src to dest, doesn't matter if src is a directory or a file. An
// optional arg opts specifies the options to the copy operations. There can be
// at most one opts.
func Copy(src, dest string, opts ...Opts) error {
var opt Opts
if len(opts) > 0 {
if len(opts) > 1 {
panic("too many opts")
}
opt = opts[0]
}
// Copy copies src to dest, doesn't matter if src is a directory or a file.
func Copy(src, dest string, opt ...Options) error {
opt = append(opt, DefaultOptions)
info, err := os.Lstat(src)
if err != nil {
return err
}
return copy(src, dest, opt, info)
return copy(src, dest, info, opt[0])
}

// copy dispatches copy-funcs according to the mode.
// Because this "copy" could be called recursively,
// "info" MUST be given here, NOT nil.
func copy(src, dest string, opts Opts, info os.FileInfo) error {
if info.Mode()&os.ModeSymlink != 0 &&
(opts.FollowSymlink == nil || !opts.FollowSymlink(src)) {
return lcopy(src, dest, info)
func copy(src, dest string, info os.FileInfo, opt Options) error {
if info.Mode()&os.ModeSymlink != 0 {
return onsymlink(src, dest, info, opt)
}
if info.IsDir() {
return dcopy(src, dest, opts, info)
return dcopy(src, dest, info, opt)
}
return fcopy(src, dest, info)
}
Expand Down Expand Up @@ -85,7 +69,7 @@ func fcopy(src, dest string, info os.FileInfo) (err error) {
// dcopy is for a directory,
// with scanning contents inside the directory
// and pass everything to "copy" recursively.
func dcopy(srcdir, destdir string, opts Opts, info os.FileInfo) (err error) {
func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) {

originalMode := info.Mode()

Expand All @@ -103,7 +87,7 @@ func dcopy(srcdir, destdir string, opts Opts, info os.FileInfo) (err error) {

for _, content := range contents {
cs, cd := filepath.Join(srcdir, content.Name()), filepath.Join(destdir, content.Name())
if err := copy(cs, cd, opts, content); err != nil {
if err := copy(cs, cd, content, opt); err != nil {
// If any error, exit immediately
return err
}
Expand All @@ -112,9 +96,35 @@ func dcopy(srcdir, destdir string, opts Opts, info os.FileInfo) (err error) {
return nil
}

func onsymlink(src, dest string, info os.FileInfo, opt Options) error {

if opt.OnSymlink == nil {
opt.OnSymlink = DefaultOptions.OnSymlink
}

switch opt.OnSymlink(src) {
case Shallow:
return lcopy(src, dest)
case Deep:
orig, err := os.Readlink(src)
if err != nil {
return err
}
info, err = os.Lstat(orig)
if err != nil {
return err
}
return copy(orig, dest, info, opt)
case Skip:
fallthrough
default:
return nil // do nothing
}
}

// lcopy is for a symlink,
// with just creating a new symlink by replicating src symlink.
func lcopy(src, dest string, info os.FileInfo) error {
func lcopy(src, dest string) error {
src, err := os.Readlink(src)
if err != nil {
return err
Expand Down
26 changes: 26 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package copy

// Options specifies optional actions on copying.
type Options struct {
// OnSymlink can specify what to do on symlink
OnSymlink func(p string) SymlinkAction
}

// SymlinkAction represents what to do on symlink.
type SymlinkAction int

const (
// Deep creates hard-copy of contents.
Deep SymlinkAction = iota
// Shallow creates new symlink to the dest of symlink.
Shallow
// Skip does nothing with symlink.
Skip
)

// DefaultOptions by default.
var DefaultOptions = Options{
OnSymlink: func(string) SymlinkAction {
return Shallow
},
}
1 change: 0 additions & 1 deletion testdata/case06/README.md

This file was deleted.

0 comments on commit ec4d969

Please sign in to comment.