Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add FileCopyMethod option / API #164

Merged
merged 3 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:

jobs:

Expand Down
50 changes: 11 additions & 39 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions copy_methods.go
Original file line number Diff line number Diff line change
@@ -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 link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the case when we use it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the user can tell if the copy failed because of no support, instead of another error like permission:

err := copy.Copy("/from", "/to", copy.Options {
    FileCopyMethod: copy.ReflinkCopy,
})

if errors.Is(err, copy.ErrUnsupportedCopyMethod) {
    // Retry with other method
    err = copy.Copy("/from", "/to", copy.Options {
        FileCopyMethod: copy.CopyBytes,
    })
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick question: Do we use this error in this current version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. It's used in a later pull request I made

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, got it. In this case I'll merge it.

Very personally speaking, I would not like to mix what we don't use at that moment. Similar idea to YAGNI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove it and add it back in the other PR, if that's preferred?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need. Thanks for your consideration. I understand why it's here and we don't have any doubt we will need it ;)

"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
},
}
19 changes: 19 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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},
Expand All @@ -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]
Expand Down
Loading