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 }