mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
e4d6ec94b5
In Hugo 0.90.0 we introduced remote support in `resources.Get`. But with remote resources comes with a higher chance of failing a build (network issues, remote server down etc.). Before this commit we always failed the build on any unexpected error. This commit allows the user to check for any error (and potentially fall back to a default local resource): ```htmlbars {{ $result := resources.Get "https://gohugo.io/img/hugo-logo.png" }} {{ with $result }} {{ if .Err }} {{/* log the error, insert a default image etc. *}} {{ else }} <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt=""> {{ end }} {{ end }} ``` Note that the default behaviour is still to fail the build, but we will delay that error until you start using the `Resource`. Fixes #9529
674 lines
16 KiB
Go
674 lines
16 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package resources
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/gohugoio/hugo/resources/internal"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
|
|
"github.com/gohugoio/hugo/media"
|
|
"github.com/gohugoio/hugo/source"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/gohugoio/hugo/common/hugio"
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
)
|
|
|
|
var (
|
|
_ resource.ContentResource = (*genericResource)(nil)
|
|
_ resource.ReadSeekCloserResource = (*genericResource)(nil)
|
|
_ resource.Resource = (*genericResource)(nil)
|
|
_ resource.Source = (*genericResource)(nil)
|
|
_ resource.Cloner = (*genericResource)(nil)
|
|
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
|
|
_ permalinker = (*genericResource)(nil)
|
|
_ resource.Identifier = (*genericResource)(nil)
|
|
_ fileInfo = (*genericResource)(nil)
|
|
)
|
|
|
|
type ResourceSourceDescriptor struct {
|
|
// TargetPaths is a callback to fetch paths's relative to its owner.
|
|
TargetPaths func() page.TargetPaths
|
|
|
|
// Need one of these to load the resource content.
|
|
SourceFile source.File
|
|
OpenReadSeekCloser resource.OpenReadSeekCloser
|
|
|
|
FileInfo os.FileInfo
|
|
|
|
// If OpenReadSeekerCloser is not set, we use this to open the file.
|
|
SourceFilename string
|
|
|
|
Fs afero.Fs
|
|
|
|
// The relative target filename without any language code.
|
|
RelTargetFilename string
|
|
|
|
// Any base paths prepended to the target path. This will also typically be the
|
|
// language code, but setting it here means that it should not have any effect on
|
|
// the permalink.
|
|
// This may be several values. In multihost mode we may publish the same resources to
|
|
// multiple targets.
|
|
TargetBasePaths []string
|
|
|
|
// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
|
|
LazyPublish bool
|
|
}
|
|
|
|
func (r ResourceSourceDescriptor) Filename() string {
|
|
if r.SourceFile != nil {
|
|
return r.SourceFile.Filename()
|
|
}
|
|
return r.SourceFilename
|
|
}
|
|
|
|
type ResourceTransformer interface {
|
|
resource.Resource
|
|
Transformer
|
|
}
|
|
|
|
type Transformer interface {
|
|
Transform(...ResourceTransformation) (ResourceTransformer, error)
|
|
}
|
|
|
|
func NewFeatureNotAvailableTransformer(key string, elements ...interface{}) ResourceTransformation {
|
|
return transformerNotAvailable{
|
|
key: internal.NewResourceTransformationKey(key, elements...),
|
|
}
|
|
}
|
|
|
|
type transformerNotAvailable struct {
|
|
key internal.ResourceTransformationKey
|
|
}
|
|
|
|
func (t transformerNotAvailable) Transform(ctx *ResourceTransformationCtx) error {
|
|
return herrors.ErrFeatureNotAvailable
|
|
}
|
|
|
|
func (t transformerNotAvailable) Key() internal.ResourceTransformationKey {
|
|
return t.key
|
|
}
|
|
|
|
type baseResourceResource interface {
|
|
resource.Cloner
|
|
resource.ContentProvider
|
|
resource.Resource
|
|
resource.Identifier
|
|
}
|
|
|
|
type baseResourceInternal interface {
|
|
resource.Source
|
|
|
|
fileInfo
|
|
metaAssigner
|
|
targetPather
|
|
|
|
ReadSeekCloser() (hugio.ReadSeekCloser, error)
|
|
|
|
// Internal
|
|
cloneWithUpdates(*transformationUpdate) (baseResource, error)
|
|
tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser
|
|
|
|
specProvider
|
|
getResourcePaths() *resourcePathDescriptor
|
|
getTargetFilenames() []string
|
|
openDestinationsForWriting() (io.WriteCloser, error)
|
|
openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error)
|
|
|
|
relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string
|
|
}
|
|
|
|
type specProvider interface {
|
|
getSpec() *Spec
|
|
}
|
|
|
|
type baseResource interface {
|
|
baseResourceResource
|
|
baseResourceInternal
|
|
}
|
|
|
|
type commonResource struct {
|
|
}
|
|
|
|
// Slice is not meant to be used externally. It's a bridge function
|
|
// for the template functions. See collections.Slice.
|
|
func (commonResource) Slice(in interface{}) (interface{}, error) {
|
|
switch items := in.(type) {
|
|
case resource.Resources:
|
|
return items, nil
|
|
case []interface{}:
|
|
groups := make(resource.Resources, len(items))
|
|
for i, v := range items {
|
|
g, ok := v.(resource.Resource)
|
|
if !ok {
|
|
return nil, fmt.Errorf("type %T is not a Resource", v)
|
|
}
|
|
groups[i] = g
|
|
{
|
|
}
|
|
}
|
|
return groups, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid slice type %T", items)
|
|
}
|
|
}
|
|
|
|
type dirFile struct {
|
|
// This is the directory component with Unix-style slashes.
|
|
dir string
|
|
// This is the file component.
|
|
file string
|
|
}
|
|
|
|
func (d dirFile) path() string {
|
|
return path.Join(d.dir, d.file)
|
|
}
|
|
|
|
type fileInfo interface {
|
|
getSourceFilename() string
|
|
setSourceFilename(string)
|
|
setSourceFs(afero.Fs)
|
|
getFileInfo() hugofs.FileMetaInfo
|
|
hash() (string, error)
|
|
size() int
|
|
}
|
|
|
|
// genericResource represents a generic linkable resource.
|
|
type genericResource struct {
|
|
*resourcePathDescriptor
|
|
*resourceFileInfo
|
|
*resourceContent
|
|
|
|
spec *Spec
|
|
|
|
title string
|
|
name string
|
|
params map[string]interface{}
|
|
data map[string]interface{}
|
|
|
|
resourceType string
|
|
mediaType media.Type
|
|
}
|
|
|
|
func (l *genericResource) Clone() resource.Resource {
|
|
return l.clone()
|
|
}
|
|
|
|
func (l *genericResource) Content() (interface{}, error) {
|
|
if err := l.initContent(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return l.content, nil
|
|
}
|
|
|
|
func (r *genericResource) Err() error {
|
|
return nil
|
|
}
|
|
|
|
func (l *genericResource) Data() interface{} {
|
|
return l.data
|
|
}
|
|
|
|
func (l *genericResource) Key() string {
|
|
return l.RelPermalink()
|
|
}
|
|
|
|
func (l *genericResource) MediaType() media.Type {
|
|
return l.mediaType
|
|
}
|
|
|
|
func (l *genericResource) setMediaType(mediaType media.Type) {
|
|
l.mediaType = mediaType
|
|
}
|
|
|
|
func (l *genericResource) Name() string {
|
|
return l.name
|
|
}
|
|
|
|
func (l *genericResource) Params() maps.Params {
|
|
return l.params
|
|
}
|
|
|
|
func (l *genericResource) Permalink() string {
|
|
return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path(), true), l.spec.BaseURL.HostURL())
|
|
}
|
|
|
|
func (l *genericResource) Publish() error {
|
|
var err error
|
|
l.publishInit.Do(func() {
|
|
var fr hugio.ReadSeekCloser
|
|
fr, err = l.ReadSeekCloser()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer fr.Close()
|
|
|
|
var fw io.WriteCloser
|
|
fw, err = helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.getTargetFilenames()...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer fw.Close()
|
|
|
|
_, err = io.Copy(fw, fr)
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func (l *genericResource) RelPermalink() string {
|
|
return l.relPermalinkFor(l.relTargetDirFile.path())
|
|
}
|
|
|
|
func (l *genericResource) ResourceType() string {
|
|
return l.resourceType
|
|
}
|
|
|
|
func (l *genericResource) String() string {
|
|
return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
|
|
}
|
|
|
|
// Path is stored with Unix style slashes.
|
|
func (l *genericResource) TargetPath() string {
|
|
return l.relTargetDirFile.path()
|
|
}
|
|
|
|
func (l *genericResource) Title() string {
|
|
return l.title
|
|
}
|
|
|
|
func (l *genericResource) createBasePath(rel string, isURL bool) string {
|
|
if l.targetPathBuilder == nil {
|
|
return rel
|
|
}
|
|
tp := l.targetPathBuilder()
|
|
|
|
if isURL {
|
|
return path.Join(tp.SubResourceBaseLink, rel)
|
|
}
|
|
|
|
// TODO(bep) path
|
|
return path.Join(filepath.ToSlash(tp.SubResourceBaseTarget), rel)
|
|
}
|
|
|
|
func (l *genericResource) initContent() error {
|
|
var err error
|
|
l.contentInit.Do(func() {
|
|
var r hugio.ReadSeekCloser
|
|
r, err = l.ReadSeekCloser()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
var b []byte
|
|
b, err = ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
l.content = string(b)
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func (l *genericResource) setName(name string) {
|
|
l.name = name
|
|
}
|
|
|
|
func (l *genericResource) getResourcePaths() *resourcePathDescriptor {
|
|
return l.resourcePathDescriptor
|
|
}
|
|
|
|
func (l *genericResource) getSpec() *Spec {
|
|
return l.spec
|
|
}
|
|
|
|
func (l *genericResource) getTargetFilenames() []string {
|
|
paths := l.relTargetPaths()
|
|
for i, p := range paths {
|
|
paths[i] = filepath.Clean(p)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (l *genericResource) setTitle(title string) {
|
|
l.title = title
|
|
}
|
|
|
|
func (r *genericResource) tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser {
|
|
fi, f, meta, found := r.spec.ResourceCache.getFromFile(key)
|
|
if !found {
|
|
return nil
|
|
}
|
|
u.sourceFilename = &fi.Name
|
|
mt, _ := r.spec.MediaTypes.GetByType(meta.MediaTypeV)
|
|
u.mediaType = mt
|
|
u.data = meta.MetaData
|
|
u.targetPath = meta.Target
|
|
return f
|
|
}
|
|
|
|
func (r *genericResource) mergeData(in map[string]interface{}) {
|
|
if len(in) == 0 {
|
|
return
|
|
}
|
|
if r.data == nil {
|
|
r.data = make(map[string]interface{})
|
|
}
|
|
for k, v := range in {
|
|
if _, found := r.data[k]; !found {
|
|
r.data[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
|
|
r := rc.clone()
|
|
|
|
if u.content != nil {
|
|
r.contentInit.Do(func() {
|
|
r.content = *u.content
|
|
r.openReadSeekerCloser = func() (hugio.ReadSeekCloser, error) {
|
|
return hugio.NewReadSeekerNoOpCloserFromString(r.content), nil
|
|
}
|
|
})
|
|
}
|
|
|
|
r.mediaType = u.mediaType
|
|
|
|
if u.sourceFilename != nil {
|
|
r.setSourceFilename(*u.sourceFilename)
|
|
}
|
|
|
|
if u.sourceFs != nil {
|
|
r.setSourceFs(u.sourceFs)
|
|
}
|
|
|
|
if u.targetPath == "" {
|
|
return nil, errors.New("missing targetPath")
|
|
}
|
|
|
|
fpath, fname := path.Split(u.targetPath)
|
|
r.resourcePathDescriptor.relTargetDirFile = dirFile{dir: fpath, file: fname}
|
|
|
|
r.mergeData(u.data)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (l genericResource) clone() *genericResource {
|
|
gi := *l.resourceFileInfo
|
|
rp := *l.resourcePathDescriptor
|
|
l.resourceFileInfo = &gi
|
|
l.resourcePathDescriptor = &rp
|
|
l.resourceContent = &resourceContent{}
|
|
return &l
|
|
}
|
|
|
|
// returns an opened file or nil if nothing to write (it may already be published).
|
|
func (l *genericResource) openDestinationsForWriting() (w io.WriteCloser, err error) {
|
|
l.publishInit.Do(func() {
|
|
targetFilenames := l.getTargetFilenames()
|
|
var changedFilenames []string
|
|
|
|
// Fast path:
|
|
// This is a processed version of the original;
|
|
// check if it already exists at the destination.
|
|
for _, targetFilename := range targetFilenames {
|
|
if _, err := l.getSpec().BaseFs.PublishFs.Stat(targetFilename); err == nil {
|
|
continue
|
|
}
|
|
|
|
changedFilenames = append(changedFilenames, targetFilename)
|
|
}
|
|
|
|
if len(changedFilenames) == 0 {
|
|
return
|
|
}
|
|
|
|
w, err = helpers.OpenFilesForWriting(l.getSpec().BaseFs.PublishFs, changedFilenames...)
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func (r *genericResource) openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) {
|
|
return helpers.OpenFilesForWriting(r.spec.BaseFs.PublishFs, r.relTargetPathsFor(relTargetPath)...)
|
|
}
|
|
|
|
func (l *genericResource) permalinkFor(target string) string {
|
|
return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.BaseURL.HostURL())
|
|
}
|
|
|
|
func (l *genericResource) relPermalinkFor(target string) string {
|
|
return l.relPermalinkForRel(target, false)
|
|
}
|
|
|
|
func (l *genericResource) relPermalinkForRel(rel string, isAbs bool) string {
|
|
return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, isAbs, true))
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string {
|
|
if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 {
|
|
panic("multiple baseTargetPathDirs")
|
|
}
|
|
var basePath string
|
|
if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 {
|
|
basePath = l.baseTargetPathDirs[0]
|
|
}
|
|
|
|
return l.relTargetPathForRelAndBasePath(rel, basePath, isAbs, isURL)
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isAbs, isURL bool) string {
|
|
rel = l.createBasePath(rel, isURL)
|
|
|
|
if basePath != "" {
|
|
rel = path.Join(basePath, rel)
|
|
}
|
|
|
|
if l.baseOffset != "" {
|
|
rel = path.Join(l.baseOffset, rel)
|
|
}
|
|
|
|
if isURL {
|
|
bp := l.spec.PathSpec.GetBasePath(!isAbs)
|
|
if bp != "" {
|
|
rel = path.Join(bp, rel)
|
|
}
|
|
}
|
|
|
|
if len(rel) == 0 || rel[0] != '/' {
|
|
rel = "/" + rel
|
|
}
|
|
|
|
return rel
|
|
}
|
|
|
|
func (l *genericResource) relTargetPaths() []string {
|
|
return l.relTargetPathsForRel(l.TargetPath())
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathsFor(target string) []string {
|
|
return l.relTargetPathsForRel(target)
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathsForRel(rel string) []string {
|
|
if len(l.baseTargetPathDirs) == 0 {
|
|
return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
|
|
}
|
|
|
|
targetPaths := make([]string, len(l.baseTargetPathDirs))
|
|
for i, dir := range l.baseTargetPathDirs {
|
|
targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false, false)
|
|
}
|
|
return targetPaths
|
|
}
|
|
|
|
func (l *genericResource) updateParams(params map[string]interface{}) {
|
|
if l.params == nil {
|
|
l.params = params
|
|
return
|
|
}
|
|
|
|
// Sets the params not already set
|
|
for k, v := range params {
|
|
if _, found := l.params[k]; !found {
|
|
l.params[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
type targetPather interface {
|
|
TargetPath() string
|
|
}
|
|
|
|
type permalinker interface {
|
|
targetPather
|
|
permalinkFor(target string) string
|
|
relPermalinkFor(target string) string
|
|
relTargetPaths() []string
|
|
relTargetPathsFor(target string) []string
|
|
}
|
|
|
|
type resourceContent struct {
|
|
content string
|
|
contentInit sync.Once
|
|
|
|
publishInit sync.Once
|
|
}
|
|
|
|
type resourceFileInfo struct {
|
|
// Will be set if this resource is backed by something other than a file.
|
|
openReadSeekerCloser resource.OpenReadSeekCloser
|
|
|
|
// This may be set to tell us to look in another filesystem for this resource.
|
|
// We, by default, use the sourceFs filesystem in the spec below.
|
|
sourceFs afero.Fs
|
|
|
|
// Absolute filename to the source, including any content folder path.
|
|
// Note that this is absolute in relation to the filesystem it is stored in.
|
|
// It can be a base path filesystem, and then this filename will not match
|
|
// the path to the file on the real filesystem.
|
|
sourceFilename string
|
|
|
|
fi hugofs.FileMetaInfo
|
|
|
|
// A hash of the source content. Is only calculated in caching situations.
|
|
h *resourceHash
|
|
}
|
|
|
|
func (fi *resourceFileInfo) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
|
|
if fi.openReadSeekerCloser != nil {
|
|
return fi.openReadSeekerCloser()
|
|
}
|
|
|
|
f, err := fi.getSourceFs().Open(fi.getSourceFilename())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getFileInfo() hugofs.FileMetaInfo {
|
|
return fi.fi
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getSourceFilename() string {
|
|
return fi.sourceFilename
|
|
}
|
|
|
|
func (fi *resourceFileInfo) setSourceFilename(s string) {
|
|
// Make sure it's always loaded by sourceFilename.
|
|
fi.openReadSeekerCloser = nil
|
|
fi.sourceFilename = s
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getSourceFs() afero.Fs {
|
|
return fi.sourceFs
|
|
}
|
|
|
|
func (fi *resourceFileInfo) setSourceFs(fs afero.Fs) {
|
|
fi.sourceFs = fs
|
|
}
|
|
|
|
func (fi *resourceFileInfo) hash() (string, error) {
|
|
var err error
|
|
fi.h.init.Do(func() {
|
|
var hash string
|
|
var f hugio.ReadSeekCloser
|
|
f, err = fi.ReadSeekCloser()
|
|
if err != nil {
|
|
err = errors.Wrap(err, "failed to open source file")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
hash, err = helpers.MD5FromFileFast(f)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fi.h.value = hash
|
|
})
|
|
|
|
return fi.h.value, err
|
|
}
|
|
|
|
func (fi *resourceFileInfo) size() int {
|
|
if fi.fi == nil {
|
|
return 0
|
|
}
|
|
|
|
return int(fi.fi.Size())
|
|
}
|
|
|
|
type resourceHash struct {
|
|
value string
|
|
init sync.Once
|
|
}
|
|
|
|
type resourcePathDescriptor struct {
|
|
// The relative target directory and filename.
|
|
relTargetDirFile dirFile
|
|
|
|
// Callback used to construct a target path relative to its owner.
|
|
targetPathBuilder func() page.TargetPaths
|
|
|
|
// This will normally be the same as above, but this will only apply to publishing
|
|
// of resources. It may be multiple values when in multihost mode.
|
|
baseTargetPathDirs []string
|
|
|
|
// baseOffset is set when the output format's path has a offset, e.g. for AMP.
|
|
baseOffset string
|
|
}
|