diff --git a/all_test.go b/all_test.go index 4dfa389..8f41734 100644 --- a/all_test.go +++ b/all_test.go @@ -73,6 +73,41 @@ func TestCopy(t *testing.T) { Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(0) }) + 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)) + + 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) + 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) + _, err = os.Stat("testdata.copy/case03.skip/case01") + Expect(t, os.IsNotExist(err)).ToBe(true) + + 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) + 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) + 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) { err := Copy("testdata/case03", "testdata.copy/case03") Expect(t, err).Not().ToBe(nil) diff --git a/copy.go b/copy.go index c9f14c5..2fdb3ea 100644 --- a/copy.go +++ b/copy.go @@ -14,24 +14,25 @@ const ( tmpPermissionForDirectory = os.FileMode(0755) ) -// Copy copies src to dest, doesn't matter if src is a directory or a file -func Copy(src, dest string) error { +// 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, 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, info os.FileInfo) error { +func copy(src, dest string, info os.FileInfo, opt Options) error { if info.Mode()&os.ModeSymlink != 0 { - return lcopy(src, dest, info) + return onsymlink(src, dest, info, opt) } if info.IsDir() { - return dcopy(src, dest, info) + return dcopy(src, dest, info, opt) } return fcopy(src, dest, info) } @@ -68,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, info os.FileInfo) (err error) { +func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) { originalMode := info.Mode() @@ -86,7 +87,7 @@ func dcopy(srcdir, destdir string, 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, content); err != nil { + if err := copy(cs, cd, content, opt); err != nil { // If any error, exit immediately return err } @@ -95,9 +96,35 @@ func dcopy(srcdir, destdir string, 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 diff --git a/options.go b/options.go new file mode 100644 index 0000000..4053a3f --- /dev/null +++ b/options.go @@ -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 + }, +}