Skip to content

Commit 7b75679

Browse files
authored
Add disk status checking and NPM cache purging functionality (#1329)
1 parent 4ee2b41 commit 7b75679

3 files changed

Lines changed: 100 additions & 16 deletions

File tree

internal/storage/storage_s3.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (s3 *s3Storage) Stat(name string) (stat Stat, err error) {
107107
if name == "" {
108108
return nil, errors.New("name is required")
109109
}
110-
if s3.fsCache != nil && !strings.HasSuffix(name, ".mjs.map") {
110+
if s3.shouldUseFSCache(name) {
111111
stat, err = s3.fsCache.Stat(name)
112112
if err == nil {
113113
return
@@ -160,7 +160,7 @@ func (s3 *s3Storage) Get(name string) (content io.ReadCloser, stat Stat, err err
160160
if name == "" {
161161
return nil, nil, errors.New("name is required")
162162
}
163-
if s3.fsCache != nil && !strings.HasSuffix(name, ".mjs.map") {
163+
if s3.shouldUseFSCache(name) {
164164
content, stat, err = s3.fsCache.Get(name)
165165
if err == nil {
166166
return
@@ -206,7 +206,7 @@ func (s3 *s3Storage) Get(name string) (content io.ReadCloser, stat Stat, err err
206206
contentLength: size,
207207
lastModified: lastModified,
208208
}
209-
if s3.fsCache != nil && !strings.HasSuffix(name, ".mjs.map") {
209+
if s3.shouldUseFSCache(name) {
210210
pr, pw := io.Pipe()
211211
go func() {
212212
unlock := s3.fsCacheLock.Lock(name)
@@ -267,7 +267,7 @@ func (s3 *s3Storage) Put(name string, content io.Reader) (err error) {
267267
err = errors.New("missing content length")
268268
return
269269
}
270-
if s3.fsCache != nil && !strings.HasSuffix(name, ".mjs.map") {
270+
if s3.shouldUseFSCache(name) {
271271
pr, pw := io.Pipe()
272272
go func(content io.Reader) {
273273
unlock := s3.fsCacheLock.Lock(name)
@@ -295,7 +295,7 @@ func (s3 *s3Storage) Delete(name string) (err error) {
295295
if name == "" {
296296
return errors.New("key is required")
297297
}
298-
if s3.fsCache != nil && !strings.HasSuffix(name, ".mjs.map") {
298+
if s3.shouldUseFSCache(name) {
299299
go s3.fsCache.Delete(name)
300300
}
301301
req, _ := http.NewRequest("DELETE", s3.apiEndpoint+"/"+name, nil)
@@ -417,6 +417,16 @@ func (s3 *s3Storage) sign(req *http.Request) {
417417
req.Header.Set("Authorization", strings.Join([]string{"AWS4-HMAC-SHA256 Credential=" + s3.accessKeyID + "/" + scope, "SignedHeaders=" + strings.Join(signedHeaders, ";"), "Signature=" + toHex(signature)}, ", "))
418418
}
419419

420+
func (s3 *s3Storage) shouldUseFSCache(name string) bool {
421+
if s3.fsCache == nil {
422+
return false
423+
}
424+
if strings.HasSuffix(name, ".mjs.map") || strings.HasSuffix(name, ".d.ts") || strings.HasSuffix(name, ".d.mts") || strings.HasSuffix(name, ".d.cts") {
425+
return false
426+
}
427+
return true
428+
}
429+
420430
func parseS3Error(resp *http.Response) error {
421431
var s3Error s3Error
422432
if xml.NewDecoder(resp.Body).Decode(&s3Error) != nil || s3Error.Code == "" {

server/disk.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package server
2+
3+
import (
4+
"os"
5+
"sync"
6+
"syscall"
7+
8+
"github.com/ije/gox/log"
9+
)
10+
11+
type DiskStatus uint8
12+
13+
const (
14+
DiskStatusError DiskStatus = iota
15+
DiskStatusOk
16+
DiskStatusLow
17+
DiskStatusFull
18+
)
19+
20+
// npmStorePurgeLock serializes purge operations on the npm store to avoid
21+
// concurrent rename/remove races within this process.
22+
var npmStorePurgeLock sync.Mutex
23+
24+
func purgeNPMCacheWhenDiskIsLowOrFull(npmrc *NpmRC, logger *log.Logger) {
25+
if status := checkDiskStatus(); status == DiskStatusOk || status == DiskStatusError {
26+
return
27+
}
28+
29+
npmStorePurgeLock.Lock()
30+
defer npmStorePurgeLock.Unlock()
31+
32+
npmDir := npmrc.StoreDir()
33+
oldDir := npmDir + "_old"
34+
35+
// Ensure any previous backup directory is removed so that the rename is idempotent.
36+
if err := os.RemoveAll(oldDir); err != nil {
37+
logger.Errorf("failed to remove previous npm cache backup directory %s: %v", oldDir, err)
38+
}
39+
40+
if err := os.Rename(npmDir, oldDir); err != nil {
41+
// If the directory does not exist, there's nothing to purge.
42+
if !os.IsNotExist(err) {
43+
logger.Errorf("failed to rename npm cache directory %s to %s: %v", npmDir, oldDir, err)
44+
}
45+
return
46+
}
47+
48+
if err := os.RemoveAll(oldDir); err != nil {
49+
logger.Errorf("failed to remove npm cache directory %s: %v", oldDir, err)
50+
}
51+
}
52+
53+
func checkDiskStatus() DiskStatus {
54+
var stat syscall.Statfs_t
55+
err := syscall.Statfs(config.WorkDir, &stat)
56+
if err == nil {
57+
avail := stat.Bavail * uint64(stat.Bsize)
58+
if avail < 100*MB {
59+
return DiskStatusFull
60+
} else if avail < 1024*MB {
61+
return DiskStatusLow
62+
}
63+
} else {
64+
return DiskStatusError
65+
}
66+
return DiskStatusOk
67+
}

server/router.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"slices"
1818
"sort"
1919
"strings"
20-
"syscall"
2120
"time"
2221

2322
"github.com/esm-dev/esm.sh/internal/fetch"
@@ -80,6 +79,18 @@ func esmRouter(esmStorage storage.Storage, logger *log.Logger) rex.Handle {
8079
}
8180
}
8281

82+
// purge npm cache when disk is low or full
83+
go func() {
84+
// run an initial check before waiting for the first ticker event
85+
purgeNPMCacheWhenDiskIsLowOrFull(npmrc, logger)
86+
87+
ticker := time.NewTicker(1 * time.Hour)
88+
defer ticker.Stop()
89+
for range ticker.C {
90+
go purgeNPMCacheWhenDiskIsLowOrFull(npmrc, logger)
91+
}
92+
}()
93+
8394
return func(ctx *rex.Context) any {
8495
pathname := ctx.R.URL.Path
8596

@@ -288,16 +299,12 @@ func esmRouter(esmStorage storage.Storage, logger *log.Logger) rex.Handle {
288299
}
289300

290301
diskStatus := "ok"
291-
var stat syscall.Statfs_t
292-
err := syscall.Statfs(config.WorkDir, &stat)
293-
if err == nil {
294-
avail := stat.Bavail * uint64(stat.Bsize)
295-
if avail < 100*MB {
296-
diskStatus = "full"
297-
} else if avail < 1024*MB {
298-
diskStatus = "low"
299-
}
300-
} else {
302+
switch checkDiskStatus() {
303+
case DiskStatusFull:
304+
diskStatus = "full"
305+
case DiskStatusLow:
306+
diskStatus = "low"
307+
case DiskStatusError:
301308
diskStatus = "error"
302309
}
303310

0 commit comments

Comments
 (0)