diff --git a/plugins/common/common.go b/plugins/common/common.go index 8b564b164..d90d9162d 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -1,14 +1,12 @@ package common import ( - "bufio" "bytes" "errors" "fmt" "io/ioutil" "os" "os/exec" - "os/user" "path/filepath" "reflect" "regexp" @@ -439,16 +437,6 @@ func ContainerIsRunning(containerID string) bool { return strings.TrimSpace(string(b[:])) == "true" } -// DirectoryExists returns if a path exists and is a directory -func DirectoryExists(filePath string) bool { - fi, err := os.Stat(filePath) - if err != nil { - return false - } - - return fi.IsDir() -} - // DockerContainerCreate creates a new container and returns the container ID func DockerContainerCreate(image string, containerCreateArgs []string) (string, error) { cmd := []string{ @@ -513,36 +501,6 @@ func DokkuApps() (apps []string, err error) { return } -// FileToSlice reads in all the lines from a file into a string slice -func FileToSlice(filePath string) (lines []string, err error) { - f, err := os.Open(filePath) - if err != nil { - return - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - text := strings.TrimSpace(scanner.Text()) - if text == "" { - continue - } - lines = append(lines, text) - } - err = scanner.Err() - return -} - -// FileExists returns if a path exists and is a file -func FileExists(filePath string) bool { - fi, err := os.Stat(filePath) - if err != nil { - return false - } - - return fi.Mode().IsRegular() -} - // GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { err := VerifyAppName(appName) @@ -565,11 +523,6 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { return } -// IsAbsPath returns 0 if input path is absolute -func IsAbsPath(path string) bool { - return strings.HasPrefix(path, "/") -} - // IsDeployed returns true if given app has a running container func IsDeployed(appName string) bool { files, err := ioutil.ReadDir(AppRoot(appName)) @@ -612,29 +565,6 @@ func GetenvWithDefault(key string, defaultValue string) (val string) { return } -// ReadFirstLine gets the first line of a file that has contents and returns it -// if there are no contents, an empty string is returned -// will also return an empty string if the file does not exist -func ReadFirstLine(filename string) (text string) { - if !FileExists(filename) { - return - } - f, err := os.Open(filename) - if err != nil { - return - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - if text = strings.TrimSpace(scanner.Text()); text == "" { - continue - } - return - } - return -} - // ReportSingleApp is an internal function that displays a report for an app func ReportSingleApp(reportType string, appName string, infoFlag string, infoFlags map[string]string, trimPrefix bool, uppercaseFirstCharacter bool) error { flags := []string{} @@ -805,36 +735,6 @@ func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session { return sh.Command("plugn", shellArgs...) } -// SetPermissions sets the proper owner and filemode for a given file -func SetPermissions(path string, fileMode os.FileMode) error { - if err := os.Chmod(path, fileMode); err != nil { - return err - } - - systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku") - systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku") - - group, err := user.LookupGroup(systemGroup) - if err != nil { - return err - } - user, err := user.Lookup(systemUser) - if err != nil { - return err - } - - uid, err := strconv.Atoi(user.Uid) - if err != nil { - return err - } - - gid, err := strconv.Atoi(group.Gid) - if err != nil { - return err - } - return os.Chown(path, uid, gid) -} - func times(str string, n int) (out string) { for i := 0; i < n; i++ { out += str diff --git a/plugins/common/common_test.go b/plugins/common/common_test.go index fedd2b1eb..4fb32ea84 100644 --- a/plugins/common/common_test.go +++ b/plugins/common/common_test.go @@ -10,10 +10,10 @@ import ( ) var ( - testAppName = "test-app-1" - testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "") - testEnvFile = strings.Join([]string{testAppDir, "/ENV"}, "") - testEnvLine = "export testKey=TESTING" + testAppName = "test-app-1" + testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "") + testEnvFile = strings.Join([]string{testAppDir, "/ENV"}, "") + testEnvLine = "export testKey=TESTING" testAppName2 = "01-test-app-1" testAppDir2 = strings.Join([]string{"/home/dokku/", testAppName2}, "") testEnvFile2 = strings.Join([]string{testAppDir2, "/ENV"}, "") @@ -95,32 +95,6 @@ func TestCommonDokkuApps(t *testing.T) { teardownTestApp() } -func TestCommonFileToSlice(t *testing.T) { - RegisterTestingT(t) - Expect(setupTestApp()).To(Succeed()) - lines, err := FileToSlice(testEnvFile) - Expect(err).NotTo(HaveOccurred()) - Expect(lines).To(Equal([]string{testEnvLine})) - teardownTestApp() -} - -func TestCommonFileExists(t *testing.T) { - RegisterTestingT(t) - Expect(setupTestApp()).To(Succeed()) - Expect(FileExists(testEnvFile)).To(BeTrue()) - teardownTestApp() -} - -func TestCommonReadFirstLine(t *testing.T) { - RegisterTestingT(t) - line := ReadFirstLine(testEnvFile) - Expect(line).To(Equal("")) - Expect(setupTestApp()).To(Succeed()) - line = ReadFirstLine(testEnvFile) - Expect(line).To(Equal(testEnvLine)) - teardownTestApp() -} - func TestCommonStripInlineComments(t *testing.T) { RegisterTestingT(t) text := StripInlineComments(strings.Join([]string{testEnvLine, "# testing comment"}, " ")) diff --git a/plugins/common/io.go b/plugins/common/io.go new file mode 100644 index 000000000..f57b20e42 --- /dev/null +++ b/plugins/common/io.go @@ -0,0 +1,191 @@ +package common + +import ( + "bufio" + "fmt" + "io" + "os" + "os/user" + "strconv" + "strings" +) + +// CopyFile copies a file from src to dst. If src and dst files exist, and are +// the same, then return success. Otherise, attempt to create a hard link +// between the two files. If that fail, copy the file contents from src to dst. +// FROM: https://stackoverflow.com/a/21067803/1515875 +func CopyFile(src, dst string) (err error) { + sfi, err := os.Stat(src) + if err != nil { + return + } + if !sfi.Mode().IsRegular() { + // cannot copy non-regular files (e.g., directories, + // symlinks, devices, etc.) + return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) + } + dfi, err := os.Stat(dst) + if err != nil { + if !os.IsNotExist(err) { + return + } + } else { + if !(dfi.Mode().IsRegular()) { + return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) + } + if os.SameFile(sfi, dfi) { + return + } + } + if err = os.Link(src, dst); err == nil { + return + } + err = copyFileContents(src, dst) + return +} + +// copyFileContents copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all it's contents will be replaced by the contents +// of the source file. +// FROM: https://stackoverflow.com/a/21067803/1515875 +func copyFileContents(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(out, in); err != nil { + return + } + err = out.Sync() + return +} + +// DirectoryExists returns if a path exists and is a directory +func DirectoryExists(filename string) bool { + fi, err := os.Stat(filename) + if err != nil { + return false + } + + return fi.IsDir() +} + +// FileToSlice reads in all the lines from a file into a string slice +func FileToSlice(filename string) (lines []string, err error) { + f, err := os.Open(filename) + if err != nil { + return + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + if text == "" { + continue + } + lines = append(lines, text) + } + err = scanner.Err() + return +} + +// FileExists returns if a path exists and is a file +func FileExists(filename string) bool { + fi, err := os.Stat(filename) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// IsAbsPath returns 0 if input path is absolute +func IsAbsPath(path string) bool { + return strings.HasPrefix(path, "/") +} + +// ReadFirstLine gets the first line of a file that has contents and returns it +// if there are no contents, an empty string is returned +// will also return an empty string if the file does not exist +func ReadFirstLine(filename string) (text string) { + if !FileExists(filename) { + return + } + f, err := os.Open(filename) + if err != nil { + return + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if text = strings.TrimSpace(scanner.Text()); text == "" { + continue + } + return + } + return +} + +// SetPermissions sets the proper owner and filemode for a given file +func SetPermissions(path string, fileMode os.FileMode) error { + if err := os.Chmod(path, fileMode); err != nil { + return err + } + + systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku") + systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku") + + group, err := user.LookupGroup(systemGroup) + if err != nil { + return err + } + user, err := user.Lookup(systemUser) + if err != nil { + return err + } + + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return err + } + + gid, err := strconv.Atoi(group.Gid) + if err != nil { + return err + } + return os.Chown(path, uid, gid) +} + +func WriteSliceToFile(filename string, lines []string) error { + file, err := os.OpenFile(filename, os.O_RDWR|os.O_TRUNC, 0600) + if err != nil { + return err + } + + w := bufio.NewWriter(file) + for _, line := range lines { + fmt.Fprintln(w, line) + } + if err = w.Flush(); err != nil { + return err + } + + file.Chmod(0600) + SetPermissions(filename, 0600) + + return nil +} diff --git a/plugins/common/io_test.go b/plugins/common/io_test.go new file mode 100644 index 000000000..856206a87 --- /dev/null +++ b/plugins/common/io_test.go @@ -0,0 +1,28 @@ + +import "testing" + +func TestCommonFileToSlice(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + lines, err := FileToSlice(testEnvFile) + Expect(err).NotTo(HaveOccurred()) + Expect(lines).To(Equal([]string{testEnvLine})) + teardownTestApp() +} + +func TestCommonFileExists(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + Expect(FileExists(testEnvFile)).To(BeTrue()) + teardownTestApp() +} + +func TestCommonReadFirstLine(t *testing.T) { + RegisterTestingT(t) + line := ReadFirstLine(testEnvFile) + Expect(line).To(Equal("")) + Expect(setupTestApp()).To(Succeed()) + line = ReadFirstLine(testEnvFile) + Expect(line).To(Equal(testEnvLine)) + teardownTestApp() +} diff --git a/plugins/docker-options/dockeroptions.go b/plugins/docker-options/dockeroptions.go index 63fce5284..a1d44e6b3 100644 --- a/plugins/docker-options/dockeroptions.go +++ b/plugins/docker-options/dockeroptions.go @@ -82,21 +82,5 @@ func touchPhaseFile(appName string, phase string) error { func writeDockerOptionsForPhase(appName string, phase string, options []string) error { phaseFilePath := getPhaseFilePath(appName, phase) - file, err := os.OpenFile(phaseFilePath, os.O_RDWR|os.O_TRUNC, 0600) - if err != nil { - return err - } - - w := bufio.NewWriter(file) - for _, option := range options { - fmt.Fprintln(w, option) - } - if err = w.Flush(); err != nil { - return fmt.Errorf("Unable to update docker options phase file %s.%s: %s", appName, phase, err.Error()) - } - - file.Chmod(0600) - common.SetPermissions(phaseFilePath, 0600) - - return nil + return common.WriteSliceToFile(phaseFilePath, options) }