• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

42z-io / hashembed / 6740961046

03 Nov 2023 03:32AM UTC coverage: 80.864% (+1.7%) from 79.137%
6740961046

push

github

reidja
Merge branch 'main' of https://github.com/42z-io/hashembed

131 of 162 relevant lines covered (80.86%)

8.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.63
/fs.go
1
// [hashembed] is an [embed.FS] with support for reading files with virtual content hashes embedded in the file name.
2
//
3
// [hashembed] is useful if you are embedding static assets directly into your application and want to
4
// facilitate serving these files with very long duration client-side caching.
5
//
6
// # File Hashing
7
//
8
// Files are hashed when you call [Generate].
9
//
10
// You can provide a custom file hasher by providing a function that matches [FileHasher].
11
//
12
// There are several built-in hashers:
13
//   - [Sha256Hasher] (default)
14
//   - [Crc32Hasher]
15
//
16
// # File Renaming
17
//
18
// Files are renamed to include their hash when you call [Generate].
19
//
20
// You can provide a custom file renamer by providing a function that matches [FileRenamer].
21
//
22
// There are two built-in renaming mechanims:
23
//   - [ExtensionRenamer] (default)
24
//   - [FullNameRenamer]
25
//
26
// # Examples
27
//
28
// [embed]: https://pkg.go.dev/embed
29
package hashembed
30

31
import (
32
        "embed"
33
        "io/fs"
34
        "slices"
35
)
36

37
// HashedFS is an [embed.FS] with support for reading files with virtual content hashes embedded in the file name.
38
type HashedFS struct {
39
        fs               embed.FS          // underlying embed.FS
40
        actualPathLookup map[string]string // lookups for the hashed path => actual path
41
        hashedPathLookup map[string]string // lookups for the actual path => hashed path
42
        integrityLookup  map[string]string // lookups for the actual path => integrity hash (sha-256)
43
        integrityHasher  FileHasher        // hasher used to generate the integrity hash
44
        cfg              Config            // configuration options for the hashed embed
45
}
46

47
// Initialize a file by generating a hash, renaming (aliasing), and adding it to the lookup.
48
func (f HashedFS) initializeFile(file PathedDirEntry) error {
15✔
49
        _, ext := file.NameAndExtension()
15✔
50
        if !slices.Contains(f.cfg.AllowedExtensions, ext) {
19✔
51
                return nil
4✔
52
        }
4✔
53

54
        data, err := f.fs.ReadFile(file.FullPath())
11✔
55
        if err != nil {
11✔
56
                return err
×
57
        }
×
58

59
        hash, err := f.cfg.Hasher.Hash(data)
11✔
60
        if err != nil {
11✔
61
                return err
×
62
        }
×
63

64
        var sha256 string
11✔
65
        if f.cfg.Hasher.Algorithm() == "sha256" {
17✔
66
                sha256 = hash
6✔
67
        } else {
11✔
68
                integrityHash, err := f.integrityHasher.Hash(data)
5✔
69
                if err != nil {
5✔
70
                        return err
×
71
                }
×
72
                sha256 = integrityHash
5✔
73
        }
74

75
        hashedPath := f.cfg.Renamer(file, hash)
11✔
76
        if err != nil {
11✔
77
                return err
×
78
        }
×
79

80
        fullPath := file.FullPath()
11✔
81
        f.actualPathLookup[hashedPath] = fullPath
11✔
82
        f.hashedPathLookup[fullPath] = hashedPath
11✔
83
        f.integrityLookup[fullPath] = sha256
11✔
84
        return nil
11✔
85
}
86

87
// Initialize a path (could be file or directory) within the embed.FS.
88
func (f HashedFS) initializePath(root PathedDirEntry) error {
10✔
89
        rootPath := root.FullPath()
10✔
90
        entries, err := f.fs.ReadDir(rootPath)
10✔
91
        if err != nil {
10✔
92
                return err
×
93
        }
×
94

95
        for _, entry := range entries {
30✔
96
                pathEntry := NewPathedDirEntry(entry, rootPath)
20✔
97
                if !entry.IsDir() {
35✔
98
                        if err := f.initializeFile(pathEntry); err != nil {
15✔
99
                                return err
×
100
                        }
×
101
                } else {
5✔
102
                        if err := f.initializePath(pathEntry); err != nil {
5✔
103
                                return err
×
104
                        }
×
105
                }
106
        }
107
        return nil
10✔
108
}
109

