| // mirrors package brings up a verdaccio mirror for each supported project. |
| package mirrors |
| |
| import ( |
| "context" |
| "fmt" |
| "html/template" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "go.skia.org/infra/go/executil" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/npm-audit-mirror/go/types" |
| ) |
| |
| const ( |
| verdaccioDirName = "verdaccio" |
| verdaccioStorageDirName = "storage" |
| verdaccioLogFileName = "verdaccio.log" |
| ) |
| |
| // VerdaccioMirror implements types.ProjectMirror. |
| type VerdaccioMirror struct { |
| workDir string |
| verdaccioDir string |
| verdaccioConfigPath string |
| verdaccioStorageDir string |
| projectName string |
| publicURL string |
| |
| // Maintains an in-memory map of download package tarballs. |
| // This map is used to determine which packages are not |
| // available on the mirror yet and will require an external |
| // network call to download. |
| downloadedPackageTarballs map[string]interface{} |
| // Mutex that contains access to the above map. |
| downloadedPackageTarballsMtx sync.RWMutex |
| } |
| |
| // NewVerdaccioMirror returns an instance of VerdaccioMirror. |
| func NewVerdaccioMirror(projectName, workDir, host string, cfgTmpl *template.Template) (types.ProjectMirror, error) { |
| // Create all necessary directories and files. |
| verdaccioDir := path.Join(workDir, verdaccioDirName) |
| if err := os.MkdirAll(verdaccioDir, 0755); err != nil { |
| return nil, skerr.Wrapf(err, "Could not create %s", verdaccioDir) |
| } |
| // Create the verdaccio config file. |
| verdaccioConfigPath, err := createCfgFile(projectName, workDir, verdaccioDir, cfgTmpl) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| // Verdaccio storage dir. This will be created automatically by verdaccio. |
| verdaccioStorageDir := path.Join(verdaccioDir, verdaccioStorageDirName) |
| |
| // Populate the in-memory map of downloaded package tarballs. |
| downloadedPackageTarballs, err := GetTarballsInMirrorStorage(verdaccioStorageDir) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| |
| // Find this mirror's publicURL. |
| publicURL := fmt.Sprintf("%s/%s/", host, projectName) |
| |
| return &VerdaccioMirror{ |
| workDir: workDir, |
| verdaccioDir: verdaccioDir, |
| verdaccioConfigPath: verdaccioConfigPath, |
| verdaccioStorageDir: verdaccioStorageDir, |
| projectName: projectName, |
| downloadedPackageTarballs: downloadedPackageTarballs, |
| publicURL: publicURL, |
| }, nil |
| } |
| |
| // createCfgFile creates the verdaccio config file using the provided template. Returns |
| // the path to the config file. |
| func createCfgFile(projectName, workDir, verdaccioDir string, cfgTmpl *template.Template) (string, error) { |
| verdaccioConfigFilePath := path.Join(verdaccioDir, "config.yaml") |
| f, err := os.Create(verdaccioConfigFilePath) |
| if err != nil { |
| return "", skerr.Wrapf(err, "Could not create %s", verdaccioConfigFilePath) |
| } |
| defer f.Close() |
| if err := cfgTmpl.Execute(f, map[string]string{ |
| "Path": workDir, |
| "ProjectName": projectName, |
| }); err != nil { |
| return "", skerr.Wrapf(err, "Could not execute template for %s", projectName) |
| } |
| |
| return verdaccioConfigFilePath, nil |
| } |
| |
| // GetProjectName implements the types.ProjectMirror interface. |
| func (m *VerdaccioMirror) GetProjectName() string { |
| return m.projectName |
| } |
| |
| // StartMirror implements the types.ProjectMirror interface. |
| func (m *VerdaccioMirror) StartMirror(ctx context.Context, port int) error { |
| go func() { |
| // Create the verdaccio log before starting verdaccio. |
| verdaccioLogPath := path.Join(m.verdaccioDir, verdaccioLogFileName) |
| verdaccioLogFile, err := os.Create(verdaccioLogPath) |
| if err != nil { |
| sklog.Fatalf("Could not create %s: %s", verdaccioLogFile, err) |
| } |
| defer verdaccioLogFile.Close() |
| // Start verdaccio. |
| m.startVerdaccioMirror(ctx, port, verdaccioLogFile) |
| }() |
| |
| return nil |
| } |
| |
| // startVerdaccioMirror brings up a running verdaccio mirror locally. |
| func (m *VerdaccioMirror) startVerdaccioMirror(ctx context.Context, port int, logFile *os.File) { |
| verdaccioCmd := executil.CommandContext(ctx, "verdaccio", "--config="+m.verdaccioConfigPath, fmt.Sprintf("--listen=%d", port)) |
| verdaccioCmd.Dir = m.workDir |
| verdaccioCmd.Stdout = logFile |
| verdaccioCmd.Env = os.Environ() |
| verdaccioCmd.Env = append(verdaccioCmd.Env, |
| fmt.Sprintf("VERDACCIO_PUBLIC_URL=%s", m.publicURL), |
| // Makes the logs more verbose and useful for debugging. |
| "NODE_DEBUG=request", |
| "DEBUG=express:*") |
| sklog.Info(verdaccioCmd.String()) |
| if err := verdaccioCmd.Run(); err != nil { |
| sklog.Fatalf("Could not start verdaccio in %s: %s", m.workDir, err) |
| } |
| } |
| |
| // AddToDownloadedPackageTarballs implements the types.ProjectMirror interface. |
| func (m *VerdaccioMirror) AddToDownloadedPackageTarballs(packageTarballName string) { |
| m.downloadedPackageTarballsMtx.Lock() |
| defer m.downloadedPackageTarballsMtx.Unlock() |
| m.downloadedPackageTarballs[packageTarballName] = true |
| } |
| |
| // IsPackageTarballDownloaded implements the types.ProjectMirror interface. |
| func (m *VerdaccioMirror) IsPackageTarballDownloaded(packageTarballName string) bool { |
| m.downloadedPackageTarballsMtx.RLock() |
| defer m.downloadedPackageTarballsMtx.RUnlock() |
| _, downloaded := m.downloadedPackageTarballs[packageTarballName] |
| return downloaded |
| } |
| |
| // GetDownloadedPackageNames implements the types.ProjectMirror interface. |
| func (m *VerdaccioMirror) GetDownloadedPackageNames() ([]string, error) { |
| downloadedPackageNames := []string{} |
| // Examine the local cache directory. |
| if _, err := os.Stat(m.verdaccioStorageDir); !os.IsNotExist(err) { |
| err = filepath.Walk(m.verdaccioStorageDir, func(path string, f os.FileInfo, err error) error { |
| if !f.IsDir() && filepath.Ext(path) == ".tgz" { |
| // Find the package name with the scope (if any). Do this by trimming the storage dir |
| // prefix and the tarball suffix. |
| // Eg: /tmp/skia-infra/verdaccio/storage/@typescript-eslint/types/types-4.22.0.tgz |
| // will return "/@typescript-eslint/types/". |
| packageName := strings.TrimSuffix(strings.TrimPrefix(path, m.verdaccioStorageDir), f.Name()) |
| // Remove the surrounding "/"s. |
| packageName = strings.Trim(packageName, "/") |
| downloadedPackageNames = append(downloadedPackageNames, packageName) |
| } |
| return nil |
| }) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Could not look for packages in %s", m.verdaccioStorageDir) |
| } |
| } |
| return downloadedPackageNames, nil |
| } |
| |
| // GetTarballsInMirrorStorage returns a map of the packages (including their versions) |
| // that are available locally on the mirror. These are the packages that the mirror |
| // does not have to hit the public NPM registry for. |
| func GetTarballsInMirrorStorage(verdaccioStorageDir string) (map[string]interface{}, error) { |
| installedPackages := map[string]interface{}{} |
| if _, err := os.Stat(verdaccioStorageDir); !os.IsNotExist(err) { |
| err = filepath.Walk(verdaccioStorageDir, func(path string, f os.FileInfo, err error) error { |
| if !f.IsDir() && filepath.Ext(path) == ".tgz" { |
| installedPackages[f.Name()] = struct{}{} |
| } |
| return nil |
| }) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Could not look for packages in %s", verdaccioStorageDir) |
| } |
| } |
| return installedPackages, nil |
| } |