Skip to content

Commit

Permalink
Add multilingual multihost support
Browse files Browse the repository at this point in the history
This commit adds multihost support when more than one language is configured and `baseURL` is set per language.

Updates #4027
  • Loading branch information
bep committed Nov 17, 2017
1 parent 6233ddf commit 2e04657
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 79 deletions.
4 changes: 4 additions & 0 deletions commands/commandeer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func (c *commandeer) PathSpec() *helpers.PathSpec {
return c.pathSpec
}

func (c *commandeer) languages() helpers.Languages {
return c.Cfg.Get("languagesSorted").(helpers.Languages)
}

func (c *commandeer) initFs(fs *hugofs.Fs) error {
c.DepsCfg.Fs = fs
ps, err := helpers.NewPathSpec(fs, c.Cfg)
Expand Down
20 changes: 20 additions & 0 deletions commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ func (c *commandeer) watchConfig() {

func (c *commandeer) build(watches ...bool) error {
if err := c.copyStatic(); err != nil {
// TODO(bep) multihost
return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err)
}
watch := false
Expand Down Expand Up @@ -593,6 +594,24 @@ func (c *commandeer) getStaticSourceFs() afero.Fs {

func (c *commandeer) copyStatic() error {
publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator
roots := c.roots()

if len(roots) == 0 {
return c.copyStaticTo(publishDir)
}

for _, root := range roots {
dir := filepath.Join(publishDir, root)
if err := c.copyStaticTo(dir); err != nil {
return err
}
}

return nil

}

func (c *commandeer) copyStaticTo(publishDir string) error {

// If root, remove the second '/'
if publishDir == "//" {
Expand Down Expand Up @@ -893,6 +912,7 @@ func (c *commandeer) newWatcher(port int) error {

if c.Cfg.GetBool("forceSyncStatic") {
c.Logger.FEEDBACK.Printf("Syncing all static files\n")
// TODO(bep) multihost
err := c.copyStatic()
if err != nil {
utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir))
Expand Down
182 changes: 139 additions & 43 deletions commands/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"github.com/gohugoio/hugo/config"

"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -137,34 +139,58 @@ func server(cmd *cobra.Command, args []string) error {
c.watchConfig()
}

l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort)))
if err == nil {
l.Close()
} else {
if serverCmd.Flags().Changed("port") {
// port set explicitly by user -- he/she probably meant it!
return newSystemErrorF("Server startup failed: %s", err)
}
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
sp, err := helpers.FindAvailablePort()
if err != nil {
return newSystemError("Unable to find alternative port to use:", err)
languages := c.languages()
serverPorts := make([]int, 1)

if languages.IsMultihost() {
serverPorts = make([]int, len(languages))
}

currentServerPort := serverPort

for i := 0; i < len(serverPorts); i++ {
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
if err == nil {
l.Close()
serverPorts[i] = currentServerPort
} else {
if i == 0 && serverCmd.Flags().Changed("port") {
// port set explicitly by user -- he/she probably meant it!
return newSystemErrorF("Server startup failed: %s", err)
}
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
sp, err := helpers.FindAvailablePort()
if err != nil {
return newSystemError("Unable to find alternative port to use:", err)
}
serverPorts[i] = sp.Port
}
serverPort = sp.Port

currentServerPort = serverPorts[i] + 1
}

c.Set("port", serverPort)
if liveReloadPort != -1 {
c.Set("liveReloadPort", liveReloadPort)
} else {
c.Set("liveReloadPort", serverPort)
c.Set("liveReloadPort", serverPorts[0])
}

baseURL, err = fixURL(c.Cfg, baseURL)
if err != nil {
return err
if languages.IsMultihost() {
for i, language := range languages {
baseURL, err = fixURL(language, baseURL, serverPorts[i])
if err != nil {
return err
}
language.Set("baseURL", baseURL)
}
} else {
baseURL, err = fixURL(c.Cfg, baseURL, serverPorts[0])
if err != nil {
return err
}
c.Cfg.Set("baseURL", baseURL)
}
c.Set("baseURL", baseURL)

if err := memStats(); err != nil {
jww.ERROR.Println("memstats error:", err)
Expand Down Expand Up @@ -208,28 +234,52 @@ func server(cmd *cobra.Command, args []string) error {
}
}

c.serve(serverPort)

return nil
}

func (c *commandeer) serve(port int) {
type fileServer struct {
basePort int
baseURLs []string
roots []string
c *commandeer
}

func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
baseURL := f.baseURLs[i]
root := f.roots[i]
port := f.basePort + i

publishDir := f.c.Cfg.GetString("publishDir")

if root != "" {
publishDir = filepath.Join(publishDir, root)
}

absPublishDir := f.c.PathSpec().AbsPathify(publishDir)

// TODO(bep) multihost unify feedback
if renderToDisk {
jww.FEEDBACK.Println("Serving pages from " + c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))
jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
} else {
jww.FEEDBACK.Println("Serving pages from memory")
}

httpFs := afero.NewHttpFs(c.Fs.Destination)
fs := filesOnlyFs{httpFs.Dir(c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))}
httpFs := afero.NewHttpFs(f.c.Fs.Destination)
fs := filesOnlyFs{httpFs.Dir(absPublishDir)}

doLiveReload := !buildWatch && !c.Cfg.GetBool("disableLiveReload")
fastRenderMode := doLiveReload && !c.Cfg.GetBool("disableFastRender")
doLiveReload := !buildWatch && !f.c.Cfg.GetBool("disableLiveReload")
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")

if fastRenderMode {
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
}

// We're only interested in the path
u, err := url.Parse(baseURL)
if err != nil {
return nil, "", fmt.Errorf("Invalid baseURL: %s", err)
}

decorate := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if noHTTPCache {
Expand All @@ -240,40 +290,86 @@ func (c *commandeer) serve(port int) {
if fastRenderMode {
p := r.RequestURI
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
c.visitedURLs.Add(p)
f.c.visitedURLs.Add(p)
}
}
h.ServeHTTP(w, r)
})
}

fileserver := decorate(http.FileServer(fs))
mu := http.NewServeMux()

// We're only interested in the path
u, err := url.Parse(c.Cfg.GetString("baseURL"))
if err != nil {
jww.ERROR.Fatalf("Invalid baseURL: %s", err)
}
if u.Path == "" || u.Path == "/" {
http.Handle("/", fileserver)
mu.Handle("/", fileserver)
} else {
http.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
}

jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface)
jww.FEEDBACK.Println("Press Ctrl+C to stop")