110
// Initialize the [HashedFS] by iterating over the files in the embed.FS.
111
func (f HashedFS) initialize() error {
5✔
112
        entries, err := f.fs.ReadDir(".")
5✔
113
        if err != nil {
5✔
114
                return err
×
115
        }
×
116

117
        for _, entry := range entries {
10✔
118
                pathEntry := NewPathedDirEntry(entry, "")
5✔
119
                if err := f.initializePath(pathEntry); err != nil {
5✔
120
                        return err
×
121
                }
×
122
        }
123
        return nil
5✔
124
}
125

126
// Generate will create a new instance of [HashedFS] using [Config] (if provided) or [ConfigDefault] if not provided.
127
func Generate(fs embed.FS, cfgs ...Config) (*HashedFS, error) {
5✔
128
        cfg := ConfigDefault
5✔
129
        if len(cfgs) > 0 {
7✔
130
                cfg = cfgs[0]
2✔
131

2✔
132
                if cfg.Hasher == nil {
2✔
133
                        cfg.Hasher = ConfigDefault.Hasher
×
134
                }
×
135

136
                if cfg.Renamer == nil {
2✔
137
                        cfg.Renamer = ConfigDefault.Renamer
×
138
                }
×
139

140
                if cfg.AllowedExtensions == nil {
3✔
141
                        cfg.AllowedExtensions = ConfigDefault.AllowedExtensions
1✔
142
                }
1✔
143
        }
144

145
        hashedEmbed := &HashedFS{
5✔
146
                fs:               fs,
5✔
147
                actualPathLookup: make(map[string]string),
5✔
148
                hashedPathLookup: make(map[string]string),
5✔
149
                integrityLookup:  make(map[string]string),
5✔
150
                integrityHasher:  Sha256Hasher{},
5✔
151
                cfg:              cfg,
5✔
152
        }
5✔
153

5✔
154
        hashedEmbed.initialize()
5✔
155
        return hashedEmbed, nil
5✔
156
}
157

158
// GetActualPath will convert the content hashed path into the actual path.
159
//
160
// If the actual path is not found it will return the provided path.
161
func (f HashedFS) GetActualPath(path string) string {
8✔
162
        if lookup, ok := f.actualPathLookup[path]; ok {
13✔
163
                return lookup
5✔
164
        }
5✔
165
        return path
3✔
166
}
167

168
// GetHashedPath will convert the actual path into the content hashed path.
169
//
170
// If the hashed path is not found it will return the provided path.
171
func (f HashedFS) GetHashedPath(path string) string {
5✔
172
        if lookup, ok := f.hashedPathLookup[path]; ok {
9✔
173
                return lookup
4✔
174
        }
4✔
175
        return path
1✔
176
}
177

178
// GetHash will get the Sha256 integrity hash for the specified path.
179
//
180
// Will only find files matched by the [Config.AllowedExtensions] list.
181
//
182
// If the hashed path is not found it will return a blank string.
183
func (f HashedFS) GetHash(path string) string {
2✔
184
        if lookup, ok := f.integrityLookup[path]; ok {
3✔
185
                return lookup
1✔
186
        }
1✔
187
        return ""
1✔
188
}
189

190
// See [embed.FS.Open]
191
//
192
// This will call [GetActualPath] on the file to get the correct name.
193
func (f HashedFS) Open(name string) (fs.File, error) {
1✔
194
        return f.fs.Open(f.GetActualPath(name))
1✔
195
}
1✔
196

197
// See [embed.FS.ReadDir]
198
//
199
// Note: This will only return files that actually exist in the [embed.FS] - hashed files are "virtual"
200
func (f HashedFS) ReadDir(name string) ([]fs.DirEntry, error) {
1✔
201
        return f.fs.ReadDir(name)
1✔
202
}
1✔
203

204
// See [embed.FS]
205
//
206
// This will call [HashedFS.GetActualPath] on the file to get the correct name.
207
func (f HashedFS) ReadFile(name string) ([]byte, error) {
4✔
208
        return f.fs.ReadFile(f.GetActualPath(name))
4✔
209
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc