From 723e3f4342dbf7b64350eb1db2d96dd53b919a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 3 Jan 2025 18:36:44 +0100 Subject: [PATCH] resources: Add FromOpts for more effective resource creation E.g. when the targetPath already contains a hash or if the resource content is expensive to create. --- common/hashing/hashing.go | 13 ++++ resources/resource_factories/create/create.go | 75 +++++++++++++++++-- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/common/hashing/hashing.go b/common/hashing/hashing.go index 70aa74ecd..0b470cafe 100644 --- a/common/hashing/hashing.go +++ b/common/hashing/hashing.go @@ -38,6 +38,19 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) { return h.Sum64(), size, nil } +// XxHashFromReaderHexEncoded calculates the xxHash for the given reader +// and returns the hash as a hex encoded string. +func XxHashFromReaderHexEncoded(r io.Reader) (string, error) { + h := getXxHashReadFrom() + defer putXxHashReadFrom(h) + _, err := io.Copy(h, r) + if err != nil { + return "", err + } + hash := h.Sum(nil) + return hex.EncodeToString(hash), nil +} + // XXHashFromString calculates the xxHash for the given string. func XXHashFromString(s string) (uint64, error) { h := xxhash.New() diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go index 581c0a854..1d78a2923 100644 --- a/resources/resource_factories/create/create.go +++ b/resources/resource_factories/create/create.go @@ -218,22 +218,83 @@ func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) }) } -// FromString creates a new Resource from a string with the given relative target path. -// TODO(bep) see #10912; we currently emit a warning for this config scenario. -func (c *Client) FromString(targetPath, content string) (resource.Resource, error) { - targetPath = path.Clean(targetPath) - key := dynacache.CleanKey(targetPath) + hashing.MD5FromStringHexEncoded(content) +type Options struct { + // The target path relative to the publish directory. + // Unix style path, i.e. "images/logo.png". + TargetPath string + + // Whether the TargetPath has a hash in it which will change if the resource changes. + // If not, we will calculate a hash from the content. + TargetPathHasHash bool + + // The content to create the Resource from. + CreateContent func() (func() (hugio.ReadSeekCloser, error), error) +} + +// FromOpts creates a new Resource from the given Options. +// Make sure to set optis.TargetPathHasHash if the TargetPath already contains a hash, +// as this avoids the need to calculate it. +// To create a new ReadSeekCloser from a string, use hugio.NewReadSeekerNoOpCloserFromString, +// or hugio.NewReadSeekerNoOpCloserFromBytes for a byte slice. +// See FromString. +func (c *Client) FromOpts(opts Options) (resource.Resource, error) { + opts.TargetPath = path.Clean(opts.TargetPath) + var hash string + var newReadSeeker func() (hugio.ReadSeekCloser, error) = nil + if !opts.TargetPathHasHash { + var err error + newReadSeeker, err = opts.CreateContent() + if err != nil { + return nil, err + } + if err := func() error { + r, err := newReadSeeker() + if err != nil { + return err + } + defer r.Close() + + hash, err = hashing.XxHashFromReaderHexEncoded(r) + if err != nil { + return err + } + return nil + }(); err != nil { + return nil, err + } + } + + key := dynacache.CleanKey(opts.TargetPath) + hash r, err := c.rs.ResourceCache.GetOrCreate(key, func() (resource.Resource, error) { + if newReadSeeker == nil { + var err error + newReadSeeker, err = opts.CreateContent() + if err != nil { + return nil, err + } + } return c.rs.NewResource( resources.ResourceSourceDescriptor{ LazyPublish: true, GroupIdentity: identity.Anonymous, // All usage of this resource are tracked via its string content. OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { - return hugio.NewReadSeekerNoOpCloserFromString(content), nil + return newReadSeeker() }, - TargetPath: targetPath, + TargetPath: opts.TargetPath, }) }) return r, err } + +// FromString creates a new Resource from a string with the given relative target path. +func (c *Client) FromString(targetPath, content string) (resource.Resource, error) { + return c.FromOpts(Options{ + TargetPath: targetPath, + CreateContent: func() (func() (hugio.ReadSeekCloser, error), error) { + return func() (hugio.ReadSeekCloser, error) { + return hugio.NewReadSeekerNoOpCloserFromString(content), nil + }, nil + }, + }) +}