Add support for multiple staticDirs

This commit adds support for multiple statDirs both on the global and language level.

A simple `config.toml` example:

```bash
staticDir = ["static1", "static2"]
[languages]
[languages.no]
staticDir = ["staticDir_override", "static_no"]
baseURL = "https://example.no"
languageName = "Norsk"
weight = 1
title = "På norsk"

[languages.en]
staticDir2 = "static_en"
baseURL = "https://example.com"
languageName = "English"
weight = 2
title = "In English"
```

In the above, with no theme used:

the English site will get its static files as a union of "static1", "static2" and "static_en". On file duplicates, the right-most version will win.
the Norwegian site will get its static files as a union of "staticDir_override" and "static_no".

This commit also concludes the Multihost support in #4027.

Fixes #36
Closes #4027
This commit is contained in:
Bjørn Erik Pedersen 2017-11-12 10:03:56 +01:00
parent 2e0465764b
commit 60dfb9a6e0
25 changed files with 825 additions and 273 deletions

6
Gopkg.lock generated
View file

@ -193,10 +193,10 @@
revision = "86672fcb3f950f35f2e675df2240550f2a50762f" revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
[[projects]] [[projects]]
branch = "master"
name = "github.com/spf13/afero" name = "github.com/spf13/afero"
packages = [".","mem"] packages = [".","mem"]
revision = "5660eeed305fe5f69c8fc6cf899132a459a97064" revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536"
version = "v1.0.0"
[[projects]] [[projects]]
name = "github.com/spf13/cast" name = "github.com/spf13/cast"
@ -285,6 +285,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "271e5ca84d4f9c63392ca282b940207c0c96995efb3a0a9fbc43114b0669bfa0" inputs-digest = "a7cec7b1df49f84fdd4073cc70139d56c62c5fffcc7e3fcea5ca29615d4b9568"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View file

@ -81,8 +81,8 @@
version = "1.5.0" version = "1.5.0"
[[constraint]] [[constraint]]
branch = "master"
name = "github.com/spf13/afero" name = "github.com/spf13/afero"
version = "1.0.0"
[[constraint]] [[constraint]]
name = "github.com/spf13/cast" name = "github.com/spf13/cast"

View file

@ -24,6 +24,7 @@ type commandeer struct {
*deps.DepsCfg *deps.DepsCfg
pathSpec *helpers.PathSpec pathSpec *helpers.PathSpec
visitedURLs *types.EvictingStringQueue visitedURLs *types.EvictingStringQueue
configured bool configured bool
} }

View file

