This commit is contained in:
2025-11-15 21:15:12 +08:00
parent 0d52bae1e8
commit bb00250dda
43 changed files with 1232 additions and 308 deletions

View File

@@ -15,7 +15,7 @@ internal/hubmodule/
## 模块约束
- **单一接口**:每个模块需要同时实现代理与缓存接口,避免跨包耦合。
- **注册流程**:在模块 `init()` 中调用 `hubmodule.Register(ModuleMetadata{...})`,注册失败必须 panic 以阻止启动。
- **缓存布局**:一律使用 `StoragePath/<Hub>/<path>.body`,如需附加目录需在 `ModuleMetadata` 中声明迁移策略
- **缓存布局**:一律使用 `StoragePath/<Hub>/<path>`,即与上游请求完全一致的磁盘路径;当某个路径既要保存正文又要作为子目录父节点时,会在该目录下写入 `__content` 文件以存放正文
- **配置注入**:模块仅通过依赖注入获取 `HubConfigEntry` 和全局参数,禁止直接读取文件或环境变量。
- **可观测性**:所有模块必须输出 `module_key`、命中/回源状态等日志字段,并在返回错误时附带 Hub 名称。

View File

@@ -3,7 +3,7 @@
// 模块作者需要:
// 1. 在 internal/hubmodule/<module-key>/ 目录下实现代理与缓存接口;
// 2. 通过本包暴露的 Register 函数在 init() 中注册模块元数据;
// 3. 保证缓存写入仍遵循 StoragePath/<Hub>/<path>.body 布局,并补充中文注释说明实现细节。
// 3. 保证缓存写入仍遵循 StoragePath/<Hub>/<path> 原始路径布局,并补充中文注释说明实现细节。
//
// 该包同时负责提供模块发现、可观测信息以及迁移状态的对外查询能力。
package hubmodule

View File

@@ -0,0 +1,29 @@
// Package docker 定义 Docker Hub 代理模块的元数据与缓存策略描述,供 registry 查表时使用。
package docker
import (
"time"
"github.com/any-hub/any-hub/internal/hubmodule"
)
const dockerDefaultTTL = 12 * time.Hour
// docker 模块继承 legacy 行为,但声明明确的缓存策略默认值,便于 hub 覆盖。
func init() {
hubmodule.MustRegister(hubmodule.ModuleMetadata{
Key: "docker",
Description: "Docker registry module with manifest/blob cache policies",
MigrationState: hubmodule.MigrationStateBeta,
SupportedProtocols: []string{
"docker",
},
CacheStrategy: hubmodule.CacheStrategyProfile{
TTLHint: dockerDefaultTTL,
ValidationMode: hubmodule.ValidationModeETag,
DiskLayout: "raw_path",
RequiresMetadataFile: false,
SupportsStreamingWrite: true,
},
})
}

View File

@@ -1,6 +1,10 @@
package hubmodule
import "time"
import (
"time"
"github.com/any-hub/any-hub/internal/cache"
)
// MigrationState 描述模块上线阶段,方便观测端区分 legacy/beta/ga。
type MigrationState string
@@ -36,9 +40,13 @@ type ModuleMetadata struct {
MigrationState MigrationState
SupportedProtocols []string
CacheStrategy CacheStrategyProfile
LocatorRewrite LocatorRewrite
}
// DefaultModuleKey 返回内置 legacy 模块的键值。
func DefaultModuleKey() string {
return defaultModuleKey
}
// LocatorRewrite 允许模块根据自身协议调整缓存路径,例如将 npm metadata 写入独立文件。
type LocatorRewrite func(cache.Locator) cache.Locator

View File

