Path: blob/main/components/content-service/pkg/initializer/download.go
2499 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package initializer56import (7"context"8"io"9"net/http"10"os"11"path/filepath"12"time"1314"github.com/gitpod-io/gitpod/common-go/log"15"github.com/gitpod-io/gitpod/common-go/tracing"16csapi "github.com/gitpod-io/gitpod/content-service/api"17"github.com/gitpod-io/gitpod/content-service/pkg/archive"18"github.com/opencontainers/go-digest"19"github.com/opentracing/opentracing-go"20"golang.org/x/sync/errgroup"21"golang.org/x/xerrors"22)2324type fileInfo struct {25URL string2627// Path is relative to the FileDownloadInitializer's TargetLocation, e.g. if TargetLocation is in `/workspace/myrepo`28// a Path of `foobar/file` would produce a file in `/workspace/myrepo/foobar/file`.29// Path must include the filename. The FileDownloadInitializer will create any parent directories30// necessary to place the file.31Path string3233// Digest is a hash of the file content in the OCI Digest format (see https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests).34// This information is used to compute subsequent35// content versions, and to validate the file content was downloaded correctly.36Digest digest.Digest37}3839type fileDownloadInitializer struct {40FilesInfos []fileInfo41TargetLocation string42HTTPClient *http.Client43RetryTimeout time.Duration44}4546// Run initializes the workspace47func (ws *fileDownloadInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (src csapi.WorkspaceInitSource, metrics csapi.InitializerMetrics, err error) {48span, ctx := opentracing.StartSpanFromContext(ctx, "FileDownloadInitializer.Run")49defer tracing.FinishSpan(span, &err)50start := time.Now()51initialSize, fsErr := getFsUsage()52if fsErr != nil {53log.WithError(fsErr).Error("could not get disk usage")54}5556for _, info := range ws.FilesInfos {57err := ws.downloadFile(ctx, info)58if err != nil {59tracing.LogError(span, xerrors.Errorf("cannot download file '%s' from '%s': %w", info.Path, info.URL, err))60return src, nil, err61}62}6364if fsErr == nil {65currentSize, fsErr := getFsUsage()66if fsErr != nil {67log.WithError(fsErr).Error("could not get disk usage")68}6970metrics = csapi.InitializerMetrics{csapi.InitializerMetric{71Type: "fileDownload",72Duration: time.Since(start),73Size: currentSize - initialSize,74}}75}7677src = csapi.WorkspaceInitFromOther78return79}8081func (ws *fileDownloadInitializer) downloadFile(ctx context.Context, info fileInfo) (err error) {82//nolint:ineffassign83span, ctx := opentracing.StartSpanFromContext(ctx, "downloadFile")84defer tracing.FinishSpan(span, &err)85span.LogKV("url", info.URL)8687fn := filepath.Join(ws.TargetLocation, info.Path)88err = os.MkdirAll(filepath.Dir(fn), 0755)89if err != nil {90tracing.LogError(span, xerrors.Errorf("cannot mkdir %s: %w", filepath.Dir(fn), err))91}9293fd, err := os.OpenFile(fn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)94if err != nil {95return err96}9798dl := func() (err error) {99req, err := http.NewRequestWithContext(ctx, "GET", info.URL, nil)100if err != nil {101return err102}103_ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))104105resp, err := ws.HTTPClient.Do(req)106if err != nil {107return err108}109defer resp.Body.Close()110if resp.StatusCode != http.StatusOK {111return xerrors.Errorf("non-OK download response: %s", resp.Status)112}113114pr, pw := io.Pipe()115body := io.TeeReader(resp.Body, pw)116117eg, _ := errgroup.WithContext(ctx)118eg.Go(func() error {119_, err = io.Copy(fd, body)120pw.Close()121return err122})123eg.Go(func() error {124dgst, err := digest.FromReader(pr)125if err != nil {126return err127}128if dgst != info.Digest {129return xerrors.Errorf("digest mismatch")130}131return nil132})133134return eg.Wait()135}136for i := 0; i < otsDownloadAttempts; i++ {137span.LogKV("attempt", i)138if i > 0 {139time.Sleep(ws.RetryTimeout)140}141142err = dl()143if err == context.Canceled || err == context.DeadlineExceeded {144return145}146if err == nil {147break148}149log.WithError(err).WithField("attempt", i).Warn("cannot download additional content files")150}151if err != nil {152return err153}154155return nil156}157158159