diff options
Diffstat (limited to 'plugins/reload/tests')
-rw-r--r-- | plugins/reload/tests/configs/.rr-reload.yaml | 8 | ||||
-rw-r--r-- | plugins/reload/tests/reload_plugin_test.go | 259 | ||||
-rw-r--r-- | plugins/reload/tests/watcher_test.go | 698 |
3 files changed, 262 insertions, 703 deletions
diff --git a/plugins/reload/tests/configs/.rr-reload.yaml b/plugins/reload/tests/configs/.rr-reload.yaml index e1149cd2..52511aa1 100644 --- a/plugins/reload/tests/configs/.rr-reload.yaml +++ b/plugins/reload/tests/configs/.rr-reload.yaml @@ -1,5 +1,5 @@ server: - command: php ../../../tests/http/client.php echo pipes + command: php ../../../tests/psr-worker-bench.php user: '' group: '' env: @@ -8,7 +8,7 @@ server: relayTimeout: 20s http: debug: true - address: '127.0.0.1:15395' + address: '127.0.0.1:22388' maxRequestSize: 1024 middleware: - '' @@ -33,9 +33,9 @@ http: reload: interval: 1s patterns: - - .go + - .txt services: http: dirs: - - '' + - './unit_tests' recursive: true diff --git a/plugins/reload/tests/reload_plugin_test.go b/plugins/reload/tests/reload_plugin_test.go index 3d50030f..c49609f1 100644 --- a/plugins/reload/tests/reload_plugin_test.go +++ b/plugins/reload/tests/reload_plugin_test.go @@ -1,14 +1,23 @@ package tests import ( + "io" + "io/ioutil" + "math/rand" + "net/http" "os" "os/signal" + "path/filepath" + "strconv" "sync" "syscall" "testing" "time" + "github.com/golang/mock/gomock" "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/mocks" "github.com/spiral/roadrunner/v2/plugins/config" httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/spiral/roadrunner/v2/plugins/logger" @@ -18,6 +27,9 @@ import ( "github.com/stretchr/testify/assert" ) +const testDir string = "unit_tests" +const hugeNumberOfFiles uint = 5000 + func TestReloadInit(t *testing.T) { cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) assert.NoError(t, err) @@ -27,9 +39,21 @@ func TestReloadInit(t *testing.T) { Prefix: "rr", } + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + defer func() { + assert.NoError(t, freeResources(testDir)) + }() + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Info("Resetting http plugin").Times(1) + err = cont.RegisterAll( cfg, - &logger.ZapLogger{}, + mockLogger, &server.Plugin{}, &httpPlugin.Plugin{}, &reload.Plugin{}, @@ -80,5 +104,238 @@ func TestReloadInit(t *testing.T) { } }() + t.Run("ReloadTestInit", reloadTestInit) + + wg.Wait() +} + +func reloadTestInit(t *testing.T) { + err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec + []byte{}, 0755) + assert.NoError(t, err) +} + +func TestReloadHugeNumberOfFiles(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + }() + + // controller := gomock.NewController(t) + // mockLogger := mocks.NewMockLogger(controller) + // + // mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + // mockLogger.EXPECT().Info("Resetting http plugin").Times(1) + + err = cont.RegisterAll( + cfg, + // mockLogger, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 160) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles) + ttt := time.Now() + t.Run("ReloadRandomlyChangeFile", randomlyChangeFile) + if time.Since(ttt).Seconds() > 140 { + t.Fatal("spend too much time on reloading") + } + t.Run("ReloadHTTPLiveAfterReset", reloadHTTPLiveAfterReset) + wg.Wait() } + +func reloadHTTPLiveAfterReset(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22388", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "hello world", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func randomlyChangeFile(t *testing.T) { + // we know, that directory contains 5000 files (0-4999) + // let's try to randomly change it + for i := 0; i < 100; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func reloadHugeNumberOfFiles(t *testing.T) { + for i := uint(0); i < hugeNumberOfFiles; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } +} + +func freeResources(path string) error { + return os.RemoveAll(path) +} + +func makeFile(filename string) error { + return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec +} + +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return errors.E(errors.Str("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + return errors.E(errors.Str("destination already exists")) + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return err + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = copyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = copyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return errors.E(err) + } + defer func() { + _ = in.Close() + }() + + out, err := os.Create(dst) + if err != nil { + return errors.E(err) + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, in) + if err != nil { + return errors.E(err) + } + + err = out.Sync() + if err != nil { + return errors.E(err) + } + + si, err := os.Stat(src) + if err != nil { + return errors.E(err) + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/plugins/reload/tests/watcher_test.go b/plugins/reload/tests/watcher_test.go deleted file mode 100644 index f1e0d737..00000000 --- a/plugins/reload/tests/watcher_test.go +++ /dev/null @@ -1,698 +0,0 @@ -package tests - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/spiral/errors" - logPlugin "github.com/spiral/roadrunner/v2/plugins/logger" - "github.com/spiral/roadrunner/v2/plugins/reload" - "go.uber.org/zap" -) - -var testServiceName = "test" - -// scenario -// Create walker instance, init with default config, check that Watcher found all files from config -func Test_Correct_Watcher_Init(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - if err != nil { - t.Fatal(err) - } - }() - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: nil, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: nil, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - if len(w.GetAllFiles(testServiceName)) != 2 { - t.Fatal("incorrect directories len") - } -} - -// scenario -// create 3 files, create walker instance -// Start poll events -// change file and see, if event had come to handler -func Test_Get_FileEvent(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: nil, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "txt"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - // should be 3 files and directory - if len(w.GetAllFiles(testServiceName)) != 4 { - t.Fatal("incorrect directories len") - } - - go limitTime(time.Second*10, t.Name(), c) - - go func() { - go func() { - time.Sleep(time.Second) - err2 := ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), //nolint:gosec - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - runtime.Goexit() - }() - - go func() { - for e := range w.Event { - if e.Path != "file2.txt" { - panic("didn't handle event when write file2") - } - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// scenario -// create 3 files with different extensions, create walker instance -// Start poll events -// change file with txt extension, and see, if event had not come to handler because it was filtered -func Test_FileExtensionFilter(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return errors.E(errors.Skip) - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (one filtered) and directory - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go limitTime(time.Second*5, t.Name(), c) - - go func() { - go func() { - err2 := ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), //nolint:gosec - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - - runtime.Goexit() - }() - - go func() { - for e := range w.Event { - fmt.Println(e.Info.Name()) - panic("handled event from filtered file") - } - }() - w.Stop() - runtime.Goexit() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// nested -// scenario -// create dir and nested dir -// make files with aaa, bbb and txt extensions, filter txt -// change not filtered file, handle event -func Test_Recursive_Support(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - if err != nil { - t.Fatal(err) - } - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return errors.E(errors.Skip) - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 3 files (2 from root dir, and 1 from nested), filtered txt - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - // change file in nested directory - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), //nolint:gosec - []byte{1, 1, 1}, 0755) - if err != nil { - panic(err) - } - go func() { - for e := range w.Event { - if e.Info.Name() != "file4.aaa" { - panic("wrong handled event from watcher in nested dir") - } - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -func Test_Wrong_Dir(t *testing.T) { - // no such file or directory - wrongDir := "askdjfhaksdlfksdf" - - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{wrongDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return errors.E(errors.Skip) - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - _, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err == nil { - t.Fatal(err) - } -} - -func Test_Filter_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - go limitTime(time.Second*10, t.Name(), c) - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - ignored, err := reload.ConvertIgnored([]string{nestedDir}) - if err != nil { - t.Fatal(err) - } - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return errors.E(errors.Skip) - }, - Files: make(map[string]os.FileInfo), - Ignored: ignored, - FilePatterns: []string{"aaa", "bbb", "txt"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - go func() { - err2 := ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), //nolint:gosec - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - }() - - go func() { - for e := range w.Event { - fmt.Println("file: " + e.Info.Name()) - panic("handled event from watcher in nested dir") - } - }() - - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - w.Stop() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// copy files from nested dir to not ignored -// should fire an event -func Test_Copy_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func() { - err = freeResources(tempDir) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), //nolint:gosec - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - ignored, err := reload.ConvertIgnored([]string{nestedDir}) - if err != nil { - t.Fatal(err) - } - - wc := reload.WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return errors.E(errors.Skip) - }, - Files: make(map[string]os.FileInfo), - Ignored: ignored, - FilePatterns: []string{"aaa", "bbb", "txt"}, - } - - logger, _ := zap.NewDevelopment() - lg := logPlugin.NewZapAdapter(logger) - - w, err := reload.NewWatcher([]reload.WatcherConfig{wc}, lg) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go limitTime(time.Second*10, t.Name(), c) - - go func() { - go func() { - err2 := copyDir(nestedDir, filepath.Join(tempDir, "copyTo")) - if err2 != nil { - panic(err2) - } - - // exit from current goroutine - runtime.Goexit() - }() - - go func() { - for range w.Event { - // here should be event, otherwise we won't stop - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -func limitTime(d time.Duration, name string, free chan struct{}) { - go func() { - ticket := time.NewTicker(d) - for { - select { - case <-ticket.C: - ticket.Stop() - panic("timeout exceed, test: " + name) - case <-free: - ticket.Stop() - return - } - } - }() -} - -func copyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer func() { - _ = in.Close() - }() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer func() { - if e := out.Close(); e != nil { - err = e - } - }() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - - err = out.Sync() - if err != nil { - return err - } - - si, err := os.Stat(src) - if err != nil { - return err - } - err = os.Chmod(dst, si.Mode()) - if err != nil { - return err - } - return nil -} - -func copyDir(src string, dst string) error { - src = filepath.Clean(src) - dst = filepath.Clean(dst) - - si, err := os.Stat(src) - if err != nil { - return err - } - if !si.IsDir() { - return fmt.Errorf("source is not a directory") - } - - _, err = os.Stat(dst) - if err != nil && !os.IsNotExist(err) { - return err - } - if err == nil { - return fmt.Errorf("destination already exists") - } - - err = os.MkdirAll(dst, si.Mode()) - if err != nil { - return err - } - - entries, err := ioutil.ReadDir(src) - if err != nil { - return err - } - - for _, entry := range entries { - srcPath := filepath.Join(src, entry.Name()) - dstPath := filepath.Join(dst, entry.Name()) - - if entry.IsDir() { - err = copyDir(srcPath, dstPath) - if err != nil { - return err - } - } else { - // Skip symlinks. - if entry.Mode()&os.ModeSymlink != 0 { - continue - } - - err = copyFile(srcPath, dstPath) - if err != nil { - return err - } - } - } - return nil -} - -func freeResources(path string) error { - return os.RemoveAll(path) -} |