From 46bd9a2de6822bda82b063b5e0af913b0707c963 Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Fri, 26 Jan 2018 12:58:48 +0100 Subject: [PATCH] create template in temp folder and clean on error --- commands/githook/githook.go | 38 +++---- commands/template/template.go | 181 ++++++++++++++++++++++++++-------- main.go | 15 ++- utils/fileutils.go | 164 ++++++++++++++++++++++++++++++ 4 files changed, 327 insertions(+), 71 deletions(-) create mode 100644 utils/fileutils.go diff --git a/commands/githook/githook.go b/commands/githook/githook.go index 14626c6..b5868b1 100644 --- a/commands/githook/githook.go +++ b/commands/githook/githook.go @@ -5,6 +5,7 @@ import ( "path" logy "github.com/apex/log" + "github.com/netzkern/butler/utils" survey "gopkg.in/AlecAivazis/survey.v1" ) @@ -35,6 +36,7 @@ type CommandData struct { // Githook command to create git hooks type Githook struct { Path string + GitDir string CommandData *CommandData } @@ -52,6 +54,13 @@ func New(options ...Option) *Githook { return v } +// WithGitDir option. +func WithGitDir(dir string) Option { + return func(g *Githook) { + g.GitDir = dir + } +} + // WithCommandData option. func WithCommandData(cd *CommandData) Option { return func(g *Githook) { @@ -62,19 +71,19 @@ func WithCommandData(cd *CommandData) Option { // Install will create hard links from local git_hooks to the corresponding git hooks func (g *Githook) Install() error { for _, h := range g.CommandData.Hooks { - hookGitPath := path.Join(g.CommandData.Path, ".git", "hooks", h) + hookGitPath := path.Join(g.GitDir, ".git", "hooks", h) hookRepoPath := path.Join(g.CommandData.Path, repoHookDir, h) - if !exists(hookGitPath) { + if !utils.Exists(hookGitPath) { dir := path.Dir(hookGitPath) logy.Debugf("Path '%s' created", dir) - err := createDirIfNotExist(dir) + err := utils.CreateDirIfNotExist(dir) if err != nil { logy.WithError(err).Error("Could not create directory") } } else { os.Remove(hookGitPath) } - if exists(hookRepoPath) { + if utils.Exists(hookRepoPath) { logy.Debugf("Create symlink old: %s, new: %s", hookGitPath, hookRepoPath) err := os.Link(hookRepoPath, hookGitPath) if err != nil { @@ -83,7 +92,7 @@ func (g *Githook) Install() error { } logy.Debugf("hook '%s' installed", h) } else { - logy.Infof("template for hook '%s' could not be found in %s", h, path.Join(g.CommandData.Path, repoHookDir)) + logy.Debugf("template for hook '%s' could not be found in %s", h, path.Join(g.CommandData.Path, repoHookDir)) } } @@ -133,22 +142,3 @@ func (g *Githook) StartCommandSurvey() error { func (g *Githook) Run() error { return g.Install() } - -func exists(name string) bool { - if _, err := os.Stat(name); err != nil { - if os.IsNotExist(err) { - return false - } - } - return true -} - -func createDirIfNotExist(dir string) error { - if _, err := os.Stat(dir); os.IsNotExist(err) { - err = os.MkdirAll(dir, 0755) - if err != nil { - return err - } - } - return nil -} diff --git a/commands/template/template.go b/commands/template/template.go index 7ddd578..fc41691 100644 --- a/commands/template/template.go +++ b/commands/template/template.go @@ -20,6 +20,7 @@ import ( "github.com/briandowns/spinner" "github.com/netzkern/butler/commands/githook" "github.com/netzkern/butler/config" + "github.com/netzkern/butler/utils" "github.com/pinzolo/casee" "github.com/pkg/errors" uuid "github.com/satori/go.uuid" @@ -62,6 +63,7 @@ type ( surveys *Survey dirRenamings map[string]string dirRemovings []string + gitDir string } // TemplateData basic template data TemplateData struct { @@ -106,24 +108,31 @@ func New(options ...Option) *Templating { return v } +// WithGitDir option. +func WithGitDir(dir string) Option { + return func(t *Templating) { + t.gitDir = dir + } +} + // WithVariables option. func WithVariables(s map[string]string) Option { - return func(v *Templating) { - v.Variables = s + return func(t *Templating) { + t.Variables = s } } // SetConfigName option. func SetConfigName(s string) Option { - return func(v *Templating) { - v.configName = s + return func(t *Templating) { + t.configName = s } } // WithTemplates option. func WithTemplates(s []config.Template) Option { - return func(v *Templating) { - v.Templates = s + return func(t *Templating) { + t.Templates = s } } @@ -141,21 +150,53 @@ func WithTemplateSurveyResults(sr map[string]interface{}) Option { } } -// cloneRepo clone a repo to the dst -func (t *Templating) cloneRepo(repoURL string, dest string) error { +// unpackTemplate clone a repo to the dst +func (t *Templating) unpackTemplate(repoURL string, dest string) error { _, err := git.PlainClone(dest, false, &git.CloneOptions{ URL: repoURL, }) - if err == git.ErrRepositoryAlreadyExists { - return errors.Wrapf(err, "respository already exists. Remove '%s' directory", dest) + if err != nil { + return err } - os.RemoveAll(filepath.Join(dest, ".git")) + // remove git files + err = os.RemoveAll(filepath.Join(dest, ".git")) + if err != nil { + return errors.Wrap(err, "remove all failed") + } return err } +func (t *Templating) packTemplate(tempDir, dest string) error { + // move files from temp to cd + if dest == "." { + err := utils.MoveDir(tempDir, ".") + if err != nil { + return errors.Wrap(err, "move failed") + } + err = os.RemoveAll(tempDir) + if err != nil { + return errors.Wrap(err, "remove all failed") + } + } else { + err := os.Rename(tempDir, dest) + if err != nil { + return errors.Wrap(err, "rename failed") + } + } + return nil +} + +func (t *Templating) cleanTemplate(tempDir string) error { + err := os.RemoveAll(tempDir) + if err != nil { + return errors.Wrap(err, "remove all failed") + } + return nil +} + // getTemplateByName returns the template by name func (t *Templating) getTemplateByName(name string) *config.Template { for _, tpl := range t.Templates { @@ -222,8 +263,9 @@ func (t *Templating) getQuestions() []*survey.Question { // Skip returns an error when a directory should be skipped or true with a file func (t *Templating) Skip(path string, info os.FileInfo) (bool, error) { + name := info.Name() // ignore hidden dirs and files - if strings.HasPrefix(info.Name(), ".") { + if len(name) > 1 && strings.HasPrefix(name, ".") { if info.IsDir() { return false, filepath.SkipDir } @@ -232,7 +274,7 @@ func (t *Templating) Skip(path string, info os.FileInfo) (bool, error) { // skip blacklisted directories if info.IsDir() { - _, ok := t.excludedDirs[info.Name()] + _, ok := t.excludedDirs[name] if ok { return false, filepath.SkipDir } @@ -240,7 +282,7 @@ func (t *Templating) Skip(path string, info os.FileInfo) (bool, error) { // skip blacklisted extensions if !info.IsDir() { - _, ok := t.excludedExts[filepath.Ext("."+info.Name())] + _, ok := t.excludedExts[filepath.Ext("."+name)] if ok { return true, nil } @@ -260,10 +302,25 @@ func (t *Templating) StartCommandSurvey() error { } t.CommandData = cd + t.CommandData.Path = path.Clean(cd.Path) return nil } +func (t *Templating) confirmPackTemplate() (bool, error) { + packTemplate := false + prompt := &survey.Confirm{ + Message: "Do you really want to proceed?", + } + + err := survey.AskOne(prompt, &packTemplate, nil) + if err != nil { + return false, errors.Wrap(err, "confirm failed") + } + + return packTemplate, nil +} + func (t *Templating) startTemplateSurvey(surveys *Survey) error { questions, err := BuildSurveys(surveys) if err != nil { @@ -282,7 +339,7 @@ func (t *Templating) startTemplateSurvey(surveys *Survey) error { } // runSurveyTemplateHooks run all template hooks -func (t *Templating) runSurveyTemplateHooks() error { +func (t *Templating) runSurveyTemplateHooks(cmdDir string) error { for i, hook := range t.surveys.AfterHooks { ctx := logy.WithFields(logy.Fields{ "cmd": hook.Cmd, @@ -309,7 +366,7 @@ func (t *Templating) runSurveyTemplateHooks() error { } cmd := exec.Command(hook.Cmd, hook.Args...) - cmd.Dir = path.Clean(t.CommandData.Path) + cmd.Dir = cmdDir // inherit process env cmd.Env = append(mapToEnvArray(t.surveyResult, envPrefix), os.Environ()...) cmd.Stdout = os.Stdout @@ -521,26 +578,45 @@ func (t *Templating) walkFiles(path string, info os.FileInfo, err error) error { } // Run the command -func (t *Templating) Run() error { +func (t *Templating) Run() (err error) { tpl := t.getTemplateByName(t.CommandData.Template) if tpl == nil { - return errors.Errorf("template %s could not be found", t.CommandData.Template) + err = errors.Errorf("template %s could not be found", t.CommandData.Template) + return } - // clone repository startTimeClone := time.Now() cloneSpinner := defaultSpinner("Cloning repository...") cloneSpinner.Start() - err := t.cloneRepo(tpl.Url, t.CommandData.Path) + + // unpack template + tempDir, err := ioutil.TempDir(t.gitDir, "butler") + if err != nil { + err = errors.Wrap(err, "create temp folder failed") + } + + err = t.unpackTemplate(tpl.Url, tempDir) + + defer func() { + r := recover() + if err != nil || r != nil { + err = t.cleanTemplate(tempDir) + if err != nil { + logy.WithError(err).Error("clean template failed") + } + logy.Debug("clean template") + } + }() + cloneSpinner.Stop() if err != nil { logy.WithError(err).Error("clone") - return err + return } - surveyFilePath := path.Join(t.CommandData.Path, t.configName) + surveyFilePath := path.Join(tempDir, t.configName) ctx := logy.WithFields(logy.Fields{ "path": surveyFilePath, }) @@ -548,14 +624,14 @@ func (t *Templating) Run() error { surveys, err := ReadSurveyConfig(surveyFilePath) if err != nil { ctx.WithError(err).Error("read survey config") - return err + return } t.surveys = surveys err = t.startTemplateSurvey(surveys) if err != nil { ctx.WithError(err).Error("start template survey") - return err + return } // spinner progress @@ -578,12 +654,15 @@ func (t *Templating) Run() error { t.generateTempFuncs() } + logy.Debugf("dir walk in path %s", tempDir) + // iterate through all directorys - walkDirErr := filepath.Walk(t.CommandData.Path, t.walkDirectories) + walkDirErr := filepath.Walk(tempDir, t.walkDirectories) if walkDirErr != nil { logy.WithError(walkDirErr).Error("walk dir") - return walkDirErr + err = walkDirErr + return } // rename and remove changed dirs from walk @@ -597,17 +676,46 @@ func (t *Templating) Run() error { os.RemoveAll(path) } + logy.Debugf("file walk in path %s", tempDir) + // iterate through all files - walkErr := filepath.Walk(t.CommandData.Path, t.walkFiles) + walkErr := filepath.Walk(tempDir, t.walkFiles) if walkErr != nil { - return walkErr + err = walkErr + return } t.stop() templatingSpinner.Stop() + startTimeHooks := time.Now() + + if t.surveyResult != nil { + logy.Debug("execute template hooks") + err = t.runSurveyTemplateHooks(tempDir) + if err != nil { + logy.WithError(err).Error("template hooks failed") + return + } + } else { + logy.Debug("skip template hooks") + } + + confirmed, err := t.confirmPackTemplate() + if confirmed { + err = t.packTemplate(tempDir, t.CommandData.Path) + if err != nil { + logy.WithError(err).Error("pack template failed") + return + } + } else { + err = errors.New("abort templating") + return + } + commandGitHook := githook.New( + githook.WithGitDir(t.gitDir), githook.WithCommandData( &githook.CommandData{ Path: t.CommandData.Path, @@ -621,20 +729,7 @@ func (t *Templating) Run() error { err = commandGitHook.Run() if err != nil { logy.WithError(err).Error("could not create git hooks") - return err - } - - startTimeHooks := time.Now() - - if t.surveyResult != nil { - logy.Debug("execute template hooks") - err := t.runSurveyTemplateHooks() - if err != nil { - logy.WithError(err).Error("template hooks failed") - return err - } - } else { - logy.Debug("skip template hooks") + return } // print summary @@ -649,7 +744,7 @@ func (t *Templating) Run() error { strconv.FormatFloat(totalDuration, 'f', 2, 64), ) - return nil + return } // startN starts n loops. diff --git a/main.go b/main.go index bf5d6ac..4bd13cf 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,7 @@ var ( ) func init() { - logy.SetLevel(logy.DebugLevel) + logy.SetLevel(logy.InfoLevel) cfg = config.ParseConfig(configName) // Windows compatible symbols @@ -76,22 +76,29 @@ func interactiveCliMode() { return } + cd, err := os.Getwd() + if err != nil { + logy.WithError(err) + return + } + switch taskType := answers.Action; taskType { case "Project Templates": command := template.New( template.WithTemplates(cfg.Templates), template.WithVariables(cfg.Variables), template.SetConfigName(surveyFilename), + template.WithGitDir(cd), ) command.StartCommandSurvey() err := command.Run() if err != nil { - logy.Errorf(err.Error()) + logy.WithError(err) } case "Install Git Hooks": - command := githook.New() + command := githook.New(githook.WithGitDir(cd)) command.StartCommandSurvey() - err := command.Run() + err = command.Run() if err != nil { logy.Errorf(err.Error()) } diff --git a/utils/fileutils.go b/utils/fileutils.go new file mode 100644 index 0000000..fcf37a1 --- /dev/null +++ b/utils/fileutils.go @@ -0,0 +1,164 @@ +package utils + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +// CopyFile 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. The file mode will be copied from the source and +// the copied data is synced/flushed to stable storage. +func CopyFile(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() { + if e := out.Close(); e != nil { + err = e + } + }() + + _, err = io.Copy(out, in) + if err != nil { + return + } + + err = out.Sync() + if err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return + } + + return +} + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +func CopyDir(src string, dst string) (err 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 + } + if err == nil { + return fmt.Errorf("destination already exists") + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return + } + + 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 + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return +} + +// MoveDir moves all files from src to dst +func MoveDir(src string, dst string) (err error) { + entries, err := ioutil.ReadDir(src) + if err != nil { + return + } + + 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 + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return nil +} + +// Exists return true when file or dir exists +func Exists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// CreateDirIfNotExist create a dir recursively when not exists +func CreateDirIfNotExist(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err = os.MkdirAll(dir, 0755) + if err != nil { + return err + } + } + return nil +}