forked from goproxy/goproxy
/
cacher.go
182 lines (157 loc) · 4.52 KB
/
cacher.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package goproxy
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
)
// Cacher defines a set of intuitive methods used to cache module files for the
// [Goproxy].
type Cacher interface {
// Get gets the matched cache for the name. It returns the
// [os.ErrNotExist] if not found.
//
// Note that the returned [io.ReadCloser] can optionally implement the
// following interfaces:
// 1. [io.Seeker], mostly for the Range request header.
// 2. interface{ LastModified() time.Time }, mostly for the
// Last-Modified response header. Also for the If-Unmodified-Since,
// If-Modified-Since and If-Range request headers when 1 is
// implemented.
// 3. interface{ ModTime() time.Time }, same as 2, but with lower
// priority.
// 4. interface{ ETag() string }, mostly for the ETag response header.
// Also for the If-Match, If-None-Match and If-Range request headers
// when 1 is implemented. Note that the return value will be assumed
// to have complied with RFC 7232, section 2.3, so it will be used
// directly without further processing.
Get(ctx context.Context, name string) (io.ReadCloser, error)
// Put puts a cache for the name with the content and sets it to expire after the given duration.
Put(ctx context.Context, name string, content io.ReadSeeker, expiration time.Duration) error
// Cleanup removes all expired cache files.
Cleanup() error
}
// DirCacher implements the [Cacher] using a directory on the local disk. If the
// directory does not exist, it will be created with 0750 permissions.
type DirCacher string
// Get implements the [Cacher].
func (dc DirCacher) Get(
ctx context.Context,
name string,
) (io.ReadCloser, error) {
filePath := filepath.Join(string(dc), filepath.FromSlash(name))
// Check if the file has expired
expired, err := isCacheExpired(filePath)
if err != nil {
return nil, err
}
if expired {
return nil, os.ErrNotExist
}
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
return nil, err
}
return &struct {
*os.File
os.FileInfo
}{f, fi}, nil
}
// Put implements the [Cacher].
func (dc DirCacher) Put(
ctx context.Context,
name string,
content io.ReadSeeker,
expiration time.Duration,
) error {
file := filepath.Join(string(dc), filepath.FromSlash(name))
dir := filepath.Dir(file)
if err := os.MkdirAll(dir, 0750); err != nil {
return err
}
f, err := ioutil.TempFile(dir, fmt.Sprintf(
".%s.tmp*",
filepath.Base(file),
))
if err != nil {
return err
}
defer os.Remove(f.Name())
if _, err := io.Copy(f, content); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(f.Name(), file); err != nil {
return err
}
// Set the expiration time
if err := setCacheExpiration(file, expiration); err != nil {
return err
}
return nil
}
// Cleanup implements the [Cacher].
func (dc DirCacher) Cleanup() error {
files, err := ioutil.ReadDir(string(dc))
if err != nil {
return err
}
for _, file := range files {
filePath := filepath.Join(string(dc), file.Name())
expired, err := isCacheExpired(filePath)
if err != nil {
return err
}
if expired {
if file.IsDir() {
// If the file is a directory, call the Cleanup() method recursively to clean the directory.
subDir := filepath.Join(string(dc), file.Name())
subDirCacher := DirCacher(subDir)
if err := subDirCacher.Cleanup(); err != nil {
return err
}
} else {
// If the file is an ordinary file, delete it directly
if err := os.Remove(filePath); err != nil {
return err
}
}
}
}
return nil
}
// isCacheExpired checks if the cache file at the specified path has expired.
func isCacheExpired(filePath string) (bool, error) {
info, err := os.Stat(filePath)
if err != nil {
return false, err
}
expirationTime := info.ModTime()
return time.Now().After(expirationTime), nil
}
// setCacheExpiration sets the expiration time for the cache file at the specified path.
func setCacheExpiration(filePath string, expiration time.Duration) error {
expirationTime := time.Now().Add(expiration)
return os.Chtimes(filePath, time.Now(), expirationTime)
}
// StartCleanupTask starts a periodic cleanup task for the cache directory.
// It cleans up expired cache files every duration interval.
func StartCleanupTask(dirCacher DirCacher, interval time.Duration) {
go func() {
for {
time.Sleep(interval)
if err := dirCacher.Cleanup(); err != nil {
fmt.Printf("Error cleaning up expired cache files: %v\n", err)
}
}
}()
}