mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
156 lines
3.5 KiB
Go
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
|
|
}
|