{"id":19666853,"url":"https://github.com/mavolin/hashets","last_synced_at":"2026-07-07T07:31:46.420Z","repository":{"id":65987390,"uuid":"603719024","full_name":"mavolin/hashets","owner":"mavolin","description":"#️⃣ Cache busting for Go by using file hashes. Hashing can be done both at run or compile time.","archived":false,"fork":false,"pushed_at":"2025-08-30T16:07:50.000Z","size":160,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"v1","last_synced_at":"2025-08-30T16:25:12.766Z","etag":null,"topics":["cache-buster","cache-busting","caching","go","hashing"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mavolin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-19T11:30:12.000Z","updated_at":"2025-08-30T16:07:54.000Z","dependencies_parsed_at":"2024-06-21T09:04:25.885Z","dependency_job_id":null,"html_url":"https://github.com/mavolin/hashets","commit_stats":{"total_commits":15,"total_committers":1,"mean_commits":15.0,"dds":0.0,"last_synced_commit":"6a021ceeaf64ced547ad74e8d82d3c60c77afe3d"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/mavolin/hashets","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mavolin%2Fhashets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mavolin%2Fhashets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mavolin%2Fhashets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mavolin%2Fhashets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mavolin","download_url":"https://codeload.github.com/mavolin/hashets/tar.gz/refs/heads/v1","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mavolin%2Fhashets/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35219594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache-buster","cache-busting","caching","go","hashing"],"created_at":"2024-11-11T16:29:14.748Z","updated_at":"2026-07-07T07:31:46.411Z","avatar_url":"https://github.com/mavolin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003ch1\u003ehashets\u003c/h1\u003e\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/mavolin/hashets.svg)](https://pkg.go.dev/github.com/mavolin/hashets)\n[![Test](https://github.com/mavolin/hashets/actions/workflows/test.yml/badge.svg)](https://github.com/mavolin/hashets/actions)\n[![Code Coverage](https://codecov.io/gh/mavolin/hashets/branch/develop/graph/badge.svg?token=ewFEQGgMES)](https://codecov.io/gh/mavolin/hashets)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mavolin/hashets)](https://goreportcard.com/report/github.com/mavolin/hashets)\n[![License MIT](https://img.shields.io/github/license/mavolin/hashets)](./LICENSE)\n\u003c/div\u003e\n\n---\n\n## About\n\nHashets (a portmanteau of 'hash' and 'assets') is a utility for handling cache busting of static assets.\nIt works by adding the hash of the file's contents to the file name.\n\n## Main Features\n\n* ⚡ Three options:\n    1. Either generate files with hashed names before compiling,\n    2. use `hashets.HashToDir` or `hashets.HashToTempDir` at runtime,\n    3. or create a `hashets.FSWrapper` which translates requests for hashed file names to their original names.\n* 🧒 Easy integration into templates by using a map of file names to hashed file names\n* 📦 Support for `fs.FS`\n* 🏖 Hassle-free versioning, that only causes refetching of files when their contents change (vs. `?v=1.2.3`)\n\n## Examples\n\nFirst impressions matter, so here are some examples of how to use hashets.\n\n### Using `hashets.WrapFS`\n\n\u003e **This method is for you, if:**\n\u003e\n\u003e * 🧒 You want the easiest solution of all\n\u003e * 🤏 Have small assets, or you don't mind if your application takes a few milliseconds longer to start\n\u003e * 🕚 You know your assets at runtime\n\u003e * 🕵 You need cache busting during development and not just in production\n\n`hashets.WrapFS` simply wraps an `fs.FS`, calculates the hashes of all its files,\nand translates requests for hashed file names to their original names:\n\nAdd a `static.go` to your `static` directory:\n\n```\nstatic\n├── file_to_hash.ext\n└── static.go\n```\n\n```go\npackage static\n\nimport (\n\t\"embed\"\n\n\t\"github.com/mavolin/hashets/hashets\"\n)\n\n//go:embed file_to_hash.ext\nvar assets embed.FS\n\nvar (\n\tFS        *hashets.FSWrapper\n\tFileNames hashets.Map\n)\n\nfunc init() {\n\tvar err error\n\tFS, FileNames, err = hashets.WrapFS(assets, hashets.Options{})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n`FS` now translates requests for `FS.Open(\"file_to_hash_generatedHash.ext\")` to `assets.Open(\"file_to_hash.ext\")`.\nAdditionally, `FileNames` maps all original file names to their hashed equivalents:\n\n```go\nvar FileNames = hashets.Map{\n    \"file_to_hash.ext\": \"file_to_hash_generatedHash.ext\",\n}\n```\n\nUse this map in your templates to generate links to your assets:\n\n```html\n\u003clink rel=\"stylesheet\" href=\"/static/{{ .FileNames.Get \"file_to_hash.ext\" }}\"\u003e\n```\n\nThen simply serve `FS` under `/static`:\n\n```go\nhttp.Handle(\"/static/\", http.FileServer(http.FS(FS)))\n```\n\nOf course, instead of an `embed.FS`, you can also use any other `fs.FS` implementation, such as `os.DirFS`, etc.\n\n### Using `go generate` with `hashets.WrapPrecomputedFS`\n\n\u003e **This method is for you, if:**\n\u003e\n\u003e * 📏 You have larger assets and need lightning fast startup times\n\u003e * 🕑 You know your assets at compile time\n\u003e * 🕵 You need cache busting during development and not just in production\n\n`hashets.WrapPrecomputedFS` is similar to `hashets.WrapFS` except it uses\na precomputed `hashets.Map` for the provided filesystem, which ideally is\ngenerated using `go generate` for the provided filesystem during compile time.\nThis saves the overhead of calculating the file hashes during startup.\n\nAdd a `static.go` to your `static` directory:\n\n```\nstatic\n├── assets\n│   └── file_to_hash.ext\n└── static.go\n```\n\n```go\npackage static\n\nimport (\n\t\"embed\"\n\n\t\"github.com/mavolin/hashets/hashets\"\n)\n\n//go:embed assets/*\nvar assets embed.FS\n\n//go:generate hashets -map-only -o . assets\n\nvar (\n\t// FileNames is defined in hashets_map.go, which is generated\n\t// by `hashets` during code generation\n\tFS = hashets.WrapPrecomputedFS(assets, FileNames)\n)\n```\n\n`FS` can now be used exactly the same as in the previous example. Using this\nmethod does not offer the same guarantees regarding integrity as computing\nthe hashes at runtime, as there is no integrity check for the hashes and\ntheir associated files. This race condition is generally known as\n[time-of-check/time-of-use][TOCTOU]. The following shell replay demonstrates\nthe problem:\n\n```bash\ngo generate ./... # this will compute the FileNames map variable\ndate \u003e static/assets/file_to_hash.ext\ndate \u003e static/assets/new_file.ext\ngo install .\n```\n\nThe filesystem `assets` embedded into the binary will now have one additional\nfile the `FileNames` map does not know about in addition to the existing file\n(`file_to_hash.ext`) having changed after computing its hash.\n\nUsing this approach is only recommended if you have tight control over the\nbuild pipeline or simply do not care about the cache busting quality of\nthe application. The example below somewhat mitigates that risk, providing\na clearer distinction between already processed files and their originals, at\nthe cost of file duplication.\n\n[TOCTOU]: https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use\n\n### Using `go generate` with a separate \"hashed\" directory\n\n\u003e **This method is for you, if:**\n\u003e\n\u003e * 📏 You have larger assets and need lightning fast startup times\n\u003e * 🕑 You know your assets at compile time\n\u003e * 🕵 You need cache busting during development and not just in production\n\nAdd a `static.go` to your `static` directory:\n\n```\nstatic\n├── orig\n│   └── file_to_hash.ext\n└── static.go\n```\n\n```go\npackage static\n\nimport \"static/hashed\"\n\n//go:generate hashets -o hashed orig\n```\n\nNow run `go generate`.\nYour file structure should now look like this:\n\n```\nstatic\n├── hashed\n│   ├── file_to_hash_generatedHash.ext\n│   └── hashets_map.go\n├── orig\n│   └── file_to_hash.ext\n└── static.go\n```\n\nBesides the hashed files, `hashets` also generated a `hashets_map.go` file,\nthat contains the `FileNames` `hashets.Map`, that maps the original file names\nto their hashed equivalents:\n\n```go\npackage hashed\n\nimport \"github.com/mavolin/hashets/hashets\"\n\nvar FileNames = hashets.Map{\n    \"file_to_hash.ext\": \"file_to_hash_generatedHash.ext\",\n}\n```\n\n### During CI\n\n\u003e **This method is for you, if:**\n\u003e\n\u003e * 📏 You have larger assets and need lightning fast startup times\n\u003e * 🕑 You know your assets at compile time\n\u003e * 🤷 You don't need cache busting during development\n\nThe `go generate` solution has one big drawback:\nIf you generate static assets in the same `go generate` run and `hashets` is\nexecuted before the files are generated, the hashes will be wrong.\n\nLuckily, there is another handy solution:\n\nAdd a `static.go` and a `hashets_map.go` to your `static` directory:\n\n```\nstatic\n├── file_to_hash.ext\n├── hashets_map.go\n└── static.go\n```\n\n`static.go`\n```go\npackage static\n\nimport \"embed\"\n\n// It is important that you use wildcards for your files, as otherwise the\n// hashed files generated by your CI won't be included in the embed.FS.\n//go:embed file_to_hash*.txt\nvar FS embed.FS\n```\n\n`hashets_map.go`\n```go\npackage static\n\nimport \"github.com/mavolin/hashets\"\n\n// FileNames maps the original file names to their hashed equivalents.\n// Unless you run hashets, this map will be nil, which causes [hashets.Map.Get]\n// to behave specially:\n// Instead of returning the hashed file name, it will return the path that it\n// is given as-is.\n//\n// That means, unless you run hashets, you will simply use your unhashed assets.\nvar FileNames hashets.Map\n```\n\nNow, in your CI, run `hashets` before compiling:\n\n```sh\nhashets -replace -ignore static.go static\n```\n\nThis will replace all of your assets with their hashed equivalents, i.e.\nreplace `file_to_hash.ext` with `file_to_hash_generatedHash.ext`.\nAdditionally, it will overwrite `hashets_map.go` with a `FileNames` map that\ncontains the correct mappings.\n\n### Using `hashets.HashToDir` and `hashets.HashToTempDir`\n\n\u003e **This method is for you, if:**\n\u003e\n\u003e * 🛠 You need maximum customizability\n\u003e * 🤏 You have smaller assets or don't mind if your application takes a few milliseconds longer to start\n\u003e * 🕚 You know your assets at compile or runtime\n\u003e * 🕵 You need cache busting during development and not just in production\n\nIf all of the above don't do the trick for you, you can also create hashes\nusing `hashets.HashToDir` and `hashets.HashToTempDir`, which will generate\nhashed files and write them to an arbitrary or a temporary directory.\n\nHead over to [pkg.go.dev](https://pkg.go.dev/github.com/mavolin/hashets) to read more.\n\n## License\n\nBuilt with ❤ by [Maximilian von Lindern](https://github.com/mavolin).\nAvailable under the [MIT License](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmavolin%2Fhashets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmavolin%2Fhashets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmavolin%2Fhashets/lists"}