在我们实际工作中,有时候下载的的东西过大,导致下载失败!重新下载又需要重新开始下载!
故,实现一个分片下载及继续在上次下载的方法,附带下载进度;话不多说上菜:
注意:总大小的时候不要加Range: bytes=start-end
func BreakPointDownload(finalPath, downUrl string, count *ReaderCount) error {
var (
target *os.File
err error
start int = 0
end int
fileTotalSize int
)
tmpFile := fmt.Sprintf("%s.tmp", finalPath)
if runtime.GOOS == "windows" {
tmpFile = strings.ReplaceAll(tmpFile, "/", "\\")
}
os.MkdirAll(filepath.Dir(tmpFile), os.ModePerm)
BeginDown:
if FileIsExisted(tmpFile) {
// 如果存在,继续下载
target, err = os.OpenFile(tmpFile, os.O_WRONLY|os.O_APPEND, 0666)
} else {
// 创建临时文件
target, err = os.Create(tmpFile)
}
if err != nil {
return err
}
down := func(start, end int, first bool) (int, error) {
// fmt.Println("下载", start, end)
req, err := http.NewRequest(http.MethodGet, downUrl, nil)
if err != nil {
return 0, err
}
if !first {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
target.Close()
return 0, err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return 0, errors.New("下载失败:http status" + resp.Status)
}
defer resp.Body.Close()
fileSize := cast.ToInt(resp.Header.Get("Content-Length"))
if first {
return fileSize, nil
}
//var total uint64
//total = cast.ToUint64(start)
//count.SetFileSize(fileSize)
//count.setTotal(&total)
count.setReader(resp.Body)
// 将下载的文件流拷贝到临时文件
if _, err = io.Copy(target, count); err != nil {
target.Close()
return 0, err
}
return fileSize, nil
}
fileTotalSize, err = down(0, 1, true)
if err != nil {
return err
}
// 一次下载1000个字节
length := 10000
times := fileTotalSize/1000 + 1
stat, err := target.Stat()
if err != nil {
target.Close()
return err
}
// 查看文件的创建时间,如果创建时间超过5小时,删除重新下载
lastModifyTime := stat.ModTime()
if time.Now().Sub(lastModifyTime) > 2*time.Hour {
// 如果修改时间超过两小时,删除重新下载
target.Close()
os.Remove(tmpFile)
goto BeginDown
}
start = cast.ToInt(stat.Size())
// 获取进度-设置参数
var total uint64
total = cast.ToUint64(start)
count.SetFileSize(fileTotalSize)
count.setTotal(&total)
for t := 0; t < times; t++ {
begin := t * length
end = begin + length - 1
if end > fileTotalSize {
end = fileTotalSize
}
if end < start {
continue
}
if begin < start && end >= start {
begin = start
}
if begin >= end {
continue
}
_, err = down(begin, end, false)
if err != nil {
return err
}
}
// 查看最后文件大小
info, err := target.Stat()
if err != nil {
target.Close()
return err
}
// 关闭临时并修改临时文件为最终文件
// 如果关闭放在rename后,windows汇报the process cannot access the file
target.Close()
if info.Size() == cast.ToInt64(fileTotalSize) {
err = os.Rename(tmpFile, finalPath)
}
return err
}
type ReaderCount struct {
FileSize int
Total *uint64
reader io.ReadCloser
over bool
err error
unzipOver bool
}
func (r *ReaderCount) Read(p []byte) (n int, err error) {
read, err := r.reader.Read(p)
if err == io.EOF {
r.over = true
}
atomic.AddUint64(r.Total, cast.ToUint64(read))
return read, err
}
func (r *ReaderCount) Close() error {
return r.reader.Close()
}
func (r *ReaderCount) SetFileSize(filesize int) {
r.FileSize = filesize
}
func (r *ReaderCount) setReader(reader io.ReadCloser) {
r.reader = reader
}
func (r *ReaderCount) setTotal(total *uint64) {
r.Total = total
}
func (r *ReaderCount) GetRate() string {
if r.FileSize == 0 || r.Total == nil {
return "0.0"
}
return fmt.Sprintf("%0.2f%%", cast.ToFloat64(r.Total)/cast.ToFloat64(r.FileSize)*100)
}
func (r *ReaderCount) IsOver() bool {
return r.over
}
func (r *ReaderCount) SetErr(err error) {
r.err = err
}
func (r *ReaderCount) GetErr() error {
return r.err
}
func (r *ReaderCount) SetUnzipOver(b bool) {
r.unzipOver = b
}
func (r *ReaderCount) GetUnzipOver() bool {
return r.unzipOver
}
func NewReader() *ReaderCount {
return &ReaderCount{}
}
测试用例:
func Test_BreakPointDownload(t *testing.T) {
downUrl := `http://xxxx.zip`
reader := utils.NewReader()
go func() {
timer := time.NewTicker(1 * time.Second)
for {
select {
case <-timer.C:
fmt.Println(reader.GetRate())
}
}
}()
dest := "./data/cms-linux-v2.1.59.zip"
fmt.Println(utils.BreakPointDownload(dest, downUrl, reader))
}