mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
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
This commit is contained in:
parent
e70cf1ace4
commit
364e69ab7f
3 changed files with 124 additions and 10 deletions
|
@ -481,17 +481,17 @@ func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the root first
|
// Handle the root first
|
||||||
fileInfo, err := lstatIfOs(fs, root)
|
fileInfo, realPath, err := getRealFileInfo(fs, root)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return walker(root, nil, err)
|
return walker(root, nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileInfo.IsDir() {
|
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
|
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
|
// Code copied from Afero's path.go
|
||||||
// if the filesystem is OsFs use Lstat, else use fs.Stat
|
// if the filesystem is OsFs use Lstat, else use fs.Stat
|
||||||
func lstatIfOs(fs afero.Fs, path string) (info os.FileInfo, err error) {
|
func lstatIfOs(fs afero.Fs, path string) (info os.FileInfo, err error) {
|
||||||
|
|
|
@ -25,6 +25,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/viper"
|
"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) {
|
func TestMakePathRelative(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
inPath, path1, path2, output string
|
inPath, path1, path2, output string
|
||||||
|
|
|
@ -483,16 +483,15 @@ func (s *Site) reBuild(events []fsnotify.Event) (whatChanged, error) {
|
||||||
logger := helpers.NewDistinctFeedbackLogger()
|
logger := helpers.NewDistinctFeedbackLogger()
|
||||||
|
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
// Need to re-read source
|
if s.isContentDirEvent(ev) {
|
||||||
if strings.HasPrefix(ev.Name, s.absContentDir()) {
|
|
||||||
logger.Println("Source changed", ev.Name)
|
logger.Println("Source changed", ev.Name)
|
||||||
sourceChanged = append(sourceChanged, ev)
|
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)
|
logger.Println("Template changed", ev.Name)
|
||||||
tmplChanged = append(tmplChanged, ev)
|
tmplChanged = append(tmplChanged, ev)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(ev.Name, s.absDataDir()) {
|
if s.isDataDirEvent(ev) {
|
||||||
logger.Println("Data changed", ev.Name)
|
logger.Println("Data changed", ev.Name)
|
||||||
dataChanged = append(dataChanged, ev)
|
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.
|
// so we do this first to prevent races.
|
||||||
if ev.Op&fsnotify.Remove == fsnotify.Remove {
|
if ev.Op&fsnotify.Remove == fsnotify.Remove {
|
||||||
//remove the file & a create will follow
|
//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)
|
s.removePageByPath(path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -564,7 +563,7 @@ func (s *Site) reBuild(events []fsnotify.Event) (whatChanged, error) {
|
||||||
if ev.Op&fsnotify.Rename == fsnotify.Rename {
|
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 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 {
|
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)
|
s.removePageByPath(path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -948,18 +947,74 @@ func (s *Site) absI18nDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("I18nDir"))
|
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 {
|
func (s *Site) absThemeDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme"))
|
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 {
|
func (s *Site) absLayoutDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("LayoutDir"))
|
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 {
|
func (s *Site) absContentDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("ContentDir"))
|
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 {
|
func (s *Site) absPublishDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("PublishDir"))
|
return helpers.AbsPathify(viper.GetString("PublishDir"))
|
||||||
}
|
}
|
||||||
|
@ -980,7 +1035,7 @@ func (s *Site) reReadFile(absFilePath string) (*source.File, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
file, err = source.NewFileFromAbs(s.absContentDir(), absFilePath, reader)
|
file, err = source.NewFileFromAbs(s.getContentDir(absFilePath), absFilePath, reader)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in a new issue