blob: 6325f6044b8e57bb0189df06c2945749523f222d [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fetch
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/mod/modfile"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/source"
)
// Version and commit time are pre specified when fetching a local module, as these
// fields are normally obtained from a proxy.
var (
LocalVersion = "latest"
LocalCommitTime = time.Time{}
)
// FetchLocalModule fetches a module from a local directory and process its contents
// to return an internal.Module and other related information. modulePath is not necessary
// if the module has a go.mod file, but if both exist, then they must match.
// FetchResult.Error should be checked to verify that the fetch succeeded. Even if the
// error is non-nil the result may contain useful data.
func FetchLocalModule(ctx context.Context, modulePath, localPath string, sourceClient *source.Client) *FetchResult {
fr := &FetchResult{
ModulePath: modulePath,
RequestedVersion: LocalVersion,
ResolvedVersion: LocalVersion,
Defer: func() {},
}
var fi *FetchInfo
defer func() {
if fr.Error != nil {
derrors.Wrap(&fr.Error, "FetchLocalModule(%q, %q)", modulePath, localPath)
fr.Status = derrors.ToStatus(fr.Error)
}
if fr.Status == 0 {
fr.Status = http.StatusOK
}
if fi != nil {
finishFetchInfo(fi, fr.Status, fr.Error)
}
log.Debugf(ctx, "memory after fetch of %s: %dM", fr.ModulePath, allocMeg())
}()
info, err := os.Stat(localPath)
if err != nil {
fr.Error = fmt.Errorf("%s: %w", err.Error(), derrors.NotFound)
return fr
}
if !info.IsDir() {
fr.Error = fmt.Errorf("%s not a directory: %w", localPath, derrors.NotFound)
return fr
}
fi = &FetchInfo{
ModulePath: fr.ModulePath,
Version: fr.ResolvedVersion,
Start: time.Now(),
}
startFetchInfo(fi)
// Options for module path are either the modulePath parameter or go.mod file.
// Accepted cases:
// - Both are given and are the same.
// - Only one is given. Note that: if modulePath is given and there's no go.mod
// file, then the package is assumed to be using GOPATH.
// Errors:
// - Both are given and are different.
// - Neither is given.
if goModBytes, err := ioutil.ReadFile(filepath.Join(localPath, "go.mod")); err != nil {
fr.GoModPath = modulePath
} else {
fr.GoModPath = modfile.ModulePath(goModBytes)
if fr.GoModPath != modulePath && modulePath != "" {
fr.Error = fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, fr.GoModPath, derrors.AlternativeModule)
return fr
}
}
if fr.GoModPath == "" {
fr.Error = fmt.Errorf("no module path: %w", derrors.BadModule)
return fr
}
fr.ModulePath = fr.GoModPath
zipReader, err := createZipReader(localPath, fr.GoModPath, LocalVersion)
if err != nil {
fr.Error = fmt.Errorf("couldn't create a zip: %s, %w", err.Error(), derrors.BadModule)
return fr
}
mod, pvs, err := processZipFile(ctx, fr.GoModPath, LocalVersion, LocalCommitTime, zipReader, sourceClient)
if err != nil {
fr.Error = err
return fr
}
fr.Module = mod
fr.PackageVersionStates = pvs
fr.Module.SourceInfo = nil // version is not known, so even if info is found it most likely is wrong.
for _, state := range fr.PackageVersionStates {
if state.Status != http.StatusOK {
fr.Status = derrors.ToStatus(derrors.HasIncompletePackages)
}
}
return fr
}
// createZipReader creates a zip file from a directory given a local path and
// returns a zip.Reader to be passed to processZipFile. The purpose of the
// function is to transform a local go module into a zip file to be processed by
// existing functions.
func createZipReader(localPath, modulePath, version string) (*zip.Reader, error) {
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
err := filepath.Walk(localPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
readFrom, err := os.Open(path)
if err != nil {
return err
}
defer readFrom.Close()
writeTo, err := w.Create(filepath.Join(moduleVersionDir(modulePath, version), strings.TrimPrefix(path, localPath)))
if err != nil {
return err
}
_, err = io.Copy(writeTo, readFrom)
return err
})
if err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
reader := bytes.NewReader(buf.Bytes())
return zip.NewReader(reader, reader.Size())
}