An open API service indexing awesome lists of open source software.

https://github.com/mavolin/hashets

#️⃣ Cache busting for Go by using file hashes. Hashing can be done both at run or compile time.
https://github.com/mavolin/hashets

cache-buster cache-busting caching go hashing

Last synced: about 11 hours ago
JSON representation

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

Awesome Lists containing this project

README

          


hashets

[![Go Reference](https://pkg.go.dev/badge/github.com/mavolin/hashets.svg)](https://pkg.go.dev/github.com/mavolin/hashets)
[![Test](https://github.com/mavolin/hashets/actions/workflows/test.yml/badge.svg)](https://github.com/mavolin/hashets/actions)
[![Code Coverage](https://codecov.io/gh/mavolin/hashets/branch/develop/graph/badge.svg?token=ewFEQGgMES)](https://codecov.io/gh/mavolin/hashets)
[![Go Report Card](https://goreportcard.com/badge/github.com/mavolin/hashets)](https://goreportcard.com/report/github.com/mavolin/hashets)
[![License MIT](https://img.shields.io/github/license/mavolin/hashets)](./LICENSE)

---

## 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
```

```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_generatedHash.ext")` to `assets.Open("file_to_hash.ext")`.
Additionally, `FileNames` maps all original file names to their hashed equivalents:

```go
var FileNames = hashets.Map{
"file_to_hash.ext": "file_to_hash_generatedHash.ext",
}
```

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

```html

```

Then simply serve `FS` under `/static`:

```go
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` with `hashets.WrapPrecomputedFS`

> **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

`hashets.WrapPrecomputedFS` is similar to `hashets.WrapFS` except it uses
a precomputed `hashets.Map` for the provided filesystem, which ideally is
generated using `go generate` for the provided filesystem during compile time.
This saves the overhead of calculating the file hashes during startup.

Add a `static.go` to your `static` directory:

```
static
├── assets
│ └── file_to_hash.ext
└── static.go
```

```go
package static

import (
"embed"

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

//go:embed assets/*
var assets embed.FS

//go:generate hashets -map-only -o . assets

var (
// FileNames is defined in hashets_map.go, which is generated
// by `hashets` during code generation
FS = hashets.WrapPrecomputedFS(assets, FileNames)
)
```

`FS` can now be used exactly the same as in the previous example. Using this
method does not offer the same guarantees regarding integrity as computing
the hashes at runtime, as there is no integrity check for the hashes and
their associated files. This race condition is generally known as
[time-of-check/time-of-use][TOCTOU]. The following shell replay demonstrates
the problem:

```bash
go generate ./... # this will compute the FileNames map variable
date > static/assets/file_to_hash.ext
date > static/assets/new_file.ext
go install .
```

The filesystem `assets` embedded into the binary will now have one additional
file the `FileNames` map does not know about in addition to the existing file
(`file_to_hash.ext`) having changed after computing its hash.

Using this approach is only recommended if you have tight control over the
build pipeline or simply do not care about the cache busting quality of
the application. The example below somewhat mitigates that risk, providing
a clearer distinction between already processed files and their originals, at
the cost of file duplication.

[TOCTOU]: https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use

### Using `go generate` with a separate "hashed" directory

> **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
```

```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_generatedHash.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:

```go
package hashed

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

var FileNames = hashets.Map{
"file_to_hash.ext": "file_to_hash_generatedHash.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`
```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`
```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:

```sh
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_generatedHash.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](https://pkg.go.dev/github.com/mavolin/hashets) to read more.

## License

Built with ❤ by [Maximilian von Lindern](https://github.com/mavolin).
Available under the [MIT License](./LICENSE).