@ -22,7 +22,6 @@ import (
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -30,6 +29,8 @@ import (
"sync" "sync"
"time" "time"
src "github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
@ -526,8 +527,7 @@ func (c *commandeer) watchConfig() {
func (c *commandeer) build(watches ...bool) error { func (c *commandeer) build(watches ...bool) error {
if err := c.copyStatic(); err != nil { if err := c.copyStatic(); err != nil {
// TODO(bep) multihost return fmt.Errorf("Error copying static files: %s", err)
return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err)
} }
watch := false watch := false
if len(watches) > 0 && watches[0] { if len(watches) > 0 && watches[0] {
@ -538,88 +538,64 @@ func (c *commandeer) build(watches ...bool) error {
} }
if buildWatch { if buildWatch {
watchDirs, err := c.getDirList()
if err != nil {
return err
}
c.Logger.FEEDBACK.Println("Watching for changes in", c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir"))) c.Logger.FEEDBACK.Println("Watching for changes in", c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")))
c.Logger.FEEDBACK.Println("Press Ctrl+C to stop") c.Logger.FEEDBACK.Println("Press Ctrl+C to stop")
utils.CheckErr(c.Logger, c.newWatcher(0)) utils.CheckErr(c.Logger, c.newWatcher(false, watchDirs...))
} }
return nil return nil
} }
func (c *commandeer) getStaticSourceFs() afero.Fs {
source := c.Fs.Source
themeDir, err := c.PathSpec().GetThemeStaticDirPath()
staticDir := c.PathSpec().GetStaticDirPath() + helpers.FilePathSeparator
useTheme := true
useStatic := true
if err != nil {
if err != helpers.ErrThemeUndefined {
c.Logger.WARN.Println(err)
}
useTheme = false
} else {
if _, err := source.Stat(themeDir); os.IsNotExist(err) {
c.Logger.WARN.Println("Unable to find Theme Static Directory:", themeDir)
useTheme = false
}
}
if _, err := source.Stat(staticDir); os.IsNotExist(err) {
c.Logger.WARN.Println("Unable to find Static Directory:", staticDir)
useStatic = false
}
if !useStatic && !useTheme {
return nil
}
if !useStatic {
c.Logger.INFO.Println(themeDir, "is the only static directory available to sync from")
return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir))
}
if !useTheme {
c.Logger.INFO.Println(staticDir, "is the only static directory available to sync from")
return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir))
}
c.Logger.INFO.Println("using a UnionFS for static directory comprised of:")
c.Logger.INFO.Println("Base:", themeDir)
c.Logger.INFO.Println("Overlay:", staticDir)
base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir))
overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir))
return afero.NewCopyOnWriteFs(base, overlay)
}
func (c *commandeer) copyStatic() error { func (c *commandeer) copyStatic() error {
publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator return c.doWithPublishDirs(c.copyStaticTo)
roots := c.roots()
if len(roots) == 0 {
return c.copyStaticTo(publishDir)
} }
for _, root := range roots { func (c *commandeer) doWithPublishDirs(f func(dirs *src.Dirs, publishDir string) error) error {
dir := filepath.Join(publishDir, root) publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator
if err := c.copyStaticTo(dir); err != nil { // If root, remove the second '/'
if publishDir == "//" {
publishDir = helpers.FilePathSeparator
}
languages := c.languages()
if !languages.IsMultihost() {
dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger)
if err != nil {
return err
}
return f(dirs, publishDir)
}
for _, l := range languages {
dir := filepath.Join(publishDir, l.Lang)
dirs, err := src.NewDirs(c.Fs, l, c.DepsCfg.Logger)
if err != nil {
return err
}
if err := f(dirs, dir); err != nil {
return err return err
} }
} }
return nil return nil
} }
func (c *commandeer) copyStaticTo(publishDir string) error { func (c *commandeer) copyStaticTo(dirs *src.Dirs, publishDir string) error {
// If root, remove the second '/' // If root, remove the second '/'
if publishDir == "//" { if publishDir == "//" {
publishDir = helpers.FilePathSeparator publishDir = helpers.FilePathSeparator
} }
// Includes both theme/static & /static staticSourceFs, err := dirs.CreateStaticFs()
staticSourceFs := c.getStaticSourceFs() if err != nil {
return err
}
if staticSourceFs == nil { if staticSourceFs == nil {
c.Logger.WARN.Println("No static directories found to sync") c.Logger.WARN.Println("No static directories found to sync")
@ -650,12 +626,17 @@ func (c *commandeer) copyStaticTo(publishDir string) error {
} }
// getDirList provides NewWatcher() with a list of directories to watch for changes. // getDirList provides NewWatcher() with a list of directories to watch for changes.
func (c *commandeer) getDirList() []string { func (c *commandeer) getDirList() ([]string, error) {
var a []string var a []string
dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir")) dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir"))
i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir")) i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir"))
staticSyncer, err := newStaticSyncer(c)
if err != nil {
return nil, err
}
layoutDir := c.PathSpec().GetLayoutDirPath() layoutDir := c.PathSpec().GetLayoutDirPath()
staticDir := c.PathSpec().GetStaticDirPath() staticDirs := staticSyncer.d.AbsStaticDirs
walker := func(path string, fi os.FileInfo, err error) error { walker := func(path string, fi os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -674,12 +655,12 @@ func (c *commandeer) getDirList() []string {
return nil return nil
} }
if os.IsNotExist(err) {
for _, staticDir := range staticDirs {
if path == staticDir && os.IsNotExist(err) { if path == staticDir && os.IsNotExist(err) {
c.Logger.WARN.Println("Skip staticDir:", err) c.Logger.WARN.Println("Skip staticDir:", err)
return nil
} }
}
if os.IsNotExist(err) {
// Ignore. // Ignore.
return nil return nil
} }
@ -726,17 +707,18 @@ func (c *commandeer) getDirList() []string {
_ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker) _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker)
_ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker)
_ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker) _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker)
for _, staticDir := range staticDirs {
_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker)
}
if c.PathSpec().ThemeSet() { if c.PathSpec().ThemeSet() {
themesDir := c.PathSpec().GetThemeDir() themesDir := c.PathSpec().GetThemeDir()
_ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker)
_ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "static"), walker)
_ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker)
_ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker)
} }
return a return a, nil
} }
func (c *commandeer) recreateAndBuildSites(watching bool) (err error) { func (c *commandeer) recreateAndBuildSites(watching bool) (err error) {
@ -798,11 +780,18 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
} }
// newWatcher creates a new watcher to watch filesystem events. // newWatcher creates a new watcher to watch filesystem events.
func (c *commandeer) newWatcher(port int) error { // if serve is set it will also start one or more HTTP servers to serve those
// files.
func (c *commandeer) newWatcher(serve bool, dirList ...string) error {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
tweakLimit() tweakLimit()
} }
staticSyncer, err := newStaticSyncer(c)
if err != nil {
return err
}
watcher, err := watcher.New(1 * time.Second) watcher, err := watcher.New(1 * time.Second)
var wg sync.WaitGroup var wg sync.WaitGroup
@ -814,7 +803,7 @@ func (c *commandeer) newWatcher(port int) error {
wg.Add(1) wg.Add(1)
for _, d := range c.getDirList() { for _, d := range dirList {
if d != "" { if d != "" {
_ = watcher.Add(d) _ = watcher.Add(d)
} }
@ -874,7 +863,7 @@ func (c *commandeer) newWatcher(port int) error {
if err := watcher.Add(path); err != nil { if err := watcher.Add(path); err != nil {
return err return err
} }
} else if !c.isStatic(path) { } else if !staticSyncer.isStatic(path) {
// Hugo's rebuilding logic is entirely file based. When you drop a new folder into // Hugo's rebuilding logic is entirely file based. When you drop a new folder into
// /content on OSX, the above logic will handle future watching of those files, // /content on OSX, the above logic will handle future watching of those files,
// but the initial CREATE is lost. // but the initial CREATE is lost.
@ -891,7 +880,7 @@ func (c *commandeer) newWatcher(port int) error {
} }
} }
if c.isStatic(ev.Name) { if staticSyncer.isStatic(ev.Name) {
staticEvents = append(staticEvents, ev) staticEvents = append(staticEvents, ev)
} else { } else {
dynamicEvents = append(dynamicEvents, ev) dynamicEvents = append(dynamicEvents, ev)
@ -899,101 +888,21 @@ func (c *commandeer) newWatcher(port int) error {
} }
if len(staticEvents) > 0 { if len(staticEvents) > 0 {
publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator
// If root, remove the second '/'
if publishDir == "//" {
publishDir = helpers.FilePathSeparator
}
c.Logger.FEEDBACK.Println("\nStatic file changes detected") c.Logger.FEEDBACK.Println("\nStatic file changes detected")
const layout = "2006-01-02 15:04:05.000 -0700" const layout = "2006-01-02 15:04:05.000 -0700"
c.Logger.FEEDBACK.Println(time.Now().Format(layout)) c.Logger.FEEDBACK.Println(time.Now().Format(layout))
if c.Cfg.GetBool("forceSyncStatic") { if c.Cfg.GetBool("forceSyncStatic") {
c.Logger.FEEDBACK.Printf("Syncing all static files\n") c.Logger.FEEDBACK.Printf("Syncing all static files\n")
// TODO(bep) multihost
err := c.copyStatic() err := c.copyStatic()
if err != nil { if err != nil {
utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir)) utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir")
} }
} else { } else {
staticSourceFs := c.getStaticSourceFs() if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
if staticSourceFs == nil {
c.Logger.WARN.Println("No static directories found to sync")
return
}
syncer := fsync.NewSyncer()
syncer.NoTimes = c.Cfg.GetBool("noTimes")
syncer.NoChmod = c.Cfg.GetBool("noChmod")
syncer.SrcFs = staticSourceFs
syncer.DestFs = c.Fs.Destination
// prevent spamming the log on changes
logger := helpers.NewDistinctFeedbackLogger()
for _, ev := range staticEvents {
// Due to our approach of layering both directories and the content's rendered output
// into one we can't accurately remove a file not in one of the source directories.
// If a file is in the local static dir and also in the theme static dir and we remove
// it from one of those locations we expect it to still exist in the destination
//
// If Hugo generates a file (from the content dir) over a static file
// the content generated file should take precedence.
//
// Because we are now watching and handling individual events it is possible that a static
// event that occupies the same path as a content generated file will take precedence
// until a regeneration of the content takes places.
//
// Hugo assumes that these cases are very rare and will permit this bad behavior
// The alternative is to track every single file and which pipeline rendered it
// and then to handle conflict resolution on every event.
fromPath := ev.Name
// If we are here we already know the event took place in a static dir
relPath, err := c.PathSpec().MakeStaticPathRelative(fromPath)
if err != nil {
c.Logger.ERROR.Println(err) c.Logger.ERROR.Println(err)
continue continue
} }
// Remove || rename is harder and will require an assumption.
// Hugo takes the following approach:
// If the static file exists in any of the static source directories after this event
// Hugo will re-sync it.
// If it does not exist in all of the static directories Hugo will remove it.
//
// This assumes that Hugo has not generated content on top of a static file and then removed
// the source of that static file. In this case Hugo will incorrectly remove that file
// from the published directory.
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) {
// If file doesn't exist in any static dir, remove it
toRemove := filepath.Join(publishDir, relPath)
logger.Println("File no longer exists in static dir, removing", toRemove)
_ = c.Fs.Destination.RemoveAll(toRemove)
} else if err == nil {
// If file still exists, sync it
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err)
}
} else {
c.Logger.ERROR.Println(err)
}
continue
}
// For all other event operations Hugo will sync static.
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err)
}
}
} }
if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { if !buildWatch && !c.Cfg.GetBool("disableLiveReload") {
@ -1002,7 +911,7 @@ func (c *commandeer) newWatcher(port int) error {
// force refresh when more than one file // force refresh when more than one file
if len(staticEvents) > 0 { if len(staticEvents) > 0 {
for _, ev := range staticEvents { for _, ev := range staticEvents {
path, _ := c.PathSpec().MakeStaticPathRelative(ev.Name) path := staticSyncer.d.MakeStaticPathRelative(ev.Name)
livereload.RefreshPath(path) livereload.RefreshPath(path)
} }
@ -1044,7 +953,7 @@ func (c *commandeer) newWatcher(port int) error {
} }
if p != nil { if p != nil {
livereload.NavigateToPath(p.RelPermalink()) livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
} else { } else {
livereload.ForceRefresh() livereload.ForceRefresh()
} }
@ -1058,14 +967,8 @@ func (c *commandeer) newWatcher(port int) error {
} }
}() }()
if port > 0 { if serve {
if !c.Cfg.GetBool("disableLiveReload") { go c.serve()
livereload.Initialize()
http.HandleFunc("/livereload.js", livereload.ServeJS)
http.HandleFunc("/livereload", livereload.Handler)
}
go c.serve(port)
} }
wg.Wait() wg.Wait()
@ -1084,10 +987,6 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
return name return name
} }
func (c *commandeer) isStatic(path string) bool {
return strings.HasPrefix(path, c.PathSpec().GetStaticDirPath()) || (len(c.PathSpec().GetThemesDirPath()) > 0 && strings.HasPrefix(path, c.PathSpec().GetThemesDirPath()))
}
// isThemeVsHugoVersionMismatch returns whether the current Hugo version is // isThemeVsHugoVersionMismatch returns whether the current Hugo version is
// less than the theme's min_version. // less than the theme's min_version.
func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) {

View file

@ -25,6 +25,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
@ -189,7 +191,7 @@ func server(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
c.Cfg.Set("baseURL", baseURL) c.Set("baseURL", baseURL)
} }
if err := memStats(); err != nil { if err := memStats(); err != nil {
@ -218,16 +220,22 @@ func server(cmd *cobra.Command, args []string) error {
// Watch runs its own server as part of the routine // Watch runs its own server as part of the routine
if serverWatch { if serverWatch {
watchDirs := c.getDirList()
baseWatchDir := c.Cfg.GetString("workingDir") watchDirs, err := c.getDirList()
for i, dir := range watchDirs { if err != nil {
watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) return err
} }
rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",") baseWatchDir := c.Cfg.GetString("workingDir")
relWatchDirs := make([]string, len(watchDirs))
for i, dir := range watchDirs {
relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir)
}
rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(relWatchDirs)), ",")
jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
err := c.newWatcher(serverPort) err = c.newWatcher(true, watchDirs...)
if err != nil { if err != nil {
return err return err
@ -238,7 +246,7 @@ func server(cmd *cobra.Command, args []string) error {
} }
type fileServer struct { type fileServer struct {
basePort int ports []int
baseURLs []string baseURLs []string
roots []string roots []string
c *commandeer c *commandeer
@ -247,7 +255,7 @@ type fileServer struct {
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) { func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
baseURL := f.baseURLs[i] baseURL := f.baseURLs[i]
root := f.roots[i] root := f.roots[i]
port := f.basePort + i port := f.ports[i]
publishDir := f.c.Cfg.GetString("publishDir") publishDir := f.c.Cfg.GetString("publishDir")
@ -257,12 +265,13 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
absPublishDir := f.c.PathSpec().AbsPathify(publishDir) absPublishDir := f.c.PathSpec().AbsPathify(publishDir)
// TODO(bep) multihost unify feedback if i == 0 {
if renderToDisk { if renderToDisk {
jww.FEEDBACK.Println("Serving pages from " + absPublishDir) jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
} else { } else {
jww.FEEDBACK.Println("Serving pages from memory") jww.FEEDBACK.Println("Serving pages from memory")
} }
}
httpFs := afero.NewHttpFs(f.c.Fs.Destination) httpFs := afero.NewHttpFs(f.c.Fs.Destination)
fs := filesOnlyFs{httpFs.Dir(absPublishDir)} fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
@ -270,7 +279,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
doLiveReload := !buildWatch && !f.c.Cfg.GetBool("disableLiveReload") doLiveReload := !buildWatch && !f.c.Cfg.GetBool("disableLiveReload")
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender") fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")
if fastRenderMode { if i == 0 && fastRenderMode {
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
} }
@ -311,49 +320,50 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
return mu, endpoint, nil return mu, endpoint, nil
} }
func (c *commandeer) roots() []string { func (c *commandeer) serve() {
var roots []string
languages := c.languages()
isMultiHost := languages.IsMultihost()
if !isMultiHost {
return roots
}
for _, l := range languages {
roots = append(roots, l.Lang)
}
return roots
}
func (c *commandeer) serve(port int) {
// TODO(bep) multihost
isMultiHost := Hugo.IsMultihost() isMultiHost := Hugo.IsMultihost()
var ( var (
baseURLs []string baseURLs []string
roots []string roots []string
ports []int
) )
if isMultiHost { if isMultiHost {
for _, s := range Hugo.Sites { for _, s := range Hugo.Sites {
baseURLs = append(baseURLs, s.BaseURL.String()) baseURLs = append(baseURLs, s.BaseURL.String())
roots = append(roots, s.Language.Lang) roots = append(roots, s.Language.Lang)
ports = append(ports, s.Info.ServerPort())
} }
} else { } else {
baseURLs = []string{Hugo.Sites[0].BaseURL.String()} s := Hugo.Sites[0]
baseURLs = []string{s.BaseURL.String()}
roots = []string{""} roots = []string{""}
ports = append(ports, s.Info.ServerPort())
} }
srv := &fileServer{ srv := &fileServer{
basePort: port, ports: ports,
baseURLs: baseURLs, baseURLs: baseURLs,
roots: roots, roots: roots,
c: c, c: c,
} }
doLiveReload := !c.Cfg.GetBool("disableLiveReload")
if doLiveReload {
livereload.Initialize()
}
for i, _ := range baseURLs { for i, _ := range baseURLs {
mu, endpoint, err := srv.createEndpoint(i) mu, endpoint, err := srv.createEndpoint(i)
if doLiveReload {
mu.HandleFunc("/livereload.js", livereload.ServeJS)
mu.HandleFunc("/livereload", livereload.Handler)
}
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", endpoint, serverInterface)
go func() { go func() {
err = http.ListenAndServe(endpoint, mu) err = http.ListenAndServe(endpoint, mu)
if err != nil { if err != nil {
@ -363,7 +373,6 @@ func (c *commandeer) serve(port int) {
}() }()
} }
// TODO(bep) multihost jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface)
jww.FEEDBACK.Println("Press Ctrl+C to stop") jww.FEEDBACK.Println("Press Ctrl+C to stop")
} }

135
commands/static_syncer.go Normal file
View file

@ -0,0 +1,135 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"os"
"path/filepath"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
src "github.com/gohugoio/hugo/source"
"github.com/spf13/fsync"
)
type staticSyncer struct {
c *commandeer
d *src.Dirs
}
func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger)
if err != nil {
return nil, err
}
return &staticSyncer{c: c, d: dirs}, nil
}
func (s *staticSyncer) isStatic(path string) bool {
return s.d.IsStatic(path)
}
func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
c := s.c
syncFn := func(dirs *src.Dirs, publishDir string) error {
staticSourceFs, err := dirs.CreateStaticFs()
if err != nil {
return err
}
if staticSourceFs == nil {
c.Logger.WARN.Println("No static directories found to sync")
return nil
}
syncer := fsync.NewSyncer()
syncer.NoTimes = c.Cfg.GetBool("noTimes")
syncer.NoChmod = c.Cfg.GetBool("noChmod")
syncer.SrcFs = staticSourceFs
syncer.DestFs = c.Fs.Destination
// prevent spamming the log on changes
logger := helpers.NewDistinctFeedbackLogger()
for _, ev := range staticEvents {
// Due to our approach of layering both directories and the content's rendered output
// into one we can't accurately remove a file not in one of the source directories.
// If a file is in the local static dir and also in the theme static dir and we remove
// it from one of those locations we expect it to still exist in the destination
//
// If Hugo generates a file (from the content dir) over a static file
// the content generated file should take precedence.
//
// Because we are now watching and handling individual events it is possible that a static
// event that occupies the same path as a content generated file will take precedence
// until a regeneration of the content takes places.
//
// Hugo assumes that these cases are very rare and will permit this bad behavior
// The alternative is to track every single file and which pipeline rendered it
// and then to handle conflict resolution on every event.
fromPath := ev.Name
// If we are here we already know the event took place in a static dir
relPath := dirs.MakeStaticPathRelative(fromPath)
if relPath == "" {
// Not member of this virtual host.
continue
}
// Remove || rename is harder and will require an assumption.
// Hugo takes the following approach:
// If the static file exists in any of the static source directories after this event
// Hugo will re-sync it.
// If it does not exist in all of the static directories Hugo will remove it.
//
// This assumes that Hugo has not generated content on top of a static file and then removed
// the source of that static file. In this case Hugo will incorrectly remove that file
// from the published directory.
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) {
// If file doesn't exist in any static dir, remove it
toRemove := filepath.Join(publishDir, relPath)
logger.Println("File no longer exists in static dir, removing", toRemove)
_ = c.Fs.Destination.RemoveAll(toRemove)
} else if err == nil {
// If file still exists, sync it
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err)
}
} else {
c.Logger.ERROR.Println(err)
}
continue
}
// For all other event operations Hugo will sync static.
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err)
}
}
return nil
}
return c.doWithPublishDirs(syncFn)
}

View file

@ -170,7 +170,7 @@ func (p *PathSpec) GetLayoutDirPath() string {
// GetStaticDirPath returns the absolute path to the static file dir // GetStaticDirPath returns the absolute path to the static file dir
// for the current Hugo project. // for the current Hugo project.
func (p *PathSpec) GetStaticDirPath() string { func (p *PathSpec) GetStaticDirPath() string {
return p.AbsPathify(p.staticDir) return p.AbsPathify(p.StaticDir())
} }
// GetThemeDir gets the root directory of the current theme, if there is one. // GetThemeDir gets the root directory of the current theme, if there is one.

View file

@ -59,7 +59,8 @@ func TestMakePath(t *testing.T) {
v := viper.New() v := viper.New()
l := NewDefaultLanguage(v) l := NewDefaultLanguage(v)
v.Set("removePathAccents", test.removeAccents) v.Set("removePathAccents", test.removeAccents)
p, _ := NewPathSpec(hugofs.NewMem(v), l) p, err := NewPathSpec(hugofs.NewMem(v), l)
require.NoError(t, err)
output := p.MakePath(test.input) output := p.MakePath(test.input)
if output != test.expected { if output != test.expected {

View file

@ -40,7 +40,7 @@ type PathSpec struct {
themesDir string themesDir string
layoutDir string layoutDir string
workingDir string workingDir string
staticDir string staticDirs []string
// The PathSpec looks up its config settings in both the current language // The PathSpec looks up its config settings in both the current language
// and then in the global Viper config. // and then in the global Viper config.
@ -72,6 +72,12 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) {
return nil, fmt.Errorf("Failed to create baseURL from %q: %s", baseURLstr, err) return nil, fmt.Errorf("Failed to create baseURL from %q: %s", baseURLstr, err)
} }
var staticDirs []string
for i := -1; i <= 10; i++ {
staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...)
}
ps := &PathSpec{ ps := &PathSpec{
Fs: fs, Fs: fs,
Cfg: cfg, Cfg: cfg,
@ -87,7 +93,7 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) {
themesDir: cfg.GetString("themesDir"), themesDir: cfg.GetString("themesDir"),
layoutDir: cfg.GetString("layoutDir"), layoutDir: cfg.GetString("layoutDir"),
workingDir: cfg.GetString("workingDir"), workingDir: cfg.GetString("workingDir"),
staticDir: cfg.GetString("staticDir"), staticDirs: staticDirs,
theme: cfg.GetString("theme"), theme: cfg.GetString("theme"),
} }
@ -98,6 +104,25 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) {
return ps, nil return ps, nil
} }
func getStringOrStringSlice(cfg config.Provider, key string, id int) []string {
if id > 0 {
key = fmt.Sprintf("%s%d", key, id)
}
var out []string
sd := cfg.Get(key)
if sds, ok := sd.(string); ok {
out = []string{sds}
} else if sdsl, ok := sd.([]string); ok {
out = sdsl
}
return out
}
// PaginatePath returns the configured root path used for paginator pages. // PaginatePath returns the configured root path used for paginator pages.
func (p *PathSpec) PaginatePath() string { func (p *PathSpec) PaginatePath() string {
return p.paginatePath return p.paginatePath
@ -108,7 +133,17 @@ func (p *PathSpec) WorkingDir() string {
return p.workingDir return p.workingDir
} }
// LayoutDir returns the relative layout dir in the currenct Hugo project. // StaticDir returns the relative static dir in the current configuration.
func (p *PathSpec) StaticDir() string {
return p.staticDirs[len(p.staticDirs)-1]
}
// StaticDirs returns the relative static dirs for the current configuration.
func (p *PathSpec) StaticDirs() []string {
return p.staticDirs
}
// LayoutDir returns the relative layout dir in the current configuration.
func (p *PathSpec) LayoutDir() string { func (p *PathSpec) LayoutDir() string {
return p.layoutDir return p.layoutDir
} }
@ -117,3 +152,8 @@ func (p *PathSpec) LayoutDir() string {
func (p *PathSpec) Theme() string { func (p *PathSpec) Theme() string {
return p.theme return p.theme
} }
// Theme returns the theme relative theme dir.
func (p *PathSpec) ThemesDir() string {
return p.themesDir
}

View file

@ -57,6 +57,6 @@ func TestNewPathSpecFromConfig(t *testing.T) {
require.Equal(t, "thethemes", p.themesDir) require.Equal(t, "thethemes", p.themesDir)
require.Equal(t, "thelayouts", p.layoutDir) require.Equal(t, "thelayouts", p.layoutDir)
require.Equal(t, "thework", p.workingDir) require.Equal(t, "thework", p.workingDir)
require.Equal(t, "thestatic", p.staticDir) require.Equal(t, "thestatic", p.StaticDir())
require.Equal(t, "thetheme", p.theme) require.Equal(t, "thetheme", p.theme)
} }

View file

@ -14,6 +14,7 @@
package hugolib package hugolib
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
@ -88,7 +89,7 @@ func LoadConfig(fs afero.Fs, relativeSourcePath, configFilename string) (*viper.
return v, nil return v, nil
} }
func loadLanguageSettings(cfg config.Provider) error { func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error {
multilingual := cfg.GetStringMap("languages") multilingual := cfg.GetStringMap("languages")
var ( var (
langs helpers.Languages langs helpers.Languages
@ -104,7 +105,56 @@ func loadLanguageSettings(cfg config.Provider) error {
} }
} }
if oldLangs != nil {
// When in multihost mode, the languages are mapped to a server, so
// some structural language changes will need a restart of the dev server.
// The validation below isn't complete, but should cover the most
// important cases.
var invalid bool
if langs.IsMultihost() != oldLangs.IsMultihost() {
invalid = true
} else {
if langs.IsMultihost() && len(langs) != len(oldLangs) {
invalid = true
}
}
if invalid {
return errors.New("language change needing a server restart detected")
}
if langs.IsMultihost() {
// We need to transfer any server baseURL to the new language
for i, ol := range oldLangs {
nl := langs[i]
nl.Set("baseURL", ol.GetString("baseURL"))
}
}
}
cfg.Set("languagesSorted", langs) cfg.Set("languagesSorted", langs)
cfg.Set("multilingual", len(langs) > 1)
// The baseURL may be provided at the language level. If that is true,
// then every language must have a baseURL. In this case we always render
// to a language sub folder, which is then stripped from all the Permalink URLs etc.
var baseURLFromLang bool
for _, l := range langs {
burl := l.GetLocal("baseURL")
if baseURLFromLang && burl == nil {
return errors.New("baseURL must be set on all or none of the languages")
}
if burl != nil {
baseURLFromLang = true
}
}
if baseURLFromLang {
cfg.Set("defaultContentLanguageInSubdir", true)
cfg.Set("multihost", true)
}
return nil return nil
} }
@ -178,5 +228,5 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
v.SetDefault("debug", false) v.SetDefault("debug", false)
v.SetDefault("disableFastRender", false) v.SetDefault("disableFastRender", false)
return loadLanguageSettings(v) return loadLanguageSettings(v, nil)
} }

View file

@ -83,46 +83,19 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
h := &HugoSites{ h := &HugoSites{
multilingual: langConfig, multilingual: langConfig,
multihost: cfg.Cfg.GetBool("multihost"),
Sites: sites} Sites: sites}
for _, s := range sites { for _, s := range sites {
s.owner = h s.owner = h
} }
// TODO(bep)
cfg.Cfg.Set("multilingual", sites[0].multilingualEnabled())
if err := applyDepsIfNeeded(cfg, sites...); err != nil { if err := applyDepsIfNeeded(cfg, sites...); err != nil {
return nil, err return nil, err
} }
h.Deps = sites[0].Deps h.Deps = sites[0].Deps
// The baseURL may be provided at the language level. If that is true,
// then every language must have a baseURL. In this case we always render
// to a language sub folder, which is then stripped from all the Permalink URLs etc.
var baseURLFromLang bool
for _, s := range sites {
burl := s.Language.GetLocal("baseURL")
if baseURLFromLang && burl == nil {
return h, errors.New("baseURL must be set on all or none of the languages")
}
if burl != nil {
baseURLFromLang = true
}
}
if baseURLFromLang {
for _, s := range sites {
// TODO(bep) multihost check
s.Info.defaultContentLanguageInSubdir = true
s.Cfg.Set("defaultContentLanguageInSubdir", true)
}
h.multihost = true
}
return h, nil return h, nil
} }
@ -237,8 +210,9 @@ func (h *HugoSites) reset() {
} }
func (h *HugoSites) createSitesFromConfig() error { func (h *HugoSites) createSitesFromConfig() error {
oldLangs, _ := h.Cfg.Get("languagesSorted").(helpers.Languages)
if err := loadLanguageSettings(h.Cfg); err != nil { if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil {
return err return err
} }
@ -269,6 +243,7 @@ func (h *HugoSites) createSitesFromConfig() error {
h.Deps = sites[0].Deps h.Deps = sites[0].Deps
h.multilingual = langConfig h.multilingual = langConfig
h.multihost = h.Deps.Cfg.GetBool("multihost")
return nil return nil
} }

View file

@ -1035,7 +1035,7 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf
if err := afero.WriteFile(mf, if err := afero.WriteFile(mf,
filepath.Join("layouts", "_default/list.html"), filepath.Join("layouts", "_default/list.html"),
[]byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}"),
0755); err != nil { 0755); err != nil {
t.Fatalf("Failed to write layout file: %s", err) t.Fatalf("Failed to write layout file: %s", err)
} }

