diff --git a/internal/cache/fs_store.go b/internal/cache/fs_store.go index 028d91d..737d04b 100644 --- a/internal/cache/fs_store.go +++ b/internal/cache/fs_store.go @@ -2,6 +2,7 @@ package cache import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -49,6 +50,10 @@ type entryLock struct { refs int } +type entryMetadata struct { + EffectiveUpstreamPath string `json:"effective_upstream_path,omitempty"` +} + func (s *fileStore) Get(ctx context.Context, locator Locator) (*ReadResult, error) { select { case <-ctx.Done(): @@ -86,6 +91,12 @@ func (s *fileStore) Get(ctx context.Context, locator Locator) (*ReadResult, erro SizeBytes: info.Size(), ModTime: info.ModTime(), } + if metadata, err := s.readMetadata(filePath); err == nil { + entry.EffectiveUpstreamPath = metadata.EffectiveUpstreamPath + } else if !errors.Is(err, fs.ErrNotExist) { + file.Close() + return nil, err + } return &ReadResult{Entry: entry, Reader: file}, nil } @@ -133,12 +144,16 @@ 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 } + if err := s.writeMetadata(filePath, opts.EffectiveUpstreamPath); err != nil { + return nil, err + } entry := Entry{ - Locator: locator, - FilePath: filePath, - SizeBytes: written, - ModTime: modTime, + Locator: locator, + FilePath: filePath, + SizeBytes: written, + ModTime: modTime, + EffectiveUpstreamPath: opts.EffectiveUpstreamPath, } return &entry, nil } @@ -157,6 +172,54 @@ func (s *fileStore) Remove(ctx context.Context, locator Locator) error { if err := os.Remove(filePath); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } + if err := os.Remove(metadataPath(filePath)); err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + return nil +} + +func (s *fileStore) readMetadata(filePath string) (entryMetadata, error) { + raw, err := os.ReadFile(metadataPath(filePath)) + if err != nil { + return entryMetadata{}, err + } + var metadata entryMetadata + if err := json.Unmarshal(raw, &metadata); err != nil { + return entryMetadata{}, err + } + return metadata, nil +} + +func (s *fileStore) writeMetadata(filePath string, effectiveUpstreamPath string) error { + metaFilePath := metadataPath(filePath) + if effectiveUpstreamPath == "" { + if err := os.Remove(metaFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + return nil + } + data, err := json.Marshal(entryMetadata{EffectiveUpstreamPath: effectiveUpstreamPath}) + if err != nil { + return err + } + tempFile, err := os.CreateTemp(filepath.Dir(metaFilePath), ".cache-meta-*") + if err != nil { + return err + } + tempName := tempFile.Name() + if _, err := tempFile.Write(data); err != nil { + tempFile.Close() + _ = os.Remove(tempName) + return err + } + if err := tempFile.Close(); err != nil { + _ = os.Remove(tempName) + return err + } + if err := os.Rename(tempName, metaFilePath); err != nil { + _ = os.Remove(tempName) + return err + } return nil } @@ -248,3 +311,7 @@ func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, func locatorKey(locator Locator) string { return locator.HubName + "::" + locator.Path } + +func metadataPath(filePath string) string { + return filePath + ".meta" +} diff --git a/internal/cache/store.go b/internal/cache/store.go index 1db5b0f..bbb928c 100644 --- a/internal/cache/store.go +++ b/internal/cache/store.go @@ -26,7 +26,8 @@ type Store interface { // PutOptions 控制写入过程中的可选属性。 type PutOptions struct { - ModTime time.Time + ModTime time.Time + EffectiveUpstreamPath string } // Locator 唯一定位一个缓存条目(Hub + 相对路径),所有路径均为 URL 路径风格。 @@ -37,10 +38,11 @@ type Locator struct { // Entry 表示一次缓存命中结果,包含绝对文件路径及文件信息。 type Entry struct { - Locator Locator `json:"locator"` - FilePath string `json:"file_path"` - SizeBytes int64 `json:"size_bytes"` - ModTime time.Time + Locator Locator `json:"locator"` + FilePath string `json:"file_path"` + SizeBytes int64 `json:"size_bytes"` + ModTime time.Time `json:"mod_time"` + EffectiveUpstreamPath string `json:"effective_upstream_path,omitempty"` } // ReadResult 组合 Entry 与正文 Reader,便于代理层直接将 Body 流式返回。 diff --git a/internal/cache/store_test.go b/internal/cache/store_test.go index 60c90e7..dae1cc5 100644 --- a/internal/cache/store_test.go +++ b/internal/cache/store_test.go @@ -5,6 +5,7 @@ import ( "context" "io" "os" + "strings" "testing" "time" ) @@ -62,6 +63,28 @@ func TestStoreRemove(t *testing.T) { } } +func TestStorePersistsEffectiveUpstreamPath(t *testing.T) { + store := newTestStore(t) + locator := Locator{HubName: "docker", Path: "/v2/coredns/manifests/v1.13.1"} + + _, err := store.Put(context.Background(), locator, strings.NewReader("body"), PutOptions{ + EffectiveUpstreamPath: "/v2/coredns/coredns/manifests/v1.13.1", + }) + if err != nil { + t.Fatalf("put error: %v", err) + } + + result, err := store.Get(context.Background(), locator) + if err != nil { + t.Fatalf("get error: %v", err) + } + defer result.Reader.Close() + + if result.Entry.EffectiveUpstreamPath != "/v2/coredns/coredns/manifests/v1.13.1" { + t.Fatalf("unexpected effective upstream path: %q", result.Entry.EffectiveUpstreamPath) + } +} + func TestStoreIgnoresDirectories(t *testing.T) { store := newTestStore(t) locator := Locator{HubName: "ghcr", Path: "/v2"}