Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
emar-kar committed Jun 2, 2023
0 parents commit 79a42a0
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Leonid Emar-Kar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# copy
Simple copy module for go applications, which allows to create copies of files and folders

### Install:

```bash
go get github.com/emar-kar/copy@latest
```
166 changes: 166 additions & 0 deletions copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package copy

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

// options allows to configure Copy behavior.
type options struct {
force bool
contentOnly bool
}

type optFunc func(*options)

// Force re-write destination if it is already exists.
func Force(o *options) { o.force = true }

// ContentOnly if copy folder and set this flag to true,
// will copy only source content without creating root folder.
func ContentOnly(o *options) { o.contentOnly = true }

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

src, err := evalSymlink(src)
if err != nil {
return err
}

srcInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("cannot get source information: %w", err)
}

if srcInfo.IsDir() {
if !opt.contentOnly {
dst = path.Join(dst, path.Base(src))
}

return copyFolder(ctx, src, dst, opt.force)
}

if _, f := path.Split(src); f != path.Base(dst) {
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
return err
}

dst = path.Join(dst, f)
}

if _, err := os.Stat(dst); !os.IsNotExist(err) || opt.force {
return copyFile(ctx, src, dst)
}

return nil
}

// copyFolder is a support function to copy whole folder.
func copyFolder(ctx context.Context, src, dst string, force bool) error {
return filepath.Walk(
src, func(root string, info fs.FileInfo, err error,
) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err != nil {
return err
}

subDst := strings.ReplaceAll(root, src, dst)
if info.IsDir() {
if err := os.MkdirAll(subDst, info.Mode()); err != nil {
return fmt.Errorf("cannot create sub-folder: %w", err)
}

return nil
}

if _, err := os.Stat(subDst); !os.IsNotExist(err) || force {
return copyFile(ctx, root, subDst)
}

return nil
}
})
}

// copyFile is a support function to copy file content. Copies with buffer.
// If context canceled during the copy, dst file will be removed before return.
func copyFile(ctx context.Context, src, dst string) error {
srcF, err := os.Open(src)
if err != nil {
return err
}
defer srcF.Close()

stat, err := os.Stat(src)
if err != nil {
return err
}

dstF, err := os.OpenFile(
dst,
os.O_RDWR|os.O_CREATE|os.O_TRUNC,
stat.Mode().Perm(),
)
if err != nil {
return err
}
defer dstF.Close()

buf := make([]byte, 4096)

for {
select {
case <-ctx.Done():
if err := os.RemoveAll(dst); err != nil {
return fmt.Errorf(
"%w: cannot remove dst file: %w", ctx.Err(), err,
)
}

return ctx.Err()
default:
b, err := srcF.Read(buf)
if err != nil && err != io.EOF {
return err
}

if b == 0 {
return nil
}

if _, err := dstF.Write(buf[:b]); err != nil {
return err
}
}
}
}

// evalSymlink returns the path name after the evaluation of any symbolic links.
// Check [filepath.EvalSymlinks] for details.
func evalSymlink(p string) (string, error) {
info, err := os.Lstat(p)
if err != nil {
return "", fmt.Errorf("cannot get %s info: %w", p, err)
}

if info.Mode()&os.ModeSymlink == os.ModeSymlink {
return filepath.EvalSymlinks(p)
}

return p, nil
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/emar-kar/copy

go 1.20

0 comments on commit 79a42a0

Please sign in to comment.