hashfs

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2023 License: MIT Imports: 13 Imported by: 2

README

hashfs

Implementation of io/fs.FS that appends SHA256 hashes to filenames to allow for aggressive HTTP caching.

For example, given a file path of scripts/main.js, the hashfs.FS filesystem will provide the server with a hashname of scripts/main-b633a..d628.js (the hash is truncated for brevity in the example). When this file path is requested by the client, the server can verify the hash and return the contents with an aggressive Cache-Control header. The client will cache this file for up to a year and does not need to re-request it in the future.

Note that this library requires Go 1.16 or higher.

Usage

To use hashfs, first wrap your embed.FS in a hashfs.FS filesystem:

//go:embed scripts stylesheets images
var embedFS embed.FS

var fsys = hashfs.NewFS(embedFS)

Then attach a hashfs.FileServer() to your router:

http.Handle("/assets/", http.StripPrefix("/assets/", hashfs.FileServer(fsys)))

Next, your html templating library can obtain the hashname of your file using the hashfs.FS.HashName() method:

func renderHTML(w io.Writer) {
	fmt.Fprintf(w, `<html>`)
	fmt.Fprintf(w, `<script src="/assets/%s"></script>`, fsys.HashName("scripts/main.js"))
	fmt.Fprintf(w, `</html>`)
}

Documentation

Overview

Package hashfs handles cache-busting of files by adding a hash of each file's contents to the filename.

How This Works:

  • You provide your files, as an fs.FS.
  • When your binary runs, a hash is calculated of a static file's contents.
  • The hash is appended to the file's name.
  • The new filename is rewritten into your HTML code.
  • When a browser requests a static file, using the filename-with-hash, the underlying file is looked up and served with aggressive caching headers.

Usage:

# Example: See the example/example.go file in the source repo.

Example FuncMap func:

  func static(originalPath string) (hashPath string) {
	//If in development mode, just return path as-is. We will serve a non-cache-
	//busted version of the file since during development we refresh the browser
	//a lot and don't want to cache things mistakenly.
	if devMode {
		return originalPath
	}

	//Trim path, if needed.
	//
	//For example, if your static files are served off of www.example.co/static/
	//and your fs.FS lists files "inside" the /static/ directory from your source
	//code repo, you need to remove the /static/ part of the URL to find the
	//matching source file. The fs.FS files will not have /static/ in their paths
	//since, the fs.FS just contains the files "inside" the /static/ directory.
	trimmedPath := strings.TrimPrefix(originalPath, "/static/")

	//Get the hashPath. This is where the hash is calculated, if it has not been
	//already (this static func was already called on this originalPath when
	//another template was being built in this run of your binary).
	hashPath := yourHashFS.GetHashPath(trimmedPath)

	//Now, we need to add the /static/ back to the path since that is how the
	//browser expects it.
	return path.Join("/", "static", hashPath)
  }

Definitions:

  • original path: the path to the on-disk source file.
  • hash path: the path where the filename includes the hash of the file's contents.
  • original name: the filename of the on-disk source file.
  • hash name: the filename inclusive of the hash of the file's contents.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FileServer

func FileServer(fsys fs.FS) http.Handler

FileServer returns an http.Handler for serving files from our custom FS. It provides a simplified implementation of http.FileServer which is used to aggressivley cache files on the client. You would use this in the same manner as http.FileServer. Ex.: http.FileServer(http.FS(someStaticFS)) -> hashfs.FileServer(hfs).

Because FileServer is focused on small known path files, several features of http.FileServer have been removed including canonicalizing directories, defaulting index.html pages, precondition checks, & content range headers.

func HashAlgo

func HashAlgo(algo crypto.Hash) optionFunc

HashAlgo specifies the algorithm to use to calculate the hash of each file's contents. Default is SHA256. MD5 is what S3 uses. This will panic if an unsupported algorithm is provided.

This should rarely be needed, since typically you don't really care about the hash algorithm. This is provided mostly for people who like looking at shorter MD5 sums.

func HashLength

func HashLength(l uint) optionFunc

HashLength trims the length of the hash added to a filename. Default is the full hash length, based on the hash algorithm. Values less than 8 should not be used since a collision is highly likely. If 0 is provided, the default hash length is used.

This should rarely be needed, since typically you want as long of a hash as possible to alleviate collision concerns. This is helpful if you want shorter filenames.

func HashLocationEnd

func HashLocationEnd() optionFunc

HashLocationEnd sets the hash to be appended to the end of the filename with the extension copied after the hash. script.min.js becomes script.min.js-a1b2c3...d4e5f6.js. This is the default hash location.

This is nice to keep the filename all together; there really is no downside to this location.

func HashLocationFirstPeriod

func HashLocationFirstPeriod() optionFunc

HashLocationFirstPeriod sets the hash to be added in the middle of the filename, specifically at the first period in the filename. This was the original designed hash location. script.min.js becomes script-a1b2c3...d4e5f6.min.js

There is really no benefit to this location, and it is a bit ugly since it breaks up the filename.

func HashLocationStart

func HashLocationStart() optionFunc

HashLocationStart sets the hash to be prepended to the beginning of the filename. script.min.js becomes a1b2c3...d4e5f6-script.min.js.

This is nice to keep the filename all together, but is a bit ugly for debugging in browser devtools since the on small/narrow screens the hash can take up all of the room where a filename will be displayed making identifying a specific file difficult.

func MaxAge

func MaxAge(d time.Duration) optionFunc

MaxAge specifies the max-age value you want to set for the Cache-Control header. Default is 1 year. If an invalid value is given, the default is used.

This should rarely be needed, since typically you want to cache files for a really long time. This is provided mostly for development and testing.

Types

type HFS

type HFS struct {
	// contains filtered or unexported fields
}

HFS represents an fs.FS with additional lookup tables for storing the calculated hashes of each file's contents. The hashes are used for aggressive client-side caching and cache-busting.

func NewFS

func NewFS(fsys fs.FS, options ...optionFunc) *HFS

NewFS returns the provided fs.FS with additional tooling to support calculating the hash of each file's contents for caching purposes.

optionFuncs are used for modifying the HFS. Optional funcs were used, versus just additional arguments, since this allows for future expansion without breaking existing uses and is cleaner than empty unused arguments.

func (*HFS) GetHashPath

func (hfs *HFS) GetHashPath(originalPath string) (hashPath string)

GetHashPath returns the hashPath for a provided originalPath. The hashPath is the originalPath with a hash of the file's contents added to the filename. The hash of the contents of the file located at the originalPath will be calculated if it has not already been done so. The hash will be saved to for future reuse and to prevent unnecessary recalculation of the hash each time the same originalPath is requested.

func (*HFS) Open

func (hfs *HFS) Open(path string) (f fs.File, err error)

Open returns a reference to the file at the provided path. The path could be an original path or a hash path. If a hash path is given, the original path will be looked up to return the file with.

This func is necessary for HFS to implement fs.FS. You should not need need to call this func directly.

Directories

Path Synopsis
Package example provides an example HTTP server for showing how hashfs is implemented and the results you can inspect.
Package example provides an example HTTP server for showing how hashfs is implemented and the results you can inspect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL