shithub: hugo

Download patch

ref: d6f7a9e28dfd5abff08b6aaf6fb3493c46bd1e39
parent: 031f948f87ac97ca49d0a487a392a8a0c6afb699
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Mon Nov 25 07:49:04 EST 2019

resources/images: Make the image cache more robust

Also allow timeout to be set as a duration string, e.g. `30s`.

Fixes #6501

--- a/cache/filecache/filecache.go
+++ b/cache/filecache/filecache.go
@@ -129,7 +129,7 @@
 // If not found a new file is created and passed to create, which should close
 // it when done.
 func (c *Cache) ReadOrCreate(id string,
-	read func(info ItemInfo, r io.Reader) error,
+	read func(info ItemInfo, r io.ReadSeeker) error,
 	create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
 	id = cleanID(id)
 
--- a/cache/filecache/filecache_test.go
+++ b/cache/filecache/filecache_test.go
@@ -250,9 +250,9 @@
 
 	var result string
 
-	rf := func(failLevel int) func(info ItemInfo, r io.Reader) error {
+	rf := func(failLevel int) func(info ItemInfo, r io.ReadSeeker) error {
 
-		return func(info ItemInfo, r io.Reader) error {
+		return func(info ItemInfo, r io.ReadSeeker) error {
 			if failLevel > 0 {
 				if failLevel > 1 {
 					return ErrFatal
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -620,7 +620,7 @@
 	v.SetDefault("disableAliases", false)
 	v.SetDefault("debug", false)
 	v.SetDefault("disableFastRender", false)
-	v.SetDefault("timeout", 30000) // 30 seconds
+	v.SetDefault("timeout", "30s")
 	v.SetDefault("enableInlineShortcodes", false)
 
 	return nil
--- a/hugolib/image_test.go
+++ b/hugolib/image_test.go
@@ -35,11 +35,12 @@
 	c.Assert(err, qt.IsNil)
 	defer clean()
 
-	newBuilder := func() *sitesBuilder {
+	newBuilder := func(timeout string) *sitesBuilder {
 
 		v := viper.New()
 		v.Set("workingDir", workDir)
 		v.Set("baseURL", "https://example.org")
+		v.Set("timeout", timeout)
 
 		b := newTestSitesBuilder(t).WithWorkingDir(workDir)
 		b.Fs = hugofs.NewDefault(v)
@@ -49,9 +50,17 @@
 title: "My bundle"
 ---
 
+{{< imgproc >}}
+
 `)
 
-		b.WithTemplatesAdded("index.html", `
+		b.WithTemplatesAdded(
+			"shortcodes/imgproc.html", `
+{{ $img := resources.Get "images/sunset.jpg" }}
+{{ $r := $img.Resize "129x239" }}
+IMG SHORTCODE: {{ $r.RelPermalink }}/{{ $r.Width }}
+`,
+			"index.html", `
 {{ $p := .Site.GetPage "mybundle" }}
 {{ $img1 := resources.Get "images/sunset.jpg" }}
 {{ $img2 := $p.Resources.GetMatch "sunset.jpg" }}
@@ -83,8 +92,8 @@
 {{ $blurryGrayscale4 := $r.Filter $filters }}
 BG4: {{ $blurryGrayscale4.RelPermalink }}/{{ $blurryGrayscale4.Width }}
 
+{{ $p.Content }}
 
-
 `)
 
 		return b
@@ -112,8 +121,8 @@
 	out.Close()
 	src.Close()
 
-	b := newBuilder()
-	b.Build(BuildCfg{})
+	// First build it with a very short timeout to trigger errors.
+	b := newBuilder("10ns")
 
 	imgExpect := `
 Resized1: images/sunset.jpg|123|234|image/jpg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg|
@@ -126,16 +135,35 @@
 BG2: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_2ae8bb993431ec1aec40fe59927b46b4.jpg/123
 BG3: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg/123
 BG4: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg/123
+IMG SHORTCODE: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg/129
 `
 
-	b.AssertFileContent(filepath.Join(workDir, "public/index.html"), imgExpect)
-	b.AssertImage(350, 219, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg")
+	assertImages := func() {
+		b.Helper()
+		b.AssertFileContent(filepath.Join(workDir, "public/index.html"), imgExpect)
+		b.AssertImage(350, 219, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg")
+		b.AssertImage(129, 239, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg")
+	}
 
+	err = b.BuildE(BuildCfg{})
+	c.Assert(err, qt.Not(qt.IsNil))
+
+	b = newBuilder("30s")
+	b.Build(BuildCfg{})
+
+	assertImages()
+
+	// Truncate one image.
+	imgInCache := filepath.Join(workDir, "resources/_gen/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg")
+	f, err := os.Create(imgInCache)
+	c.Assert(err, qt.IsNil)
+	f.Close()
+
 	// Build it again to make sure we read images from file cache.
-	b = newBuilder()
+	b = newBuilder("30s")
 	b.Build(BuildCfg{})
 
-	b.AssertFileContent(filepath.Join(workDir, "public/index.html"), imgExpect)
+	assertImages()
 
 }
 
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -180,7 +180,7 @@
 		needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
 
 		if needTimeout {
-			cp.initMain = parent.BranchdWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+			cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
 				return nil, initContent()
 			})
 		} else {
@@ -249,8 +249,10 @@
 }
 
 func (p *pageContentOutput) Content() (interface{}, error) {
-	p.p.s.initInit(p.initMain, p.p)
-	return p.content, nil
+	if p.p.s.initInit(p.initMain, p.p) {
+		return p.content, nil
+	}
+	return nil, nil
 }
 
 func (p *pageContentOutput) FuzzyWordCount() int {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -181,11 +181,12 @@
 	init.menus.Reset()
 }
 
-func (s *Site) initInit(init *lazy.Init, pctx pageContext) {
+func (s *Site) initInit(init *lazy.Init, pctx pageContext) bool {
 	_, err := init.Do()
 	if err != nil {
 		s.h.FatalError(pctx.wrapError(err))
 	}
+	return err == nil
 }
 
 func (s *Site) prepareInits() {
@@ -410,10 +411,23 @@
 		return nil, err
 	}
 
+	timeout := 30 * time.Second
+	if cfg.Language.IsSet("timeout") {
+		switch v := cfg.Language.Get("timeout").(type) {
+		case int:
+			timeout = time.Duration(v) * time.Millisecond
+		case string:
+			d, err := time.ParseDuration(v)
+			if err == nil {
+				timeout = d
+			}
+		}
+	}
+
 	siteConfig := siteConfigHolder{
 		sitemap:          config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")),
 		taxonomiesConfig: taxonomies,
-		timeout:          time.Duration(cfg.Language.GetInt("timeout")) * time.Millisecond,
+		timeout:          timeout,
 		hasCJKLanguage:   cfg.Language.GetBool("hasCJKLanguage"),
 		enableEmoji:      cfg.Language.Cfg.GetBool("enableEmoji"),
 	}
--- a/lazy/init.go
+++ b/lazy/init.go
@@ -64,7 +64,7 @@
 }
 
 // BranchdWithTimeout is same as Branch, but with a timeout.
-func (ini *Init) BranchdWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
+func (ini *Init) BranchWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
 	return ini.Branch(func() (interface{}, error) {
 		return ini.withTimeout(timeout, f)
 	})
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -158,7 +158,7 @@
 			name := fmt.Sprintf("goldmark_%s.txt", c.ctx.DocumentID)
 			filename := filepath.Join(dir, name)
 			afero.WriteFile(hugofs.Os, filename, ctx.Src, 07555)
-			err = errors.Errorf("[BUG] goldmark: create an issue on GitHub attaching the file in: %s", filename)
+			err = errors.Errorf("[BUG] goldmark: %s: create an issue on GitHub attaching the file in: %s", r, filename)
 
 		}
 	}()
--- a/resources/image.go
+++ b/resources/image.go
@@ -88,7 +88,7 @@
 
 		key := i.getImageMetaCacheTargetPath()
 
-		read := func(info filecache.ItemInfo, r io.Reader) error {
+		read := func(info filecache.ItemInfo, r io.ReadSeeker) error {
 			meta := &imageMeta{}
 			data, err := ioutil.ReadAll(r)
 			if err != nil {
--- a/resources/image_cache.go
+++ b/resources/image_cache.go
@@ -96,12 +96,18 @@
 	// These funcs are protected by a named lock.
 	// read clones the parent to its new name and copies
 	// the content to the destinations.
-	read := func(info filecache.ItemInfo, r io.Reader) error {
+	read := func(info filecache.ItemInfo, r io.ReadSeeker) error {
 		img = parent.clone(nil)
 		rp := img.getResourcePaths()
 		rp.relTargetDirFile.file = relTarget.file
 		img.setSourceFilename(info.Name)
 
+		if err := img.InitConfig(r); err != nil {
+			return err
+		}
+
+		r.Seek(0, 0)
+
 		w, err := img.openDestinationsForWriting()
 		if err != nil {
 			return err
@@ -114,6 +120,7 @@
 
 		defer w.Close()
 		_, err = io.Copy(w, r)
+
 		return err
 	}
 
--- a/resources/images/image.go
+++ b/resources/images/image.go
@@ -123,6 +123,15 @@
 	return &i
 }
 
+// InitConfig reads the image config from the given reader.
+func (i *Image) InitConfig(r io.Reader) error {
+	var err error
+	i.configInit.Do(func() {
+		i.config, _, err = image.DecodeConfig(r)
+	})
+	return err
+}
+
 func (i *Image) initConfig() error {
 	var err error
 	i.configInit.Do(func() {
@@ -130,10 +139,7 @@
 			return
 		}
 
-		var (
-			f      hugio.ReadSeekCloser
-			config image.Config
-		)
+		var f hugio.ReadSeekCloser
 
 		f, err = i.Spec.ReadSeekCloser()
 		if err != nil {
@@ -141,11 +147,7 @@
 		}
 		defer f.Close()
 
-		config, _, err = image.DecodeConfig(f)
-		if err != nil {
-			return
-		}
-		i.config = config
+		i.config, _, err = image.DecodeConfig(f)
 	})
 
 	if err != nil {