github.com/pietrocarrara/hugo@v0.47.1/resource/transform.go (about) 1 // Copyright 2018 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 resource 15 16 import ( 17 "bytes" 18 "path" 19 "strconv" 20 "strings" 21 22 "github.com/gohugoio/hugo/common/errors" 23 "github.com/gohugoio/hugo/helpers" 24 "github.com/mitchellh/hashstructure" 25 "github.com/spf13/afero" 26 27 "fmt" 28 "io" 29 "sync" 30 31 "github.com/gohugoio/hugo/media" 32 33 bp "github.com/gohugoio/hugo/bufferpool" 34 ) 35 36 var ( 37 _ ContentResource = (*transformedResource)(nil) 38 _ ReadSeekCloserResource = (*transformedResource)(nil) 39 ) 40 41 func (s *Spec) Transform(r Resource, t ResourceTransformation) (Resource, error) { 42 return &transformedResource{ 43 Resource: r, 44 transformation: t, 45 transformedResourceMetadata: transformedResourceMetadata{MetaData: make(map[string]interface{})}, 46 cache: s.ResourceCache}, nil 47 } 48 49 type ResourceTransformationCtx struct { 50 // The content to transform. 51 From io.Reader 52 53 // The target of content transformation. 54 // The current implementation requires that r is written to w 55 // even if no transformation is performed. 56 To io.Writer 57 58 // This is the relative path to the original source. Unix styled slashes. 59 SourcePath string 60 61 // This is the relative target path to the resource. Unix styled slashes. 62 InPath string 63 64 // The relative target path to the transformed resource. Unix styled slashes. 65 OutPath string 66 67 // The input media type 68 InMediaType media.Type 69 70 // The media type of the transformed resource. 71 OutMediaType media.Type 72 73 // Data data can be set on the transformed Resource. Not that this need 74 // to be simple types, as it needs to be serialized to JSON and back. 75 Data map[string]interface{} 76 77 // This is used to publis additional artifacts, e.g. source maps. 78 // We may improve this. 79 OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error) 80 } 81 82 // AddOutPathIdentifier transforming InPath to OutPath adding an identifier, 83 // eg '.min' before any extension. 84 func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) { 85 ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier) 86 } 87 88 func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string { 89 dir, file := path.Split(inPath) 90 base, ext := helpers.PathAndExt(file) 91 return path.Join(dir, (base + identifier + ext)) 92 } 93 94 // ReplaceOutPathExtension transforming InPath to OutPath replacing the file 95 // extension, e.g. ".scss" 96 func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) { 97 dir, file := path.Split(ctx.InPath) 98 base, _ := helpers.PathAndExt(file) 99 ctx.OutPath = path.Join(dir, (base + newExt)) 100 } 101 102 // PublishSourceMap writes the content to the target folder of the main resource 103 // with the ".map" extension added. 104 func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error { 105 target := ctx.OutPath + ".map" 106 f, err := ctx.OpenResourcePublisher(target) 107 if err != nil { 108 return err 109 } 110 defer f.Close() 111 _, err = f.Write([]byte(content)) 112 return err 113 } 114 115 // ResourceTransformationKey are provided by the different transformation implementations. 116 // It identifies the transformation (name) and its configuration (elements). 117 // We combine this in a chain with the rest of the transformations 118 // with the target filename and a content hash of the origin to use as cache key. 119 type ResourceTransformationKey struct { 120 name string 121 elements []interface{} 122 } 123 124 // NewResourceTransformationKey creates a new ResourceTransformationKey from the transformation 125 // name and elements. We will create a 64 bit FNV hash from the elements, which when combined 126 // with the other key elements should be unique for all practical applications. 127 func NewResourceTransformationKey(name string, elements ...interface{}) ResourceTransformationKey { 128 return ResourceTransformationKey{name: name, elements: elements} 129 } 130 131 // Do not change this without good reasons. 132 func (k ResourceTransformationKey) key() string { 133 if len(k.elements) == 0 { 134 return k.name 135 } 136 137 sb := bp.GetBuffer() 138 defer bp.PutBuffer(sb) 139 140 sb.WriteString(k.name) 141 for _, element := range k.elements { 142 hash, err := hashstructure.Hash(element, nil) 143 if err != nil { 144 panic(err) 145 } 146 sb.WriteString("_") 147 sb.WriteString(strconv.FormatUint(hash, 10)) 148 } 149 150 return sb.String() 151 } 152 153 // ResourceTransformation is the interface that a resource transformation step 154 // needs to implement. 155 type ResourceTransformation interface { 156 Key() ResourceTransformationKey 157 Transform(ctx *ResourceTransformationCtx) error 158 } 159 160 // We will persist this information to disk. 161 type transformedResourceMetadata struct { 162 Target string `json:"Target"` 163 MediaTypeV string `json:"MediaType"` 164 MetaData map[string]interface{} `json:"Data"` 165 } 166 167 type transformedResource struct { 168 cache *ResourceCache 169 170 // This is the filename inside resources/_gen/assets 171 sourceFilename string 172 173 linker permalinker 174 175 // The transformation to apply. 176 transformation ResourceTransformation 177 178 // We apply the tranformations lazily. 179 transformInit sync.Once 180 transformErr error 181 182 // The transformed values 183 content string 184 contentInit sync.Once 185 transformedResourceMetadata 186 187 // The source 188 Resource 189 } 190 191 func (r *transformedResource) ReadSeekCloser() (ReadSeekCloser, error) { 192 if err := r.initContent(); err != nil { 193 return nil, err 194 } 195 return NewReadSeekerNoOpCloserFromString(r.content), nil 196 } 197 198 func (r *transformedResource) transferTransformedValues(another *transformedResource) { 199 if another.content != "" { 200 r.contentInit.Do(func() { 201 r.content = another.content 202 }) 203 } 204 r.transformedResourceMetadata = another.transformedResourceMetadata 205 } 206 207 func (r *transformedResource) tryTransformedFileCache(key string) io.ReadCloser { 208 f, meta, found := r.cache.getFromFile(key) 209 if !found { 210 return nil 211 } 212 r.transformedResourceMetadata = meta 213 r.sourceFilename = f.Name() 214 215 return f 216 } 217 218 func (r *transformedResource) Content() (interface{}, error) { 219 if err := r.initTransform(true); err != nil { 220 return nil, err 221 } 222 if err := r.initContent(); err != nil { 223 return "", err 224 } 225 return r.content, nil 226 } 227 228 func (r *transformedResource) Data() interface{} { 229 return r.MetaData 230 } 231 232 func (r *transformedResource) MediaType() media.Type { 233 if err := r.initTransform(false); err != nil { 234 return media.Type{} 235 } 236 m, _ := r.cache.rs.MediaTypes.GetByType(r.MediaTypeV) 237 return m 238 } 239 240 func (r *transformedResource) Permalink() string { 241 if err := r.initTransform(false); err != nil { 242 return "" 243 } 244 return r.linker.permalinkFor(r.Target) 245 } 246 247 func (r *transformedResource) RelPermalink() string { 248 if err := r.initTransform(false); err != nil { 249 return "" 250 } 251 return r.linker.relPermalinkFor(r.Target) 252 } 253 254 func (r *transformedResource) initContent() error { 255 var err error 256 r.contentInit.Do(func() { 257 var b []byte 258 b, err := afero.ReadFile(r.cache.rs.Resources.Fs, r.sourceFilename) 259 if err != nil { 260 return 261 } 262 r.content = string(b) 263 }) 264 return err 265 } 266 267 func (r *transformedResource) transform(setContent bool) (err error) { 268 269 openPublishFileForWriting := func(relTargetPath string) (io.WriteCloser, error) { 270 return helpers.OpenFilesForWriting(r.cache.rs.PublishFs, r.linker.relTargetPathsFor(relTargetPath)...) 271 } 272 273 // This can be the last resource in a chain. 274 // Rewind and create a processing chain. 275 var chain []Resource 276 current := r 277 for { 278 rr := current.Resource 279 chain = append(chain[:0], append([]Resource{rr}, chain[0:]...)...) 280 if tr, ok := rr.(*transformedResource); ok { 281 current = tr 282 } else { 283 break 284 } 285 } 286 287 // Append the current transformer at the end 288 chain = append(chain, r) 289 290 first := chain[0] 291 292 // Files with a suffix will be stored in cache (both on disk and in memory) 293 // partitioned by their suffix. There will be other files below /other. 294 // This partition is also how we determine what to delete on server reloads. 295 var key, base string 296 for _, element := range chain { 297 switch v := element.(type) { 298 case *transformedResource: 299 key = key + "_" + v.transformation.Key().key() 300 case permalinker: 301 r.linker = v 302 p := v.targetPath() 303 if p == "" { 304 panic("target path needed for key creation") 305 } 306 partition := ResourceKeyPartition(p) 307 base = partition + "/" + p 308 default: 309 return fmt.Errorf("transformation not supported for type %T", element) 310 } 311 } 312 313 key = r.cache.cleanKey(base + "_" + helpers.MD5String(key)) 314 315 cached, found := r.cache.get(key) 316 if found { 317 r.transferTransformedValues(cached.(*transformedResource)) 318 return 319 } 320 321 // Acquire a write lock for the named transformation. 322 r.cache.nlocker.Lock(key) 323 // Check the cache again. 324 cached, found = r.cache.get(key) 325 if found { 326 r.transferTransformedValues(cached.(*transformedResource)) 327 r.cache.nlocker.Unlock(key) 328 return 329 } 330 331 defer r.cache.nlocker.Unlock(key) 332 defer r.cache.set(key, r) 333 334 b1 := bp.GetBuffer() 335 b2 := bp.GetBuffer() 336 defer bp.PutBuffer(b1) 337 defer bp.PutBuffer(b2) 338 339 tctx := &ResourceTransformationCtx{ 340 Data: r.transformedResourceMetadata.MetaData, 341 OpenResourcePublisher: openPublishFileForWriting, 342 } 343 344 tctx.InMediaType = first.MediaType() 345 tctx.OutMediaType = first.MediaType() 346 347 contentrc, err := contentReadSeekerCloser(first) 348 if err != nil { 349 return err 350 } 351 defer contentrc.Close() 352 353 tctx.From = contentrc 354 tctx.To = b1 355 356 if r.linker != nil { 357 tctx.InPath = r.linker.targetPath() 358 tctx.SourcePath = tctx.InPath 359 } 360 361 counter := 0 362 363 var transformedContentr io.Reader 364 365 for _, element := range chain { 366 tr, ok := element.(*transformedResource) 367 if !ok { 368 continue 369 } 370 counter++ 371 if counter != 1 { 372 tctx.InMediaType = tctx.OutMediaType 373 } 374 if counter%2 == 0 { 375 tctx.From = b1 376 b2.Reset() 377 tctx.To = b2 378 } else { 379 if counter != 1 { 380 // The first reader is the file. 381 tctx.From = b2 382 } 383 b1.Reset() 384 tctx.To = b1 385 } 386 387 if err := tr.transformation.Transform(tctx); err != nil { 388 if err == errors.FeatureNotAvailableErr { 389 // This transformation is not available in this 390 // Hugo installation (scss not compiled in, PostCSS not available etc.) 391 // If a prepared bundle for this transformation chain is available, use that. 392 f := r.tryTransformedFileCache(key) 393 if f == nil { 394 return fmt.Errorf("%s: failed to transform %q (%s): %s", strings.ToUpper(tr.transformation.Key().name), tctx.InPath, tctx.InMediaType.Type(), err) 395 } 396 transformedContentr = f 397 defer f.Close() 398 399 // The reader above is all we need. 400 break 401 } 402 403 // Abort. 404 return err 405 } 406 407 if tctx.OutPath != "" { 408 tctx.InPath = tctx.OutPath 409 tctx.OutPath = "" 410 } 411 } 412 413 if transformedContentr == nil { 414 r.Target = tctx.InPath 415 r.MediaTypeV = tctx.OutMediaType.Type() 416 } 417 418 publicw, err := openPublishFileForWriting(r.Target) 419 if err != nil { 420 r.transformErr = err 421 return 422 } 423 defer publicw.Close() 424 425 publishwriters := []io.Writer{publicw} 426 427 if transformedContentr == nil { 428 // Also write it to the cache 429 metaw, err := r.cache.writeMeta(key, r.transformedResourceMetadata) 430 if err != nil { 431 return err 432 } 433 r.sourceFilename = metaw.Name() 434 defer metaw.Close() 435 436 publishwriters = append(publishwriters, metaw) 437 438 if counter > 0 { 439 transformedContentr = tctx.To.(*bytes.Buffer) 440 } else { 441 transformedContentr = contentrc 442 } 443 } 444 445 // Also write it to memory 446 var contentmemw *bytes.Buffer 447 448 if setContent { 449 contentmemw = bp.GetBuffer() 450 defer bp.PutBuffer(contentmemw) 451 publishwriters = append(publishwriters, contentmemw) 452 } 453 454 publishw := io.MultiWriter(publishwriters...) 455 _, r.transformErr = io.Copy(publishw, transformedContentr) 456 457 if setContent { 458 r.contentInit.Do(func() { 459 r.content = contentmemw.String() 460 }) 461 } 462 463 return nil 464 465 } 466 func (r *transformedResource) initTransform(setContent bool) error { 467 r.transformInit.Do(func() { 468 if err := r.transform(setContent); err != nil { 469 r.transformErr = err 470 r.cache.rs.Logger.ERROR.Println("error: failed to transform resource:", err) 471 } 472 }) 473 return r.transformErr 474 } 475 476 // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource. 477 func contentReadSeekerCloser(r Resource) (ReadSeekCloser, error) { 478 switch rr := r.(type) { 479 case ReadSeekCloserResource: 480 rc, err := rr.ReadSeekCloser() 481 if err != nil { 482 return nil, err 483 } 484 return rc, nil 485 default: 486 return nil, fmt.Errorf("cannot transform content of Resource of type %T", r) 487 488 } 489 }