stage 1
This commit is contained in:
@@ -15,7 +15,7 @@ internal/hubmodule/
|
||||
## 模块约束
|
||||
- **单一接口**:每个模块需要同时实现代理与缓存接口,避免跨包耦合。
|
||||
- **注册流程**:在模块 `init()` 中调用 `hubmodule.Register(ModuleMetadata{...})`,注册失败必须 panic 以阻止启动。
|
||||
- **缓存布局**:一律使用 `StoragePath/<Hub>/<path>.body`,如需附加目录需在 `ModuleMetadata` 中声明迁移策略。
|
||||
- **缓存布局**:一律使用 `StoragePath/<Hub>/<path>`,即与上游请求完全一致的磁盘路径;当某个路径既要保存正文又要作为子目录父节点时,会在该目录下写入 `__content` 文件以存放正文。
|
||||
- **配置注入**:模块仅通过依赖注入获取 `HubConfigEntry` 和全局参数,禁止直接读取文件或环境变量。
|
||||
- **可观测性**:所有模块必须输出 `module_key`、命中/回源状态等日志字段,并在返回错误时附带 Hub 名称。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 模块作者需要:
|
||||
// 1. 在 internal/hubmodule/<module-key>/ 目录下实现代理与缓存接口;
|
||||
// 2. 通过本包暴露的 Register 函数在 init() 中注册模块元数据;
|
||||
// 3. 保证缓存写入仍遵循 StoragePath/<Hub>/<path>.body 布局,并补充中文注释说明实现细节。
|
||||
// 3. 保证缓存写入仍遵循 StoragePath/<Hub>/<path> 原始路径布局,并补充中文注释说明实现细节。
|
||||
//
|
||||
// 该包同时负责提供模块发现、可观测信息以及迁移状态的对外查询能力。
|
||||
package hubmodule
|
||||
|
||||
29
internal/hubmodule/docker/module.go
Normal file
29
internal/hubmodule/docker/module.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
65
internal/hubmodule/legacy/state.go
Normal file
65
internal/hubmodule/legacy/state.go
Normal 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
|
||||
}
|
||||
41
internal/hubmodule/npm/locator.go
Normal file
41
internal/hubmodule/npm/locator.go
Normal 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
|
||||
}
|
||||
30
internal/hubmodule/npm/module.go
Normal file
30
internal/hubmodule/npm/module.go
Normal 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,
|
||||
})
|
||||
}
|
||||
52
internal/hubmodule/npm/module_test.go
Normal file
52
internal/hubmodule/npm/module_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
29
internal/hubmodule/pypi/module.go
Normal file
29
internal/hubmodule/pypi/module.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Package template 提供编写新模块时可复制的骨架示例。
|
||||
package template
|
||||
|
||||
import "github.com/any-hub/any-hub/internal/hubmodule"
|
||||
|
||||
// Package template 提供编写新模块时可复制的骨架示例。
|
||||
//
|
||||
// 使用方式:复制整个目录到 internal/hubmodule/<module-key>/ 并替换字段。
|
||||
// - 将 TemplateModule 重命名为实际模块类型。
|
||||
|
||||
Reference in New Issue
Block a user