Files
pmg/pkg/models/models.go
T

156 lines
3.5 KiB
Go

package models
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/pkg/common/utils"
)
type PackageAnalysisItem struct {
Name string
Version string
}
func (p PackageAnalysisItem) Id() string {
return fmt.Sprintf("%s@%s", p.Name, p.Version)
}
type PackageInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Dependencies map[string]string `json:"dependencies"`
}
type DependencyNode struct {
Name string
Version string
Dependencies map[string]*DependencyNode
}
// FlatteningFetcher fetches package dependencies and builds a dependency tree
type FlatteningFetcher struct {
visitedMu sync.RWMutex
visited map[string]bool
client *http.Client
}
func NewFlatteningFetcher() *FlatteningFetcher {
return &FlatteningFetcher{
visited: make(map[string]bool),
client: &http.Client{Timeout: 10 * time.Second},
}
}
func (ff *FlatteningFetcher) GetPackageDependencies(packageName, version string) (*DependencyNode, error) {
return ff.fetchDependenciesConcurrent(packageName, version)
}
func (ff *FlatteningFetcher) isVisited(key string) bool {
ff.visitedMu.RLock()
defer ff.visitedMu.RUnlock()
return ff.visited[key]
}
func (ff *FlatteningFetcher) markVisited(key string) {
ff.visitedMu.Lock()
defer ff.visitedMu.Unlock()
ff.visited[key] = true
}
func (ff *FlatteningFetcher) fetchDependenciesConcurrent(packageName, version string) (*DependencyNode, error) {
cacheKey := packageName + "@" + version
if ff.isVisited(cacheKey) {
return &DependencyNode{
Name: packageName,
Version: version,
}, nil
}
ff.markVisited(cacheKey)
packageInfo, err := ff.fetchPackageInfo(packageName, version)
if err != nil {
return nil, fmt.Errorf("failed to fetch package info for %s: %v", packageName, err)
}
dependencies := packageInfo.Dependencies
node := &DependencyNode{
Name: packageName,
Version: version,
Dependencies: make(map[string]*DependencyNode),
}
if len(dependencies) == 0 {
return node, nil
}
// Create channels for concurrent processing
type result struct {
name string
node *DependencyNode
err error
}
var wg sync.WaitGroup
resultChan := make(chan result, len(dependencies))
// Process dependencies concurrently
for depName, depVersion := range dependencies {
wg.Add(1)
go func(name, version string) {
defer wg.Done()
version = utils.CleanVersion(version)
depNode, err := ff.fetchDependenciesConcurrent(name, version)
resultChan <- result{name, depNode, err}
}(depName, depVersion)
}
// Wait for all goroutines to complete in a separate goroutine
go func() {
wg.Wait()
close(resultChan)
}()
// Collect results
for res := range resultChan {
if res.err != nil {
log.Warnf("Failed to fetch dependency %s: %v\n", res.name, res.err)
continue
}
node.Dependencies[res.name] = res.node
}
return node, nil
}
func (ff *FlatteningFetcher) fetchPackageInfo(packageName, version string) (*PackageInfo, error) {
url := fmt.Sprintf("https://registry.npmjs.org/%s/%s", packageName, version)
resp, err := ff.client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch package info: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var packageInfo PackageInfo
if err := json.Unmarshal(body, &packageInfo); err != nil {
return nil, err
}
return &packageInfo, nil
}