Repository: otiai10/copy
Pull request #101 (2023-04-03 11:30:16)

Let caller hander error cases
Please consider this.
This PR introduces `onErr` option in a form of `func(error)error` which lets caller to handler error cases .
It enables caller to log error by error and gives a possibility to continue with copying in case error is not that severe by returning nil (swallowing an error). 


Patch:
diff --git a/README.md b/README.md
index 32edfd8..dc66c4e 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,9 @@ type Options struct {
 	// OnDirExists can specify what to do when there is a directory already existing in destination.
 	OnDirExists func(src, dest string) DirExistsAction
 
+	// OnError can let users decide how to handle errors (e.g., you can suppress specific error).
+	OnError func(src, dest, string, err error) error
+
 	// Skip can specify which files should be skipped
 	Skip func(srcinfo os.FileInfo, src, dest string) (bool, error)
 
diff --git a/copy.go b/copy.go
index 60643dd..71ff844 100644
--- a/copy.go
+++ b/copy.go
@@ -15,20 +15,20 @@ type timespec struct {
 }
 
 // Copy copies src to dest, doesn't matter if src is a directory or a file.
-func Copy(src, dest string, opt ...Options) error {
+func Copy(src, dest string, opts ...Options) error {
+	opt := assureOptions(src, dest, opts...)
 	info, err := os.Lstat(src)
 	if err != nil {
-		return err
+		return onError(src, dest, err, opt)
 	}
-	return switchboard(src, dest, info, assureOptions(src, dest, opt...))
+	return switchboard(src, dest, info, opt)
 }
 
 // switchboard switches proper copy functions regarding file type, etc...
 // If there would be anything else here, add a case to this switchboard.
 func switchboard(src, dest string, info os.FileInfo, opt Options) (err error) {
-
 	if info.Mode()&os.ModeDevice != 0 && !opt.Specials {
-		return err
+		return onError(src, dest, err, opt)
 	}
 
 	switch {
@@ -42,7 +42,7 @@ func switchboard(src, dest string, info os.FileInfo, opt Options) (err error) {
 		err = fcopy(src, dest, info, opt)
 	}
 
-	return err
+	return onError(src, dest, err, opt)
 }
 
 // copyNextOrSkip decide if this src should be copied or not.
@@ -130,7 +130,6 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) {
 // with scanning contents inside the directory
 // and pass everything to "copy" recursively.
 func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) {
-
 	if skip, err := onDirExists(opt, srcdir, destdir); err != nil {
 		return err
 	} else if skip {
@@ -235,3 +234,13 @@ func fclose(f *os.File, reported *error) {
 		*reported = err
 	}
 }
+
+// onError lets caller to handle errors
+// occured when copying a file.
+func onError(src, dest string, err error, opt Options) error {
+	if opt.OnError == nil {
+		return err
+	}
+
+	return opt.OnError(src, dest, err)
+}
diff --git a/options.go b/options.go
index bd18d56..52d636c 100644
--- a/options.go
+++ b/options.go
@@ -14,6 +14,9 @@ type Options struct {
 	// OnDirExists can specify what to do when there is a directory already existing in destination.
 	OnDirExists func(src, dest string) DirExistsAction
 
+	// OnErr lets called decide whether or not to continue on particular copy error.
+	OnError func(src, dest string, err error) error
+
 	// Skip can specify which files should be skipped
 	Skip func(srcinfo os.FileInfo, src, dest string) (bool, error)
 
@@ -95,6 +98,7 @@ func getDefaultOptions(src, dest string) Options {
 			return Shallow // Do shallow copy
 		},
 		OnDirExists:       nil,                // Default behavior is "Merge".
+		OnError:           nil,                // Default is "accept error"
 		Skip:              nil,                // Do not skip anything
 		AddPermission:     0,                  // Add nothing
 		PermissionControl: PerservePermission, // Just preserve permission


Test patch:
diff --git a/all_test.go b/all_test.go
index 910570a..a370e44 100644
--- a/all_test.go
+++ b/all_test.go
@@ -362,6 +362,46 @@ func TestOptions_CopyRateLimit(t *testing.T) {
 	Expect(t, elapsed > 5*time.Second).ToBe(true)
 }
 
+func TestOptions_OnFileError(t *testing.T) {
+	opt := Options{
+		OnError: nil,
+	}
+
+	// existing, process nromally
+	err := Copy("test/data/case17", "test/data.copy/case17", opt)
+	Expect(t, err).ToBe(nil)
+
+	// not existing, process err
+	err = Copy("test/data/case17/non-existing", "test/data.copy/case17/non-existing", opt)
+	Expect(t, os.IsNotExist(err)).ToBe(true)
+
+	_, err = os.Stat("test/data.copy/case17/non-existing")
+	Expect(t, os.IsNotExist(err)).ToBe(true)
+
+	// existing, nil err not passed
+	opt.OnError = func(_, _ string, err error) error {
+		return err
+	}
+	err = Copy("test/data/case17", "test/data.copy/case17", opt)
+	Expect(t, err).ToBe(nil)
+
+	// not existing, process err
+	opt.OnError = func(_, _ string, err error) error { return err }
+	err = Copy("test/data/case17/non-existing", "test/data.copy/case17/non-existing", opt)
+	Expect(t, os.IsNotExist(err)).ToBe(true)
+
+	_, err = os.Stat("test/data.copy/case17/non-existing")
+	Expect(t, os.IsNotExist(err)).ToBe(true)
+
+	// not existing, ignore err
+	opt.OnError = func(_, _ string, err error) error { return nil }
+	err = Copy("test/data/case17/non-existing", "test/data.copy/case17/non-existing", opt)
+	Expect(t, err).ToBe(nil)
+
+	_, err = os.Stat("test/data.copy/case17/non-existing")
+	Expect(t, os.IsNotExist(err)).ToBe(true)
+}
+
 type SleepyReader struct {
 	src *os.File
 	sec time.Duration
diff --git a/test/data/case17/README.md b/test/data/case17/README.md
new file mode 100644
index 0000000..bfce975
--- /dev/null
+++ b/test/data/case17/README.md
@@ -0,0 +1,5 @@
+So if you wanted to ignore error you should add something like this:
+```go
+opt.OnError = func(src, dst string, _ error) error { return nil } 
+```
+The default value is nil and accepts raised error.
\ No newline at end of file