github.com/SuCicada/su-hugo@v1.0.0/deps/deps.go (about)

     1  package deps
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/gohugoio/hugo/cache/filecache"
    12  	"github.com/gohugoio/hugo/common/hexec"
    13  	"github.com/gohugoio/hugo/common/loggers"
    14  	"github.com/gohugoio/hugo/config"
    15  	"github.com/gohugoio/hugo/config/security"
    16  	"github.com/gohugoio/hugo/helpers"
    17  	"github.com/gohugoio/hugo/hugofs"
    18  	"github.com/gohugoio/hugo/langs"
    19  	"github.com/gohugoio/hugo/media"
    20  	"github.com/gohugoio/hugo/resources/page"
    21  	"github.com/gohugoio/hugo/resources/postpub"
    22  
    23  	"github.com/gohugoio/hugo/metrics"
    24  	"github.com/gohugoio/hugo/output"
    25  	"github.com/gohugoio/hugo/resources"
    26  	"github.com/gohugoio/hugo/source"
    27  	"github.com/gohugoio/hugo/tpl"
    28  	"github.com/spf13/cast"
    29  	jww "github.com/spf13/jwalterweatherman"
    30  )
    31  
    32  // Deps holds dependencies used by many.
    33  // There will be normally only one instance of deps in play
    34  // at a given time, i.e. one per Site built.
    35  type Deps struct {
    36  
    37  	// The logger to use.
    38  	Log loggers.Logger `json:"-"`
    39  
    40  	// Used to log errors that may repeat itself many times.
    41  	LogDistinct loggers.Logger
    42  
    43  	ExecHelper *hexec.Exec
    44  
    45  	// The templates to use. This will usually implement the full tpl.TemplateManager.
    46  	tmpl tpl.TemplateHandler
    47  
    48  	// We use this to parse and execute ad-hoc text templates.
    49  	textTmpl tpl.TemplateParseFinder
    50  
    51  	// The file systems to use.
    52  	Fs *hugofs.Fs `json:"-"`
    53  
    54  	// The PathSpec to use
    55  	*helpers.PathSpec `json:"-"`
    56  
    57  	// The ContentSpec to use
    58  	*helpers.ContentSpec `json:"-"`
    59  
    60  	// The SourceSpec to use
    61  	SourceSpec *source.SourceSpec `json:"-"`
    62  
    63  	// The Resource Spec to use
    64  	ResourceSpec *resources.Spec
    65  
    66  	// The configuration to use
    67  	Cfg config.Provider `json:"-"`
    68  
    69  	// The file cache to use.
    70  	FileCaches filecache.Caches
    71  
    72  	// The translation func to use
    73  	Translate func(translationID string, templateData any) string `json:"-"`
    74  
    75  	// The language in use. TODO(bep) consolidate with site
    76  	Language *langs.Language
    77  
    78  	// The site building.
    79  	Site page.Site
    80  
    81  	// All the output formats available for the current site.
    82  	OutputFormatsConfig output.Formats
    83  
    84  	// FilenameHasPostProcessPrefix is a set of filenames in /public that
    85  	// contains a post-processing prefix.
    86  	FilenameHasPostProcessPrefix []string
    87  
    88  	templateProvider ResourceProvider
    89  	WithTemplate     func(templ tpl.TemplateManager) error `json:"-"`
    90  
    91  	// Used in tests
    92  	OverloadedTemplateFuncs map[string]any
    93  
    94  	translationProvider ResourceProvider
    95  
    96  	Metrics metrics.Provider
    97  
    98  	// Timeout is configurable in site config.
    99  	Timeout time.Duration
   100  
   101  	// BuildStartListeners will be notified before a build starts.
   102  	BuildStartListeners *Listeners
   103  
   104  	// Resources that gets closed when the build is done or the server shuts down.
   105  	BuildClosers *Closers
   106  
   107  	// Atomic values set during a build.
   108  	// This is common/global for all sites.
   109  	BuildState *BuildState
   110  
   111  	// Whether we are in running (server) mode
   112  	Running bool
   113  
   114  	*globalErrHandler
   115  }
   116  
   117  type globalErrHandler struct {
   118  	// Channel for some "hard to get to" build errors
   119  	buildErrors chan error
   120  }
   121  
   122  // SendErr sends the error on a channel to be handled later.
   123  // This can be used in situations where returning and aborting the current
   124  // operation isn't practical.
   125  func (e *globalErrHandler) SendError(err error) {
   126  	if e.buildErrors != nil {
   127  		select {
   128  		case e.buildErrors <- err:
   129  		default:
   130  		}
   131  		return
   132  	}
   133  
   134  	jww.ERROR.Println(err)
   135  }
   136  
   137  func (e *globalErrHandler) StartErrorCollector() chan error {
   138  	e.buildErrors = make(chan error, 10)
   139  	return e.buildErrors
   140  }
   141  
   142  // Listeners represents an event listener.
   143  type Listeners struct {
   144  	sync.Mutex
   145  
   146  	// A list of funcs to be notified about an event.
   147  	listeners []func()
   148  }
   149  
   150  // Add adds a function to a Listeners instance.
   151  func (b *Listeners) Add(f func()) {
   152  	if b == nil {
   153  		return
   154  	}
   155  	b.Lock()
   156  	defer b.Unlock()
   157  	b.listeners = append(b.listeners, f)
   158  }
   159  
   160  // Notify executes all listener functions.
   161  func (b *Listeners) Notify() {
   162  	b.Lock()
   163  	defer b.Unlock()
   164  	for _, notify := range b.listeners {
   165  		notify()
   166  	}
   167  }
   168  
   169  // ResourceProvider is used to create and refresh, and clone resources needed.
   170  type ResourceProvider interface {
   171  	Update(deps *Deps) error
   172  	Clone(deps *Deps) error
   173  }
   174  
   175  func (d *Deps) Tmpl() tpl.TemplateHandler {
   176  	return d.tmpl
   177  }
   178  
   179  func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
   180  	return d.textTmpl
   181  }
   182  
   183  func (d *Deps) SetTmpl(tmpl tpl.TemplateHandler) {
   184  	d.tmpl = tmpl
   185  }
   186  
   187  func (d *Deps) SetTextTmpl(tmpl tpl.TemplateParseFinder) {
   188  	d.textTmpl = tmpl
   189  }
   190  
   191  // LoadResources loads translations and templates.
   192  func (d *Deps) LoadResources() error {
   193  	// Note that the translations need to be loaded before the templates.
   194  	if err := d.translationProvider.Update(d); err != nil {
   195  		return fmt.Errorf("loading translations: %w", err)
   196  	}
   197  
   198  	if err := d.templateProvider.Update(d); err != nil {
   199  		return fmt.Errorf("loading templates: %w", err)
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  // New initializes a Dep struct.
   206  // Defaults are set for nil values,
   207  // but TemplateProvider, TranslationProvider and Language are always required.
   208  func New(cfg DepsCfg) (*Deps, error) {
   209  	var (
   210  		logger = cfg.Logger
   211  		fs     = cfg.Fs
   212  		d      *Deps
   213  	)
   214  
   215  	if cfg.TemplateProvider == nil {
   216  		panic("Must have a TemplateProvider")
   217  	}
   218  
   219  	if cfg.TranslationProvider == nil {
   220  		panic("Must have a TranslationProvider")
   221  	}
   222  
   223  	if cfg.Language == nil {
   224  		panic("Must have a Language")
   225  	}
   226  
   227  	if logger == nil {
   228  		logger = loggers.NewErrorLogger()
   229  	}
   230  
   231  	if fs == nil {
   232  		// Default to the production file system.
   233  		fs = hugofs.NewDefault(cfg.Language)
   234  	}
   235  
   236  	if cfg.MediaTypes == nil {
   237  		cfg.MediaTypes = media.DefaultTypes
   238  	}
   239  
   240  	if cfg.OutputFormats == nil {
   241  		cfg.OutputFormats = output.DefaultFormats
   242  	}
   243  
   244  	securityConfig, err := security.DecodeConfig(cfg.Cfg)
   245  	if err != nil {
   246  		return nil, fmt.Errorf("failed to create security config from configuration: %w", err)
   247  	}
   248  	execHelper := hexec.New(securityConfig)
   249  
   250  	var filenameHasPostProcessPrefixMu sync.Mutex
   251  	hashBytesReceiverFunc := func(name string, match bool) {
   252  		if !match {
   253  			return
   254  		}
   255  		filenameHasPostProcessPrefixMu.Lock()
   256  		d.FilenameHasPostProcessPrefix = append(d.FilenameHasPostProcessPrefix, name)
   257  		filenameHasPostProcessPrefixMu.Unlock()
   258  	}
   259  
   260  	// Skip binary files.
   261  	hashBytesSHouldCheck := func(name string) bool {
   262  		ext := strings.TrimPrefix(filepath.Ext(name), ".")
   263  		mime, _, found := cfg.MediaTypes.GetBySuffix(ext)
   264  		if !found {
   265  			return false
   266  		}
   267  		switch mime.MainType {
   268  		case "text", "application":
   269  			return true
   270  		default:
   271  			return false
   272  		}
   273  	}
   274  	fs.PublishDir = hugofs.NewHasBytesReceiver(fs.PublishDir, hashBytesSHouldCheck, hashBytesReceiverFunc, []byte(postpub.PostProcessPrefix))
   275  
   276  	ps, err := helpers.NewPathSpec(fs, cfg.Language, logger)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("create PathSpec: %w", err)
   279  	}
   280  
   281  	fileCaches, err := filecache.NewCaches(ps)
   282  	if err != nil {
   283  		return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
   284  	}
   285  
   286  	errorHandler := &globalErrHandler{}
   287  	buildState := &BuildState{}
   288  
   289  	resourceSpec, err := resources.NewSpec(ps, fileCaches, buildState, logger, errorHandler, execHelper, cfg.OutputFormats, cfg.MediaTypes)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	contentSpec, err := helpers.NewContentSpec(cfg.Language, logger, ps.BaseFs.Content.Fs, execHelper)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	sp := source.NewSourceSpec(ps, nil, fs.Source)
   300  
   301  	timeoutms := cfg.Language.GetInt("timeout")
   302  	if timeoutms <= 0 {
   303  		timeoutms = 3000
   304  	}
   305  
   306  	ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
   307  	ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
   308  
   309  	logDistinct := helpers.NewDistinctLogger(logger)
   310  
   311  	d = &Deps{
   312  		Fs:                      fs,
   313  		Log:                     ignorableLogger,
   314  		LogDistinct:             logDistinct,
   315  		ExecHelper:              execHelper,
   316  		templateProvider:        cfg.TemplateProvider,
   317  		translationProvider:     cfg.TranslationProvider,
   318  		WithTemplate:            cfg.WithTemplate,
   319  		OverloadedTemplateFuncs: cfg.OverloadedTemplateFuncs,
   320  		PathSpec:                ps,
   321  		ContentSpec:             contentSpec,
   322  		SourceSpec:              sp,
   323  		ResourceSpec:            resourceSpec,
   324  		Cfg:                     cfg.Language,
   325  		Language:                cfg.Language,
   326  		Site:                    cfg.Site,
   327  		FileCaches:              fileCaches,
   328  		BuildStartListeners:     &Listeners{},
   329  		BuildClosers:            &Closers{},
   330  		BuildState:              buildState,
   331  		Running:                 cfg.Running,
   332  		Timeout:                 time.Duration(timeoutms) * time.Millisecond,
   333  		globalErrHandler:        errorHandler,
   334  	}
   335  
   336  	if cfg.Cfg.GetBool("templateMetrics") {
   337  		d.Metrics = metrics.NewProvider(cfg.Cfg.GetBool("templateMetricsHints"))
   338  	}
   339  
   340  	return d, nil
   341  }
   342  
   343  func (d *Deps) Close() error {
   344  	return d.BuildClosers.Close()
   345  }
   346  
   347  // ForLanguage creates a copy of the Deps with the language dependent
   348  // parts switched out.
   349  func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) {
   350  	l := cfg.Language
   351  	var err error
   352  
   353  	d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, l, d.Log, d.BaseFs)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	d.ContentSpec, err = helpers.NewContentSpec(l, d.Log, d.BaseFs.Content.Fs, d.ExecHelper)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	d.Site = cfg.Site
   364  
   365  	// These are common for all sites, so reuse.
   366  	// TODO(bep) clean up these inits.
   367  	resourceCache := d.ResourceSpec.ResourceCache
   368  	postBuildAssets := d.ResourceSpec.PostBuildAssets
   369  	d.ResourceSpec, err = resources.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.BuildState, d.Log, d.globalErrHandler, d.ExecHelper, cfg.OutputFormats, cfg.MediaTypes)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  	d.ResourceSpec.ResourceCache = resourceCache
   374  	d.ResourceSpec.PostBuildAssets = postBuildAssets
   375  
   376  	d.Cfg = l
   377  	d.Language = l
   378  
   379  	if onCreated != nil {
   380  		if err = onCreated(&d); err != nil {
   381  			return nil, err
   382  		}
   383  	}
   384  
   385  	if err := d.translationProvider.Clone(&d); err != nil {
   386  		return nil, err
   387  	}
   388  
   389  	if err := d.templateProvider.Clone(&d); err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	d.BuildStartListeners = &Listeners{}
   394  
   395  	return &d, nil
   396  }
   397  
   398  // DepsCfg contains configuration options that can be used to configure Hugo
   399  // on a global level, i.e. logging etc.
   400  // Nil values will be given default values.
   401  type DepsCfg struct {
   402  
   403  	// The Logger to use.
   404  	Logger loggers.Logger
   405  
   406  	// The file systems to use
   407  	Fs *hugofs.Fs
   408  
   409  	// The language to use.
   410  	Language *langs.Language
   411  
   412  	// The Site in use
   413  	Site page.Site
   414  
   415  	// The configuration to use.
   416  	Cfg config.Provider
   417  
   418  	// The media types configured.
   419  	MediaTypes media.Types
   420  
   421  	// The output formats configured.
   422  	OutputFormats output.Formats
   423  
   424  	// Template handling.
   425  	TemplateProvider ResourceProvider
   426  	WithTemplate     func(templ tpl.TemplateManager) error
   427  	// Used in tests
   428  	OverloadedTemplateFuncs map[string]any
   429  
   430  	// i18n handling.
   431  	TranslationProvider ResourceProvider
   432  
   433  	// Whether we are in running (server) mode
   434  	Running bool
   435  }
   436  
   437  // BuildState are flags that may be turned on during a build.
   438  type BuildState struct {
   439  	counter uint64
   440  }
   441  
   442  func (b *BuildState) Incr() int {
   443  	return int(atomic.AddUint64(&b.counter, uint64(1)))
   444  }
   445  
   446  func NewBuildState() BuildState {
   447  	return BuildState{}
   448  }
   449  
   450  type Closer interface {
   451  	Close() error
   452  }
   453  
   454  type Closers struct {
   455  	mu sync.Mutex
   456  	cs []Closer
   457  }
   458  
   459  func (cs *Closers) Add(c Closer) {
   460  	cs.mu.Lock()
   461  	defer cs.mu.Unlock()
   462  	cs.cs = append(cs.cs, c)
   463  }
   464  
   465  func (cs *Closers) Close() error {
   466  	cs.mu.Lock()
   467  	defer cs.mu.Unlock()
   468  	for _, c := range cs.cs {
   469  		c.Close()
   470  	}
   471  
   472  	cs.cs = cs.cs[:0]
   473  
   474  	return nil
   475  }