Repository: otiai10/copy (go)

Issue:
Add support for embed directories
It would be super useful if this package could copy embed directories. 

For example:

Given the following fs tree:

```
/
  /assets
    /foo
       foo.txt
       bar.txt
    hello.txt
    goodbye.txt
  main.go
```

It could use the embed FS to use the copy function.

```golang
//go:embed assets
var assets embed.FS

func foo() error {
  file, err := assets.Open("assets")
  if err != nil {
    return err
  }

  err := Copy(file, "destination")
  if err != nil {
    return err
  }
  return nil
}
```

Or maybe use the Option structure 

```golang
opt := Options{
	FS: assets,
}
err := Copy("assets/foo", "new/destination", opt)
```

Pull request description:
Support embed.FS (fs.FS)
Fix #71 

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 919cdf4..8413309 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -14,7 +14,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        go: ['1.15', '1.16', '1.17', '1.18', '1.19', '1.20']
+        go: ['1.18', '1.19', '1.20']
     steps:
 
     - name: Set up Go
diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml
index 5663666..59c61f9 100644
--- a/.github/workflows/wasm.yml
+++ b/.github/workflows/wasm.yml
@@ -14,7 +14,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        go: ['1.17', '1.18', '1.19', '1.20']
+        go: ['1.18', '1.19', '1.20']
     steps:
 
     - name: Set up Go
