📦 包信息
获取 Packagist 上某个包的元数据、版本、下载统计与变更跟踪。这组方法覆盖单个包的全部「读」维度,是依赖治理、镜像同步、监控告警的基础。
何时使用
- 📦 依赖看板:拉取项目所有直接依赖的包信息(描述、维护者、star、下载量)做可视化。
- 🛠️ 镜像同步:用
GetPackageChanges增量同步自建镜像,避免每次全量拉取。 - 📊 下载监控:定期记录
GetPackageStats的每日/每月下载,绘制趋势。 - 🧩 版本探测:用
GetPackageWithV2Metadata/GetPackageDevVersions拿到 Composer V2 解析器所需的完整版本树(含 dev 分支)。
数据模型
PackageInfo
pkg/domain/package.go 中定义,对应 https://packagist.org/packages/{name}.json 响应里 package 字段的内容。
type PackageInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Time time.Time `json:"time"`
Maintainers []*Maintainer `json:"maintainers"`
Versions map[string]*Version `json:"versions"`
Type string `json:"type"`
Repository string `json:"repository"`
GithubStars int `json:"github_stars"`
GithubWatchers int `json:"github_watchers"`
GithubForks int `json:"github_forks"`
GithubOpenIssues int `json:"github_open_issues"`
Language string `json:"language"`
Dependents int `json:"dependents"`
Suggesters int `json:"suggesters"`
Downloads PackageDownloads `json:"downloads"`
Favers int `json:"favers"`
}| 字段 | 类型 | 说明 |
|---|---|---|
Name | string | 包名(vendor/package) |
Description | string | 包描述 |
Time | time.Time | 包信息最近更新时间 |
Maintainers | []*Maintainer | 维护者列表(Name、AvatarURL) |
Versions | map[string]*Version | 版本号 → 版本详情,键如 "v6.4.0" |
Type | string | 包类型,如 library、composer-plugin |
Repository | string | 源代码仓库 URL |
GithubStars 等 | int | 仓库的 star / watcher / fork / open issue 计数 |
Language | string | 仓库主语言 |
Dependents | int | 被多少个其它包依赖 |
Suggesters | int | 建议安装数 |
Downloads | PackageDownloads | 下载统计(见下) |
Favers | int | 收藏数 |
PackageDownloads
type PackageDownloads struct {
Total int `json:"total"`
Monthly int `json:"monthly"`
Daily int `json:"daily"`
}| 字段 | 类型 | 说明 |
|---|---|---|
Total | int | 历史总下载 |
Monthly | int | 本月下载 |
Daily | int | 今日下载 |
ComposerPackageInfo
GetPackage 返回的顶层结构,在 PackageInfo 外再包了一层,加上包名、时间戳等本地化字段:
type ComposerPackageInfo struct {
PackageName string `json:"package_name"`
PackageNameLowercase string `json:"package_name_lowercase"`
Package PackageInfo `json:"package"`
PackageInfoMd5 string `json:"package_info_md5"`
CreateTime *time.Time `json:"create_time"`
UpdateTime *time.Time `json:"update_time"`
ChangeTime *time.Time `json:"change_time"`
}| 字段 | 类型 | 说明 |
|---|---|---|
PackageName | string | 包名(与请求时传入一致) |
PackageNameLowercase | string | 小写包名,用于不区分大小写的查询 |
Package | PackageInfo | 真正的包信息 |
PackageInfoMd5 | string | 信息 MD5,用于识别是否较之前发生变化(SDK 当前不自动填充) |
CreateTime / UpdateTime / ChangeTime | *time.Time | 创建 / 更新 / 变更时间戳(GetPackage 会把当前时间填入前两者) |
PackageStatsResponse
pkg/domain/package_stats.go,对应 /packages/{name}/stats.json。
type PackageStatsResponse struct {
Downloads PackageDownloads `json:"downloads"`
Versions []string `json:"versions"`
Date string `json:"date"`
}| 字段 | 类型 | 说明 |
|---|---|---|
Downloads | PackageDownloads | 下载统计(total/monthly/daily) |
Versions | []string | 可用版本列表 |
Date | string | 统计起始日期 |
ChangeTrackingResponse / ChangeAction
pkg/domain/package_stats.go,对应 /metadata/changes.json。
type ChangeTrackingResponse struct {
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Actions []ChangeAction `json:"actions,omitempty"`
}
type ChangeAction struct {
Type string `json:"type"`
Package string `json:"package"`
Time int64 `json:"time"`
}| 字段 | 类型 | 说明 |
|---|---|---|
Error | string | since 参数缺失或无效时返回的错误信息 |
Timestamp | int64 | 当前时间戳,作为下次增量同步的游标 |
Actions | []ChangeAction | 变更操作列表 |
Actions[].Type | string | 操作类型:update 或 delete |
Actions[].Package | string | 发生变更的包名 |
Actions[].Time | int64 | 操作发生时间的 Unix 时间戳 |
GetPackage
📦 获取指定包的完整信息(含版本、维护者、下载统计、GitHub 指标)。对应 GET https://packagist.org/packages/{name}.json。
签名
func (c *ComposerClient) GetPackage(packageName string) (*domain.ComposerPackageInfo, error)参数
| 参数 | 类型 | 说明 |
|---|---|---|
packageName | string | 包名,格式 vendor/package,如 symfony/console |
返回值
| 值 | 类型 | 说明 |
|---|---|---|
| 结果 | *domain.ComposerPackageInfo | 包信息,Package 字段为详细元数据 |
| 错误 | error | HTTP 失败、非 200、JSON 解析失败时返回 |
示例
package main
import (
"fmt"
"log"
"time"
"github.com/scagogogo/composer-skills/pkg/client"
)
func main() {
c := client.NewComposerClient(30 * time.Second)
info, err := c.GetPackage("symfony/console")
if err != nil {
log.Fatalf("获取包信息失败: %v", err)
}
fmt.Printf("包名: %s\n", info.PackageName)
fmt.Printf("描述: %s\n", info.Package.Description)
fmt.Printf("类型: %s\n", info.Package.Type)
fmt.Printf("GitHub stars: %d\n", info.Package.GithubStars)
fmt.Printf("总下载: %d (本月 %d, 今日 %d)\n",
info.Package.Downloads.Total,
info.Package.Downloads.Monthly,
info.Package.Downloads.Daily,
)
fmt.Printf("版本数: %d\n", len(info.Package.Versions))
for v := range info.Package.Versions {
fmt.Println(" -", v)
}
}响应解析
/packages/{name}.json 返回 {"package": {...}} 外层包装。GetPackage 内部先解到 wrapper 结构取出 PackageInfo,再封装为 ComposerPackageInfo,并把 CreateTime / UpdateTime 填为当前时间,方便直接入库。
GetPackageWithV2Metadata
📦 获取 Composer V2 元数据格式的包信息(原始 JSON 字节)。对应 GET https://repo.packagist.org/p2/{name}.json。
签名
func (c *ComposerClient) GetPackageWithV2Metadata(packageName string) ([]byte, error)参数
| 参数 | 类型 | 说明 |
|---|---|---|
packageName | string | 包名,如 symfony/console |
返回值
| 值 | 类型 | 说明 |
|---|---|---|
| 数据 | []byte | V2 元数据原始 JSON 字节 |
| 错误 | error | HTTP 失败、非 200 时返回 |
示例
data, err := c.GetPackageWithV2Metadata("symfony/console")
if err != nil {
log.Fatal(err)
}
// 自行按 V2 schema 解析,或直接落盘
fmt.Println(string(data))为什么返回 []byte?
V2 元数据结构较复杂(含 packages.{name}.{version} 多层嵌套与 minified 字段),不同使用方关心的字段不同,因此 SDK 不强行建模,把原始 JSON 交给调用方按需解析。
GetPackageDevVersions
📦 获取包的开发版本(分支版本)信息(原始 JSON 字节)。对应 GET https://repo.packagist.org/p2/{name}~dev.json。
签名
func (c *ComposerClient) GetPackageDevVersions(packageName string) ([]byte, error)参数
| 参数 | 类型 | 说明 |
|---|---|---|
packageName | string | 包名,如 symfony/console |
返回值
| 值 | 类型 | 说明 |
|---|---|---|
| 数据 | []byte | dev 版本元数据原始 JSON 字节 |
| 错误 | error | HTTP 失败、非 200 时返回 |
示例
data, err := c.GetPackageDevVersions("symfony/console")
if err != nil {
log.Fatal(err)
}
// 包含 dev-master / dev-main 等分支版本
fmt.Println(string(data))URL 约定
Packagist 用包名后追加 ~dev 表示请求开发分支版本,SDK 自动拼接为 {repoURL}/p2/{name}~dev.json。
GetPackageStats
📦 获取包的下载统计与可用版本列表。对应 GET https://packagist.org/packages/{name}/stats.json。
签名
func (c *ComposerClient) GetPackageStats(packageName string) (*domain.PackageStatsResponse, error)参数
| 参数 | 类型 | 说明 |
|---|---|---|
packageName | string | 包名,如 symfony/console |
返回值
| 值 | 类型 | 说明 |
|---|---|---|
| 结果 | *domain.PackageStatsResponse | 含下载统计、版本列表、统计日期 |
| 错误 | error | HTTP 失败、非 200、JSON 解析失败时返回 |
示例
stats, err := c.GetPackageStats("symfony/console")
if err != nil {
log.Fatal(err)
}
fmt.Printf("总下载: %d\n", stats.Downloads.Total)
fmt.Printf("本月下载: %d\n", stats.Downloads.Monthly)
fmt.Printf("今日下载: %d\n", stats.Downloads.Daily)
fmt.Printf("可用版本数: %d\n", len(stats.Versions))
fmt.Printf("统计起始日期: %s\n", stats.Date)GetPackageChanges
📦 获取包元数据的增量变更记录。对应 GET https://packagist.org/metadata/changes.json?since={timestamp}。
签名
func (c *ComposerClient) GetPackageChanges(ctx context.Context, since int64) (*domain.ChangeTrackingResponse, error)参数
| 参数 | 类型 | 说明 |
|---|---|---|
ctx | context.Context | 上下文,用于超时与取消 |
since | int64 | 增量游标(Unix 时间戳);传 0 则返回当前时间戳但 Actions 为空,用于首次获取游标 |
返回值
| 值 | 类型 | 说明 |
|---|---|---|
| 结果 | *domain.ChangeTrackingResponse | 含 Timestamp(下次游标)与 Actions 变更列表 |
| 错误 | error | HTTP 失败、非 200、JSON 解析失败时返回 |
示例
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 首次同步:拿一个起始游标
resp, err := c.GetPackageChanges(ctx, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("起始游标: %d\n", resp.Timestamp)
// 下次用这个游标做增量
resp, err = c.GetPackageChanges(ctx, resp.Timestamp)
if err != nil {
log.Fatal(err)
}
for _, a := range resp.Actions {
fmt.Printf("[%s] %s @ %d\n", a.Type, a.Package, a.Time)
}
fmt.Printf("下次游标: %d\n", resp.Timestamp)增量同步模式
since=0 时只返回当前 Timestamp 而不返回历史变更。正确做法是:① 先用 since=0 拿到起始 Timestamp 并持久化;② 此后每次用上次的 Timestamp 作为 since,处理返回的 Actions,再保存新的 Timestamp。若 since 无效,响应 Error 字段会有错误说明。
进阶
镜像同步典型流程
结合 GetPackageChanges + GetPackage 可以构建一个轻量自建镜像:
- 首次:
GetPackageChanges(ctx, 0)拿起始游标T0。 - 周期性:
GetPackageChanges(ctx, T0)拿到Actions,对每个Type=update的包调GetPackage刷新本地缓存,对Type=delete的包从本地删除。 - 把响应的
Timestamp存为新的T0,进入下一轮。