summaryrefslogtreecommitdiffstats
path: root/resources/resource.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-08-18 11:21:27 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-08-26 15:00:44 +0200
commitf9978ed16476ca6d233a89669c62c798cdf9db9d (patch)
tree02edb31008b997a3e77055060a34971fe9e8c5a4 /resources/resource.go
parent58d4c0a8be8beefbd7437b17bf7a9a381164d09b (diff)
Image resource refactor
This commit pulls most of the image related logic into its own package, to make it easier to reason about and extend. This is also a rewrite of the transformation logic used in Hugo Pipes, mostly to allow constructs like the one below: {{ ($myimg | fingerprint ).Width }} Fixes #5903 Fixes #6234 Fixes #6266
Diffstat (limited to 'resources/resource.go')
-rw-r--r--resources/resource.go827
1 files changed, 352 insertions, 475 deletions
diff --git a/resources/resource.go b/resources/resource.go
index 92bcbd0fc..3859e6044 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -17,30 +17,23 @@ import (
"fmt"
"io"
"io/ioutil"
- "mime"
"os"
"path"
"path/filepath"
- "strings"
"sync"
"github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/source"
- "github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/tpl"
"github.com/pkg/errors"
- "github.com/gohugoio/hugo/cache/filecache"
- "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/hugio"
- "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/helpers"
- "github.com/gohugoio/hugo/source"
)
var (
@@ -51,80 +44,10 @@ var (
_ resource.Cloner = (*genericResource)(nil)
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
_ permalinker = (*genericResource)(nil)
- _ collections.Slicer = (*genericResource)(nil)
_ resource.Identifier = (*genericResource)(nil)
+ _ fileInfo = (*genericResource)(nil)
)
-var noData = make(map[string]interface{})
-
-type permalinker interface {
- relPermalinkFor(target string) string
- permalinkFor(target string) string
- relTargetPathsFor(target string) []string
- relTargetPaths() []string
- TargetPath() string
-}
-
-type Spec struct {
- *helpers.PathSpec
-
- MediaTypes media.Types
- OutputFormats output.Formats
-
- Logger *loggers.Logger
-
- TextTemplates tpl.TemplateParseFinder
-
- Permalinks page.PermalinkExpander
-
- // Holds default filter settings etc.
- imaging *Imaging
-
- imageCache *imageCache
- ResourceCache *ResourceCache
- FileCaches filecache.Caches
-}
-
-func NewSpec(
- s *helpers.PathSpec,
- fileCaches filecache.Caches,
- logger *loggers.Logger,
- outputFormats output.Formats,
- mimeTypes media.Types) (*Spec, error) {
-
- imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging"))
- if err != nil {
- return nil, err
- }
-
- if logger == nil {
- logger = loggers.NewErrorLogger()
- }
-
- permalinks, err := page.NewPermalinkExpander(s)
- if err != nil {
- return nil, err
- }
-
- rs := &Spec{PathSpec: s,
- Logger: logger,
- imaging: &imaging,
- MediaTypes: mimeTypes,
- OutputFormats: outputFormats,
- Permalinks: permalinks,
- FileCaches: fileCaches,
- imageCache: newImageCache(
- fileCaches.ImageCache(),
-
- s,
- )}
-
- rs.ResourceCache = newResourceCache(rs)
-
- return rs, nil
-
-}
-
type ResourceSourceDescriptor struct {
// TargetPaths is a callback to fetch paths's relative to its owner.
TargetPaths func() page.TargetPaths
@@ -161,136 +84,77 @@ func (r ResourceSourceDescriptor) Filename() string {
return r.SourceFilename
}
-func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
- return r.newResourceFor(fd)
+type ResourceTransformer interface {
+ resource.Resource
+ Transformer
}
-func (r *Spec) newResourceFor(fd ResourceSourceDescriptor) (resource.Resource, error) {
- if fd.OpenReadSeekCloser == nil {
- if fd.SourceFile != nil && fd.SourceFilename != "" {
- return nil, errors.New("both SourceFile and AbsSourceFilename provided")
- } else if fd.SourceFile == nil && fd.SourceFilename == "" {
- return nil, errors.New("either SourceFile or AbsSourceFilename must be provided")
- }
- }
-
- if fd.RelTargetFilename == "" {
- fd.RelTargetFilename = fd.Filename()
- }
-
- if len(fd.TargetBasePaths) == 0 {
- // If not set, we publish the same resource to all hosts.
- fd.TargetBasePaths = r.MultihostTargetBasePaths
- }
-
- return r.newResource(fd.Fs, fd)
+type Transformer interface {
+ Transform(...ResourceTransformation) (ResourceTransformer, error)
}
-func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
- fi := fd.FileInfo
- var sourceFilename string
-
- if fd.OpenReadSeekCloser != nil {
- } else if fd.SourceFilename != "" {
- var err error
- fi, err = sourceFs.Stat(fd.SourceFilename)
- if err != nil {
- if os.IsNotExist(err) {
- return nil, nil
- }
- return nil, err
- }
- sourceFilename = fd.SourceFilename
- } else {
- sourceFilename = fd.SourceFile.Filename()
- }
-
- if fd.RelTargetFilename == "" {
- fd.RelTargetFilename = sourceFilename
- }
+type baseResourceResource interface {
+ resource.Cloner
+ resource.ContentProvider
+ resource.Resource
+ resource.Identifier
+}
- ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename))
- mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
- // TODO(bep) we need to handle these ambigous types better, but in this context
- // we most likely want the application/xml type.
- if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" {
- mimeType, found = r.MediaTypes.GetByType("application/xml")
- }
+type baseResourceInternal interface {
+ resource.Source
- if !found {
- // A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
- // so we should configure media types to avoid this lookup for most
- // situations.
- mimeStr := mime.TypeByExtension(ext)
- if mimeStr != "" {
- mimeType, _ = media.FromStringAndExt(mimeStr, ext)
- }
- }
+ fileInfo
+ metaAssigner
+ targetPather
- gr := r.newGenericResourceWithBase(
- sourceFs,
- fd.LazyPublish,
- fd.OpenReadSeekCloser,
- fd.TargetBasePaths,
- fd.TargetPaths,
- fi,
- sourceFilename,
- fd.RelTargetFilename,
- mimeType)
-
- if mimeType.MainType == "image" {
- imgFormat, ok := imageFormats[ext]
- if !ok {
- // This allows SVG etc. to be used as resources. They will not have the methods of the Image, but
- // that would not (currently) have worked.
- return gr, nil
- }
+ ReadSeekCloser() (hugio.ReadSeekCloser, error)
- if err := gr.initHash(); err != nil {
- return nil, err
- }
+ // Internal
+ cloneWithUpdates(*transformationUpdate) (baseResource, error)
+ tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser
- return &Image{
- format: imgFormat,
- imaging: r.imaging,
- genericResource: gr}, nil
- }
- return gr, nil
+ specProvider
+ getResourcePaths() *resourcePathDescriptor
+ getTargetFilenames() []string
+ openDestinationsForWriting() (io.WriteCloser, error)
+ openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error)
+ relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string
}
-// TODO(bep) unify
-func (r *Spec) IsInImageCache(key string) bool {
- // This is used for cache pruning. We currently only have images, but we could
- // imagine expanding on this.
- return r.imageCache.isInCache(key)
+type specProvider interface {
+ getSpec() *Spec
}
-func (r *Spec) DeleteCacheByPrefix(prefix string) {
- r.imageCache.deleteByPrefix(prefix)
+type baseResource interface {
+ baseResourceResource
+ baseResourceInternal
}
-func (r *Spec) ClearCaches() {
- r.imageCache.clear()
- r.ResourceCache.clear()
+type commonResource struct {
}
-func (r *Spec) CacheStats() string {
- r.imageCache.mu.RLock()
- defer r.imageCache.mu.RUnlock()
-
- s := fmt.Sprintf("Cache entries: %d", len(r.imageCache.store))
-
- count := 0
- for k := range r.imageCache.store {
- if count > 5 {
- break
+// 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
+ {
+ }
}
- s += "\n" + k
- count++
+ return groups, nil
+ default:
+ return nil, fmt.Errorf("invalid slice type %T", items)
}
-
- return s
}
type dirFile struct {
@@ -304,91 +168,33 @@ func (d dirFile) path() string {
return path.Join(d.dir, d.file)
}
-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 mulltiple 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
-}
-
-type resourceContent struct {
- content string
- contentInit sync.Once
-}
-
-type resourceHash struct {
- hash string
- hashInit sync.Once
-}
-
-type publishOnce struct {
- publisherInit sync.Once
- publisherErr error
- logger *loggers.Logger
-}
-
-func (l *publishOnce) publish(s resource.Source) error {
- l.publisherInit.Do(func() {
- l.publisherErr = s.Publish()
- if l.publisherErr != nil {
- l.logger.ERROR.Printf("failed to publish Resource: %s", l.publisherErr)
- }
- })
- return l.publisherErr
+type fileInfo interface {
+ getSourceFilename() string
+ setSourceFilename(string)
+ setSourceFs(afero.Fs)
+ hash() (string, error)
+ size() int
}
// genericResource represents a generic linkable resource.
type genericResource struct {
- commonResource
- resourcePathDescriptor
+ *resourcePathDescriptor
+ *resourceFileInfo
+ *resourceContent
+
+ spec *Spec
title string
name string
params map[string]interface{}
-
- // 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
-
- // Will be set if this resource is backed by something other than a file.
- openReadSeekerCloser resource.OpenReadSeekCloser
-
- // A hash of the source content. Is only calculated in caching situations.
- *resourceHash
-
- // 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
-
- spec *Spec
+ data map[string]interface{}
resourceType string
mediaType media.Type
-
- osFileInfo os.FileInfo
-
- // We create copies of this struct, so this needs to be a pointer.
- *resourceContent
-
- // May be set to signal lazy/delayed publishing.
- *publishOnce
}
-type commonResource struct {
-}
-
-func (l *genericResource) Data() interface{} {
- return noData
+func (l *genericResource) Clone() resource.Resource {
+ return l.clone()
}
func (l *genericResource) Content() (interface{}, error) {
@@ -399,72 +205,80 @@ func (l *genericResource) Content() (interface{}, error) {
return l.content, nil
}
-func (l *genericResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
- if l.openReadSeekerCloser != nil {
- return l.openReadSeekerCloser()
+func (l *genericResource) Data() interface{} {
+ return l.data
+}
+
+func (l *genericResource) Key() string {
+ return l.relTargetDirFile.path()
+}
+
+func (l *genericResource) MediaType() media.Type {
+ return l.mediaType
+}
+
+func (l *genericResource) Name() string {
+ return l.name
+}
+
+func (l *genericResource) Params() map[string]interface{} {
+ 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 {
+ fr, err := l.ReadSeekCloser()
+ if err != nil {
+ return err
}
+ defer fr.Close()
- f, err := l.getSourceFs().Open(l.sourceFilename)
+ fw, err := helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.getTargetFilenames()...)
if err != nil {
- return nil, err
+ return err
}
- return f, nil
+ defer fw.Close()
+ _, err = io.Copy(fw, fr)
+ return err
}
-func (l *genericResource) MediaType() media.Type {
- return l.mediaType
+func (l *genericResource) RelPermalink() string {
+ return l.relPermalinkFor(l.relTargetDirFile.path())
}
-// Implement the Cloner interface.
-func (l genericResource) WithNewBase(base string) resource.Resource {
- l.baseOffset = base
- l.resourceContent = &resourceContent{}
- return &l
+func (l *genericResource) ResourceType() string {
+ return l.resourceType
}
-// 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)
- }
+func (l *genericResource) String() string {
+ return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
}
-func (l *genericResource) initHash() error {
- var err error
- l.hashInit.Do(func() {
- var hash string
- var f hugio.ReadSeekCloser
- f, err = l.ReadSeekCloser()
- if err != nil {
- err = errors.Wrap(err, "failed to open source file")
- return
- }
- defer f.Close()
+// Path is stored with Unix style slashes.
+func (l *genericResource) TargetPath() string {
+ return l.relTargetDirFile.path()
+}
- hash, err = helpers.MD5FromFileFast(f)
- if err != nil {
- return
- }
- l.hash = hash
+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()
- return err
+ if isURL {
+ return path.Join(tp.SubResourceBaseLink, rel)
+ }
+
+ // TODO(bep) path
+ return path.Join(filepath.ToSlash(tp.SubResourceBaseTarget), rel)
}
func (l *genericResource) initContent() error {
@@ -484,100 +298,141 @@ func (l *genericResource) initContent() error {
}
l.content = string(b)
-
})
return err
}
-func (l *genericResource) getSourceFs() afero.Fs {
- return l.sourceFs
+func (l *genericResource) setName(name string) {
+ l.name = name
}
-func (l *genericResource) publishIfNeeded() {
- if l.publishOnce != nil {
- l.publishOnce.publish(l)
- }
+func (l *genericResource) getResourcePaths() *resourcePathDescriptor {
+ return l.resourcePathDescriptor
}
-func (l *genericResource) Permalink() string {
- l.publishIfNeeded()
- return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path(), true), l.spec.BaseURL.HostURL())
+func (l *genericResource) getSpec() *Spec {
+ return l.spec
}
-func (l *genericResource) RelPermalink() string {
- l.publishIfNeeded()
- return l.relPermalinkFor(l.relTargetDirFile.path())
+func (l *genericResource) getTargetFilenames() []string {
+ paths := l.relTargetPaths()
+ for i, p := range paths {
+ paths[i] = filepath.Clean(p)
+ }
+ return paths
}
-func (l *genericResource) Key() string {
- return l.relTargetDirFile.path()
+func (l *genericResource) setTitle(title string) {
+ l.title = title
}
-func (l *genericResource) relPermalinkFor(target string) string {
- return l.relPermalinkForRel(target, false)
-
+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 (l *genericResource) permalinkFor(target string) string {
- return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.BaseURL.HostURL())
-}
-func (l *genericResource) relTargetPathsFor(target string) []string {
- return l.relTargetPathsForRel(target)
+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 (l *genericResource) relTargetPaths() []string {
- return l.relTargetPathsForRel(l.TargetPath())
-}
+func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
+ r := rc.clone()
-func (l *genericResource) Name() string {
- return l.name
-}
+ if u.content != nil {
+ r.contentInit.Do(func() {
+ r.content = *u.content
+ r.openReadSeekerCloser = func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromString(r.content), nil
+ }
+ })
+ }
-func (l *genericResource) Title() string {
- return l.title
-}
+ r.mediaType = u.mediaType
-func (l *genericResource) Params() map[string]interface{} {
- return l.params
-}
+ if u.sourceFilename != nil {
+ r.setSourceFilename(*u.sourceFilename)
+ }
-func (l *genericResource) setTitle(title string) {
- l.title = title
+ 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) setName(name string) {
- l.name = name
+func (l genericResource) clone() *genericResource {
+ gi := *l.resourceFileInfo
+ rp := *l.resourcePathDescriptor
+ l.resourceFileInfo = &gi
+ l.resourcePathDescriptor = &rp
+ l.resourceContent = &resourceContent{}
+ return &l
}
-func (l *genericResource) updateParams(params map[string]interface{}) {
- if l.params == nil {
- l.params = params
- return
- }
+// returns an opened file or nil if nothing to write.
+func (l *genericResource) openDestinationsForWriting() (io.WriteCloser, error) {
+ targetFilenames := l.getTargetFilenames()
+ var changedFilenames []string
- // Sets the params not already set
- for k, v := range params {
- if _, found := l.params[k]; !found {
- l.params[k] = v
+ // Fast path:
+ // This is a processed version of the original;
+ // check if it already existis 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 nil, nil
+ }
+
+ return helpers.OpenFilesForWriting(l.getSpec().BaseFs.PublishFs, changedFilenames...)
}
-func (l *genericResource) relPermalinkForRel(rel string, isAbs bool) string {
- return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, isAbs, true))
+func (r *genericResource) openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) {
+ return helpers.OpenFilesForWriting(r.spec.BaseFs.PublishFs, r.relTargetPathsFor(relTargetPath)...)
}
-func (l *genericResource) relTargetPathsForRel(rel string) []string {
- if len(l.baseTargetPathDirs) == 0 {
- return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
- }
+func (l *genericResource) permalinkFor(target string) string {
+ return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.BaseURL.HostURL())
+}
- var 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) 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 {
@@ -592,20 +447,6 @@ func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isA
return l.relTargetPathForRelAndBasePath(rel, basePath, isAbs, isURL)
}
-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) relTargetPathForRelAndBasePath(rel, basePath string, isAbs, isURL bool) string {
rel = l.createBasePath(rel, isURL)
@@ -631,117 +472,153 @@ func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, i
return rel
}
-func (l *genericResource) ResourceType() string {
- return l.resourceType
+func (l *genericResource) relTargetPaths() []string {
+ return l.relTargetPathsForRel(l.TargetPath())
}
-func (l *genericResource) String() string {
- return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
+func (l *genericResource) relTargetPathsFor(target string) []string {
+ return l.relTargetPathsForRel(target)
}
-func (l *genericResource) Publish() error {
- fr, err := l.ReadSeekCloser()
- if err != nil {
- return err
+func (l *genericResource) relTargetPathsForRel(rel string) []string {
+ if len(l.baseTargetPathDirs) == 0 {
+ return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
}
- defer fr.Close()
- fw, err := helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.targetFilenames()...)
- if err != nil {
- return err
+ targetPaths := make([]string, len(l.baseTargetPathDirs))
+ for i, dir := range l.baseTargetPathDirs {
+ targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false, false)
}
- defer fw.Close()
+ return targetPaths
+}
- _, err = io.Copy(fw, fr)
- return err
+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
+ }
+ }
}
-// Path is stored with Unix style slashes.
-func (l *genericResource) TargetPath() string {
- return l.relTargetDirFile.path()
+type targetPather interface {
+ TargetPath() string
}
-func (l *genericResource) targetFilenames() []string {
- paths := l.relTargetPaths()
- for i, p := range paths {
- paths[i] = filepath.Clean(p)
- }
- return paths
+type permalinker interface {
+ targetPather
+ permalinkFor(target string) string
+ relPermalinkFor(target string) string
+ relTargetPaths() []string
+ relTargetPathsFor(target string) []string
}
-// TODO(bep) clean up below
-func (r *Spec) newGenericResource(sourceFs afero.Fs,
- targetPathBuilder func() page.TargetPaths,
- osFileInfo os.FileInfo,
- sourceFilename,
- baseFilename string,
- mediaType media.Type) *genericResource {
- return r.newGenericResourceWithBase(
- sourceFs,
- false,
- nil,
- nil,
- targetPathBuilder,
- osFileInfo,
- sourceFilename,
- baseFilename,
- mediaType,
- )
-
-}
-
-func (r *Spec) newGenericResourceWithBase(
- sourceFs afero.Fs,
- lazyPublish bool,
- openReadSeekerCloser resource.OpenReadSeekCloser,
- targetPathBaseDirs []string,
- targetPathBuilder func() page.TargetPaths,
- osFileInfo os.FileInfo,
- sourceFilename,
- baseFilename string,
- mediaType media.Type) *genericResource {
-
- if osFileInfo != nil && osFileInfo.IsDir() {
- panic(fmt.Sprintf("dirs not supported resource types: %v", osFileInfo))
- }
+type resourceContent struct {
+ content string
+ contentInit sync.Once
+}
- // This value is used both to construct URLs and file paths, but start
- // with a Unix-styled path.
- baseFilename = helpers.ToSlashTrimLeading(baseFilename)
- fpath, fname := path.Split(baseFilename)
+type resourceFileInfo struct {
+ // Will be set if this resource is backed by something other than a file.
+ openReadSeekerCloser resource.OpenReadSeekCloser
- var resourceType string
- if mediaType.MainType == "image" {
- resourceType = mediaType.MainType
- } else {
- resourceType = mediaType.SubType
- }
+ // 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
- pathDescriptor := resourcePathDescriptor{
- baseTargetPathDirs: helpers.UniqueStringsReuse(targetPathBaseDirs),
- targetPathBuilder: targetPathBuilder,
- relTargetDirFile: dirFile{dir: fpath, file: fname},
+ // 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 os.FileInfo
+
+ // 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()
}
- var po *publishOnce
- if lazyPublish {
- po = &publishOnce{logger: r.Logger}
+ f, err := fi.getSourceFs().Open(fi.getSourceFilename())
+ if err != nil {
+ return nil, err
}
+ return f, nil
+}
+
+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 &genericResource{
- openReadSeekerCloser: openReadSeekerCloser,
- publishOnce: po,
- resourcePathDescriptor: pathDescriptor,
- sourceFs: sourceFs,
- osFileInfo: osFileInfo,
- sourceFilename: sourceFilename,
- mediaType: mediaType,
- resourceType: resourceType,
- spec: r,
- params: make(map[string]interface{}),
- name: baseFilename,
- title: baseFilename,
- resourceContent: &resourceContent{},
- resourceHash: &resourceHash{},
+ 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 resourcePathDesc