github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/transform.go (about) 1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package resources 15 16 import ( 17 "bytes" 18 "fmt" 19 "image" 20 "io" 21 "path" 22 "strings" 23 "sync" 24 25 "github.com/gohugoio/hugo/common/paths" 26 27 "github.com/pkg/errors" 28 29 "github.com/gohugoio/hugo/resources/images/exif" 30 "github.com/spf13/afero" 31 32 bp "github.com/gohugoio/hugo/bufferpool" 33 34 "github.com/gohugoio/hugo/common/herrors" 35 "github.com/gohugoio/hugo/common/hugio" 36 "github.com/gohugoio/hugo/common/maps" 37 "github.com/gohugoio/hugo/helpers" 38 "github.com/gohugoio/hugo/resources/internal" 39 "github.com/gohugoio/hugo/resources/resource" 40 41 "github.com/gohugoio/hugo/media" 42 ) 43 44 var ( 45 _ resource.ContentResource = (*resourceAdapter)(nil) 46 _ resource.ReadSeekCloserResource = (*resourceAdapter)(nil) 47 _ resource.Resource = (*resourceAdapter)(nil) 48 _ resource.Source = (*resourceAdapter)(nil) 49 _ resource.Identifier = (*resourceAdapter)(nil) 50 _ resource.ResourceMetaProvider = (*resourceAdapter)(nil) 51 ) 52 53 // These are transformations that need special support in Hugo that may not 54 // be available when building the theme/site so we write the transformation 55 // result to disk and reuse if needed for these, 56 // TODO(bep) it's a little fragile having these constants redefined here. 57 var transformationsToCacheOnDisk = map[string]bool{ 58 "postcss": true, 59 "tocss": true, 60 "tocss-dart": true, 61 } 62 63 func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResource) *resourceAdapter { 64 var po *publishOnce 65 if lazyPublish { 66 po = &publishOnce{} 67 } 68 return &resourceAdapter{ 69 resourceTransformations: &resourceTransformations{}, 70 resourceAdapterInner: &resourceAdapterInner{ 71 spec: spec, 72 publishOnce: po, 73 target: target, 74 }, 75 } 76 } 77 78 // ResourceTransformation is the interface that a resource transformation step 79 // needs to implement. 80 type ResourceTransformation interface { 81 Key() internal.ResourceTransformationKey 82 Transform(ctx *ResourceTransformationCtx) error 83 } 84 85 type ResourceTransformationCtx struct { 86 // The content to transform. 87 From io.Reader 88 89 // The target of content transformation. 90 // The current implementation requires that r is written to w 91 // even if no transformation is performed. 92 To io.Writer 93 94 // This is the relative path to the original source. Unix styled slashes. 95 SourcePath string 96 97 // This is the relative target path to the resource. Unix styled slashes. 98 InPath string 99 100 // The relative target path to the transformed resource. Unix styled slashes. 101 OutPath string 102 103 // The input media type 104 InMediaType media.Type 105 106 // The media type of the transformed resource. 107 OutMediaType media.Type 108 109 // Data data can be set on the transformed Resource. Not that this need 110 // to be simple types, as it needs to be serialized to JSON and back. 111 Data map[string]interface{} 112 113 // This is used to publish additional artifacts, e.g. source maps. 114 // We may improve this. 115 OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error) 116 } 117 118 // AddOutPathIdentifier transforming InPath to OutPath adding an identifier, 119 // eg '.min' before any extension. 120 func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) { 121 ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier) 122 } 123 124 // PublishSourceMap writes the content to the target folder of the main resource 125 // with the ".map" extension added. 126 func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error { 127 target := ctx.OutPath + ".map" 128 f, err := ctx.OpenResourcePublisher(target) 129 if err != nil { 130 return err 131 } 132 defer f.Close() 133 _, err = f.Write([]byte(content)) 134 return err 135 } 136 137 // ReplaceOutPathExtension transforming InPath to OutPath replacing the file 138 // extension, e.g. ".scss" 139 func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) { 140 dir, file := path.Split(ctx.InPath) 141 base, _ := paths.PathAndExt(file) 142 ctx.OutPath = path.Join(dir, (base + newExt)) 143 } 144 145 func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string { 146 dir, file := path.Split(inPath) 147 base, ext := paths.PathAndExt(file) 148 return path.Join(dir, (base + identifier + ext)) 149 } 150 151 type publishOnce struct { 152 publisherInit sync.Once 153 publisherErr error 154 } 155 156 type resourceAdapter struct { 157 commonResource 158 *resourceTransformations 159 *resourceAdapterInner 160 } 161 162 func (r *resourceAdapter) Content() (interface{}, error) { 163 r.init(false, true) 164 if r.transformationsErr != nil { 165 return nil, r.transformationsErr 166 } 167 return r.target.Content() 168 } 169 170 func (r *resourceAdapter) Data() interface{} { 171 r.init(false, false) 172 return r.target.Data() 173 } 174 175 func (r *resourceAdapter) Fill(spec string) (resource.Image, error) { 176 return r.getImageOps().Fill(spec) 177 } 178 179 func (r *resourceAdapter) Fit(spec string) (resource.Image, error) { 180 return r.getImageOps().Fit(spec) 181 } 182 183 func (r *resourceAdapter) Filter(filters ...interface{}) (resource.Image, error) { 184 return r.getImageOps().Filter(filters...) 185 } 186 187 func (r *resourceAdapter) Height() int { 188 return r.getImageOps().Height() 189 } 190 191 func (r *resourceAdapter) Exif() *exif.Exif { 192 return r.getImageOps().Exif() 193 } 194 195 func (r *resourceAdapter) Key() string { 196 r.init(false, false) 197 return r.target.(resource.Identifier).Key() 198 } 199 200 func (r *resourceAdapter) MediaType() media.Type { 201 r.init(false, false) 202 return r.target.MediaType() 203 } 204 205 func (r *resourceAdapter) Name() string { 206 r.init(false, false) 207 return r.target.Name() 208 } 209 210 func (r *resourceAdapter) Params() maps.Params { 211 r.init(false, false) 212 return r.target.Params() 213 } 214 215 func (r *resourceAdapter) Permalink() string { 216 r.init(true, false) 217 return r.target.Permalink() 218 } 219 220 func (r *resourceAdapter) Publish() error { 221 r.init(false, false) 222 223 return r.target.Publish() 224 } 225 226 func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) { 227 r.init(false, false) 228 return r.target.ReadSeekCloser() 229 } 230 231 func (r *resourceAdapter) RelPermalink() string { 232 r.init(true, false) 233 return r.target.RelPermalink() 234 } 235 236 func (r *resourceAdapter) Resize(spec string) (resource.Image, error) { 237 return r.getImageOps().Resize(spec) 238 } 239 240 func (r *resourceAdapter) ResourceType() string { 241 r.init(false, false) 242 return r.target.ResourceType() 243 } 244 245 func (r *resourceAdapter) String() string { 246 return r.Name() 247 } 248 249 func (r *resourceAdapter) Title() string { 250 r.init(false, false) 251 return r.target.Title() 252 } 253 254 func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) { 255 r.resourceTransformations = &resourceTransformations{ 256 transformations: append(r.transformations, t...), 257 } 258 259 r.resourceAdapterInner = &resourceAdapterInner{ 260 spec: r.spec, 261 publishOnce: &publishOnce{}, 262 target: r.target, 263 } 264 265 return &r, nil 266 } 267 268 func (r *resourceAdapter) Width() int { 269 return r.getImageOps().Width() 270 } 271 272 func (r *resourceAdapter) DecodeImage() (image.Image, error) { 273 return r.getImageOps().DecodeImage() 274 } 275 276 func (r *resourceAdapter) getImageOps() resource.ImageOps { 277 img, ok := r.target.(resource.ImageOps) 278 if !ok { 279 panic(fmt.Sprintf("%T is not an image", r.target)) 280 } 281 r.init(false, false) 282 return img 283 } 284 285 func (r *resourceAdapter) getMetaAssigner() metaAssigner { 286 return r.target 287 } 288 289 func (r *resourceAdapter) getSpec() *Spec { 290 return r.spec 291 } 292 293 func (r *resourceAdapter) publish() { 294 if r.publishOnce == nil { 295 return 296 } 297 298 r.publisherInit.Do(func() { 299 r.publisherErr = r.target.Publish() 300 301 if r.publisherErr != nil { 302 r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr) 303 } 304 }) 305 } 306 307 func (r *resourceAdapter) TransformationKey() string { 308 // Files with a suffix will be stored in cache (both on disk and in memory) 309 // partitioned by their suffix. 310 var key string 311 for _, tr := range r.transformations { 312 key = key + "_" + tr.Key().Value() 313 } 314 315 base := ResourceCacheKey(r.target.Key()) 316 return r.spec.ResourceCache.cleanKey(base) + "_" + helpers.MD5String(key) 317 } 318 319 func (r *resourceAdapter) transform(publish, setContent bool) error { 320 cache := r.spec.ResourceCache 321 322 key := r.TransformationKey() 323 324 cached, found := cache.get(key) 325 326 if found { 327 r.resourceAdapterInner = cached.(*resourceAdapterInner) 328 return nil 329 } 330 331 // Acquire a write lock for the named transformation. 332 cache.nlocker.Lock(key) 333 // Check the cache again. 334 cached, found = cache.get(key) 335 if found { 336 r.resourceAdapterInner = cached.(*resourceAdapterInner) 337 cache.nlocker.Unlock(key) 338 return nil 339 } 340 341 defer cache.nlocker.Unlock(key) 342 defer cache.set(key, r.resourceAdapterInner) 343 344 b1 := bp.GetBuffer() 345 b2 := bp.GetBuffer() 346 defer bp.PutBuffer(b1) 347 defer bp.PutBuffer(b2) 348 349 tctx := &ResourceTransformationCtx{ 350 Data: make(map[string]interface{}), 351 OpenResourcePublisher: r.target.openPublishFileForWriting, 352 } 353 354 tctx.InMediaType = r.target.MediaType() 355 tctx.OutMediaType = r.target.MediaType() 356 357 startCtx := *tctx 358 updates := &transformationUpdate{startCtx: startCtx} 359 360 var contentrc hugio.ReadSeekCloser 361 362 contentrc, err := contentReadSeekerCloser(r.target) 363 if err != nil { 364 return err 365 } 366 367 defer contentrc.Close() 368 369 tctx.From = contentrc 370 tctx.To = b1 371 372 tctx.InPath = r.target.TargetPath() 373 tctx.SourcePath = tctx.InPath 374 375 counter := 0 376 writeToFileCache := false 377 378 var transformedContentr io.Reader 379 380 for i, tr := range r.transformations { 381 if i != 0 { 382 tctx.InMediaType = tctx.OutMediaType 383 } 384 385 mayBeCachedOnDisk := transformationsToCacheOnDisk[tr.Key().Name] 386 if !writeToFileCache { 387 writeToFileCache = mayBeCachedOnDisk 388 } 389 390 if i > 0 { 391 hasWrites := tctx.To.(*bytes.Buffer).Len() > 0 392 if hasWrites { 393 counter++ 394 // Switch the buffers 395 if counter%2 == 0 { 396 tctx.From = b2 397 b1.Reset() 398 tctx.To = b1 399 } else { 400 tctx.From = b1 401 b2.Reset() 402 tctx.To = b2 403 } 404 } 405 } 406 407 newErr := func(err error) error { 408 msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type()) 409 410 if err == herrors.ErrFeatureNotAvailable { 411 var errMsg string 412 if tr.Key().Name == "postcss" { 413 // This transformation is not available in this 414 // Most likely because PostCSS is not installed. 415 errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/" 416 } else if tr.Key().Name == "tocss" { 417 errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS." 418 } else if tr.Key().Name == "tocss-dart" { 419 errMsg = ". You need dart-sass-embedded in your system $PATH." 420 421 } else if tr.Key().Name == "babel" { 422 errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/" 423 } 424 425 return errors.Wrap(err, msg+errMsg) 426 } 427 428 return errors.Wrap(err, msg) 429 } 430 431 var tryFileCache bool 432 433 if mayBeCachedOnDisk && r.spec.BuildConfig.UseResourceCache(nil) { 434 tryFileCache = true 435 } else { 436 err = tr.Transform(tctx) 437 if err != nil && err != herrors.ErrFeatureNotAvailable { 438 return newErr(err) 439 } 440 441 if mayBeCachedOnDisk { 442 tryFileCache = r.spec.BuildConfig.UseResourceCache(err) 443 } 444 if err != nil && !tryFileCache { 445 return newErr(err) 446 } 447 } 448 449 if tryFileCache { 450 f := r.target.tryTransformedFileCache(key, updates) 451 if f == nil { 452 if err != nil { 453 return newErr(err) 454 } 455 return newErr(errors.Errorf("resource %q not found in file cache", key)) 456 } 457 transformedContentr = f 458 updates.sourceFs = cache.fileCache.Fs 459 defer f.Close() 460 461 // The reader above is all we need. 462 break 463 } 464 465 if tctx.OutPath != "" { 466 tctx.InPath = tctx.OutPath 467 tctx.OutPath = "" 468 } 469 } 470 471 if transformedContentr == nil { 472 updates.updateFromCtx(tctx) 473 } 474 475 var publishwriters []io.WriteCloser 476 477 if publish { 478 publicw, err := r.target.openPublishFileForWriting(updates.targetPath) 479 if err != nil { 480 return err 481 } 482 publishwriters = append(publishwriters, publicw) 483 } 484 485 if transformedContentr == nil { 486 if writeToFileCache { 487 // Also write it to the cache 488 fi, metaw, err := cache.writeMeta(key, updates.toTransformedResourceMetadata()) 489 if err != nil { 490 return err 491 } 492 updates.sourceFilename = &fi.Name 493 updates.sourceFs = cache.fileCache.Fs 494 publishwriters = append(publishwriters, metaw) 495 } 496 497 // Any transformations reading from From must also write to To. 498 // This means that if the target buffer is empty, we can just reuse 499 // the original reader. 500 if b, ok := tctx.To.(*bytes.Buffer); ok && b.Len() > 0 { 501 transformedContentr = tctx.To.(*bytes.Buffer) 502 } else { 503 transformedContentr = contentrc 504 } 505 } 506 507 // Also write it to memory 508 var contentmemw *bytes.Buffer 509 510 setContent = setContent || !writeToFileCache 511 512 if setContent { 513 contentmemw = bp.GetBuffer() 514 defer bp.PutBuffer(contentmemw) 515 publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw)) 516 } 517 518 publishw := hugio.NewMultiWriteCloser(publishwriters...) 519 _, err = io.Copy(publishw, transformedContentr) 520 if err != nil { 521 return err 522 } 523 publishw.Close() 524 525 if setContent { 526 s := contentmemw.String() 527 updates.content = &s 528 } 529 530 newTarget, err := r.target.cloneWithUpdates(updates) 531 if err != nil { 532 return err 533 } 534 r.target = newTarget 535 536 return nil 537 } 538 539 func (r *resourceAdapter) init(publish, setContent bool) { 540 r.initTransform(publish, setContent) 541 } 542 543 func (r *resourceAdapter) initTransform(publish, setContent bool) { 544 r.transformationsInit.Do(func() { 545 if len(r.transformations) == 0 { 546 // Nothing to do. 547 return 548 } 549 550 if publish { 551 // The transformation will write the content directly to 552 // the destination. 553 r.publishOnce = nil 554 } 555 556 r.transformationsErr = r.transform(publish, setContent) 557 if r.transformationsErr != nil { 558 if r.spec.ErrorSender != nil { 559 r.spec.ErrorSender.SendError(r.transformationsErr) 560 } else { 561 r.spec.Logger.Errorf("Transformation failed: %s", r.transformationsErr) 562 } 563 } 564 }) 565 566 if publish && r.publishOnce != nil { 567 r.publish() 568 } 569 } 570 571 type resourceAdapterInner struct { 572 target transformableResource 573 574 spec *Spec 575 576 // Handles publishing (to /public) if needed. 577 *publishOnce 578 } 579 580 type resourceTransformations struct { 581 transformationsInit sync.Once 582 transformationsErr error 583 transformations []ResourceTransformation 584 } 585 586 type transformableResource interface { 587 baseResourceInternal 588 589 resource.ContentProvider 590 resource.Resource 591 resource.Identifier 592 } 593 594 type transformationUpdate struct { 595 content *string 596 sourceFilename *string 597 sourceFs afero.Fs 598 targetPath string 599 mediaType media.Type 600 data map[string]interface{} 601 602 startCtx ResourceTransformationCtx 603 } 604 605 func (u *transformationUpdate) isContentChanged() bool { 606 return u.content != nil || u.sourceFilename != nil 607 } 608 609 func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata { 610 return transformedResourceMetadata{ 611 MediaTypeV: u.mediaType.Type(), 612 Target: u.targetPath, 613 MetaData: u.data, 614 } 615 } 616 617 func (u *transformationUpdate) updateFromCtx(ctx *ResourceTransformationCtx) { 618 u.targetPath = ctx.OutPath 619 u.mediaType = ctx.OutMediaType 620 u.data = ctx.Data 621 u.targetPath = ctx.InPath 622 } 623 624 // We will persist this information to disk. 625 type transformedResourceMetadata struct { 626 Target string `json:"Target"` 627 MediaTypeV string `json:"MediaType"` 628 MetaData map[string]interface{} `json:"Data"` 629 } 630 631 // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource. 632 func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) { 633 switch rr := r.(type) { 634 case resource.ReadSeekCloserResource: 635 rc, err := rr.ReadSeekCloser() 636 if err != nil { 637 return nil, err 638 } 639 return rc, nil 640 default: 641 return nil, fmt.Errorf("cannot transform content of Resource of type %T", r) 642 643 } 644 }