@@ -1,3 +1,4 @@
// Package legacy 提供旧版共享代理+缓存实现的适配器,确保未迁移 Hub 可继续运行。
package legacy
import "github.com/any-hub/any-hub/internal/hubmodule"
@@ -12,7 +13,7 @@ func init() {
"docker", "npm", "go", "pypi",
},
CacheStrategy: hubmodule.CacheStrategyProfile{
DiskLayout: ".body",
DiskLayout: "raw_path",
ValidationMode: hubmodule.ValidationModeETag,
SupportsStreamingWrite: true,
},

View File

@@ -0,0 +1,65 @@
package legacy
import (
"sort"
"strings"
"sync"
)
// RolloutFlag 描述 legacy 模块迁移阶段。
type RolloutFlag string
const (
RolloutLegacyOnly RolloutFlag = "legacy-only"
RolloutDual RolloutFlag = "dual"
RolloutModular RolloutFlag = "modular"
)
// AdapterState 记录特定 Hub 在 legacy 适配器中的运行状态。
type AdapterState struct {
HubName string
ModuleKey string
Rollout RolloutFlag
}
var (
stateMu sync.RWMutex
state = make(map[string]AdapterState)
)
// RecordAdapterState 更新指定 Hub 的 rollout 状态,供诊断端和日志使用。
func RecordAdapterState(hubName, moduleKey string, flag RolloutFlag) {
if hubName == "" {
return
}
key := strings.ToLower(hubName)
stateMu.Lock()
state[key] = AdapterState{
HubName: hubName,
ModuleKey: moduleKey,
Rollout: flag,
}
stateMu.Unlock()
}
// SnapshotAdapterStates 返回所有 Hub 的 rollout 状态,按名称排序。
func SnapshotAdapterStates() []AdapterState {
stateMu.RLock()
defer stateMu.RUnlock()
if len(state) == 0 {
return nil
}
keys := make([]string, 0, len(state))
for k := range state {
keys = append(keys, k)
}
sort.Strings(keys)
result := make([]AdapterState, 0, len(keys))
for _, key := range keys {
result = append(result, state[key])
}
return result
}

View File

@@ -0,0 +1,41 @@
package npm
import (
"strings"
"github.com/any-hub/any-hub/internal/cache"
)
// rewriteLocator 将 npm metadata JSON 落盘至 package.json避免与 tarball
// 路径的 `/-/` 子目录冲突,同时保持 tarball 使用原始路径。
func rewriteLocator(loc cache.Locator) cache.Locator {
path := loc.Path
if path == "" {
return loc
}
var qsSuffix string
core := path
if idx := strings.Index(core, "/__qs/"); idx >= 0 {
qsSuffix = core[idx:]
core = core[:idx]
}
if strings.Contains(core, "/-/") {
loc.Path = core + qsSuffix
return loc
}
clean := strings.TrimSuffix(core, "/")
if clean == "" {
clean = "/"
}
if clean == "/" {
loc.Path = "/package.json" + qsSuffix
return loc
}
loc.Path = clean + "/package.json" + qsSuffix
return loc
}

View File

@@ -0,0 +1,30 @@
// Package npm 描述 npm Registry 模块的默认策略与注册逻辑,方便新 Hub 直接启用。
package npm
import (
"time"
"github.com/any-hub/any-hub/internal/hubmodule"
)
const npmDefaultTTL = 30 * time.Minute
// npm 模块描述 NPM Registry 的默认缓存策略,并允许通过 [[Hub]] 覆盖 TTL/Validation。
func init() {
hubmodule.MustRegister(hubmodule.ModuleMetadata{
Key: "npm",
Description: "NPM proxy module with cache strategy overrides for metadata/tarballs",
MigrationState: hubmodule.MigrationStateBeta,
SupportedProtocols: []string{
"npm",
},
CacheStrategy: hubmodule.CacheStrategyProfile{
TTLHint: npmDefaultTTL,
ValidationMode: hubmodule.ValidationModeLastModified,
DiskLayout: "raw_path",
RequiresMetadataFile: false,
SupportsStreamingWrite: true,
},
LocatorRewrite: rewriteLocator,
})
}

View File

