shithub: hugo

Download patch

ref: 364e69ab7f54ab7a9901644647125f21cd39e98c
parent: e70cf1ace45498366d029e699af39441fab6bd0f
author: Bjørn Erik Pedersen <[email protected]>
date: Sun Jul 10 15:37:27 EDT 2016

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

--- a/helpers/path.go
+++ b/helpers/path.go
@@ -481,7 +481,7 @@
 	}
 
 	// Handle the root first
-	fileInfo, err := lstatIfOs(fs, root)
+	fileInfo, realPath, err := getRealFileInfo(fs, root)
 
 	if err != nil {
 		return walker(root, nil, err)
@@ -488,10 +488,10 @@
 	}
 
 	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
 	}
 
@@ -509,6 +509,40 @@
 
 	return nil
 
+}
+
+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
--- a/helpers/path_test.go
+++ b/helpers/path_test.go
@@ -25,6 +25,8 @@
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/assert"
+
 	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 )
@@ -139,6 +141,29 @@
 		}
 
 	}
+}
+
+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) {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -483,16 +483,15 @@
 	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 @@
 		// 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 @@
 		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 @@
 	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 @@
 	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