| // 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()) |
| } |