From a1d66191e56f93f98085c76979924ec4542684d0 Mon Sep 17 00:00:00 2001 From: Yaz Saito Date: Mon, 2 Mar 2020 22:31:09 -0800 Subject: [PATCH 1/2] Add an option to follow symlinks --- all_test.go | 26 +++++++++++++++++++++++++- copy.go | 33 +++++++++++++++++++++++++-------- testdata/case06/README.md | 1 + 3 files changed, 51 insertions(+), 9 deletions(-) create mode 120000 testdata/case06/README.md diff --git a/all_test.go b/all_test.go index 4dfa389..5232557 100644 --- a/all_test.go +++ b/all_test.go @@ -73,13 +73,37 @@ 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, "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 }}) + Expect(t, err).ToBe(nil) + Expect(t, err).ToBe(nil) + info, err := os.Lstat("testdata.copy/case06-2/README.md") + Expect(t, err).ToBe(nil) + Expect(t, int(info.Mode()&os.ModeSymlink)).ToBe(0) + }) + + 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") + Expect(t, err).ToBe(nil) + Expect(t, err).ToBe(nil) + info, err := os.Lstat("testdata.copy/case06/README.md") + Expect(t, err).ToBe(nil) + Expect(t, info.Mode()&os.ModeSymlink).ToBe(os.ModeSymlink) + os.RemoveAll("testdata.copy/case06") + }) + 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) }) When(t, "try to copy READ-not-allowed source", func(t *testing.T) { - err := Copy("testdata/case06", "testdata.copy/case06") + err := Copy("testdata/case07", "testdata.copy/case07") Expect(t, err).Not().ToBe(nil) }) diff --git a/copy.go b/copy.go index c9f14c5..bd339c4 100644 --- a/copy.go +++ b/copy.go @@ -14,24 +14,41 @@ 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 { +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] + } info, err := os.Lstat(src) if err != nil { return err } - return copy(src, dest, info) + return copy(src, dest, opt, info) } // 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 { - if info.Mode()&os.ModeSymlink != 0 { +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) } if info.IsDir() { - return dcopy(src, dest, info) + return dcopy(src, dest, opts, info) } return fcopy(src, dest, info) } @@ -68,7 +85,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, opts Opts, info os.FileInfo) (err error) { originalMode := info.Mode() @@ -86,7 +103,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, opts, content); err != nil { // If any error, exit immediately return err } diff --git a/testdata/case06/README.md b/testdata/case06/README.md new file mode 120000 index 0000000..ea72647 --- /dev/null +++ b/testdata/case06/README.md @@ -0,0 +1 @@ +../case05/README.md \ No newline at end of file From ec4d9694cc3f45ff52e0ed209dd08fa1d1311186 Mon Sep 17 00:00:00 2001 From: Hiromu OCHIAI Date: Thu, 5 Mar 2020 23:33:54 +0200 Subject: [PATCH 2/2] Add OnSymlink options: Shallow/Deep/Skip --- all_test.go | 41 ++++++++++++++++--------- copy.go | 64 ++++++++++++++++++++++----------------- options.go | 26 ++++++++++++++++ testdata/case06/README.md | 1 - 4 files changed, 89 insertions(+), 43 deletions(-) create mode 100644 options.go delete mode 120000 testdata/case06/README.md diff --git a/all_test.go b/all_test.go index 5232557..8f41734 100644 --- a/all_test.go +++ b/all_test.go @@ -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) { @@ -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) }) diff --git a/copy.go b/copy.go index bd339c4..2fdb3ea 100644 --- a/copy.go +++ b/copy.go @@ -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) } @@ -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() @@ -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 } @@ -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 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 + }, +} diff --git a/testdata/case06/README.md b/testdata/case06/README.md deleted file mode 120000 index ea72647..0000000 --- a/testdata/case06/README.md +++ /dev/null @@ -1 +0,0 @@ -../case05/README.md \ No newline at end of file