blob: bde9238c4df29be85a38de633b37a91125205eaa [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lib
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"runtime/pprof"
"strings"
"sync"
"github.com/maruel/subcommands"
"go.chromium.org/luci/client/downloader"
"go.chromium.org/luci/common/data/caching/cache"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/isolated"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/system/signals"
)
// CmdDownload returns an object for the `download` subcommand.
func CmdDownload(options CommandOptions) *subcommands.Command {
return &subcommands.Command{
UsageLine: "download <options>...",
ShortDesc: "downloads a file or a .isolated tree from an isolate server.",
LongDesc: `Downloads one or multiple files, or a isolated tree from the isolate server.
Files are referenced by their hash`,
CommandRun: func() subcommands.CommandRun {
c := downloadRun{
CommandOptions: options,
}
c.commonFlags.Init(options.DefaultAuthOpts)
c.cachePolicies.AddFlags(&c.Flags)
// TODO(mknyszek): Add support for downloading individual files.
c.Flags.StringVar(&c.outputDir, "output-dir", ".", "The directory where files will be downloaded to.")
c.Flags.StringVar(&c.outputFiles, "output-files", "", "File into which the full list of downloaded files is written to.")
c.Flags.StringVar(&c.isolated, "isolated", "", "Hash of a .isolated tree to download.")
c.Flags.StringVar(&c.cacheDir, "cache-dir", "", "Cache directory to store downloaded files.")
c.Flags.StringVar(&c.resultJSON, "fetch-and-map-result-json", "", "This is created only for crbug.com/932396, do not use other than from run_isolated.py.")
return &c
},
}
}
type downloadRun struct {
commonFlags
CommandOptions
outputDir string
outputFiles string
isolated string
resultJSON string
cacheDir string
cachePolicies cache.Policies
}
func (c *downloadRun) Parse(a subcommands.Application, args []string) error {
if err := c.commonFlags.Parse(); err != nil {
return err
}
if len(args) != 0 {
return errors.New("position arguments not expected")
}
if c.isolated == "" {
return errors.New("isolated is required")
}
if c.cacheDir == "" && !c.cachePolicies.IsDefault() {
return errors.New("cache-dir is necessary when cache-max-size, cache-max-items or cache-min-free-space are specified")
}
return nil
}
type results struct {
ItemsCold []byte `json:"items_cold"`
ItemsHot []byte `json:"items_hot"`
InitialNumberItems int64 `json:"initial_number_items"`
InitialSize int64 `json:"initial_size"`
Isolated *isolated.Isolated `json:"isolated"`
}
func (c *downloadRun) outputResults(cache *cache.Cache, initStats initCacheStats, dl *downloader.Downloader) error {
if c.resultJSON == "" {
return nil
}
itemsCold, itemsHot, err := downloader.GetCacheStats(cache)
if err != nil {
return errors.Annotate(err, "failed to call GetCacheStats").Err()
}
root, err := dl.RootIsolated()
if err != nil {
return errors.Annotate(err, "failed to get root isolated").Err()
}
resultJSON, err := json.Marshal(results{
ItemsCold: itemsCold,
ItemsHot: itemsHot,
InitialNumberItems: initStats.numItems,
InitialSize: initStats.totalSize,
Isolated: root,
})
if err != nil {
return errors.Annotate(err, "failed to marshal result json").Err()
}
if err := ioutil.WriteFile(c.resultJSON, resultJSON, 0664); err != nil {
return errors.Annotate(err, "failed to write result json to %s", c.resultJSON).Err()
}
return nil
}
func (c *downloadRun) main(a subcommands.Application, args []string) error {
// Prepare isolated client.
ctx, cancel := context.WithCancel(c.defaultFlags.MakeLoggingContext(os.Stderr))
signals.HandleInterrupt(func() {
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
cancel()
})
if err := c.runMain(ctx, a, args); err != nil {
errors.Log(ctx, err)
return err
}
return nil
}
type initCacheStats struct {
numItems int64
totalSize int64
}
func (c *downloadRun) runMain(ctx context.Context, a subcommands.Application, args []string) error {
client, err := c.createIsolatedClient(ctx, c.CommandOptions)
if err != nil {
return errors.Annotate(err, "failed to create isolated client").Err()
}
var filesMu sync.Mutex
var files []string
var diskCache *cache.Cache
var initStats initCacheStats
if c.cacheDir != "" {
if err := os.MkdirAll(c.cacheDir, os.ModePerm); err != nil {
return errors.Annotate(err, "failed to create cache dir: %s", c.cacheDir).Err()
}
diskCache, err = cache.New(c.cachePolicies, c.cacheDir, isolated.GetHash(c.isolatedFlags.Namespace))
if err != nil && diskCache == nil {
return errors.Annotate(err, "failed to initialize disk cache in %s", c.cacheDir).Err()
}
if err != nil {
logging.WithError(err).Warningf(ctx, "There is (ignorable?) error when initializing disk cache in %s", c.cacheDir)
}
defer diskCache.Close()
initStats.numItems = int64(len(diskCache.Keys()))
initStats.totalSize = int64(diskCache.TotalSize())
}
if err := os.MkdirAll(c.outputDir, os.ModePerm); err != nil {
return errors.Annotate(err, "failed to create output dir: %s", c.outputDir).Err()
}
dl := downloader.New(ctx, client, isolated.HexDigest(c.isolated), c.outputDir, &downloader.Options{
FileCallback: func(name string, _ *isolated.File) {
filesMu.Lock()
files = append(files, name)
filesMu.Unlock()
},
Cache: diskCache,
})
if err := dl.Wait(); err != nil {
return errors.Annotate(err, "failed to call FetchIsolated()").Err()
}
if c.outputFiles != "" {
filesData := strings.Join(files, "\n")
if len(files) > 0 {
filesData += "\n"
}
if err := ioutil.WriteFile(c.outputFiles, []byte(filesData), 0664); err != nil {
return errors.Annotate(err, "failed to call WriteFile(%s, ...)", c.outputFiles).Err()
}
}
return c.outputResults(diskCache, initStats, dl)
}
func (c *downloadRun) Run(a subcommands.Application, args []string, _ subcommands.Env) int {
if err := c.Parse(a, args); err != nil {
fmt.Fprintf(a.GetErr(), "%s: failed to call Parse(%s): %v\n", a.GetName(), args, err)
return 1
}
defer c.profilerFlags.Stop()
if err := c.main(a, args); err != nil {
fmt.Fprintf(a.GetErr(), "%s: failed to call main(%s): %v\n", a.GetName(), args, err)
return 1
}
return 0
}