1 Commits

Author SHA1 Message Date
9692219e0f fix: npm proxy issues
Some checks failed
docker-release / build-and-push (push) Failing after 10m2s
2025-11-14 22:00:04 +08:00
4 changed files with 228 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
// Package cache defines the disk-backed store responsible for translating hub
// requests into StoragePath/<hub>/<path> files. The store exposes read/write
// requests into StoragePath/<hub>/<path>.body files. The store exposes read/write
// primitives with safe semantics (temp file + rename) and surfaces file info
// (size, modtime) for higher layers to implement conditional revalidation.
// Proxy handlers depend on this package to stream cached responses or trigger

View File

@@ -11,9 +11,12 @@ import (
"path/filepath"
"strings"
"sync"
"syscall"
"time"
)
const cacheFileSuffix = ".body"
// NewStore 以 basePath 为根目录构建磁盘缓存,整站复用一份实例。
func NewStore(basePath string) (Store, error) {
if basePath == "" {
@@ -55,27 +58,13 @@ func (s *fileStore) Get(ctx context.Context, locator Locator) (*ReadResult, erro
default:
}
filePath, err := s.path(locator)
primary, legacy, err := s.entryPaths(locator)
if err != nil {
return nil, err
}
info, err := os.Stat(filePath)
filePath, info, f, err := s.openEntryFile(primary, legacy)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrNotFound
}
return nil, err
}
if info.IsDir() {
return nil, ErrNotFound
}
f, err := os.Open(filePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrNotFound
}
return nil, err
}
@@ -99,12 +88,12 @@ func (s *fileStore) Put(ctx context.Context, locator Locator, body io.Reader, op
}
defer unlock()
filePath, err := s.path(locator)
filePath, legacyPath, err := s.entryPaths(locator)
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
if err := s.ensureDirWithUpgrade(filepath.Dir(filePath)); err != nil {
return nil, err
}
@@ -136,6 +125,7 @@ func (s *fileStore) Put(ctx context.Context, locator Locator, body io.Reader, op
if err := os.Chtimes(filePath, modTime, modTime); err != nil {
return nil, err
}
_ = os.Remove(legacyPath)
entry := Entry{
Locator: locator,
@@ -153,13 +143,16 @@ func (s *fileStore) Remove(ctx context.Context, locator Locator) error {
}
defer unlock()
filePath, err := s.path(locator)
filePath, legacyPath, err := s.entryPaths(locator)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err := os.Remove(legacyPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
return nil
}
@@ -201,13 +194,141 @@ func (s *fileStore) path(locator Locator) (string, error) {
rel = "root"
}
filePath := filepath.Join(s.basePath, locator.HubName, filepath.FromSlash(rel))
if !strings.HasPrefix(filePath, filepath.Join(s.basePath, locator.HubName)) {
hubRoot := filepath.Join(s.basePath, locator.HubName)
filePath := filepath.Join(hubRoot, filepath.FromSlash(rel))
hubPrefix := hubRoot + string(os.PathSeparator)
if filePath != hubRoot && !strings.HasPrefix(filePath, hubPrefix) {
return "", errors.New("invalid cache path")
}
return filePath, nil
}
func (s *fileStore) entryPaths(locator Locator) (string, string, error) {
legacyPath, err := s.path(locator)
if err != nil {
return "", "", err
}
return legacyPath + cacheFileSuffix, legacyPath, nil
}
func (s *fileStore) openEntryFile(primaryPath, legacyPath string) (string, fs.FileInfo, *os.File, error) {
info, err := os.Stat(primaryPath)
if err == nil {
if info.IsDir() {
return "", nil, nil, ErrNotFound
}
f, err := os.Open(primaryPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) || isNotDirError(err) {
return "", nil, nil, ErrNotFound
}
return "", nil, nil, err
}
return primaryPath, info, f, nil
}
if !errors.Is(err, fs.ErrNotExist) && !isNotDirError(err) {
return "", nil, nil, err
}
info, err = os.Stat(legacyPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) || isNotDirError(err) {
return "", nil, nil, ErrNotFound
}
return "", nil, nil, err
}
if info.IsDir() {
return "", nil, nil, ErrNotFound
}
if migrateErr := s.migrateLegacyFile(primaryPath, legacyPath); migrateErr == nil {
return s.openEntryFile(primaryPath, legacyPath)
}
f, err := os.Open(legacyPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) || isNotDirError(err) {
return "", nil, nil, ErrNotFound
}
return "", nil, nil, err
}
return legacyPath, info, f, nil
}
func (s *fileStore) migrateLegacyFile(primaryPath, legacyPath string) error {
if legacyPath == "" || primaryPath == legacyPath {
return nil
}
if _, err := os.Stat(legacyPath); err != nil {
return err
}
if _, err := os.Stat(primaryPath); err == nil {
if removeErr := os.Remove(legacyPath); removeErr != nil && !errors.Is(removeErr, fs.ErrNotExist) {
return removeErr
}
return nil
}
return os.Rename(legacyPath, primaryPath)
}
func (s *fileStore) ensureDirWithUpgrade(dir string) error {
for i := 0; i < 8; i++ {
if err := os.MkdirAll(dir, 0o755); err != nil {
if isNotDirError(err) {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
if upgradeErr := s.upgradeLegacyNode(pathErr.Path); upgradeErr != nil {
return upgradeErr
}
continue
}
}
return err
}
return nil
}
return fmt.Errorf("ensure cache directory failed for %s", dir)
}
func (s *fileStore) upgradeLegacyNode(conflictPath string) error {
if conflictPath == "" {
return errors.New("empty conflict path")
}
rel, err := filepath.Rel(s.basePath, conflictPath)
if err != nil {
return err
}
if strings.HasPrefix(rel, "..") {
return fmt.Errorf("conflict path outside storage: %s", conflictPath)
}
info, err := os.Stat(conflictPath)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(conflictPath, cacheFileSuffix) {
return nil
}
newPath := conflictPath + cacheFileSuffix
if _, err := os.Stat(newPath); err == nil {
return os.Remove(conflictPath)
}
return os.Rename(conflictPath, newPath)
}
func isNotDirError(err error) bool {
if err == nil {
return false
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return errors.Is(pathErr.Err, syscall.ENOTDIR)
}
return false
}
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {
var copied int64
buf := make([]byte, 32*1024)

View File

@@ -9,7 +9,7 @@ import (
// Store 负责管理磁盘缓存的读写。磁盘布局遵循:
//
// <StoragePath>/<HubName>/<path> # 实际正文
// <StoragePath>/<HubName>/<path>.body # 实际正文
//
// 每个条目仅由正文文件组成,文件的 ModTime/Size 由文件系统提供。
type Store interface {

View File

@@ -3,8 +3,12 @@ package cache
import (
"bytes"
"context"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
@@ -38,6 +42,9 @@ func TestStorePutAndGet(t *testing.T) {
if !result.Entry.ModTime.Equal(modTime) {
t.Fatalf("modtime mismatch: expected %v got %v", modTime, result.Entry.ModTime)
}
if !strings.HasSuffix(result.Entry.FilePath, cacheFileSuffix) {
t.Fatalf("expected cache file suffix %s, got %s", cacheFileSuffix, result.Entry.FilePath)
}
}
func TestStoreGetMissing(t *testing.T) {
@@ -75,7 +82,7 @@ func TestStoreIgnoresDirectories(t *testing.T) {
if err != nil {
t.Fatalf("path error: %v", err)
}
if err := os.MkdirAll(filePath, 0o755); err != nil {
if err := os.MkdirAll(filePath+cacheFileSuffix, 0o755); err != nil {
t.Fatalf("mkdir error: %v", err)
}
@@ -84,6 +91,82 @@ func TestStoreIgnoresDirectories(t *testing.T) {
}
}
func TestStoreMigratesLegacyEntryOnGet(t *testing.T) {
store := newTestStore(t)
fs, ok := store.(*fileStore)
if !ok {
t.Fatalf("unexpected store type %T", store)
}
locator := Locator{HubName: "npm", Path: "/pkg"}
legacyPath, err := fs.path(locator)
if err != nil {
t.Fatalf("path error: %v", err)
}
if err := os.MkdirAll(filepath.Dir(legacyPath), 0o755); err != nil {
t.Fatalf("mkdir error: %v", err)
}
if err := os.WriteFile(legacyPath, []byte("legacy"), 0o644); err != nil {
t.Fatalf("write legacy error: %v", err)
}
result, err := store.Get(context.Background(), locator)
if err != nil {
t.Fatalf("get legacy error: %v", err)
}
body, err := io.ReadAll(result.Reader)
if err != nil {
t.Fatalf("read legacy error: %v", err)
}
result.Reader.Close()
if string(body) != "legacy" {
t.Fatalf("unexpected legacy body: %s", string(body))
}
if !strings.HasSuffix(result.Entry.FilePath, cacheFileSuffix) {
t.Fatalf("expected migrated file suffix, got %s", result.Entry.FilePath)
}
if _, statErr := os.Stat(legacyPath); !errors.Is(statErr, fs.ErrNotExist) {
t.Fatalf("expected legacy path removed, got %v", statErr)
}
}
func TestStoreHandlesAncestorFileConflict(t *testing.T) {
store := newTestStore(t)
fs, ok := store.(*fileStore)
if !ok {
t.Fatalf("unexpected store type %T", store)
}
metaLocator := Locator{HubName: "npm", Path: "/pkg"}
legacyPath, err := fs.path(metaLocator)
if err != nil {
t.Fatalf("path error: %v", err)
}
if err := os.MkdirAll(filepath.Dir(legacyPath), 0o755); err != nil {
t.Fatalf("mkdir error: %v", err)
}
if err := os.WriteFile(legacyPath, []byte("legacy"), 0o644); err != nil {
t.Fatalf("write legacy error: %v", err)
}
tarLocator := Locator{HubName: "npm", Path: "/pkg/-/pkg-1.0.0.tgz"}
if _, err := store.Put(context.Background(), tarLocator, bytes.NewReader([]byte("tar")), PutOptions{}); err != nil {
t.Fatalf("put tar error: %v", err)
}
if _, err := os.Stat(legacyPath); !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("expected legacy metadata renamed, got %v", err)
}
if _, err := os.Stat(legacyPath + cacheFileSuffix); err != nil {
t.Fatalf("expected migrated legacy cache, got %v", err)
}
primary, _, err := fs.entryPaths(tarLocator)
if err != nil {
t.Fatalf("entry path error: %v", err)
}
if _, err := os.Stat(primary); err != nil {
t.Fatalf("expected tar cache file, got %v", err)
}
}
// newTestStore returns a Store backed by a temporary directory.
func newTestStore(t *testing.T) Store {
t.Helper()