339 lines
8.9 KiB
Go
339 lines
8.9 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 dfget
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-http-utils/headers"
|
|
"github.com/pkg/errors"
|
|
"github.com/schollz/progressbar/v3"
|
|
|
|
"d7y.io/dragonfly/v2/client/config"
|
|
logger "d7y.io/dragonfly/v2/internal/dflog"
|
|
"d7y.io/dragonfly/v2/pkg/basic"
|
|
"d7y.io/dragonfly/v2/pkg/rpc/base"
|
|
"d7y.io/dragonfly/v2/pkg/rpc/dfdaemon"
|
|
daemonclient "d7y.io/dragonfly/v2/pkg/rpc/dfdaemon/client"
|
|
"d7y.io/dragonfly/v2/pkg/source"
|
|
"d7y.io/dragonfly/v2/pkg/util/digestutils"
|
|
"d7y.io/dragonfly/v2/pkg/util/stringutils"
|
|
)
|
|
|
|
func Download(cfg *config.DfgetConfig, client daemonclient.DaemonClient) error {
|
|
var (
|
|
ctx = context.Background()
|
|
cancel context.CancelFunc
|
|
wLog = logger.With("url", cfg.URL)
|
|
downError error
|
|
)
|
|
|
|
wLog.Info("init success and start to download")
|
|
fmt.Println("init success and start to download")
|
|
|
|
if cfg.Timeout > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
|
|
} else {
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
}
|
|
|
|
go func() {
|
|
downError = download(ctx, client, cfg, wLog)
|
|
cancel()
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return errors.Errorf("download timeout(%s)", cfg.Timeout)
|
|
}
|
|
return downError
|
|
}
|
|
|
|
func download(ctx context.Context, client daemonclient.DaemonClient, cfg *config.DfgetConfig, wLog *logger.SugaredLoggerOnWith) error {
|
|
if cfg.Recursive {
|
|
return recursiveDownload(ctx, client, cfg)
|
|
}
|
|
return singleDownload(ctx, client, cfg, wLog)
|
|
}
|
|
|
|
func singleDownload(ctx context.Context, client daemonclient.DaemonClient, cfg *config.DfgetConfig, wLog *logger.SugaredLoggerOnWith) error {
|
|
hdr := parseHeader(cfg.Header)
|
|
|
|
if client == nil {
|
|
return downloadFromSource(ctx, cfg, hdr)
|
|
}
|
|
|
|
var (
|
|
start = time.Now()
|
|
stream *daemonclient.DownResultStream
|
|
result *dfdaemon.DownResult
|
|
pb *progressbar.ProgressBar
|
|
request = newDownRequest(cfg, hdr)
|
|
downError error
|
|
)
|
|
|
|
if stream, downError = client.Download(ctx, request); downError == nil {
|
|
if cfg.ShowProgress {
|
|
pb = newProgressBar(-1)
|
|
}
|
|
|
|
for {
|
|
if result, downError = stream.Recv(); downError != nil {
|
|
break
|
|
}
|
|
|
|
if result.CompletedLength > 0 && pb != nil {
|
|
_ = pb.Set64(int64(result.CompletedLength))
|
|
}
|
|
|
|
// success
|
|
if result.Done {
|
|
if pb != nil {
|
|
pb.Describe("Downloaded")
|
|
_ = pb.Close()
|
|
}
|
|
|
|
wLog.Infof("download from daemon success, length: %d bytes cost: %d ms", result.CompletedLength, time.Now().Sub(start).Milliseconds())
|
|
fmt.Printf("finish total length %d bytes\n", result.CompletedLength)
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if downError != nil && !cfg.KeepOriginalOffset {
|
|
wLog.Warnf("daemon downloads file error: %v", downError)
|
|
fmt.Printf("daemon downloads file error: %v\n", downError)
|
|
downError = downloadFromSource(ctx, cfg, hdr)
|
|
}
|
|
|
|
return downError
|
|
}
|
|
|
|
func downloadFromSource(ctx context.Context, cfg *config.DfgetConfig, hdr map[string]string) error {
|
|
if cfg.DisableBackSource {
|
|
return errors.New("try to download from source but back source is disabled")
|
|
}
|
|
|
|
var (
|
|
wLog = logger.With("url", cfg.URL)
|
|
start = time.Now()
|
|
target *os.File
|
|
response *source.Response
|
|
err error
|
|
written int64
|
|
)
|
|
|
|
wLog.Info("try to download from source and ignore rate limit")
|
|
fmt.Println("try to download from source and ignore rate limit")
|
|
|
|
if target, err = os.CreateTemp(filepath.Dir(cfg.Output), ".df_"); err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(target.Name())
|
|
defer target.Close()
|
|
|
|
downloadRequest, err := source.NewRequestWithContext(ctx, cfg.URL, hdr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if response, err = source.Download(downloadRequest); err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if written, err = io.Copy(target, response.Body); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !stringutils.IsBlank(cfg.Digest) {
|
|
parsedHash := digestutils.Parse(cfg.Digest)
|
|
realHash := digestutils.HashFile(target.Name(), digestutils.Algorithms[parsedHash[0]])
|
|
|
|
if realHash != "" && realHash != parsedHash[1] {
|
|
return errors.Errorf("%s digest is not matched: real[%s] expected[%s]", parsedHash[0], realHash, parsedHash[1])
|
|
}
|
|
}
|
|
|
|
// change file owner
|
|
if err = os.Chown(target.Name(), basic.UserID, basic.UserGroup); err != nil {
|
|
return errors.Wrapf(err, "change file owner to uid[%d] gid[%d]", basic.UserID, basic.UserGroup)
|
|
}
|
|
|
|
if err = os.Rename(target.Name(), cfg.Output); err != nil {
|
|
return err
|
|
}
|
|
|
|
wLog.Infof("download from source success, length: %d bytes cost: %d ms", written, time.Now().Sub(start).Milliseconds())
|
|
fmt.Printf("finish total length %d bytes\n", written)
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseHeader(s []string) map[string]string {
|
|
hdr := make(map[string]string)
|
|
var key, value string
|
|
for _, h := range s {
|
|
idx := strings.Index(h, ":")
|
|
if idx > 0 {
|
|
key = strings.TrimSpace(h[:idx])
|
|
value = strings.TrimSpace(h[idx+1:])
|
|
hdr[key] = value
|
|
}
|
|
}
|
|
|
|
return hdr
|
|
}
|
|
|
|
func newDownRequest(cfg *config.DfgetConfig, hdr map[string]string) *dfdaemon.DownRequest {
|
|
var rg string
|
|
if r, ok := hdr[headers.Range]; ok {
|
|
rg = strings.TrimLeft(r, "bytes=")
|
|
} else {
|
|
rg = cfg.Range
|
|
}
|
|
return &dfdaemon.DownRequest{
|
|
Url: cfg.URL,
|
|
Output: cfg.Output,
|
|
Timeout: uint64(cfg.Timeout),
|
|
Limit: float64(cfg.RateLimit),
|
|
DisableBackSource: cfg.DisableBackSource,
|
|
UrlMeta: &base.UrlMeta{
|
|
Digest: cfg.Digest,
|
|
Tag: cfg.Tag,
|
|
Range: rg,
|
|
Filter: cfg.Filter,
|
|
Header: hdr,
|
|
},
|
|
Pattern: cfg.Pattern,
|
|
Callsystem: cfg.CallSystem,
|
|
Uid: int64(basic.UserID),
|
|
Gid: int64(basic.UserGroup),
|
|
KeepOriginalOffset: cfg.KeepOriginalOffset,
|
|
}
|
|
}
|
|
|
|
func newProgressBar(max int64) *progressbar.ProgressBar {
|
|
return progressbar.NewOptions64(max,
|
|
progressbar.OptionShowBytes(true),
|
|
progressbar.OptionShowIts(),
|
|
progressbar.OptionSetPredictTime(true),
|
|
progressbar.OptionUseANSICodes(true),
|
|
progressbar.OptionEnableColorCodes(true),
|
|
progressbar.OptionFullWidth(),
|
|
progressbar.OptionSetDescription("[cyan]Downloading...[reset]"),
|
|
progressbar.OptionSetRenderBlankState(true),
|
|
progressbar.OptionSetTheme(progressbar.Theme{
|
|
Saucer: "[green]=[reset]",
|
|
SaucerHead: "[green]>[reset]",
|
|
SaucerPadding: " ",
|
|
BarStart: "[",
|
|
BarEnd: "]",
|
|
}))
|
|
}
|
|
|
|
func accept(u string, parent, sub string, level uint, accept, reject string) bool {
|
|
if !checkDirectoryLevel(parent, sub, level) {
|
|
logger.Debugf("url %s not accept by level: %d", u, level)
|
|
return false
|
|
}
|
|
if !acceptRegex(u, accept) {
|
|
logger.Debugf("url %s not accept by regex: %s", u, accept)
|
|
return false
|
|
}
|
|
if rejectRegex(u, reject) {
|
|
logger.Debugf("url %s rejected by regex: %s", u, reject)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkDirectoryLevel(parent, sub string, level uint) bool {
|
|
if level == 0 {
|
|
return true
|
|
}
|
|
dirs := strings.Split(strings.Trim(strings.TrimLeft(sub, parent), "/"), "/")
|
|
return uint(len(dirs)) <= level
|
|
}
|
|
|
|
func acceptRegex(u string, accept string) bool {
|
|
if accept == "" {
|
|
return true
|
|
}
|
|
return regexp.MustCompile(accept).Match([]byte(u))
|
|
}
|
|
|
|
func rejectRegex(u string, reject string) bool {
|
|
if reject == "" {
|
|
return false
|
|
}
|
|
return regexp.MustCompile(reject).Match([]byte(u))
|
|
}
|
|
|
|
func recursiveDownload(ctx context.Context, client daemonclient.DaemonClient, cfg *config.DfgetConfig) error {
|
|
request, err := source.NewRequestWithContext(ctx, cfg.URL, parseHeader(cfg.Header))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dirURL, err := url.Parse(cfg.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Debugf("dirURL: %s", cfg.URL)
|
|
|
|
urls, err := source.List(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, u := range urls {
|
|
// reuse dfget config
|
|
c := *cfg
|
|
// update some attributes
|
|
c.Recursive, c.URL, c.Output = false, u.String(), path.Join(cfg.Output, strings.TrimPrefix(u.Path, dirURL.Path))
|
|
if !accept(c.URL, dirURL.Path, u.Path, cfg.RecursiveLevel, cfg.RecursiveAcceptRegex, cfg.RecursiveRejectRegex) {
|
|
logger.Debugf("url %s is not accepted, skip", c.URL)
|
|
continue
|
|
}
|
|
if cfg.RecursiveList {
|
|
fmt.Printf("%s\n", u.String())
|
|
continue
|
|
}
|
|
// validate new dfget config
|
|
if err = c.Validate(); err != nil {
|
|
logger.Errorf("validate failed: %s", err)
|
|
return err
|
|
}
|
|
|
|
logger.Debugf("download %s to %s", c.URL, c.Output)
|
|
err = download(ctx, client, &c, logger.With("url", c.URL))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|