@@ -0,0 +1,52 @@
package npm
import (
"testing"
"time"
"github.com/any-hub/any-hub/internal/hubmodule"
)
func TestNPMMetadataRegistration(t *testing.T) {
meta, ok := hubmodule.Resolve("npm")
if !ok {
t.Fatalf("npm module not registered")
}
if meta.Key != "npm" {
t.Fatalf("unexpected module key: %s", meta.Key)
}
if meta.MigrationState == "" {
t.Fatalf("migration state must be set")
}
if len(meta.SupportedProtocols) == 0 {
t.Fatalf("supported protocols must not be empty")
}
if meta.CacheStrategy.TTLHint != npmDefaultTTL {
t.Fatalf("expected default ttl %s, got %s", npmDefaultTTL, meta.CacheStrategy.TTLHint)
}
if meta.CacheStrategy.ValidationMode != hubmodule.ValidationModeLastModified {
t.Fatalf("expected validation mode last-modified, got %s", meta.CacheStrategy.ValidationMode)
}
if !meta.CacheStrategy.SupportsStreamingWrite {
t.Fatalf("npm strategy should support streaming writes")
}
}
func TestNPMStrategyOverrides(t *testing.T) {
meta, ok := hubmodule.Resolve("npm")
if !ok {
t.Fatalf("npm module not registered")
}
overrideTTL := 10 * time.Minute
strategy := hubmodule.ResolveStrategy(meta, hubmodule.StrategyOptions{
TTLOverride: overrideTTL,
ValidationOverride: hubmodule.ValidationModeETag,
})
if strategy.TTLHint != overrideTTL {
t.Fatalf("expected ttl override %s, got %s", overrideTTL, strategy.TTLHint)
}
if strategy.ValidationMode != hubmodule.ValidationModeETag {
t.Fatalf("expected validation mode override to etag, got %s", strategy.ValidationMode)
}
}

View File

@@ -0,0 +1,29 @@
// Package pypi 聚焦 PyPI simple index 模块,提供 TTL/验证策略的注册样例。
package pypi
import (
"time"
"github.com/any-hub/any-hub/internal/hubmodule"
)
const pypiDefaultTTL = 15 * time.Minute
// pypi 模块负责 simple index + 分发包的策略声明,默认使用 Last-Modified 校验。
func init() {
hubmodule.MustRegister(hubmodule.ModuleMetadata{
Key: "pypi",
Description: "PyPI simple index module with per-hub cache overrides",
MigrationState: hubmodule.MigrationStateBeta,
SupportedProtocols: []string{
"pypi",
},
CacheStrategy: hubmodule.CacheStrategyProfile{
TTLHint: pypiDefaultTTL,
ValidationMode: hubmodule.ValidationModeLastModified,
DiskLayout: "raw_path",
RequiresMetadataFile: false,
SupportsStreamingWrite: true,
},
})
}

View File

@@ -17,5 +17,18 @@ func ResolveStrategy(meta ModuleMetadata, opts StrategyOptions) CacheStrategyPro
if opts.ValidationOverride != "" {
strategy.ValidationMode = opts.ValidationOverride
}
return strategy
return normalizeStrategy(strategy)
}
func normalizeStrategy(profile CacheStrategyProfile) CacheStrategyProfile {
if profile.TTLHint < 0 {
profile.TTLHint = 0
}
if profile.ValidationMode == "" {
profile.ValidationMode = ValidationModeETag
}
if profile.DiskLayout == "" {
profile.DiskLayout = "raw_path"
}
return profile
}

View File

@@ -1,8 +1,7 @@
// Package template 提供编写新模块时可复制的骨架示例。
package template
import "github.com/any-hub/any-hub/internal/hubmodule"
// Package template 提供编写新模块时可复制的骨架示例。
//
// 使用方式:复制整个目录到 internal/hubmodule/<module-key>/ 并替换字段。
// - 将 TemplateModule 重命名为实际模块类型。