233 lines
7.3 KiB
Go
233 lines
7.3 KiB
Go
/*
|
|
* Copyright 2020 The Dragonfly 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 cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"d7y.io/dragonfly/v2/client/config"
|
|
"d7y.io/dragonfly/v2/client/dfget"
|
|
"d7y.io/dragonfly/v2/cmd/dependency"
|
|
"d7y.io/dragonfly/v2/internal/constants"
|
|
logger "d7y.io/dragonfly/v2/internal/dflog"
|
|
"d7y.io/dragonfly/v2/internal/dflog/logcore"
|
|
"d7y.io/dragonfly/v2/internal/dfpath"
|
|
"d7y.io/dragonfly/v2/pkg/basic"
|
|
"d7y.io/dragonfly/v2/pkg/basic/dfnet"
|
|
"d7y.io/dragonfly/v2/pkg/rpc/dfdaemon/client"
|
|
"d7y.io/dragonfly/v2/pkg/unit"
|
|
"d7y.io/dragonfly/v2/pkg/util/net/iputils"
|
|
"d7y.io/dragonfly/v2/version"
|
|
"github.com/gofrs/flock"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var (
|
|
dfgetConfig *config.DfgetConfig
|
|
)
|
|
|
|
var dfgetDescription = `dfget is the client of dragonfly which takes a role of peer in a P2P network.
|
|
When user triggers a file downloading task, dfget will download the pieces of
|
|
file from other peers. Meanwhile, it will act as an uploader to support other
|
|
peers to download pieces from it if it owns them. In addition, dfget has the
|
|
abilities to provide more advanced functionality, such as network bandwidth
|
|
limit, transmission encryption and so on.`
|
|
|
|
// rootCmd represents the base command when called without any subcommands
|
|
var rootCmd = &cobra.Command{
|
|
Use: "dfget url -O path",
|
|
Short: "the P2P client of dragonfly",
|
|
Long: dfgetDescription,
|
|
Args: cobra.MaximumNArgs(1),
|
|
DisableAutoGenTag: true,
|
|
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
start := time.Now()
|
|
|
|
if err := logcore.InitDfget(dfgetConfig.Console); err != nil {
|
|
return errors.Wrap(err, "init client dfget logger")
|
|
}
|
|
|
|
// Convert config
|
|
if err := dfgetConfig.Convert(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate config
|
|
if err := dfgetConfig.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("--%s-- %s\n", start.Format("2006-01-02 15:04:05"), dfgetConfig.URL)
|
|
fmt.Printf("current user[%s] output path[%s]\n", basic.Username, dfgetConfig.Output)
|
|
fmt.Printf("dfget version[%s] default peer ip[%s]\n", version.GitVersion, iputils.HostIP)
|
|
|
|
// do get file
|
|
err := runDfget()
|
|
|
|
msg := fmt.Sprintf("download success:%t cost:%dms error:[%v]", err == nil, time.Now().Sub(start).Milliseconds(), err)
|
|
logger.With("url", dfgetConfig.URL).Info(msg)
|
|
fmt.Println(msg)
|
|
|
|
return errors.Wrapf(err, "download url[%s]", dfgetConfig.URL)
|
|
},
|
|
}
|
|
|
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
|
func Execute() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
logger.Error(err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
// Initialize default dfget config
|
|
dfgetConfig = config.NewDfgetConfig()
|
|
// Initialize cobra
|
|
dependency.InitCobra(rootCmd, false, dfgetConfig)
|
|
|
|
// Add flags
|
|
flagSet := rootCmd.Flags()
|
|
|
|
flagSet.StringP("url", "u", dfgetConfig.URL,
|
|
"Download one file from the url, equivalent to the command's first position argument")
|
|
|
|
flagSet.StringP("output", "O", dfgetConfig.Output,
|
|
"Destination path which is used to store the downloaded file, it must be a full path")
|
|
|
|
flagSet.Duration("timeout", dfgetConfig.Timeout, "Timeout for the downloading task, 0 is infinite")
|
|
|
|
flagSet.String("limit", unit.Bytes(dfgetConfig.RateLimit).String(),
|
|
"The downloading network bandwidth limit per second in format of G(B)/g/M(B)/m/K(B)/k/B, pure number will be parsed as Byte, 0 is infinite")
|
|
|
|
flagSet.String("digest", dfgetConfig.Digest,
|
|
"Check the integrity of the downloaded file with digest, in format of md5:xxx or sha256:yyy")
|
|
|
|
flagSet.String("tag", dfgetConfig.Tag,
|
|
"Different tags for the same url will be divided into different P2P overlay, it conflicts with --digest")
|
|
|
|
flagSet.String("filter", dfgetConfig.Filter,
|
|
"Filter the query parameters of the url, P2P overlay is the same one if the filtered url is same, "+
|
|
"in format of key&sign, which will filter 'key' and 'sign' query parameters")
|
|
|
|
flagSet.StringArrayP("header", "H", dfgetConfig.Header, "url header, eg: --header='Accept: *' --header='Host: abc'")
|
|
|
|
flagSet.Bool("disable-back-source", dfgetConfig.DisableBackSource,
|
|
"Disable downloading directly from source when the daemon fails to download file")
|
|
|
|
flagSet.StringP("pattern", "p", dfgetConfig.Pattern, "The downloading pattern: p2p/cdn/source")
|
|
|
|
flagSet.BoolP("show-progress", "b", dfgetConfig.ShowProgress, "Show progress bar, it conflicts with --console")
|
|
|
|
flagSet.String("callsystem", dfgetConfig.CallSystem, "The caller name which is mainly used for statistics and access control")
|
|
|
|
// Bind cmd flags
|
|
if err := viper.BindPFlags(flagSet); err != nil {
|
|
panic(errors.Wrap(err, "bind dfget flags to viper"))
|
|
}
|
|
}
|
|
|
|
// runDfget does some init operations and starts to download.
|
|
func runDfget() error {
|
|
// Dfget config values
|
|
s, _ := yaml.Marshal(dfgetConfig)
|
|
logger.Infof("client dfget configuration:\n%s", string(s))
|
|
|
|
ff := dependency.InitMonitor(dfgetConfig.Verbose, dfgetConfig.PProfPort, dfgetConfig.Telemetry.Jaeger)
|
|
defer ff()
|
|
|
|
var (
|
|
daemonClient client.DaemonClient
|
|
err error
|
|
)
|
|
|
|
if dfgetConfig.Pattern != constants.SourcePattern {
|
|
logger.Info("start to check and spawn daemon")
|
|
if daemonClient, err = checkAndSpawnDaemon(); err != nil {
|
|
logger.Errorf("check and spawn daemon error:%v", err)
|
|
} else {
|
|
logger.Info("check and spawn daemon success")
|
|
}
|
|
}
|
|
|
|
return dfget.Download(dfgetConfig, daemonClient)
|
|
}
|
|
|
|
// checkAndSpawnDaemon do checking at three checkpoints
|
|
func checkAndSpawnDaemon() (client.DaemonClient, error) {
|
|
target := dfnet.NetAddr{Type: dfnet.UNIX, Addr: dfpath.DaemonSockPath}
|
|
daemonClient, err := client.GetClientByAddr([]dfnet.NetAddr{target})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.Check without lock
|
|
if daemonClient.CheckHealth(context.Background(), target) == nil {
|
|
return daemonClient, nil
|
|
}
|
|
|
|
lock := flock.New(dfpath.DfgetLockPath)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
// 2.Check with lock
|
|
if daemonClient.CheckHealth(context.Background(), target) == nil {
|
|
return daemonClient, nil
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "daemon", "--launcher", strconv.Itoa(os.Getpid()))
|
|
cmd.Stdin = nil
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
|
|
logger.Info("do start daemon")
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 3. check health with at least 5s timeout
|
|
tick := time.NewTicker(50 * time.Millisecond)
|
|
defer tick.Stop()
|
|
timeout := time.After(5 * time.Second)
|
|
|
|
for {
|
|
select {
|
|
case <-timeout:
|
|
return nil, errors.New("the daemon is unhealthy")
|
|
case <-tick.C:
|
|
if err = daemonClient.CheckHealth(context.Background(), target); err != nil {
|
|
continue
|
|
}
|
|
return daemonClient, nil
|
|
}
|
|
}
|
|
}
|