View file

@ -69,4 +69,10 @@ languageName = "Nynorsk"
th.assertFileContentStraight("public/fr/index.html", "French Home Page") th.assertFileContentStraight("public/fr/index.html", "French Home Page")
th.assertFileContentStraight("public/en/index.html", "Default Home Page") th.assertFileContentStraight("public/en/index.html", "Default Home Page")
// Check paginators
th.assertFileContent("public/en/page/1/index.html", `refresh" content="0; url=https://example.com/"`)
th.assertFileContent("public/nn/page/1/index.html", `refresh" content="0; url=https://example.no/"`)
th.assertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "https://example.com/sect/", "\"/sect/page/3/")
th.assertFileContent("public/fr/sect/page/2/index.html", "List Page 2", "Bonjour", "https://example.fr/sect/")
} }

View file

@ -1755,7 +1755,6 @@ func (p *Page) shouldAddLanguagePrefix() bool {
} }
if p.s.owner.IsMultihost() { if p.s.owner.IsMultihost() {
// TODO(bep) multihost check vs lang below
return true return true
} }

View file

@ -41,7 +41,7 @@ type PageOutput struct {
} }
func (p *PageOutput) targetPath(addends ...string) (string, error) { func (p *PageOutput) targetPath(addends ...string) (string, error) {
tp, err := p.createTargetPath(p.outputFormat, addends...) tp, err := p.createTargetPath(p.outputFormat, false, addends...)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -125,12 +125,16 @@ func (p *Page) initTargetPathDescriptor() error {
// createTargetPath creates the target filename for this Page for the given // createTargetPath creates the target filename for this Page for the given
// output.Format. Some additional URL parts can also be provided, the typical // output.Format. Some additional URL parts can also be provided, the typical
// use case being pagination. // use case being pagination.
func (p *Page) createTargetPath(t output.Format, addends ...string) (string, error) { func (p *Page) createTargetPath(t output.Format, noLangPrefix bool, addends ...string) (string, error) {
d, err := p.createTargetPathDescriptor(t) d, err := p.createTargetPathDescriptor(t)
if err != nil { if err != nil {
return "", nil return "", nil
} }
if noLangPrefix {
d.LangPrefix = ""
}
if len(addends) > 0 { if len(addends) > 0 {
d.Addends = filepath.Join(addends...) d.Addends = filepath.Join(addends...)
} }
@ -246,7 +250,7 @@ func (p *Page) createRelativePermalink() string {
} }
func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string { func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string {
tp, err := p.createTargetPath(f) tp, err := p.createTargetPath(f, p.s.owner.IsMultihost())
if err != nil { if err != nil {
p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err) p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err)
@ -257,10 +261,6 @@ func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string {
tp = strings.TrimSuffix(tp, f.BaseFilename()) tp = strings.TrimSuffix(tp, f.BaseFilename())
} }
if p.s.owner.IsMultihost() {
tp = strings.TrimPrefix(tp, helpers.FilePathSeparator+p.s.Info.Language.Lang)
}
return p.s.PathSpec.URLizeFilename(tp) return p.s.PathSpec.URLizeFilename(tp)
} }

View file

@ -285,7 +285,11 @@ func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) {
return return
} }
pagers, err := paginatePages(p.targetPathDescriptor, p.Data["Pages"], pagerSize) pathDescriptor := p.targetPathDescriptor
if p.s.owner.IsMultihost() {
pathDescriptor.LangPrefix = ""
}
pagers, err := paginatePages(pathDescriptor, p.Data["Pages"], pagerSize)
if err != nil { if err != nil {
initError = err initError = err
@ -333,7 +337,12 @@ func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager,
if p.paginator != nil { if p.paginator != nil {
return return
} }
pagers, err := paginatePages(p.targetPathDescriptor, seq, pagerSize)
pathDescriptor := p.targetPathDescriptor
if p.s.owner.IsMultihost() {
pathDescriptor.LangPrefix = ""
}
pagers, err := paginatePages(pathDescriptor, seq, pagerSize)
if err != nil { if err != nil {
initError = err initError = err
@ -528,7 +537,6 @@ func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory {
targetPath := createTargetPath(pathDescriptor) targetPath := createTargetPath(pathDescriptor)
targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename()) targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename())
link := d.PathSpec.PrependBasePath(targetPath) link := d.PathSpec.PrependBasePath(targetPath)
// Note: The targetPath is massaged with MakePathSanitized // Note: The targetPath is massaged with MakePathSanitized
return d.PathSpec.URLizeFilename(link) return d.PathSpec.URLizeFilename(link)
} }

View file

@ -393,6 +393,19 @@ func (s *SiteInfo) BaseURL() template.URL {
return template.URL(s.s.PathSpec.BaseURL.String()) return template.URL(s.s.PathSpec.BaseURL.String())
} }
// ServerPort returns the port part of the BaseURL, 0 if none found.
func (s *SiteInfo) ServerPort() int {
ps := s.s.PathSpec.BaseURL.URL().Port()
if ps == "" {
return 0
}
p, err := strconv.Atoi(ps)
if err != nil {
return 0
}
return p
}
// Used in tests. // Used in tests.
type siteBuilderCfg struct { type siteBuilderCfg struct {
@ -1806,7 +1819,7 @@ func (s *Site) renderAndWriteXML(name string, dest string, d interface{}, layout
if s.Info.relativeURLs { if s.Info.relativeURLs {
path = []byte(helpers.GetDottedRelativePath(dest)) path = []byte(helpers.GetDottedRelativePath(dest))
} else { } else {
s := s.Cfg.GetString("baseURL") s := s.PathSpec.BaseURL.String()
if !strings.HasSuffix(s, "/") { if !strings.HasSuffix(s, "/") {
s += "/" s += "/"
} }
@ -1864,7 +1877,7 @@ func (s *Site) renderAndWritePage(name string, dest string, p *PageOutput, layou
if s.Info.relativeURLs { if s.Info.relativeURLs {
path = []byte(helpers.GetDottedRelativePath(dest)) path = []byte(helpers.GetDottedRelativePath(dest))
} else if s.Info.canonifyURLs { } else if s.Info.canonifyURLs {
url := s.Cfg.GetString("baseURL") url := s.PathSpec.BaseURL.String()
if !strings.HasSuffix(url, "/") { if !strings.HasSuffix(url, "/") {
url += "/" url += "/"
} }

View file

@ -147,7 +147,7 @@ func (s *Site) renderPaginator(p *PageOutput) error {
// write alias for page 1 // write alias for page 1
addend := fmt.Sprintf("/%s/%d", paginatePath, 1) addend := fmt.Sprintf("/%s/%d", paginatePath, 1)
target, err := p.createTargetPath(p.outputFormat, addend) target, err := p.createTargetPath(p.outputFormat, false, addend)
if err != nil { if err != nil {
return err return err
} }

View file

@ -38,7 +38,9 @@ package livereload
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -47,7 +49,31 @@ import (
// Prefix to signal to LiveReload that we need to navigate to another path. // Prefix to signal to LiveReload that we need to navigate to another path.
const hugoNavigatePrefix = "__hugo_navigate" const hugoNavigatePrefix = "__hugo_navigate"
var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} var upgrader = &websocket.Upgrader{
// Hugo may potentially spin up multiple HTTP servers, so we need to exclude the
// port when checking the origin.
CheckOrigin: func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
u, err := url.Parse(origin[0])
if err != nil {
return false
}
h1, _, err := net.SplitHostPort(u.Host)
if err != nil {
return false
}
h2, _, err := net.SplitHostPort(r.Host)
if err != nil {
return false
}
return h1 == h2
},
ReadBufferSize: 1024, WriteBufferSize: 1024}
// Handler is a HandlerFunc handling the livereload // Handler is a HandlerFunc handling the livereload
// Websocket interaction. // Websocket interaction.
@ -79,13 +105,28 @@ func NavigateToPath(path string) {
RefreshPath(hugoNavigatePrefix + path) RefreshPath(hugoNavigatePrefix + path)
} }
// NavigateToPathForPort is similar to NavigateToPath but will also
// set window.location.port to the given port value.
func NavigateToPathForPort(path string, port int) {
refreshPathForPort(hugoNavigatePrefix+path, port)
}
// RefreshPath tells livereload to refresh only the given path. // RefreshPath tells livereload to refresh only the given path.
// If that path points to a CSS stylesheet or an image, only the changes // If that path points to a CSS stylesheet or an image, only the changes
// will be updated in the browser, not the entire page. // will be updated in the browser, not the entire page.
func RefreshPath(s string) { func RefreshPath(s string) {
refreshPathForPort(s, -1)
}
func refreshPathForPort(s string, port int) {
// Tell livereload a file has changed - will force a hard refresh if not CSS or an image // Tell livereload a file has changed - will force a hard refresh if not CSS or an image
urlPath := filepath.ToSlash(s) urlPath := filepath.ToSlash(s)
wsHub.broadcast <- []byte(`{"command":"reload","path":"` + urlPath + `","originalPath":"","liveCSS":true,"liveImg":true}`) portStr := ""
if port > 0 {
portStr = fmt.Sprintf(`, "overrideURL": %d`, port)
}
msg := fmt.Sprintf(`{"command":"reload","path":%q,"originalPath":"","liveCSS":true,"liveImg":true%s}`, urlPath, portStr)
wsHub.broadcast <- []byte(msg)
} }
// ServeJS serves the liverreload.js who's reference is injected into the page. // ServeJS serves the liverreload.js who's reference is injected into the page.
@ -123,10 +164,14 @@ HugoReload.prototype.reload = function(path, options) {
path = path.substring(prefix.length); path = path.substring(prefix.length);
if (window.location.pathname === path) { if (!options.overrideURL && window.location.pathname === path) {
window.location.reload(); window.location.reload();
} else { } else {
window.location.href = path; if (options.overrideURL) {
window.location = location.protocol + "//" + location.hostname + ":" + options.overrideURL + path;
} else {
window.location.pathname = path;
}
} }
return true; return true;

191
source/dirs.go Normal file
View file

@ -0,0 +1,191 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package source
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
jww "github.com/spf13/jwalterweatherman"
)
// Dirs holds the source directories for a given build.
// In case where there are more than one of a kind, the order matters:
// It will be used to construct a union filesystem, so the right-most directory
// will "win" on duplicates. Typically, the theme version will be the first.
type Dirs struct {
logger *jww.Notepad
pathSpec *helpers.PathSpec
staticDirs []string
AbsStaticDirs []string
publishDir string
}
// NewDirs creates a new dirs with the given configuration and filesystem.
func NewDirs(fs *hugofs.Fs, cfg config.Provider, logger *jww.Notepad) (*Dirs, error) {
ps, err := helpers.NewPathSpec(fs, cfg)
if err != nil {
return nil, err
}
d := &Dirs{pathSpec: ps, logger: logger}
return d, d.init(cfg)
}
func (d *Dirs) init(cfg config.Provider) error {
var (
statics []string
)
if d.pathSpec.Theme() != "" {
statics = append(statics, filepath.Join(d.pathSpec.ThemesDir(), d.pathSpec.Theme(), "static"))
}
_, isLanguage := cfg.(*helpers.Language)
languages, hasLanguages := cfg.Get("languagesSorted").(helpers.Languages)
if !isLanguage && !hasLanguages {
return errors.New("missing languagesSorted in config")
}
if !isLanguage {
// Merge all the static dirs.
for _, l := range languages {
addend, err := d.staticDirsFor(l)
if err != nil {
return err
}
statics = append(statics, addend...)
}
} else {
addend, err := d.staticDirsFor(cfg)
if err != nil {
return err
}
statics = append(statics, addend...)
}
d.staticDirs = removeDuplicatesKeepRight(statics)
d.AbsStaticDirs = make([]string, len(d.staticDirs))
for i, di := range d.staticDirs {
d.AbsStaticDirs[i] = d.pathSpec.AbsPathify(di) + helpers.FilePathSeparator
}
d.publishDir = d.pathSpec.AbsPathify(cfg.GetString("publishDir")) + helpers.FilePathSeparator
return nil
}
func (d *Dirs) staticDirsFor(cfg config.Provider) ([]string, error) {
var statics []string
ps, err := helpers.NewPathSpec(d.pathSpec.Fs, cfg)
if err != nil {
return statics, err
}
statics = append(statics, ps.StaticDirs()...)
return statics, nil
}
// CreateStaticFs will create a union filesystem with the static paths configured.
// Any missing directories will be logged as warnings.
func (d *Dirs) CreateStaticFs() (afero.Fs, error) {
var (
source = d.pathSpec.Fs.Source
absPaths []string
)
for _, staticDir := range d.AbsStaticDirs {
if _, err := source.Stat(staticDir); os.IsNotExist(err) {
d.logger.WARN.Printf("Unable to find Static Directory: %s", staticDir)
} else {
absPaths = append(absPaths, staticDir)
}
}
if len(absPaths) == 0 {
return nil, nil
}
return d.createOverlayFs(absPaths), nil
}
// IsStatic returns whether the given filename is located in one of the static
// source dirs.
func (d *Dirs) IsStatic(filename string) bool {
for _, absPath := range d.AbsStaticDirs {
if strings.HasPrefix(filename, absPath) {
return true
}
}
return false
}
// MakeStaticPathRelative creates a relative path from the given filename.
// It will return an empty string if the filename is not a member of dirs.
func (d *Dirs) MakeStaticPathRelative(filename string) string {
for _, currentPath := range d.AbsStaticDirs {
if strings.HasPrefix(filename, currentPath) {
return strings.TrimPrefix(filename, currentPath)
}
}
return ""
}
func (d *Dirs) createOverlayFs(absPaths []string) afero.Fs {
source := d.pathSpec.Fs.Source
if len(absPaths) == 1 {
return afero.NewReadOnlyFs(afero.NewBasePathFs(source, absPaths[0]))
}
base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, absPaths[0]))
overlay := d.createOverlayFs(absPaths[1:])
return afero.NewCopyOnWriteFs(base, overlay)
}
func removeDuplicatesKeepRight(in []string) []string {
seen := make(map[string]bool)
var out []string
for i := len(in) - 1; i >= 0; i-- {
v := in[i]
if seen[v] {
continue
}
out = append([]string{v}, out...)
seen[v] = true
}
return out
}

177
source/dirs_test.go Normal file
View file

@ -0,0 +1,177 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package source
import (
"testing"
"github.com/gohugoio/hugo/helpers"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
var logger = jww.NewNotepad(jww.LevelInfo, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
func TestStaticDirs(t *testing.T) {
assert := require.New(t)
tests := []struct {
setup func(cfg config.Provider, fs *hugofs.Fs) config.Provider
expected []string
}{
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("staticDir", "s1")
return cfg
}, []string{"s1"}},
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("staticDir", []string{"s2", "s1", "s2"})
return cfg
}, []string{"s1", "s2"}},
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("theme", "mytheme")
cfg.Set("themesDir", "themes")
cfg.Set("staticDir", []string{"s1", "s2"})
return cfg
}, []string{filepath.FromSlash("themes/mytheme/static"), "s1", "s2"}},
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("staticDir", "s1")
l1 := helpers.NewLanguage("en", cfg)
l1.Set("staticDir", []string{"l1s1", "l1s2"})
return l1
}, []string{"l1s1", "l1s2"}},
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("staticDir", "s1")
l1 := helpers.NewLanguage("en", cfg)
l1.Set("staticDir2", []string{"l1s1", "l1s2"})
return l1
}, []string{"s1", "l1s1", "l1s2"}},
{func(cfg config.Provider, fs *hugofs.Fs) config.Provider {
cfg.Set("staticDir", "s1")
l1 := helpers.NewLanguage("en", cfg)
l1.Set("staticDir2", []string{"l1s1", "l1s2"})
l2 := helpers.NewLanguage("nn", cfg)
l2.Set("staticDir3", []string{"l2s1", "l2s2"})
l2.Set("staticDir", []string{"l2"})
cfg.Set("languagesSorted", helpers.Languages{l1, l2})
return cfg
}, []string{"s1", "l1s1", "l1s2", "l2", "l2s1", "l2s2"}},
}
for i, test := range tests {
if i != 0 {
break
}
msg := fmt.Sprintf("Test %d", i)
v := viper.New()
fs := hugofs.NewMem(v)
cfg := test.setup(v, fs)
cfg.Set("workingDir", filepath.FromSlash("/work"))
_, isLanguage := cfg.(*helpers.Language)
if !isLanguage && !cfg.IsSet("languagesSorted") {
cfg.Set("languagesSorted", helpers.Languages{helpers.NewDefaultLanguage(cfg)})
}
dirs, err := NewDirs(fs, cfg, logger)
assert.NoError(err)
assert.Equal(test.expected, dirs.staticDirs, msg)
assert.Len(dirs.AbsStaticDirs, len(dirs.staticDirs))
for i, d := range dirs.staticDirs {
abs := dirs.AbsStaticDirs[i]
assert.Equal(filepath.Join("/work", d)+helpers.FilePathSeparator, abs)
assert.True(dirs.IsStatic(filepath.Join(abs, "logo.png")))
rel := dirs.MakeStaticPathRelative(filepath.Join(abs, "logo.png"))
assert.Equal("logo.png", rel)
}
assert.False(dirs.IsStatic(filepath.FromSlash("/some/other/dir/logo.png")))
}
}
func TestStaticDirsFs(t *testing.T) {
assert := require.New(t)
v := viper.New()
fs := hugofs.NewMem(v)
v.Set("workingDir", filepath.FromSlash("/work"))
v.Set("theme", "mytheme")
v.Set("themesDir", "themes")
v.Set("staticDir", []string{"s1", "s2"})
v.Set("languagesSorted", helpers.Languages{helpers.NewDefaultLanguage(v)})
writeToFs(t, fs.Source, "/work/s1/f1.txt", "s1-f1")
writeToFs(t, fs.Source, "/work/s2/f2.txt", "s2-f2")
writeToFs(t, fs.Source, "/work/s1/f2.txt", "s1-f2")
writeToFs(t, fs.Source, "/work/themes/mytheme/static/f1.txt", "theme-f1")
writeToFs(t, fs.Source, "/work/themes/mytheme/static/f3.txt", "theme-f3")
dirs, err := NewDirs(fs, v, logger)
assert.NoError(err)
sfs, err := dirs.CreateStaticFs()
assert.NoError(err)
assert.Equal("s1-f1", readFileFromFs(t, sfs, "f1.txt"))
assert.Equal("s2-f2", readFileFromFs(t, sfs, "f2.txt"))
assert.Equal("theme-f3", readFileFromFs(t, sfs, "f3.txt"))
}
func TestRemoveDuplicatesKeepRight(t *testing.T) {
in := []string{"a", "b", "c", "a"}
out := removeDuplicatesKeepRight(in)
require.Equal(t, []string{"b", "c", "a"}, out)
}
func writeToFs(t testing.TB, fs afero.Fs, filename, content string) {
if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil {
t.Fatalf("Failed to write file: %s", err)
}
}
func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
filename = filepath.FromSlash(filename)
b, err := afero.ReadFile(fs, filename)
if err != nil {
afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
fmt.Println(" ", path, " ", info)
return nil
})
t.Fatalf("Failed to read file: %s", err)
}
return string(b)
}

View file

@ -18,6 +18,7 @@ import (
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
"github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -26,7 +27,7 @@ func TestInit(t *testing.T) {
var ns *internal.TemplateFuncsNamespace var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns = nsf(&deps.Deps{}) ns = nsf(&deps.Deps{Cfg: viper.New()})
if ns.Name == name { if ns.Name == name {
found = true found = true
break break

View file

@ -27,12 +27,14 @@ import (
func New(deps *deps.Deps) *Namespace { func New(deps *deps.Deps) *Namespace {
return &Namespace{ return &Namespace{
deps: deps, deps: deps,
multihost: deps.Cfg.GetBool("multihost"),
} }
} }
// Namespace provides template functions for the "urls" namespace. // Namespace provides template functions for the "urls" namespace.
type Namespace struct { type Namespace struct {
deps *deps.Deps deps *deps.Deps
multihost bool
} }
// AbsURL takes a given string and converts it to an absolute URL. // AbsURL takes a given string and converts it to an absolute URL.
@ -109,7 +111,7 @@ func (ns *Namespace) RelLangURL(a interface{}) (template.HTML, error) {
return "", err return "", err
} }
return template.HTML(ns.deps.PathSpec.RelURL(s, true)), nil return template.HTML(ns.deps.PathSpec.RelURL(s, !ns.multihost)), nil
} }
// AbsLangURL takes a given string and converts it to an absolute URL according // AbsLangURL takes a given string and converts it to an absolute URL according
@ -121,5 +123,5 @@ func (ns *Namespace) AbsLangURL(a interface{}) (template.HTML, error) {
return "", err return "", err
} }
return template.HTML(ns.deps.PathSpec.AbsURL(s, true)), nil return template.HTML(ns.deps.PathSpec.AbsURL(s, !ns.multihost)), nil
} }