feat: support apk

This commit is contained in:
2025-11-18 12:16:28 +08:00
parent 68b6bb78e6
commit ba5544c28d
28 changed files with 1412 additions and 54 deletions

View File

@@ -0,0 +1,102 @@
// Package debian defines hook behaviors for APT (Debian/Ubuntu) proxying.
// 索引Release/InRelease/Packages*需要再验证包体pool/ 和 by-hash视为不可变直接缓存。
// 日志字段沿用通用 proxy命中/上游状态),无需额外改写。
package debian
import (
"path"
"strings"
"github.com/any-hub/any-hub/internal/proxy/hooks"
)
func init() {
hooks.MustRegister("debian", hooks.Hooks{
NormalizePath: normalizePath,
CachePolicy: cachePolicy,
ContentType: contentType,
})
}
func normalizePath(_ *hooks.RequestContext, p string, rawQuery []byte) (string, []byte) {
clean := path.Clean("/" + strings.TrimSpace(p))
return clean, rawQuery
}
func cachePolicy(_ *hooks.RequestContext, locatorPath string, current hooks.CachePolicy) hooks.CachePolicy {
clean := canonicalPath(locatorPath)
switch {
case isAptIndexPath(clean):
// 索引类Release/Packages需要 If-None-Match/If-Modified-Since 再验证。
current.AllowCache = true
current.AllowStore = true
current.RequireRevalidate = true
case isAptImmutablePath(clean):
// pool/*.deb 与 by-hash 路径视为不可变,直接缓存后续不再 HEAD。
current.AllowCache = true
current.AllowStore = true
current.RequireRevalidate = false
default:
current.AllowCache = false
current.AllowStore = false
current.RequireRevalidate = false
}
return current
}
func contentType(_ *hooks.RequestContext, locatorPath string) string {
switch {
case strings.HasSuffix(locatorPath, ".gz"):
return "application/gzip"
case strings.HasSuffix(locatorPath, ".xz"):
return "application/x-xz"
case strings.HasSuffix(locatorPath, "Release.gpg"):
return "application/pgp-signature"
case isAptIndexPath(locatorPath):
return "text/plain"
default:
return ""
}
}
func isAptIndexPath(p string) bool {
clean := canonicalPath(p)
if isByHashPath(clean) {
return false
}
if strings.HasPrefix(clean, "/dists/") {
if strings.HasSuffix(clean, "/release") || strings.HasSuffix(clean, "/inrelease") || strings.HasSuffix(clean, "/release.gpg") {
return true
}
if strings.Contains(clean, "/packages") {
return true
}
}
return false
}
func isAptImmutablePath(p string) bool {
clean := canonicalPath(p)
if isByHashPath(clean) {
return true
}
if strings.HasPrefix(clean, "/pool/") {
return true
}
return false
}
func isByHashPath(p string) bool {
clean := canonicalPath(p)
if !strings.HasPrefix(clean, "/dists/") {
return false
}
return strings.Contains(clean, "/by-hash/")
}
func canonicalPath(p string) string {
if p == "" {
return "/"
}
return strings.ToLower(path.Clean("/" + strings.TrimSpace(p)))
}

View File

@@ -0,0 +1,64 @@
package debian
import (
"testing"
"github.com/any-hub/any-hub/internal/proxy/hooks"
)
func TestCachePolicyIndexesRevalidate(t *testing.T) {
current := cachePolicy(nil, "/dists/bookworm/Release", hooks.CachePolicy{})
if !current.AllowCache || !current.AllowStore || !current.RequireRevalidate {
t.Fatalf("expected index to allow cache/store and revalidate")
}
current = cachePolicy(nil, "/dists/bookworm/main/binary-amd64/Packages.gz", hooks.CachePolicy{})
if !current.AllowCache || !current.AllowStore || !current.RequireRevalidate {
t.Fatalf("expected packages index to revalidate")
}
}
func TestCachePolicyImmutable(t *testing.T) {
tests := []struct {
name string
path string
}{
{name: "by-hash index snapshot", path: "/dists/bookworm/by-hash/sha256/abc"},
{name: "by-hash nested", path: "/dists/bookworm/main/binary-amd64/by-hash/SHA256/def"},
{name: "pool package", path: "/pool/main/h/hello.deb"},
{name: "pool canonicalized", path: " /PoOl/main/../main/h/hello_1.0_amd64.DeB "},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
current := cachePolicy(nil, tt.path, hooks.CachePolicy{})
if !current.AllowCache || !current.AllowStore || current.RequireRevalidate {
t.Fatalf("expected immutable cache for %s", tt.path)
}
})
}
}
func TestCachePolicyNonAptPath(t *testing.T) {
current := cachePolicy(nil, "/other/path", hooks.CachePolicy{})
if current.AllowCache || current.AllowStore || current.RequireRevalidate {
t.Fatalf("expected non-APT path to disable cache/store")
}
}
func TestNormalizePath(t *testing.T) {
path, _ := normalizePath(nil, "dists/bookworm/Release", nil)
if path != "/dists/bookworm/Release" {
t.Fatalf("unexpected path: %s", path)
}
}
func TestContentType(t *testing.T) {
if ct := contentType(nil, "/dists/bookworm/Release"); ct != "text/plain" {
t.Fatalf("expected text/plain for Release, got %s", ct)
}
if ct := contentType(nil, "/dists/bookworm/Release.gpg"); ct != "application/pgp-signature" {
t.Fatalf("expected signature content-type, got %s", ct)
}
if ct := contentType(nil, "/dists/bookworm/main/binary-amd64/Packages.gz"); ct != "application/gzip" {
t.Fatalf("expected gzip content-type, got %s", ct)
}
}

View File

@@ -0,0 +1,29 @@
// Package debian registers metadata for Debian/Ubuntu APT proxying.
package debian
import (
"time"
"github.com/any-hub/any-hub/internal/hubmodule"
)
const debianDefaultTTL = 6 * time.Hour
func init() {
// 仅声明模块元数据(缓存策略等);具体 hooks 在后续实现。
hubmodule.MustRegister(hubmodule.ModuleMetadata{
Key: "debian",
Description: "APT proxy with cached indexes and packages",
MigrationState: hubmodule.MigrationStateBeta,
SupportedProtocols: []string{
"debian",
},
CacheStrategy: hubmodule.CacheStrategyProfile{
TTLHint: 0, // 索引每次再验证,由 ETag/Last-Modified 控制
ValidationMode: hubmodule.ValidationModeLastModified, // 索引使用 Last-Modified/ETag 再验证
DiskLayout: "raw_path", // 复用通用原始路径布局
RequiresMetadataFile: false,
SupportsStreamingWrite: true, // 包体需要流式写入,避免大文件占用内存
},
})
}