diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a6b05d2..39f75cd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,6 +5,7 @@ on: branches: [ main, develop ] pull_request: branches: [ main, develop ] + workflow_dispatch: jobs: diff --git a/copy.go b/copy.go index f9787cd..b938adc 100644 --- a/copy.go +++ b/copy.go @@ -86,66 +86,38 @@ 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) { - - 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 - } + if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { return } - defer fclose(readcloser, &err) - if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { - return + // Use FileCopyMethod to do copy. + err, skipFile := opt.FileCopyMethod.fcopy(src, dest, info, opt) + if skipFile { + return nil } - f, err := os.Create(dest) if err != nil { - return + return err } - defer fclose(f, &err) + // Change file permissions. chmodfunc, err := opt.PermissionControl(info, dest) if err != nil { return err } - chmodfunc(&err) - - var buf []byte = nil - var w io.Writer = f - var r io.Reader = readcloser - - if opt.WrapReader != nil { - r = opt.WrapReader(r) - } - - if opt.CopyBufferSize != 0 { - buf = make([]byte, opt.CopyBufferSize) - // Disable using `ReadFrom` by io.CopyBuffer. - // See https://github.com/otiai10/copy/pull/60#discussion_r627320811 for more details. - w = struct{ io.Writer }{f} - // r = struct{ io.Reader }{s} - } - if _, err = io.CopyBuffer(w, r, buf); err != nil { + chmodfunc(&err) + if err != nil { return err } - if opt.Sync { - err = f.Sync() - } - + // Preserve file ownership and times. if opt.PreserveOwner { if err := preserveOwner(src, dest, info); err != nil { return err } } + if opt.PreserveTimes { if err := preserveTimes(info, dest); err != nil { return err diff --git a/copy_methods.go b/copy_methods.go new file mode 100644 index 0000000..7245fe4 --- /dev/null +++ b/copy_methods.go @@ -0,0 +1,65 @@ +package copy + +import ( + "errors" + "io" + "os" +) + +// ErrUnsupportedCopyMethod is returned when the FileCopyMethod specified in +// Options is not supported. +var ErrUnsupportedCopyMethod = errors.New( + "copy method not supported", +) + +// CopyBytes copies the file contents by reading the source file into a buffer, +// then writing the buffer back to the destination file. +var CopyBytes = FileCopyMethod{ + fcopy: func(src, dest string, info os.FileInfo, opt Options) (err error, skipFile bool) { + 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, true + } + return + } + defer fclose(readcloser, &err) + + f, err := os.Create(dest) + if err != nil { + return + } + defer fclose(f, &err) + + var buf []byte = nil + var w io.Writer = f + var r io.Reader = readcloser + + if opt.WrapReader != nil { + r = opt.WrapReader(r) + } + + if opt.CopyBufferSize != 0 { + buf = make([]byte, opt.CopyBufferSize) + // Disable using `ReadFrom` by io.CopyBuffer. + // See https://github.com/otiai10/copy/pull/60#discussion_r627320811 for more details. + w = struct{ io.Writer }{f} + // r = struct{ io.Reader }{s} + } + + if _, err = io.CopyBuffer(w, r, buf); err != nil { + return err, false + } + + if opt.Sync { + err = f.Sync() + } + + return + }, +} diff --git a/options.go b/options.go index c1db48c..709c7f4 100644 --- a/options.go +++ b/options.go @@ -27,6 +27,16 @@ type Options struct { // RenameDestination can specify the destination file or dir name if needed to rename. RenameDestination func(src, dest string) (string, error) + // FileCopyMethod specifies the method by which a regular file is copied. + // The default is CopyBytes. + // + // Available implementations: + // - CopyBytes (best compatibility) + // + // Some implementations may not be supported on the target GOOS, or on + // the user's filesystem. When these fail, an error will be returned. + FileCopyMethod FileCopyMethod + // Specials includes special files to be copied. default false. Specials bool @@ -119,6 +129,11 @@ const ( Untouchable ) +// FileCopyMethod represents one of the ways that a regular file can be copied. +type FileCopyMethod struct { + fcopy func(src, dest string, info os.FileInfo, opt Options) (err error, skipFile bool) +} + // getDefaultOptions provides default options, // which would be modified by usage-side. func getDefaultOptions(src, dest string) Options { @@ -134,6 +149,7 @@ func getDefaultOptions(src, dest string) Options { Sync: false, // Do not sync Specials: false, // Do not copy special files PreserveTimes: false, // Do not preserve the modification time + FileCopyMethod: CopyBytes, // Copy by bytes CopyBufferSize: 0, // Do not specify, use default bufsize (32*1024) WrapReader: nil, // Do not wrap src files, use them as they are. intent: intent{src, dest, nil, nil}, @@ -158,6 +174,9 @@ func assureOptions(src, dest string, opts ...Options) Options { } else if opts[0].PermissionControl == nil { opts[0].PermissionControl = PerservePermission } + if opts[0].FileCopyMethod.fcopy == nil { + opts[0].FileCopyMethod = defopt.FileCopyMethod + } opts[0].intent.src = defopt.intent.src opts[0].intent.dest = defopt.intent.dest return opts[0]