123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- package oss
- import (
- "crypto/md5"
- "encoding/base64"
- "encoding/json"
- "errors"
- "hash"
- "hash/crc64"
- "io"
- "io/ioutil"
- "os"
- "strconv"
- )
- //
- // DownloadFile 分片下载文件
- //
- // objectKey object key。
- // filePath 本地文件。objectKey下载到文件。
- // partSize 本次上传文件片的大小,字节数。比如100 * 1024为每片100KB。
- // options Object的属性限制项。详见GetObject。
- //
- // error 操作成功error为nil,非nil为错误信息。
- //
- func (bucket Bucket) DownloadFile(objectKey, filePath string, partSize int64, options ...Option) error {
- if partSize < 1 {
- return errors.New("oss: part size smaller than 1")
- }
- cpConf, err := getCpConfig(options, filePath)
- if err != nil {
- return err
- }
- uRange, err := getRangeConfig(options)
- if err != nil {
- return err
- }
- routines := getRoutines(options)
- if cpConf.IsEnable {
- return bucket.downloadFileWithCp(objectKey, filePath, partSize, options, cpConf.FilePath, routines, uRange)
- }
- return bucket.downloadFile(objectKey, filePath, partSize, options, routines, uRange)
- }
- // 获取下载范围
- func getRangeConfig(options []Option) (*unpackedRange, error) {
- rangeOpt, err := findOption(options, HTTPHeaderRange, nil)
- if err != nil || rangeOpt == nil {
- return nil, err
- }
- return parseRange(rangeOpt.(string))
- }
- // ----- 并发无断点的下载 -----
- // 工作协程参数
- type downloadWorkerArg struct {
- bucket *Bucket
- key string
- filePath string
- options []Option
- hook downloadPartHook
- enableCRC bool
- }
- // Hook用于测试
- type downloadPartHook func(part downloadPart) error
- var downloadPartHooker downloadPartHook = defaultDownloadPartHook
- func defaultDownloadPartHook(part downloadPart) error {
- return nil
- }
- // 默认ProgressListener,屏蔽GetObject的Options中ProgressListener
- type defaultDownloadProgressListener struct {
- }
- // ProgressChanged 静默处理
- func (listener *defaultDownloadProgressListener) ProgressChanged(event *ProgressEvent) {
- }
- // 工作协程
- func downloadWorker(id int, arg downloadWorkerArg, jobs <-chan downloadPart, results chan<- downloadPart, failed chan<- error, die <-chan bool) {
- for part := range jobs {
- if err := arg.hook(part); err != nil {
- failed <- err
- break
- }
- // resolve options
- r := Range(part.Start, part.End)
- p := Progress(&defaultDownloadProgressListener{})
- opts := make([]Option, len(arg.options)+2)
- // append orderly, can not be reversed!
- opts = append(opts, arg.options...)
- opts = append(opts, r, p)
- rd, err := arg.bucket.GetObject(arg.key, opts...)
- if err != nil {
- failed <- err
- break
- }
- defer rd.Close()
- var crcCalc hash.Hash64
- if arg.enableCRC {
- crcCalc = crc64.New(crcTable())
- contentLen := part.End - part.Start + 1
- rd = ioutil.NopCloser(TeeReader(rd, crcCalc, contentLen, nil, nil))
- }
- defer rd.Close()
- select {
- case <-die:
- return
- default:
- }
- fd, err := os.OpenFile(arg.filePath, os.O_WRONLY, FilePermMode)
- if err != nil {
- failed <- err
- break
- }
- _, err = fd.Seek(part.Start-part.Offset, os.SEEK_SET)
- if err != nil {
- fd.Close()
- failed <- err
- break
- }
- _, err = io.Copy(fd, rd)
- if err != nil {
- fd.Close()
- failed <- err
- break
- }
- if arg.enableCRC {
- part.CRC64 = crcCalc.Sum64()
- }
- fd.Close()
- results <- part
- }
- }
- // 调度协程
- func downloadScheduler(jobs chan downloadPart, parts []downloadPart) {
- for _, part := range parts {
- jobs <- part
- }
- close(jobs)
- }
- // 下载片
- type downloadPart struct {
- Index int // 片序号,从0开始编号
- Start int64 // 片起始位置
- End int64 // 片结束位置
- Offset int64 // 文件中的偏移位置
- CRC64 uint64 // 片的校验值
- }
- // 文件分片
- func getDownloadParts(bucket *Bucket, objectKey string, partSize int64, uRange *unpackedRange) ([]downloadPart, bool, uint64, error) {
- meta, err := bucket.GetObjectDetailedMeta(objectKey)
- if err != nil {
- return nil, false, 0, err
- }
- parts := []downloadPart{}
- objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 0)
- if err != nil {
- return nil, false, 0, err
- }
- enableCRC := false
- crcVal := (uint64)(0)
- if bucket.getConfig().IsEnableCRC && meta.Get(HTTPHeaderOssCRC64) != "" {
- if uRange == nil || (!uRange.hasStart && !uRange.hasEnd) {
- enableCRC = true
- crcVal, _ = strconv.ParseUint(meta.Get(HTTPHeaderOssCRC64), 10, 0)
- }
- }
- part := downloadPart{}
- i := 0
- start, end := adjustRange(uRange, objectSize)
- for offset := start; offset < end; offset += partSize {
- part.Index = i
- part.Start = offset
- part.End = GetPartEnd(offset, end, partSize)
- part.Offset = start
- part.CRC64 = 0
- parts = append(parts, part)
- i++
- }
- return parts, enableCRC, crcVal, nil
- }
- // 文件大小
- func getObjectBytes(parts []downloadPart) int64 {
- var ob int64
- for _, part := range parts {
- ob += (part.End - part.Start + 1)
- }
- return ob
- }
- // 计算连续分片总的CRC
- func combineCRCInParts(dps []downloadPart) uint64 {
- if dps == nil || len(dps) == 0 {
- return 0
- }
- crc := dps[0].CRC64
- for i := 1; i < len(dps); i++ {
- crc = CRC64Combine(crc, dps[i].CRC64, (uint64)(dps[i].End-dps[i].Start+1))
- }
- return crc
- }
- // 并发无断点续传的下载
- func (bucket Bucket) downloadFile(objectKey, filePath string, partSize int64, options []Option, routines int, uRange *unpackedRange) error {
- tempFilePath := filePath + TempFileSuffix
- listener := getProgressListener(options)
- // 如果文件不存在则创建,存在不清空,下载分片会重写文件内容
- fd, err := os.OpenFile(tempFilePath, os.O_WRONLY|os.O_CREATE, FilePermMode)
- if err != nil {
- return err
- }
- fd.Close()
- // 分割文件
- parts, enableCRC, expectedCRC, err := getDownloadParts(&bucket, objectKey, partSize, uRange)
- if err != nil {
- return err
- }
- jobs := make(chan downloadPart, len(parts))
- results := make(chan downloadPart, len(parts))
- failed := make(chan error)
- die := make(chan bool)
- var completedBytes int64
- totalBytes := getObjectBytes(parts)
- event := newProgressEvent(TransferStartedEvent, 0, totalBytes)
- publishProgress(listener, event)
- // 启动工作协程
- arg := downloadWorkerArg{&bucket, objectKey, tempFilePath, options, downloadPartHooker, enableCRC}
- for w := 1; w <= routines; w++ {
- go downloadWorker(w, arg, jobs, results, failed, die)
- }
- // 并发上传分片
- go downloadScheduler(jobs, parts)
- // 等待分片下载完成
- completed := 0
- for completed < len(parts) {
- select {
- case part := <-results:
- completed++
- completedBytes += (part.End - part.Start + 1)
- parts[part.Index].CRC64 = part.CRC64
- event = newProgressEvent(TransferDataEvent, completedBytes, totalBytes)
- publishProgress(listener, event)
- case err := <-failed:
- close(die)
- event = newProgressEvent(TransferFailedEvent, completedBytes, totalBytes)
- publishProgress(listener, event)
- return err
- }
- if completed >= len(parts) {
- break
- }
- }
- event = newProgressEvent(TransferCompletedEvent, completedBytes, totalBytes)
- publishProgress(listener, event)
- if enableCRC {
- actualCRC := combineCRCInParts(parts)
- err = checkDownloadCRC(actualCRC, expectedCRC)
- if err != nil {
- return err
- }
- }
- return os.Rename(tempFilePath, filePath)
- }
- // ----- 并发有断点的下载 -----
- const downloadCpMagic = "92611BED-89E2-46B6-89E5-72F273D4B0A3"
- type downloadCheckpoint struct {
- Magic string // magic
- MD5 string // cp内容的MD5
- FilePath string // 本地文件
- Object string // key
- ObjStat objectStat // 文件状态
- Parts []downloadPart // 全部分片
- PartStat []bool // 分片下载是否完成
- Start int64 // 起点
- End int64 // 终点
- enableCRC bool // 是否有CRC校验
- CRC uint64 // CRC校验值
- }
- type objectStat struct {
- Size int64 // 大小
- LastModified string // 最后修改时间
- Etag string // etag
- }
- // CP数据是否有效,CP有效且Object没有更新时有效
- func (cp downloadCheckpoint) isValid(bucket *Bucket, objectKey string, uRange *unpackedRange) (bool, error) {
- // 比较CP的Magic及MD5
- cpb := cp
- cpb.MD5 = ""
- js, _ := json.Marshal(cpb)
- sum := md5.Sum(js)
- b64 := base64.StdEncoding.EncodeToString(sum[:])
- if cp.Magic != downloadCpMagic || b64 != cp.MD5 {
- return false, nil
- }
- // 确认object没有更新
- meta, err := bucket.GetObjectDetailedMeta(objectKey)
- if err != nil {
- return false, err
- }
- objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 0)
- if err != nil {
- return false, err
- }
- // 比较Object的大小/最后修改时间/etag
- if cp.ObjStat.Size != objectSize ||
- cp.ObjStat.LastModified != meta.Get(HTTPHeaderLastModified) ||
- cp.ObjStat.Etag != meta.Get(HTTPHeaderEtag) {
- return false, nil
- }
- // 确认下载范围是否变化
- if uRange != nil {
- start, end := adjustRange(uRange, objectSize)
- if start != cp.Start || end != cp.End {
- return false, nil
- }
- }
- return true, nil
- }
- // 从文件中load
- func (cp *downloadCheckpoint) load(filePath string) error {
- contents, err := ioutil.ReadFile(filePath)
- if err != nil {
- return err
- }
- err = json.Unmarshal(contents, cp)
- return err
- }
- // dump到文件
- func (cp *downloadCheckpoint) dump(filePath string) error {
- bcp := *cp
- // 计算MD5
- bcp.MD5 = ""
- js, err := json.Marshal(bcp)
- if err != nil {
- return err
- }
- sum := md5.Sum(js)
- b64 := base64.StdEncoding.EncodeToString(sum[:])
- bcp.MD5 = b64
- // 序列化
- js, err = json.Marshal(bcp)
- if err != nil {
- return err
- }
- // dump
- return ioutil.WriteFile(filePath, js, FilePermMode)
- }
- // 未完成的分片
- func (cp downloadCheckpoint) todoParts() []downloadPart {
- dps := []downloadPart{}
- for i, ps := range cp.PartStat {
- if !ps {
- dps = append(dps, cp.Parts[i])
- }
- }
- return dps
- }
- // 完成的字节数
- func (cp downloadCheckpoint) getCompletedBytes() int64 {
- var completedBytes int64
- for i, part := range cp.Parts {
- if cp.PartStat[i] {
- completedBytes += (part.End - part.Start + 1)
- }
- }
- return completedBytes
- }
- // 初始化下载任务
- func (cp *downloadCheckpoint) prepare(bucket *Bucket, objectKey, filePath string, partSize int64, uRange *unpackedRange) error {
- // cp
- cp.Magic = downloadCpMagic
- cp.FilePath = filePath
- cp.Object = objectKey
- // object
- meta, err := bucket.GetObjectDetailedMeta(objectKey)
- if err != nil {
- return err
- }
- objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 0)
- if err != nil {
- return err
- }
- cp.ObjStat.Size = objectSize
- cp.ObjStat.LastModified = meta.Get(HTTPHeaderLastModified)
- cp.ObjStat.Etag = meta.Get(HTTPHeaderEtag)
- // parts
- cp.Parts, cp.enableCRC, cp.CRC, err = getDownloadParts(bucket, objectKey, partSize, uRange)
- if err != nil {
- return err
- }
- cp.PartStat = make([]bool, len(cp.Parts))
- for i := range cp.PartStat {
- cp.PartStat[i] = false
- }
- return nil
- }
- func (cp *downloadCheckpoint) complete(cpFilePath, downFilepath string) error {
- os.Remove(cpFilePath)
- return os.Rename(downFilepath, cp.FilePath)
- }
- // 并发带断点的下载
- func (bucket Bucket) downloadFileWithCp(objectKey, filePath string, partSize int64, options []Option, cpFilePath string, routines int, uRange *unpackedRange) error {
- tempFilePath := filePath + TempFileSuffix
- listener := getProgressListener(options)
- // LOAD CP数据
- dcp := downloadCheckpoint{}
- err := dcp.load(cpFilePath)
- if err != nil {
- os.Remove(cpFilePath)
- }
- // LOAD出错或数据无效重新初始化下载
- valid, err := dcp.isValid(&bucket, objectKey, uRange)
- if err != nil || !valid {
- if err = dcp.prepare(&bucket, objectKey, filePath, partSize, uRange); err != nil {
- return err
- }
- os.Remove(cpFilePath)
- }
- // 如果文件不存在则创建,存在不清空,下载分片会重写文件内容
- fd, err := os.OpenFile(tempFilePath, os.O_WRONLY|os.O_CREATE, FilePermMode)
- if err != nil {
- return err
- }
- fd.Close()
- // 未完成的分片
- parts := dcp.todoParts()
- jobs := make(chan downloadPart, len(parts))
- results := make(chan downloadPart, len(parts))
- failed := make(chan error)
- die := make(chan bool)
- completedBytes := dcp.getCompletedBytes()
- event := newProgressEvent(TransferStartedEvent, completedBytes, dcp.ObjStat.Size)
- publishProgress(listener, event)
- // 启动工作协程
- arg := downloadWorkerArg{&bucket, objectKey, tempFilePath, options, downloadPartHooker, dcp.enableCRC}
- for w := 1; w <= routines; w++ {
- go downloadWorker(w, arg, jobs, results, failed, die)
- }
- // 并发下载分片
- go downloadScheduler(jobs, parts)
- // 等待分片下载完成
- completed := 0
- for completed < len(parts) {
- select {
- case part := <-results:
- completed++
- dcp.PartStat[part.Index] = true
- dcp.Parts[part.Index].CRC64 = part.CRC64
- dcp.dump(cpFilePath)
- completedBytes += (part.End - part.Start + 1)
- event = newProgressEvent(TransferDataEvent, completedBytes, dcp.ObjStat.Size)
- publishProgress(listener, event)
- case err := <-failed:
- close(die)
- event = newProgressEvent(TransferFailedEvent, completedBytes, dcp.ObjStat.Size)
- publishProgress(listener, event)
- return err
- }
- if completed >= len(parts) {
- break
- }
- }
- event = newProgressEvent(TransferCompletedEvent, completedBytes, dcp.ObjStat.Size)
- publishProgress(listener, event)
- if dcp.enableCRC {
- actualCRC := combineCRCInParts(dcp.Parts)
- err = checkDownloadCRC(actualCRC, dcp.CRC)
- if err != nil {
- return err
- }
- }
- return dcp.complete(cpFilePath, tempFilePath)
- }
|