diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-08-18 11:21:27 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-08-26 15:00:44 +0200 |
commit | f9978ed16476ca6d233a89669c62c798cdf9db9d (patch) | |
tree | 02edb31008b997a3e77055060a34971fe9e8c5a4 /resources/resource.go | |
parent | 58d4c0a8be8beefbd7437b17bf7a9a381164d09b (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.go | 827 |
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 |