/
filesystem_cache.go
130 lines (110 loc) · 3.02 KB
/
filesystem_cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package httputil
import (
// nolint:gosec
"crypto/md5"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/authgear/authgear-server/pkg/util/filepathutil"
)
// FilesystemCache is a helper to write the response into the tmp directory.
// The response is then served with http.FileServer,
// with the advantage of supporting range request and cache validation.
// If the file is not modified, the response is a 304.
// For even better performance, we need to add Cache-Control header
// to take advantage of the fact that the filename is hashed.
// However, http.FileServer does not support Cache-Control.
// Unconditionally adding Cache-Control for non-existent file is problematic.
type FilesystemCache struct {
mutexForMapping sync.RWMutex
mapping map[string]string
mutexForFile sync.Mutex
}
func NewFilesystemCache() *FilesystemCache {
return &FilesystemCache{
mapping: make(map[string]string),
}
}
func (c *FilesystemCache) makeFilePath(filename string) string {
return filepath.Join(os.TempDir(), filename)
}
func (c *FilesystemCache) write(filePath string, bytes []byte) error {
c.mutexForFile.Lock()
defer c.mutexForFile.Unlock()
// nolint: gosec
return os.WriteFile(filePath, bytes, 0666)
}
func (c *FilesystemCache) Clear() error {
c.mutexForFile.Lock()
c.mutexForMapping.Lock()
defer c.mutexForFile.Unlock()
defer c.mutexForMapping.Unlock()
for _, mappedFilename := range c.mapping {
filePath := c.makeFilePath(mappedFilename)
err := os.Remove(filePath)
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
if err != nil {
return err
}
}
c.mapping = make(map[string]string)
return nil
}
func (c *FilesystemCache) Serve(r *http.Request, make func() ([]byte, error)) (handler http.Handler) {
var err error
var bytes []byte
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
originalPath := r.URL.Path
filename := filepathutil.EscapePath(originalPath)
c.mutexForMapping.RLock()
mappedFilename, ok := c.mapping[filename]
c.mutexForMapping.RUnlock()
if ok {
r.URL.Path = fmt.Sprintf("/%v", mappedFilename)
} else {
// This will result in 404.
r.URL.Path = fmt.Sprintf("/%v", filename)
}
http.FileServer(http.Dir(os.TempDir())).ServeHTTP(w, r)
})
originalPath := r.URL.Path
filename := filepathutil.EscapePath(originalPath)
c.mutexForMapping.RLock()
mappedFilename, ok := c.mapping[filename]
c.mutexForMapping.RUnlock()
needWrite := false
if ok {
filePath := c.makeFilePath(mappedFilename)
_, err = os.Stat(filePath)
if err != nil {
needWrite = true
}
} else {
needWrite = true
}
if needWrite {
bytes, err = make()
if err != nil {
return
}
// nolint:gosec
hashBytes := md5.Sum(bytes)
hash := fmt.Sprintf("%x", hashBytes)
mappedFilename := filepathutil.MakeHashedPath(filename, hash)
filePath := c.makeFilePath(mappedFilename)
err = c.write(filePath, bytes)
if err != nil {
return
}
c.mutexForMapping.Lock()
c.mapping[filename] = mappedFilename
c.mutexForMapping.Unlock()
}
return
}