endpoint := net.JoinHostPort(serverInterface, strconv.Itoa(port))
err = http.ListenAndServe(endpoint, nil)
if err != nil {
jww.ERROR.Printf("Error: %s\n", err.Error())
os.Exit(1)

return mu, endpoint, nil
}

func (c *commandeer) roots() []string {
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()

var (
baseURLs []string
roots []string
)

if isMultiHost {
for _, s := range Hugo.Sites {
baseURLs = append(baseURLs, s.BaseURL.String())
roots = append(roots, s.Language.Lang)
}
} else {
baseURLs = []string{Hugo.Sites[0].BaseURL.String()}
roots = []string{""}
}

srv := &fileServer{
basePort: port,
baseURLs: baseURLs,
roots: roots,
c: c,
}

for i, _ := range baseURLs {
mu, endpoint, err := srv.createEndpoint(i)

go func() {
err = http.ListenAndServe(endpoint, mu)
if err != nil {
jww.ERROR.Printf("Error: %s\n", err.Error())
os.Exit(1)
}
}()
}

// 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")
}

// fixURL massages the baseURL into a form needed for serving
// all pages correctly.
func fixURL(cfg config.Provider, s string) (string, error) {
func fixURL(cfg config.Provider, s string, port int) (string, error) {
useLocalhost := false
if s == "" {
s = cfg.GetString("baseURL")
Expand Down Expand Up @@ -315,7 +411,7 @@ func fixURL(cfg config.Provider, s string) (string, error) {
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err)
}
}
u.Host += fmt.Sprintf(":%d", serverPort)
u.Host += fmt.Sprintf(":%d", port)
}

return u.String(), nil
Expand Down
2 changes: 1 addition & 1 deletion commands/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestFixURL(t *testing.T) {
v.Set("baseURL", test.CfgBaseURL)
serverAppend = test.AppendPort
serverPort = test.Port
result, err := fixURL(v, baseURL)
result, err := fixURL(v, baseURL, serverPort)
if err != nil {
t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err)
}
Expand Down
24 changes: 23 additions & 1 deletion helpers/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ func (l *Language) Params() map[string]interface{} {
return l.params
}

// IsMultihost returns whether the languages has baseURL specificed on the
// language level.
func (l Languages) IsMultihost() bool {
for _, lang := range l {
if lang.GetLocal("baseURL") != nil {
return true
}
}
return false
}

// SetParam sets param with the given key and value.
// SetParam is case-insensitive.
func (l *Language) SetParam(k string, v interface{}) {
Expand Down Expand Up @@ -132,6 +143,17 @@ func (l *Language) GetStringMapString(key string) map[string]string {
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func (l *Language) Get(key string) interface{} {
local := l.GetLocal(key)
if local != nil {
return local
}
return l.Cfg.Get(key)
}

// GetLocal gets a configuration value set on language level. It will
// not fall back to any global value.
// It will return nil if a value with the given key cannot be found.
func (l *Language) GetLocal(key string) interface{} {
if l == nil {
panic("language not set")
}
Expand All @@ -141,7 +163,7 @@ func (l *Language) Get(key string) interface{} {
return v
}
}
return l.Cfg.Get(key)
return nil
}

// Set sets the value for the key in the language's params.
Expand Down
1 change: 0 additions & 1 deletion helpers/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ func (p *PathSpec) AbsPathify(inPath string) string {
return filepath.Clean(inPath)
}

// TODO(bep): Consider moving workingDir to argument list
return filepath.Join(p.workingDir, inPath)
}

Expand Down
Loading

0 comments on commit 2e04657

Please sign in to comment.