From 364e69ab7f54ab7a9901644647125f21cd39e98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 10 Jul 2016 19:37:27 +0200 Subject: [PATCH] Handle symlink change event Hugo 0.16 announced support for symbolic links for the root folders, /content, /static etc., but this got broken pretty fast. The main problem this commit tries to solve is the matching of file change events to "what changed". An example: ContentDir: /mysites/site/content where /mysites/site/content is a symlink to /mycontent /mycontent: /mypost1.md /post/mypost2.md * A change to mypost1.md (on OS X) will trigger a file change event with name "/mycontent/mypost1.md" * A change to mypost2.md gives event with name "/mysites/site/content/mypost2.md" The first change will not trigger a correct update of Hugo before this commit. This commit fixes this by doing a two-step check: 1. Check if "/mysites/site/content/mypost2.md" is within /mysites/site/content 2. Check if "/mysites/site/content/mypost2.md" is within the real path that /mysites/site/content points to Fixes #2265 Closes #2273 --- helpers/path.go | 40 +++++++++++++++++++++++-- helpers/path_test.go | 25 ++++++++++++++++ hugolib/site.go | 69 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/helpers/path.go b/helpers/path.go index a8a50aab3..478512efa 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -481,17 +481,17 @@ func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error { } // Handle the root first - fileInfo, err := lstatIfOs(fs, root) + fileInfo, realPath, err := getRealFileInfo(fs, root) if err != nil { return walker(root, nil, err) } if !fileInfo.IsDir() { - return nil + return fmt.Errorf("Cannot walk regular file %s", root) } - if err := walker(root, fileInfo, err); err != nil && err != filepath.SkipDir { + if err := walker(realPath, fileInfo, err); err != nil && err != filepath.SkipDir { return err } @@ -511,6 +511,40 @@ func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error { } +func getRealFileInfo(fs afero.Fs, path string) (os.FileInfo, string, error) { + fileInfo, err := lstatIfOs(fs, path) + realPath := path + + if err != nil { + return nil, "", err + } + + if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, "", fmt.Errorf("Cannot read symbolic link '%s', error was: %s", path, err) + } + fileInfo, err = lstatIfOs(fs, link) + if err != nil { + return nil, "", fmt.Errorf("Cannot stat '%s', error was: %s", link, err) + } + realPath = link + } + return fileInfo, realPath, nil +} + +// GetRealPath returns the real file path for the given path, whether it is a +// symlink or not. +func GetRealPath(fs afero.Fs, path string) (string, error) { + _, realPath, err := getRealFileInfo(fs, path) + + if err != nil { + return "", err + } + + return realPath, nil +} + // Code copied from Afero's path.go // if the filesystem is OsFs use Lstat, else use fs.Stat func lstatIfOs(fs afero.Fs, path string) (info os.FileInfo, err error) { diff --git a/helpers/path_test.go b/helpers/path_test.go index a1769f1da..bd8f8ed49 100644 --- a/helpers/path_test.go +++ b/helpers/path_test.go @@ -25,6 +25,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/spf13/afero" "github.com/spf13/viper" ) @@ -141,6 +143,29 @@ func TestGetRelativePath(t *testing.T) { } } +func TestGetRealPath(t *testing.T) { + d1, err := ioutil.TempDir("", "d1") + defer os.Remove(d1) + fs := afero.NewOsFs() + + rp1, err := GetRealPath(fs, d1) + assert.NoError(t, err) + assert.Equal(t, d1, rp1) + + sym := filepath.Join(os.TempDir(), "d1sym") + err = os.Symlink(d1, sym) + defer os.Remove(sym) + assert.NoError(t, err) + + rp2, err := GetRealPath(fs, sym) + assert.NoError(t, err) + + // On OS X, the temp folder is itself a symbolic link (to /private...) + // This has to do for now. + assert.True(t, strings.HasSuffix(rp2, d1)) + +} + func TestMakePathRelative(t *testing.T) { type test struct { inPath, path1, path2, output string diff --git a/hugolib/site.go b/hugolib/site.go index 6bff3b038..c2acd493c 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -483,16 +483,15 @@ func (s *Site) reBuild(events []fsnotify.Event) (whatChanged, error) { logger := helpers.NewDistinctFeedbackLogger() for _, ev := range events { - // Need to re-read source - if strings.HasPrefix(ev.Name, s.absContentDir()) { + if s.isContentDirEvent(ev) { logger.Println("Source changed", ev.Name) sourceChanged = append(sourceChanged, ev) } - if strings.HasPrefix(ev.Name, s.absLayoutDir()) || strings.HasPrefix(ev.Name, s.absThemeDir()) { + if s.isLayoutDirEvent(ev) || s.isThemeDirEvent(ev) { logger.Println("Template changed", ev.Name) tmplChanged = append(tmplChanged, ev) } - if strings.HasPrefix(ev.Name, s.absDataDir()) { + if s.isDataDirEvent(ev) { logger.Println("Data changed", ev.Name) dataChanged = append(dataChanged, ev) } @@ -553,7 +552,7 @@ func (s *Site) reBuild(events []fsnotify.Event) (whatChanged, error) { // so we do this first to prevent races. if ev.Op&fsnotify.Remove == fsnotify.Remove { //remove the file & a create will follow - path, _ := helpers.GetRelativePath(ev.Name, s.absContentDir()) + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) s.removePageByPath(path) continue } @@ -564,7 +563,7 @@ func (s *Site) reBuild(events []fsnotify.Event) (whatChanged, error) { if ev.Op&fsnotify.Rename == fsnotify.Rename { // If the file is still on disk, it's only been updated, if it's not, it's been moved if ex, err := afero.Exists(hugofs.Source(), ev.Name); !ex || err != nil { - path, _ := helpers.GetRelativePath(ev.Name, s.absContentDir()) + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) s.removePageByPath(path) continue } @@ -948,18 +947,74 @@ func (s *Site) absI18nDir() string { return helpers.AbsPathify(viper.GetString("I18nDir")) } +func (s *Site) isDataDirEvent(e fsnotify.Event) bool { + return s.getDataDir(e.Name) != "" +} + +func (s *Site) getDataDir(path string) string { + return getRealDir(s.absDataDir(), path) +} + func (s *Site) absThemeDir() string { return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme")) } +func (s *Site) isThemeDirEvent(e fsnotify.Event) bool { + return s.getThemeDir(e.Name) != "" +} + +func (s *Site) getThemeDir(path string) string { + return getRealDir(s.absThemeDir(), path) +} + func (s *Site) absLayoutDir() string { return helpers.AbsPathify(viper.GetString("LayoutDir")) } +func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool { + return s.getLayoutDir(e.Name) != "" +} + +func (s *Site) getLayoutDir(path string) string { + return getRealDir(s.absLayoutDir(), path) +} + func (s *Site) absContentDir() string { return helpers.AbsPathify(viper.GetString("ContentDir")) } +func (s *Site) isContentDirEvent(e fsnotify.Event) bool { + return s.getContentDir(e.Name) != "" +} + +func (s *Site) getContentDir(path string) string { + return getRealDir(s.absContentDir(), path) +} + +// getRealDir gets the base path of the given path, also handling the case where +// base is a symlinked folder. +func getRealDir(base, path string) string { + + if strings.HasPrefix(path, base) { + return base + } + + realDir, err := helpers.GetRealPath(hugofs.Source(), base) + + if err != nil { + if !os.IsNotExist(err) { + jww.ERROR.Printf("Failed to get real path for %s: %s", path, err) + } + return "" + } + + if strings.HasPrefix(path, realDir) { + return realDir + } + + return "" +} + func (s *Site) absPublishDir() string { return helpers.AbsPathify(viper.GetString("PublishDir")) } @@ -980,7 +1035,7 @@ func (s *Site) reReadFile(absFilePath string) (*source.File, error) { if err != nil { return nil, err } - file, err = source.NewFileFromAbs(s.absContentDir(), absFilePath, reader) + file, err = source.NewFileFromAbs(s.getContentDir(absFilePath), absFilePath, reader) if err != nil { return nil, err