Skip to content

mavolin/hashets

Repository files navigation

hashets

Go Reference Test Code Coverage Go Report Card License MIT


About

Hashets (a portmanteau of 'hash' and 'assets') is a utility for handling cache busting of static assets. It works by adding the hash of the file's contents to the file name.

Main Features

  • ⚡ Three options:
    1. Either generate files with hashed names before compiling,
    2. use hashets.HashToDir or hashets.HashToTempDir at runtime,
    3. or create a hashets.FSWrapper which translates requests for hashed file names to their original names.
  • 🧒 Easy integration into templates by using a map of file names to hashed file names
  • 📦 Support for fs.FS
  • 🏖 Hassle-free versioning, that only causes refetching of files when their contents change (vs. ?v=1.2.3)

Examples

First impressions matter, so here are some examples of how to use hashets.

Using hashets.WrapFS

This method is for you, if:

  • 🧒 You want the easiest solution of all
  • 🤏 Have small assets, or you don't mind if your application takes a few milliseconds longer to start
  • 🕚 You know your assets at runtime
  • 🕵 You need cache busting during development and not just in production

hashets.WrapFS simply wraps an fs.FS, calculates the hashes of all its files, and translates requests for hashed file names to their original names:

Add a static.go to your static directory:

static
├── file_to_hash.ext
└── static.go
package static

import (
	"embed"

	"github.com/mavolin/hashets/hashets"
)

//go:embed file_to_hash.ext
var assets embed.FS

var (
	FS        *hashets.FSWrapper
	FileNames hashets.Map
)

func init() {
	var err error
	FS, FileNames, err = hashets.WrapFS(assets, hashets.Options{})
	if err != nil {
		panic(err)
	}
}

FS now translates requests for FS.Open("file_to_hash_generateHash.ext") to assets.Open("file_to_hash.ext"). Additionally, FileNames maps all original file names to their hashed equivalents:

var FileNames = hashets.Map{
    "file_to_hash.ext": "file_to_hash_generateHash.ext",
}

Use this map in your templates to generate links to your assets:

<link rel="stylesheet" href="/static/{{ .FileNames.Get "file_to_hash.ext" }}">

Then simply serve FS under /static:

http.Handle("/static/", http.FileServer(http.FS(FS)))

Of course, instead of an embed.FS, you can also use any other fs.FS implementation, such as os.DirFS, etc.

Using go generate

This method is for you, if:

  • 📏 You have larger assets and need lightning fast startup times
  • 🕑 You know your assets at compile time
  • 🕵 You need cache busting during development and not just in production

Add a static.go to your static directory:

static
├── orig
│   └── file_to_hash.ext
└── static.go
package static

import "static/hashed"

//go:generate hashets -o hashed orig

Now run go generate. Your file structure should now look like this:

static
├── hashed
│   ├── file_to_hash_generateHash.ext
│   └── hashets_map.go
├── orig
│   └── file_to_hash.ext
└── static.go

Besides the hashed files, hashets also generated a hashets_map.go file, that contains the FileNames hashets.Map, that maps the original file names to their hashed equivalents:

package hashed

import "github.com/mavolin/hashets/hashets"

var FileNames = hashets.Map{
    "file_to_hash.ext": "file_to_hash_generateHash.ext",
}

During CI

This method is for you, if:

  • 📏 You have larger assets and need lightning fast startup times
  • 🕑 You know your assets at compile time
  • 🤷 You don't need cache busting during development

The go generate solution has one big drawback: If you generate static assets in the same go generate run and hashets is executed before the files are generated, the hashes will be wrong.

Luckily, there is another handy solution:

Add a static.go and a hashets_map.go to your static directory:

static
├── file_to_hash.ext
├── hashets_map.go
└── static.go

static.go

package static

import "embed"

// It is important that you use wildcards for your files, as otherwise the
// hashed files generated by your CI won't be included in the embed.FS.
//go:embed file_to_hash*.txt
var FS embed.FS

hashets_map.go

package static

import "github.com/mavolin/hashets"

// FileNames maps the original file names to their hashed equivalents.
// Unless you run hashets, this map will be nil, which causes [hashets.Map.Get]
// to behave specially:
// Instead of returning the hashed file name, it will return the path that it
// is given as-is.
//
// That means, unless you run hashets, you will simply use your unhashed assets.
var FileNames hashets.Map

Now, in your CI, run hashets before compiling:

hashets -replace -ignore static.go static

This will replace all of your assets with their hashed equivalents, i.e. replace file_to_hash.ext with file_to_hash_generateHash.ext. Additionally, it will overwrite hashets_map.go with a FileNames map that contains the correct mappings.

Using hashets.HashToDir and hashets.HashToTempDir

This method is for you, if:

  • 🛠 You need maximum customizability
  • 🤏 You have smaller assets or don't mind if your application takes a few milliseconds longer to start
  • 🕚 You know your assets at compile or runtime
  • 🕵 You need cache busting during development and not just in production

If all of the above don't do the trick for you, you can also create hashes using hashets.HashToDir and hashets.HashToTempDir, which will generate hashed files and write them to an arbitrary or a temporary directory.

Head over to pkg.go.dev to read more.

License

Built with ❤ by Maximilian von Lindern. Available under the MIT License.

About

#️⃣ Cache busting for Go by using file hashes. Hashing can be done both at run or compile time.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages