URI: 
       Handle symlink change event - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit 364e69ab7f54ab7a9901644647125f21cd39e98c
   DIR parent e70cf1ace45498366d029e699af39441fab6bd0f
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sun, 10 Jul 2016 19:37:27 +0200
       
       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
       
       Diffstat:
         M helpers/path.go                     |      40 ++++++++++++++++++++++++++++---
         M helpers/path_test.go                |      25 +++++++++++++++++++++++++
         M hugolib/site.go                     |      69 +++++++++++++++++++++++++++----
       
       3 files changed, 124 insertions(+), 10 deletions(-)
       ---
   DIR diff --git 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) {
   DIR diff --git 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
   DIR diff --git 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