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

v1.4.0 #3

Merged
merged 4 commits into from
Dec 11, 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
45 changes: 36 additions & 9 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,50 @@ package copy

import (
"context"
"errors"
"fmt"
"hash"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strings"
)

// Copy copies source file/folder to destination with given options.
func Copy(ctx context.Context, src, dst string, opts ...optFunc) error {
func Copy(ctx context.Context, src, dst string, opts ...optFunc) (err error) {
opt := defaultOptions()
for _, fn := range opts {
fn(opt)
}

src, err := resolvePath(src)
if err != nil {
return err
if opt.follow {
src, err = resolvePath(src)
if err != nil {
return err
}
}

if excludePath(opt.exclude, src) {
return nil
}

// Attempt to rename file/folder instead of copying and then removing.
// If call to rename was finished with an error, it will be ignored
// and copy algorithm will be used.
// and copy algorithm will be used. In case of renaming a folder,
// hash is not calculated and will be empty.
if opt.move {
if err := rename(src, dst, opt.hash); err == nil {
return os.RemoveAll(src)
}

// Reset hash if rename was unsuccessful, since it will be
// recalculated with copy if needed.
opt.hash.Reset()
if opt.hash != nil {
// Reset hash if rename was unsuccessful, since it will be
// recalculated with copy if needed.
opt.hash.Reset()
}
}

return copy(ctx, src, dst, opt)
Expand Down Expand Up @@ -118,6 +129,10 @@ func copyFolder(ctx context.Context, src, dst string, opt *options) error {
return err
}

if excludePath(opt.exclude, root) {
return nil
}

subDst := strings.ReplaceAll(root, src, dst)
if info.IsDir() {
select {
Expand Down Expand Up @@ -206,7 +221,7 @@ func copyBytes(ctx context.Context, r io.Reader, w io.Writer, size int, h hash.H

for {
b, err := srcReader.Read(buf)
if err != nil && err != io.EOF {
if err != nil && !errors.Is(err, io.EOF) {
return err
}

Expand Down Expand Up @@ -261,3 +276,15 @@ func (w *writerWithContext) Write(b []byte) (int, error) {

return w.w.Write(b)
}

// excludePath checks if given path should be excluded.
func excludePath(exclude []string, p string) bool {
if exclude != nil {
return false
}

return slices.ContainsFunc(
exclude,
func(s string) bool { return strings.Contains(p, s) },
)
}
14 changes: 14 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ const defaultBufferSize = 4096

// options allows to configure Copy behavior.
type options struct {
exclude []string
hash hash.Hash
bufSize int
force bool
contentOnly bool
move bool
revert bool
follow bool
}

func defaultOptions() *options {
Expand All @@ -32,6 +34,10 @@ func Force(o *options) { o.force = true }
// root folder in destination.
func ContentOnly(o *options) { o.contentOnly = true }

// FollowSymlink resolves source file path before copy, to follow symlink
// if needed.
func FollowSymlink(o *options) { o.follow = true }

// WithMove removes source after copying process is finished.
func WithMove(o *options) { o.move = true }

Expand Down Expand Up @@ -59,3 +65,11 @@ func WithHash(h hash.Hash) optFunc {
o.hash = h
}
}

// WithExclude excludes paths from copy which includes one of the given
// strings.
func WithExclude(s ...string) optFunc {
return func(o *options) {
o.exclude = append(o.exclude, s...)
}
}