stage 1
This commit is contained in:
13
internal/cache/doc.go
vendored
13
internal/cache/doc.go
vendored
@@ -1,7 +1,10 @@
|
||||
// Package cache defines the disk-backed store responsible for translating hub
|
||||
// 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
|
||||
// upstream fetches without duplicating filesystem logic.
|
||||
// requests into StoragePath/<hub>/<path> directories that mirror upstream
|
||||
// paths. When a given path also needs to act as the parent of other entries
|
||||
// (例如 npm metadata + tarball目录), the body is stored in a `__content` file
|
||||
// under that directory so两种形态可以共存。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 upstream fetches without
|
||||
// duplicating filesystem logic.
|
||||
package cache
|
||||
|
||||
157
internal/cache/fs_store.go
vendored
157
internal/cache/fs_store.go
vendored
@@ -15,8 +15,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const cacheFileSuffix = ".body"
|
||||
|
||||
// NewStore 以 basePath 为根目录构建磁盘缓存,整站复用一份实例。
|
||||
func NewStore(basePath string) (Store, error) {
|
||||
if basePath == "" {
|
||||
@@ -58,13 +56,27 @@ func (s *fileStore) Get(ctx context.Context, locator Locator) (*ReadResult, erro
|
||||
default:
|
||||
}
|
||||
|
||||
primary, legacy, err := s.entryPaths(locator)
|
||||
filePath, err := s.entryPath(locator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filePath, info, f, err := s.openEntryFile(primary, legacy)
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) || isNotDirError(err) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) || isNotDirError(err) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -74,11 +86,7 @@ func (s *fileStore) Get(ctx context.Context, locator Locator) (*ReadResult, erro
|
||||
SizeBytes: info.Size(),
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
|
||||
return &ReadResult{
|
||||
Entry: entry,
|
||||
Reader: f,
|
||||
}, nil
|
||||
return &ReadResult{Entry: entry, Reader: file}, nil
|
||||
}
|
||||
|
||||
func (s *fileStore) Put(ctx context.Context, locator Locator, body io.Reader, opts PutOptions) (*Entry, error) {
|
||||
@@ -88,12 +96,12 @@ func (s *fileStore) Put(ctx context.Context, locator Locator, body io.Reader, op
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
filePath, legacyPath, err := s.entryPaths(locator)
|
||||
filePath, err := s.entryPath(locator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.ensureDirWithUpgrade(filepath.Dir(filePath)); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -109,12 +117,12 @@ func (s *fileStore) Put(ctx context.Context, locator Locator, body io.Reader, op
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
os.Remove(tempName)
|
||||
_ = os.Remove(tempName)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.Rename(tempName, filePath); err != nil {
|
||||
os.Remove(tempName)
|
||||
_ = os.Remove(tempName)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -125,7 +133,6 @@ 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,
|
||||
@@ -143,16 +150,13 @@ func (s *fileStore) Remove(ctx context.Context, locator Locator) error {
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
filePath, legacyPath, err := s.entryPaths(locator)
|
||||
filePath, err := s.entryPath(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
|
||||
}
|
||||
|
||||
@@ -179,7 +183,7 @@ func (s *fileStore) lockEntry(locator Locator) (func(), error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *fileStore) path(locator Locator) (string, error) {
|
||||
func (s *fileStore) entryPath(locator Locator) (string, error) {
|
||||
if locator.HubName == "" {
|
||||
return "", errors.New("hub name required")
|
||||
}
|
||||
@@ -203,121 +207,6 @@ func (s *fileStore) path(locator Locator) (string, error) {
|
||||
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
|
||||
|
||||
2
internal/cache/store.go
vendored
2
internal/cache/store.go
vendored
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// Store 负责管理磁盘缓存的读写。磁盘布局遵循:
|
||||
//
|
||||
// <StoragePath>/<HubName>/<path>.body # 实际正文
|
||||
// <StoragePath>/<HubName>/<path> # 实际正文(与请求路径一致)
|
||||
//
|
||||
// 每个条目仅由正文文件组成,文件的 ModTime/Size 由文件系统提供。
|
||||
type Store interface {
|
||||
|
||||
85
internal/cache/store_test.go
vendored
85
internal/cache/store_test.go
vendored
@@ -7,8 +7,6 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -42,9 +40,6 @@ 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) {
|
||||
@@ -78,11 +73,11 @@ func TestStoreIgnoresDirectories(t *testing.T) {
|
||||
t.Fatalf("unexpected store type %T", store)
|
||||
}
|
||||
|
||||
filePath, err := fs.path(locator)
|
||||
filePath, err := fs.entryPath(locator)
|
||||
if err != nil {
|
||||
t.Fatalf("path error: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filePath+cacheFileSuffix, 0o755); err != nil {
|
||||
if err := os.MkdirAll(filePath, 0o755); err != nil {
|
||||
t.Fatalf("mkdir error: %v", err)
|
||||
}
|
||||
|
||||
@@ -91,82 +86,6 @@ 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()
|
||||
|
||||
57
internal/cache/writer.go
vendored
Normal file
57
internal/cache/writer.go
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
)
|
||||
|
||||
// ErrStoreUnavailable 表示当前模块未注入缓存存储实例。
|
||||
var ErrStoreUnavailable = errors.New("cache store unavailable")
|
||||
|
||||
// StrategyWriter 注入模块的缓存策略,提供 TTL 决策与写入封装。
|
||||
type StrategyWriter struct {
|
||||
store Store
|
||||
strategy hubmodule.CacheStrategyProfile
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewStrategyWriter 构造策略感知的写入器,默认使用 time.Now 作为时钟。
|
||||
func NewStrategyWriter(store Store, strategy hubmodule.CacheStrategyProfile) StrategyWriter {
|
||||
return StrategyWriter{
|
||||
store: store,
|
||||
strategy: strategy,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled 返回当前是否具备缓存写入能力。
|
||||
func (w StrategyWriter) Enabled() bool {
|
||||
return w.store != nil
|
||||
}
|
||||
|
||||
// Put 写入缓存正文,并保持与 Store 相同的语义。
|
||||
func (w StrategyWriter) Put(ctx context.Context, locator Locator, body io.Reader, opts PutOptions) (*Entry, error) {
|
||||
if w.store == nil {
|
||||
return nil, ErrStoreUnavailable
|
||||
}
|
||||
return w.store.Put(ctx, locator, body, opts)
|
||||
}
|
||||
|
||||
// ShouldBypassValidation 根据策略 TTL 判断是否可以直接复用缓存,避免重复 HEAD。
|
||||
func (w StrategyWriter) ShouldBypassValidation(entry Entry) bool {
|
||||
ttl := w.strategy.TTLHint
|
||||
if ttl <= 0 {
|
||||
return false
|
||||
}
|
||||
expireAt := entry.ModTime.Add(ttl)
|
||||
return w.now().Before(expireAt)
|
||||
}
|
||||
|
||||
// SupportsValidation 返回当前策略是否允许通过 HEAD/Etag 等方式再验证。
|
||||
func (w StrategyWriter) SupportsValidation() bool {
|
||||
return w.strategy.ValidationMode != hubmodule.ValidationModeNever
|
||||
}
|
||||
Reference in New Issue
Block a user