diff --git a/README.md b/README.md
index dc66c4e..1cc8fc8 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,15 @@ type Options struct {
 	// If zero, the internal default buffer of 32KB is used.
 	// See https://golang.org/pkg/io/#CopyBuffer for more information.
 	CopyBufferSize uint
+
+	// If you want to add some limitation on reading src file,
+	// you can wrap the src and provide new reader,
+	// such as `RateLimitReader` in the test case.
+	WrapReader func(src io.Reader) io.Reader
+
+	// If given, copy.Copy refers to this fs.FS instead of the OS filesystem.
+	// e.g., You can use embed.FS to copy files from embedded filesystem.
+	FS fs.FS
 }
 ```
 
diff --git a/copy.go b/copy.go
index a84ff7b..085db78 100644
--- a/copy.go
+++ b/copy.go
@@ -2,6 +2,7 @@ package copy
 
 import (
 	"io"
+	"io/fs"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -17,6 +18,13 @@ type timespec struct {
 // Copy copies src to dest, doesn't matter if src is a directory or a file.
 func Copy(src, dest string, opts ...Options) error {
 	opt := assureOptions(src, dest, opts...)
+	if opt.FS != nil {
+		info, err := fs.Stat(opt.FS, src)
+		if err != nil {
+			return onError(src, dest, err, opt)
+		}
+		return switchboard(src, dest, info, opt)
+	}
 	info, err := os.Lstat(src)
 	if err != nil {
 		return onError(src, dest, err, opt)
@@ -65,14 +73,20 @@ func copyNextOrSkip(src, dest string, info os.FileInfo, opt Options) error {
 // with considering existence of parent directory
 // and file permission.
 func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) {
-	s, err := os.Open(src)
+
+	var readcloser io.ReadCloser
+	if opt.FS != nil {
+		readcloser, err = opt.FS.Open(src)
+	} else {
+		readcloser, err = os.Open(src)
+	}
 	if err != nil {
 		if os.IsNotExist(err) {
 			return nil
 		}
 		return
 	}
-	defer fclose(s, &err)
+	defer fclose(readcloser, &err)
 
 	if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
 		return
@@ -92,10 +106,10 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) {
 
 	var buf []byte = nil
 	var w io.Writer = f
-	var r io.Reader = s
+	var r io.Reader = readcloser
 
 	if opt.WrapReader != nil {
-		r = opt.WrapReader(s)
+		r = opt.WrapReader(r)
 	}
 
 	if opt.CopyBufferSize != 0 {
@@ -145,7 +159,23 @@ func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) {
 	}
 	defer chmodfunc(&err)
 
-	contents, err := ioutil.ReadDir(srcdir)
+	var contents []os.FileInfo
+	if opt.FS != nil {
+		entries, err := fs.ReadDir(opt.FS, srcdir)
+		if err != nil {
+			return err
+		}
+		for _, e := range entries {
+			info, err := e.Info()
+			if err != nil {
+				return err
+			}
+			contents = append(contents, info)
+		}
+	} else {
+		contents, err = ioutil.ReadDir(srcdir)
+	}
+
 	if err != nil {
 		if os.IsNotExist(err) {
 			return nil
@@ -237,7 +267,7 @@ func lcopy(src, dest string) error {
 // fclose ANYHOW closes file,
 // with asiging error raised during Close,
 // BUT respecting the error already reported.
-func fclose(f *os.File, reported *error) {
+func fclose(f io.Closer, reported *error) {
 	if err := f.Close(); *reported == nil {
 		*reported = err
 	}
diff --git a/options.go b/options.go
index 52d636c..1b4e508 100644
--- a/options.go
+++ b/options.go
@@ -2,6 +2,7 @@ package copy
 
 import (
 	"io"
+	"io/fs"
 	"os"
 )
 
@@ -58,7 +59,11 @@ type Options struct {
 	// If you want to add some limitation on reading src file,
 	// you can wrap the src and provide new reader,
 	// such as `RateLimitReader` in the test case.
-	WrapReader func(src *os.File) io.Reader
+	WrapReader func(src io.Reader) io.Reader
+
+	// If given, copy.Copy refers to this fs.FS instead of the OS filesystem.
+	// e.g., You can use embed.FS to copy files from embedded filesystem.
+	FS fs.FS
 
 	intent struct {
 		src  string


Test patch:
diff --git a/.github/workflows/vagrant-test.yml b/.github/workflows/vagrant-test.yml
index 408bd54..c36cad9 100644
--- a/.github/workflows/vagrant-test.yml
+++ b/.github/workflows/vagrant-test.yml
@@ -8,7 +8,7 @@ on:
 
 jobs:
   vagrant-test:
-    runs-on: macos-10.15
+    runs-on: macos-12
     strategy:
       matrix:
         runtime:
diff --git a/all_test.go b/all_test.go
index 3a24744..fbd12e5 100644
--- a/all_test.go
+++ b/all_test.go
@@ -1,6 +1,7 @@
 package copy
 
 import (
+	"embed"
 	"errors"
 	"io"
 	"io/ioutil"
@@ -14,6 +15,9 @@ import (
 	. "github.com/otiai10/mint"
 )
 
+//go:embed test/data/case18/assets
+var assets embed.FS
+
 func TestMain(m *testing.M) {
 	setup(m)
 	code := m.Run()
@@ -26,6 +30,8 @@ func teardown(m *testing.M) {
 	os.RemoveAll("test/data.copy")
 	os.RemoveAll("test/data.copyTime")
 	os.RemoveAll("test/owned-by-root") // Do not check the error ;)
+	Copy("test/data/case18/assets.backup", "test/data/case18/assets")
+	os.RemoveAll("test/data/case18/assets.backup")
 }
 
 func TestCopy(t *testing.T) {
@@ -394,7 +400,7 @@ func TestOptions_CopyRateLimit(t *testing.T) {
 		return
 	}
 
-	opt := Options{WrapReader: func(src *os.File) io.Reader {
+	opt := Options{WrapReader: func(src io.Reader) io.Reader {
 		return &SleepyReader{src, 1}
 	}}
 
@@ -445,8 +451,17 @@ func TestOptions_OnFileError(t *testing.T) {
 	Expect(t, os.IsNotExist(err)).ToBe(true)
 }
 
+func TestOptions_FS(t *testing.T) {
+	os.RemoveAll("test/data/case18/assets")
+	err := Copy("test/data/case18/assets", "test/data.copy/case18/assets", Options{
+		FS:                assets,
+		PermissionControl: AddPermission(200), // FIXME
+	})
+	Expect(t, err).ToBe(nil)
+}
+
 type SleepyReader struct {
-	src *os.File
+	src io.Reader
 	sec time.Duration
 }
 
diff --git a/test/data/case18/assets/README.md b/test/data/case18/assets/README.md
new file mode 100644
index 0000000..5570c7f
--- /dev/null
+++ b/test/data/case18/assets/README.md
@@ -0,0 +1,1 @@
+# Hello
\ No newline at end of file
diff --git a/test_setup.go b/test_setup.go
index f3c2d83..64a5292 100644
--- a/test_setup.go
+++ b/test_setup.go
@@ -16,4 +16,5 @@ func setup(m *testing.M) {
 	os.Chmod("test/data/case07/dir_0555", 0o555)
 	os.Chmod("test/data/case07/file_0444", 0o444)
 	syscall.Mkfifo("test/data/case11/foo/bar", 0o555)
+	Copy("test/data/case18/assets", "test/data/case18/assets.backup")
 }
diff --git a/test_setup_x.go b/test_setup_x.go
index fc56b73..4c35b14 100644
--- a/test_setup_x.go
+++ b/test_setup_x.go
@@ -9,6 +9,7 @@ import (
 )
 
 func setup(m *testing.M) {
+	os.RemoveAll("test/data.copy")
 	os.MkdirAll("test/data.copy", os.ModePerm)
 	os.Symlink("test/data/case01", "test/data/case03/case01")
 	os.Chmod("test/data/case07/dir_0555", 0555)