feat(host): transfer sparse file over network

This commit is contained in:
ioito
2022-01-22 15:48:18 +08:00
parent 6b8b74714f
commit 63d3fa8af8
40 changed files with 2637 additions and 91 deletions
+121
View File
@@ -15,11 +15,26 @@
package compute
import (
"compress/zlib"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/cheggaaa/pb/v3"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/sparsefile"
)
func init() {
@@ -309,4 +324,110 @@ func init() {
return nil
})
type DiskDownloadOptions struct {
ID string `help:"ID or name of disk" json:"-"`
Compress bool
Sparse bool
Timeout int `help:"Timeout hours for download" default:"5"`
Debug bool
FILE string
}
R(&DiskDownloadOptions{}, "disk-download", "Download disk from host", func(s *mcclient.ClientSession, args *DiskDownloadOptions) error {
disk, err := modules.Disks.GetById(s, args.ID, nil)
if err != nil {
return err
}
storageId, _ := disk.GetString("storage_id")
storage, err := modules.Storages.GetById(s, storageId, nil)
if err != nil {
return err
}
hostsInfo := []struct {
Id string
}{}
storage.Unmarshal(&hostsInfo, "hosts")
header := http.Header{}
header.Set("X-Auth-Token", s.GetToken().GetTokenString())
if args.Compress {
header.Set("X-Compress-Content", "zlib")
}
if args.Sparse {
header.Set("X-Sparse-Content", "true")
}
client := httputils.GetTimeoutClient(time.Hour * time.Duration(args.Timeout))
for _, host := range hostsInfo {
host, err := modules.Hosts.GetById(s, host.Id, nil)
if err != nil {
return err
}
managerUri, _ := host.GetString("manager_uri")
if len(managerUri) == 0 {
continue
}
url := fmt.Sprintf("%s/download/disks/%s/%s", managerUri, storageId, args.ID)
resp, err := httputils.Request(client, context.Background(), httputils.GET, url, header, nil, args.Debug)
if err != nil {
log.Errorf("request %s error: %v", err)
continue
}
defer resp.Body.Close()
totalSize, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
sparseHeader, _ := strconv.ParseInt(resp.Header.Get("X-Sparse-Header"), 10, 64)
fi, err := os.Create(args.FILE)
if err != nil {
return errors.Wrapf(err, "os.Create(%s)", args.FILE)
}
defer fi.Close()
var reader = resp.Body
if args.Compress {
zlibRC, err := zlib.NewReader(resp.Body)
if err != nil {
return errors.Wrapf(err, "zlib.NewReader")
}
defer zlibRC.Close()
reader = zlibRC
}
var writer io.Writer = fi
if sparseHeader > 0 {
writer = sparsefile.NewSparseFileWriter(fi, sparseHeader, totalSize)
}
bar := pb.Full.Start64(totalSize)
barReader := bar.NewProxyReader(reader)
_, err = io.Copy(writer, barReader)
return err
}
return fmt.Errorf("no available download url")
})
type SparseHoleOptions struct {
FILE string
}
R(&SparseHoleOptions{}, "sparse-file-hole", "Show sparse file holes", func(s *mcclient.ClientSession, args *SparseHoleOptions) error {
fi, err := os.Open(args.FILE)
if err != nil {
return err
}
defer fi.Close()
sp, err := sparsefile.NewSparseFileReader(fi)
if err != nil {
return err
}
holes := sp.GetHoles()
printObject(jsonutils.Marshal(holes))
return nil
})
}
+2 -2
View File
@@ -36,6 +36,7 @@ require (
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/c-bata/go-prompt v0.2.1
github.com/cheggaaa/pb/v3 v3.0.8
github.com/coredns/coredns v1.3.0
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
@@ -83,7 +84,6 @@ require (
github.com/lestrrat/go-pdebug v0.0.0-20180220043741-569c97477ae8 // indirect
github.com/libvirt/libvirt-go-xml v5.2.0+incompatible
github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f // indirect
github.com/mdlayher/arp v0.0.0-20190313224443-98a83c8a2717
@@ -105,7 +105,6 @@ require (
github.com/pkg/term v0.0.0-20181116001808-27bbf2edb814 // indirect
github.com/pquerna/otp v1.2.0
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sergi/go-diff v1.2.0
github.com/serialx/hashring v0.0.0-20180504054112-49a4782e9908
@@ -135,6 +134,7 @@ require (
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
golang.org/x/text v0.3.3
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987
+4
View File
@@ -84,6 +84,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWso
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845/go.mod h1:c8Mh99Cw82nrsAnPgxQSZHkswVOJF7/MqZb1ZdvriLM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -154,6 +156,8 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w
github.com/c-bata/go-prompt v0.2.1 h1:HTPxmnfY4y5x3Myo7Gq4ce989jfnvZIBgdBp6y8bjYM=
github.com/c-bata/go-prompt v0.2.1/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA=
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+33 -43
View File
@@ -25,6 +25,9 @@ import (
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/util/pb"
"yunion.io/x/onecloud/pkg/util/sparsefile"
)
const (
@@ -37,13 +40,14 @@ type SDownloadProvider struct {
w http.ResponseWriter
rateLimit int
compress bool
sparse bool
}
func NewDownloadProvider(w http.ResponseWriter, compress bool, rateLimit int) *SDownloadProvider {
func NewDownloadProvider(w http.ResponseWriter, compress, sparse bool, rateLimit int) *SDownloadProvider {
if rateLimit <= 0 {
rateLimit = DEFAULT_RATE_LIMIT
}
return &SDownloadProvider{w, rateLimit, compress}
return &SDownloadProvider{w: w, rateLimit: rateLimit, compress: compress, sparse: sparse}
}
func (d *SDownloadProvider) Start(
@@ -63,7 +67,7 @@ func (d *SDownloadProvider) Start(
d.w.Header().Add(k, headers.Get(k))
}
log.Infof("Downloader Start Transfer %s, compress %t", downloadFilePath, d.compress)
log.Infof("Downloader Start Transfer %s, compress %t sparse %t rateLimit: %dMiB/s", downloadFilePath, d.compress, d.sparse, d.rateLimit)
spath, err := filepath.EvalSymlinks(downloadFilePath)
if err == nil {
downloadFilePath = spath
@@ -78,16 +82,24 @@ func (d *SDownloadProvider) Start(
if err != nil {
return errors.Wrapf(err, "fi.Stat")
}
d.w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
var (
end = false
chunk = make([]byte, CHUNK_SIZE)
writer io.Writer = d.w
startTime = time.Now()
sendBytes = 0
writeChunk []byte
)
var reader io.Reader
reader = fi
size := stat.Size()
if d.sparse {
sparse, err := sparsefile.NewSparseFileReader(fi)
if err != nil {
return errors.Wrapf(err, "NewSparseFileReader")
}
size = sparse.Size()
d.w.Header().Set("X-Sparse-Header", fmt.Sprintf("%d", sparse.HeaderSize()))
reader = sparse
}
d.w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
var writer io.Writer = d.w
if d.compress {
zw, err := zlib.NewWriterLevel(d.w, COMPRESS_LEVEL)
@@ -100,40 +112,18 @@ func (d *SDownloadProvider) Start(
defer zw.Flush() // it's cool
}
for !end {
size, err := fi.Read(chunk)
if err != nil {
if err != io.EOF {
log.Errorln(err)
return err
} else {
end = true
}
}
pb := pb.NewProxyReader(reader, size)
pb.SetRateLimit(d.rateLimit)
pb.SetRefreshRate(time.Second * 10)
pb.SetCallback(func() {
log.Infof("transfer %s rate: %.2f MiB p/s percent: %.2f%%", downloadFilePath, pb.Rate(), pb.Percent())
})
writeChunk = chunk[:size]
if size, err = writer.Write(writeChunk); err != nil {
log.Errorln(err)
return err
} else {
sendBytes += size
timeDur := time.Now().Sub(startTime)
exceptDur := float64(sendBytes) / 1000.0 / 1000.0 / float64(d.rateLimit)
if exceptDur > timeDur.Seconds() {
time.Sleep(time.Duration(exceptDur-timeDur.Seconds()) * time.Second)
}
}
_, err = io.Copy(writer, pb)
if err != nil {
return errors.Wrapf(err, "io.Copy")
}
// if d.compress {
// zw := writer.(*zlib.Writer)
// zw.Flush()
// }
sendMb := float64(sendBytes) / 1000.0 / 1000.0
timeDur := time.Now().Sub(startTime)
log.Infof("Send data: %fMB rate: %fMB/sec", sendMb, sendMb/timeDur.Seconds())
if onDownloadComplete != nil {
onDownloadComplete()
}
+16 -7
View File
@@ -84,6 +84,10 @@ func isCompress(r *http.Request) bool {
return r.Header.Get("X-Compress-Content") == "zlib"
}
func isSparse(r *http.Request) bool {
return r.Header.Get("X-Sparse-Content") == "true"
}
func download(ctx context.Context, w http.ResponseWriter, r *http.Request) {
var (
params, _, _ = appsrv.FetchEnv(ctx, w, r)
@@ -91,11 +95,12 @@ func download(ctx context.Context, w http.ResponseWriter, r *http.Request) {
action = params["<action>"]
rateLimit = options.HostOptions.BandwidthLimit
compress = isCompress(r)
sparse = isSparse(r)
)
switch action {
case "images":
hand := NewImageCacheDownloadProvider(w, compress, rateLimit, id)
hand := NewImageCacheDownloadProvider(w, compress, sparse, rateLimit, id)
if !fileutils2.Exists(hand.downloadFilePath()) {
httperrors.NotFoundError(ctx, w, "Image cache %s not found", id)
} else {
@@ -104,7 +109,7 @@ func download(ctx context.Context, w http.ResponseWriter, r *http.Request) {
}
}
case "servers":
hand := NewGuestDownloadProvider(w, compress, rateLimit, id)
hand := NewGuestDownloadProvider(w, compress, sparse, rateLimit, id)
if !fileutils2.Exists(hand.fullPath()) {
httperrors.NotFoundError(ctx, w, "Guest %s not found", id)
} else {
@@ -142,8 +147,9 @@ func diskDownload(ctx context.Context, w http.ResponseWriter, r *http.Request) {
hostutils.Response(ctx, w, err)
} else {
var compress = isCompress(r)
var sparse = isSparse(r)
hand := NewImageDownloadProvider(w,
compress, options.HostOptions.BandwidthLimit, disk, "")
compress, sparse, options.HostOptions.BandwidthLimit, disk, "")
if err := hand.Start(); err != nil {
hostutils.Response(ctx, w, err)
}
@@ -156,8 +162,9 @@ func diskHead(ctx context.Context, w http.ResponseWriter, r *http.Request) {
hostutils.Response(ctx, w, err)
} else {
var compress = isCompress(r)
var sparse = isSparse(r)
hand := NewImageDownloadProvider(w,
compress, options.HostOptions.BandwidthLimit, disk, "")
compress, sparse, options.HostOptions.BandwidthLimit, disk, "")
if err := hand.HandlerHead(); err != nil {
hostutils.Response(ctx, w, err)
}
@@ -187,8 +194,9 @@ func snapshotDownload(ctx context.Context, w http.ResponseWriter, r *http.Reques
hostutils.Response(ctx, w, err)
} else {
var compress = isCompress(r)
var sparse = isSparse(r)
hand := NewSnapshotDownloadProvider(w,
compress, options.HostOptions.BandwidthLimit, snapshotPath)
compress, sparse, options.HostOptions.BandwidthLimit, snapshotPath)
if err := hand.Start(); err != nil {
hostutils.Response(ctx, w, err)
}
@@ -201,8 +209,9 @@ func snapshotHead(ctx context.Context, w http.ResponseWriter, r *http.Request) {
hostutils.Response(ctx, w, err)
} else {
var compress = isCompress(r)
var sparse = isSparse(r)
hand := NewSnapshotDownloadProvider(w,
compress, options.HostOptions.BandwidthLimit, snapshotPath)
compress, sparse, options.HostOptions.BandwidthLimit, snapshotPath)
if err := hand.HandlerHead(); err != nil {
hostutils.Response(ctx, w, err)
}
@@ -215,7 +224,7 @@ func imageCacheHead(ctx context.Context, w http.ResponseWriter, r *http.Request)
rateLimit := options.HostOptions.BandwidthLimit
compress := isCompress(r)
hand := NewImageCacheDownloadProvider(w, compress, rateLimit, imageId)
hand := NewImageCacheDownloadProvider(w, compress, false, rateLimit, imageId)
if err := hand.HandlerHead(); err != nil {
hostutils.Response(ctx, w, err)
+2 -2
View File
@@ -32,10 +32,10 @@ type SGuestDownloadProvider struct {
}
func NewGuestDownloadProvider(
w http.ResponseWriter, compress bool, rateLimit int, sid string,
w http.ResponseWriter, compress, sparse bool, rateLimit int, sid string,
) *SGuestDownloadProvider {
return &SGuestDownloadProvider{
SDownloadProvider: NewDownloadProvider(w, compress, rateLimit),
SDownloadProvider: NewDownloadProvider(w, compress, sparse, rateLimit),
serverId: sid,
}
}
+2 -2
View File
@@ -33,9 +33,9 @@ type SImageDownloadProvider struct {
compressFormat string
}
func NewImageDownloadProvider(w http.ResponseWriter, compress bool, rateLimit int, disk storageman.IDisk, compressFormat string) *SImageDownloadProvider {
func NewImageDownloadProvider(w http.ResponseWriter, compress, sparse bool, rateLimit int, disk storageman.IDisk, compressFormat string) *SImageDownloadProvider {
return &SImageDownloadProvider{
SDownloadProvider: NewDownloadProvider(w, compress, rateLimit),
SDownloadProvider: NewDownloadProvider(w, compress, sparse, rateLimit),
disk: disk,
compressFormat: compressFormat,
}
@@ -30,10 +30,10 @@ type SImageCacheDownloadProvider struct {
}
func NewImageCacheDownloadProvider(
w http.ResponseWriter, compress bool, rateLimit int, imageId string,
w http.ResponseWriter, compress, sparse bool, rateLimit int, imageId string,
) *SImageCacheDownloadProvider {
return &SImageCacheDownloadProvider{
SDownloadProvider: NewDownloadProvider(w, compress, rateLimit),
SDownloadProvider: NewDownloadProvider(w, compress, sparse, rateLimit),
imageId: imageId,
}
}
@@ -26,10 +26,10 @@ type SSnapshotDownloadProvider struct {
}
func NewSnapshotDownloadProvider(
w http.ResponseWriter, compress bool, rateLimit int, snapshotPath string,
w http.ResponseWriter, compress, sparse bool, rateLimit int, snapshotPath string,
) *SSnapshotDownloadProvider {
return &SSnapshotDownloadProvider{
SDownloadProvider: NewDownloadProvider(w, compress, rateLimit),
SDownloadProvider: NewDownloadProvider(w, compress, sparse, rateLimit),
snapshotPath: snapshotPath,
}
}
+21 -31
View File
@@ -31,6 +31,8 @@ import (
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/pb"
"yunion.io/x/onecloud/pkg/util/sparsefile"
)
type SImageDesc struct {
@@ -194,6 +196,7 @@ func (r *SRemoteFile) downloadInternal(getData bool, preChksum string, callback
if r.compress {
header.Set("X-Compress-Content", "zlib")
}
header.Set("X-Sparse-Content", "true")
if len(r.extraHeaders) > 0 {
for k, v := range r.extraHeaders {
header.Set(k, v)
@@ -214,6 +217,7 @@ func (r *SRemoteFile) downloadInternal(getData bool, preChksum string, callback
return errors.Wrapf(err, "request %s %s", method, url)
}
totalSize, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
sparseHeader, _ := strconv.ParseInt(resp.Header.Get("X-Sparse-Header"), 10, 64)
defer resp.Body.Close()
if resp.StatusCode < 300 {
if getData {
@@ -234,38 +238,24 @@ func (r *SRemoteFile) downloadInternal(getData bool, preChksum string, callback
defer zlibRC.Close()
reader = zlibRC
}
var finishChan = make(chan struct{})
go func() {
defer recover()
preSizeMb := int64(0)
for {
select {
case <-time.After(10 * time.Second):
info, err := fi.Stat()
if err != nil {
log.Errorf("failed stat file %s", r.tmpPath)
return
}
percentInfo, percent := "", 0.0
if totalSize > 0 {
percent = float64(info.Size()) / float64(totalSize) * 100
percentInfo = fmt.Sprintf("(%.2f%%)", percent)
}
log.Infof("written file %s size %dM%s", r.tmpPath, info.Size()/1024/1024, percentInfo)
if callback != nil && percent > 0 {
callback(percent, float64(info.Size()-preSizeMb)/1024/1024, totalSize/1024/1024)
}
preSizeMb = info.Size()
case <-finishChan:
if callback != nil {
callback(100, 0, totalSize/1024/1024)
}
return
}
var writer io.Writer = fi
if sparseHeader > 0 {
writer = sparsefile.NewSparseFileWriter(fi, sparseHeader, totalSize)
}
pb := pb.NewProxyReader(reader, totalSize)
pb.SetCallback(func() {
if callback != nil {
go func() {
callback(pb.Percent(), pb.Rate(), totalSize/1024/1024)
}()
}
}()
_, err = io.Copy(fi, reader)
close(finishChan)
log.Infof("written file %s rate: %.2f MiB p/s percent: %.2f%%", r.tmpPath, pb.Rate(), pb.Percent())
})
_, err = io.Copy(writer, pb)
if err != nil {
return errors.Wrapf(err, "io.Copy to tmpPath %s from reader", r.tmpPath)
}
+1
View File
@@ -0,0 +1 @@
package pb // import "yunion.io/x/onecloud/pkg/util/pb"
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2019 Yunion
//
// 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 pb
import (
"context"
"io"
"time"
"golang.org/x/time/rate"
)
const (
BURSTS = 1024 * 1024 * 1024
)
type SProxyReader struct {
reader io.Reader
size int64
cur int64
lastCur int64
finish chan bool
debug bool
ticker *time.Ticker
start time.Time
callback func()
ctx context.Context
refreshRate time.Duration
// Mbps
rateLimit *rate.Limiter
}
func NewProxyReader(reader io.Reader, size int64) *SProxyReader {
return &SProxyReader{
reader: reader,
size: size,
finish: make(chan bool),
ctx: context.Background(),
refreshRate: time.Second * 1,
}
}
func (self *SProxyReader) SetRateLimit(mb int) {
self.rateLimit = rate.NewLimiter(rate.Limit(mb*1024*1024), BURSTS)
self.rateLimit.AllowN(time.Now(), BURSTS)
}
// 设置刷新频率
// 仅在读取数据前生效
func (self *SProxyReader) SetRefreshRate(rate time.Duration) {
if rate > time.Second && self.cur == 0 {
self.refreshRate = rate
}
}
func (self *SProxyReader) SetCallback(callback func()) {
self.callback = callback
}
func (self *SProxyReader) Percent() float64 {
return float64(self.cur) / float64(self.size) * 100.0
}
func (self *SProxyReader) AvgRate() float64 {
return float64(self.cur) / float64(1024) / float64(1024) / float64(time.Now().Sub(self.start).Seconds())
}
func (self *SProxyReader) Rate() float64 {
return float64(self.cur-self.lastCur) / float64(1024) / float64(1024) / float64(self.refreshRate.Seconds())
}
func (self *SProxyReader) Read(p []byte) (n int, err error) {
defer func() {
if err != nil {
self.finish <- true
close(self.finish)
}
}()
if self.start.IsZero() {
self.start = time.Now()
self.ticker = time.NewTicker(self.refreshRate)
go self.refresh(self.finish)
}
n, err = self.reader.Read(p)
if err != nil {
return n, err
}
if self.rateLimit != nil {
err = self.rateLimit.WaitN(self.ctx, n)
if err != nil {
return n, err
}
}
self.cur += int64(n)
return n, nil
}
func (self *SProxyReader) refresh(finishChan chan bool) {
defer self.ticker.Stop()
for {
select {
case <-self.ticker.C:
if self.callback != nil {
self.callback()
}
self.lastCur = self.cur
case finished := <-finishChan:
if finished {
return
}
}
}
}
+1
View File
@@ -0,0 +1 @@
package sparsefile // import "yunion.io/x/onecloud/pkg/util/sparsefile"
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2019 Yunion
//
// 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.
//go:build !linux
// +build !linux
package sparsefile
import (
"os"
)
func detectHoles(file *os.File) ([]sSparseHole, error) {
return []sSparseHole{}, nil
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2019 Yunion
//
// 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.
//go:build linux
// +build linux
package sparsefile
import (
"io"
"os"
"syscall"
"golang.org/x/sys/unix"
)
const (
SEEK_DATA = 3
SEEK_HOLE = 4
)
func detectHoles(file *os.File) ([]sSparseHole, error) {
holes := []sSparseHole{}
offset := int64(0)
for {
start, err := unix.Seek(int(file.Fd()), offset, SEEK_HOLE)
if err != nil {
if e, ok := err.(syscall.Errno); ok && e == syscall.ENXIO {
break
}
return nil, err
}
end, err := unix.Seek(int(file.Fd()), start, SEEK_DATA)
if err != nil {
if e, ok := err.(syscall.Errno); ok && e == syscall.ENXIO {
end, _ = file.Seek(0, io.SeekEnd)
if end > start {
holes = append(holes, sSparseHole{Offset: start, Length: end - start})
}
break
}
return nil, err
}
offset = end
holes = append(holes, sSparseHole{Offset: start, Length: end - start})
}
return holes, nil
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2019 Yunion
//
// 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 sparsefile
import (
"bytes"
"encoding/json"
"io"
"os"
"yunion.io/x/pkg/errors"
)
type sSparseHole struct {
Offset int64
Length int64
}
type SparseFileReader struct {
file *os.File
holes []sSparseHole
header []byte
size int64
headerSize int64
realReadLen int64
}
func (self *SparseFileReader) Close() error {
return self.file.Close()
}
func (self *SparseFileReader) HeaderSize() int64 {
return self.headerSize
}
func (self *SparseFileReader) GetHoles() []sSparseHole {
return self.holes
}
func (self *SparseFileReader) Size() int64 {
holeSize := int64(0)
for _, hole := range self.holes {
holeSize += hole.Length
}
return self.size - holeSize + self.headerSize
}
func (self *SparseFileReader) Read(p []byte) (int, error) {
if len(self.header) > 0 {
reader := bytes.NewReader(self.header)
n, err := reader.Read(p)
if err != nil {
return n, err
}
self.header = self.header[n:]
return n, nil
}
for _, hole := range self.holes {
if self.realReadLen > hole.Offset {
continue
}
if self.realReadLen < hole.Offset {
body := io.LimitReader(self.file, hole.Offset-self.realReadLen)
n, err := body.Read(p)
if err != nil {
return n, err
}
self.realReadLen += int64(n)
return n, nil
} else if self.realReadLen == hole.Offset {
n, err := self.file.Seek(hole.Length, io.SeekCurrent)
if err != nil {
return int(n), err
}
self.realReadLen += int64(n)
}
}
return self.file.Read(p)
}
func (self *SparseFileReader) probeHoles() error {
var err error
self.holes, err = detectHoles(self.file)
if err != nil {
return err
}
if len(self.holes) > 0 {
self.header, err = json.Marshal(self.holes)
if err != nil {
return errors.Wrapf(err, "json.Marshal")
}
self.headerSize = int64(len(self.header))
}
return nil
}
func NewSparseFileReader(file *os.File) (*SparseFileReader, error) {
ret := &SparseFileReader{file: file, holes: []sSparseHole{}, realReadLen: 0}
stat, err := ret.file.Stat()
if err != nil {
return nil, errors.Wrapf(err, "Stat")
}
ret.size = stat.Size()
err = ret.probeHoles()
if err != nil {
return nil, err
}
_, err = ret.file.Seek(0, io.SeekStart)
return ret, err
}
type SparseFileWrite struct {
f *os.File
headerSize int64
size int64
header []byte
holes []sSparseHole
bodyWriteLen int64
readed int
}
func (self *SparseFileWrite) Close() error {
return self.f.Close()
}
type zero struct{}
func (zero) Read(p []byte) (int, error) {
for index := range p {
p[index] = 0
}
return len(p), nil
}
func (self *SparseFileWrite) initHeader() error {
err := json.Unmarshal(self.header, &self.holes)
if err != nil {
return errors.Wrapf(err, "unmarshal header")
}
for _, h := range self.holes {
self.size += h.Length
}
return nil
}
func (self *SparseFileWrite) Write(p []byte) (int, error) {
if len(self.header) < int(self.headerSize) {
n := int(self.headerSize) - len(self.header)
if len(p) >= n {
self.header = append(self.header, p[:n]...)
err := self.initHeader()
if err != nil {
return n, err
}
self.readed = n
} else {
self.header = append(self.header, p...)
return len(p), nil
}
}
if self.readed == len(p) {
self.readed = 0
return len(p), nil
}
for _, hole := range self.holes {
if self.bodyWriteLen > hole.Offset {
continue
}
if self.bodyWriteLen < hole.Offset {
data := p[self.readed:]
if len(p[self.readed:]) > int(hole.Offset-self.bodyWriteLen) {
data = p[self.readed : hole.Offset-self.bodyWriteLen]
}
n, err := self.f.Write(data)
if err != nil {
return n, err
}
self.readed += n
self.bodyWriteLen += int64(n)
if len(p) == self.readed {
self.readed = 0
return len(p), nil
}
} else if self.bodyWriteLen == hole.Offset {
n, err := self.f.Seek(hole.Length, io.SeekCurrent)
if err != nil {
return int(n), err
}
self.bodyWriteLen += int64(n)
}
}
return self.f.Write(p)
}
func NewSparseFileWriter(f *os.File, headerSize int64, size int64) *SparseFileWrite {
return &SparseFileWrite{
f: f,
headerSize: headerSize,
header: []byte{},
holes: []sSparseHole{},
size: size,
}
}
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
.*.sw?
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2013 VividCortex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+140
View File
@@ -0,0 +1,140 @@
# EWMA [![GoDoc](https://godoc.org/github.com/VividCortex/ewma?status.svg)](https://godoc.org/github.com/VividCortex/ewma) ![Build Status](https://circleci.com/gh/VividCortex/moving_average.png?circle-token=1459fa37f9ca0e50cef05d1963146d96d47ea523)
This repo provides Exponentially Weighted Moving Average algorithms, or EWMAs for short, [based on our
Quantifying Abnormal Behavior talk](https://vividcortex.com/blog/2013/07/23/a-fast-go-library-for-exponential-moving-averages/).
### Exponentially Weighted Moving Average
An exponentially weighted moving average is a way to continuously compute a type of
average for a series of numbers, as the numbers arrive. After a value in the series is
added to the average, its weight in the average decreases exponentially over time. This
biases the average towards more recent data. EWMAs are useful for several reasons, chiefly
their inexpensive computational and memory cost, as well as the fact that they represent
the recent central tendency of the series of values.
The EWMA algorithm requires a decay factor, alpha. The larger the alpha, the more the average
is biased towards recent history. The alpha must be between 0 and 1, and is typically
a fairly small number, such as 0.04. We will discuss the choice of alpha later.
The algorithm works thus, in pseudocode:
1. Multiply the next number in the series by alpha.
2. Multiply the current value of the average by 1 minus alpha.
3. Add the result of steps 1 and 2, and store it as the new current value of the average.
4. Repeat for each number in the series.
There are special-case behaviors for how to initialize the current value, and these vary
between implementations. One approach is to start with the first value in the series;
another is to average the first 10 or so values in the series using an arithmetic average,
and then begin the incremental updating of the average. Each method has pros and cons.
It may help to look at it pictorially. Suppose the series has five numbers, and we choose
alpha to be 0.50 for simplicity. Here's the series, with numbers in the neighborhood of 300.
![Data Series](https://user-images.githubusercontent.com/279875/28242350-463289a2-6977-11e7-88ca-fd778ccef1f0.png)
Now let's take the moving average of those numbers. First we set the average to the value
of the first number.
![EWMA Step 1](https://user-images.githubusercontent.com/279875/28242353-464c96bc-6977-11e7-9981-dc4e0789c7ba.png)
Next we multiply the next number by alpha, multiply the current value by 1-alpha, and add
them to generate a new value.
![EWMA Step 2](https://user-images.githubusercontent.com/279875/28242351-464abefa-6977-11e7-95d0-43900f29bef2.png)
This continues until we are done.
![EWMA Step N](https://user-images.githubusercontent.com/279875/28242352-464c58f0-6977-11e7-8cd0-e01e4efaac7f.png)
Notice how each of the values in the series decays by half each time a new value
is added, and the top of the bars in the lower portion of the image represents the
size of the moving average. It is a smoothed, or low-pass, average of the original
series.
For further reading, see [Exponentially weighted moving average](http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) on wikipedia.
### Choosing Alpha
Consider a fixed-size sliding-window moving average (not an exponentially weighted moving average)
that averages over the previous N samples. What is the average age of each sample? It is N/2.
Now suppose that you wish to construct a EWMA whose samples have the same average age. The formula
to compute the alpha required for this is: alpha = 2/(N+1). Proof is in the book
"Production and Operations Analysis" by Steven Nahmias.
So, for example, if you have a time-series with samples once per second, and you want to get the
moving average over the previous minute, you should use an alpha of .032786885. This, by the way,
is the constant alpha used for this repository's SimpleEWMA.
### Implementations
This repository contains two implementations of the EWMA algorithm, with different properties.
The implementations all conform to the MovingAverage interface, and the constructor returns
that type.
Current implementations assume an implicit time interval of 1.0 between every sample added.
That is, the passage of time is treated as though it's the same as the arrival of samples.
If you need time-based decay when samples are not arriving precisely at set intervals, then
this package will not support your needs at present.
#### SimpleEWMA
A SimpleEWMA is designed for low CPU and memory consumption. It **will** have different behavior than the VariableEWMA
for multiple reasons. It has no warm-up period and it uses a constant
decay. These properties let it use less memory. It will also behave
differently when it's equal to zero, which is assumed to mean
uninitialized, so if a value is likely to actually become zero over time,
then any non-zero value will cause a sharp jump instead of a small change.
#### VariableEWMA
Unlike SimpleEWMA, this supports a custom age which must be stored, and thus uses more memory.
It also has a "warmup" time when you start adding values to it. It will report a value of 0.0
until you have added the required number of samples to it. It uses some memory to store the
number of samples added to it. As a result it uses a little over twice the memory of SimpleEWMA.
## Usage
### API Documentation
View the GoDoc generated documentation [here](http://godoc.org/github.com/VividCortex/ewma).
```go
package main
import "github.com/VividCortex/ewma"
func main() {
samples := [100]float64{
4599, 5711, 4746, 4621, 5037, 4218, 4925, 4281, 5207, 5203, 5594, 5149,
}
e := ewma.NewMovingAverage() //=> Returns a SimpleEWMA if called without params
a := ewma.NewMovingAverage(5) //=> returns a VariableEWMA with a decay of 2 / (5 + 1)
for _, f := range samples {
e.Add(f)
a.Add(f)
}
e.Value() //=> 13.577404704631077
a.Value() //=> 1.5806140565521463e-12
}
```
## Contributing
We only accept pull requests for minor fixes or improvements. This includes:
* Small bug fixes
* Typos
* Documentation or comments
Please open issues to discuss new features. Pull requests for new features will be rejected,
so we recommend forking the repository and making changes in your fork for your use case.
## License
This repository is Copyright (c) 2013 VividCortex, Inc. All rights reserved.
It is licensed under the MIT license. Please see the LICENSE file for applicable license terms.
+126
View File
@@ -0,0 +1,126 @@
// Package ewma implements exponentially weighted moving averages.
package ewma
// Copyright (c) 2013 VividCortex, Inc. All rights reserved.
// Please see the LICENSE file for applicable license terms.
const (
// By default, we average over a one-minute period, which means the average
// age of the metrics in the period is 30 seconds.
AVG_METRIC_AGE float64 = 30.0
// The formula for computing the decay factor from the average age comes
// from "Production and Operations Analysis" by Steven Nahmias.
DECAY float64 = 2 / (float64(AVG_METRIC_AGE) + 1)
// For best results, the moving average should not be initialized to the
// samples it sees immediately. The book "Production and Operations
// Analysis" by Steven Nahmias suggests initializing the moving average to
// the mean of the first 10 samples. Until the VariableEwma has seen this
// many samples, it is not "ready" to be queried for the value of the
// moving average. This adds some memory cost.
WARMUP_SAMPLES uint8 = 10
)
// MovingAverage is the interface that computes a moving average over a time-
// series stream of numbers. The average may be over a window or exponentially
// decaying.
type MovingAverage interface {
Add(float64)
Value() float64
Set(float64)
}
// NewMovingAverage constructs a MovingAverage that computes an average with the
// desired characteristics in the moving window or exponential decay. If no
// age is given, it constructs a default exponentially weighted implementation
// that consumes minimal memory. The age is related to the decay factor alpha
// by the formula given for the DECAY constant. It signifies the average age
// of the samples as time goes to infinity.
func NewMovingAverage(age ...float64) MovingAverage {
if len(age) == 0 || age[0] == AVG_METRIC_AGE {
return new(SimpleEWMA)
}
return &VariableEWMA{
decay: 2 / (age[0] + 1),
}
}
// A SimpleEWMA represents the exponentially weighted moving average of a
// series of numbers. It WILL have different behavior than the VariableEWMA
// for multiple reasons. It has no warm-up period and it uses a constant
// decay. These properties let it use less memory. It will also behave
// differently when it's equal to zero, which is assumed to mean
// uninitialized, so if a value is likely to actually become zero over time,
// then any non-zero value will cause a sharp jump instead of a small change.
// However, note that this takes a long time, and the value may just
// decays to a stable value that's close to zero, but which won't be mistaken
// for uninitialized. See http://play.golang.org/p/litxBDr_RC for example.
type SimpleEWMA struct {
// The current value of the average. After adding with Add(), this is
// updated to reflect the average of all values seen thus far.
value float64
}
// Add adds a value to the series and updates the moving average.
func (e *SimpleEWMA) Add(value float64) {
if e.value == 0 { // this is a proxy for "uninitialized"
e.value = value
} else {
e.value = (value * DECAY) + (e.value * (1 - DECAY))
}
}
// Value returns the current value of the moving average.
func (e *SimpleEWMA) Value() float64 {
return e.value
}
// Set sets the EWMA's value.
func (e *SimpleEWMA) Set(value float64) {
e.value = value
}
// VariableEWMA represents the exponentially weighted moving average of a series of
// numbers. Unlike SimpleEWMA, it supports a custom age, and thus uses more memory.
type VariableEWMA struct {
// The multiplier factor by which the previous samples decay.
decay float64
// The current value of the average.
value float64
// The number of samples added to this instance.
count uint8
}
// Add adds a value to the series and updates the moving average.
func (e *VariableEWMA) Add(value float64) {
switch {
case e.count < WARMUP_SAMPLES:
e.count++
e.value += value
case e.count == WARMUP_SAMPLES:
e.count++
e.value = e.value / float64(WARMUP_SAMPLES)
e.value = (value * e.decay) + (e.value * (1 - e.decay))
default:
e.value = (value * e.decay) + (e.value * (1 - e.decay))
}
}
// Value returns the current value of the average, or 0.0 if the series hasn't
// warmed up yet.
func (e *VariableEWMA) Value() float64 {
if e.count <= WARMUP_SAMPLES {
return 0.0
}
return e.value
}
// Set sets the EWMA's value.
func (e *VariableEWMA) Set(value float64) {
e.value = value
if e.count <= WARMUP_SAMPLES {
e.count = WARMUP_SAMPLES + 1
}
}
+12
View File
@@ -0,0 +1,12 @@
Copyright (c) 2012-2015, Sergey Cherepanov
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+330
View File
@@ -0,0 +1,330 @@
package pb
import (
"bytes"
"fmt"
"math"
"strings"
"sync"
"time"
)
const (
adElPlaceholder = "%_ad_el_%"
adElPlaceholderLen = len(adElPlaceholder)
)
var (
defaultBarEls = [5]string{"[", "-", ">", "_", "]"}
)
// Element is an interface for bar elements
type Element interface {
ProgressElement(state *State, args ...string) string
}
// ElementFunc type implements Element interface and created for simplify elements
type ElementFunc func(state *State, args ...string) string
// ProgressElement just call self func
func (e ElementFunc) ProgressElement(state *State, args ...string) string {
return e(state, args...)
}
var elementsM sync.Mutex
var elements = map[string]Element{
"percent": ElementPercent,
"counters": ElementCounters,
"bar": adaptiveWrap(ElementBar),
"speed": ElementSpeed,
"rtime": ElementRemainingTime,
"etime": ElementElapsedTime,
"string": ElementString,
"cycle": ElementCycle,
}
// RegisterElement give you a chance to use custom elements
func RegisterElement(name string, el Element, adaptive bool) {
if adaptive {
el = adaptiveWrap(el)
}
elementsM.Lock()
elements[name] = el
elementsM.Unlock()
}
type argsHelper []string
func (args argsHelper) getOr(n int, value string) string {
if len(args) > n {
return args[n]
}
return value
}
func (args argsHelper) getNotEmptyOr(n int, value string) (v string) {
if v = args.getOr(n, value); v == "" {
return value
}
return
}
func adaptiveWrap(el Element) Element {
return ElementFunc(func(state *State, args ...string) string {
state.recalc = append(state.recalc, ElementFunc(func(s *State, _ ...string) (result string) {
s.adaptive = true
result = el.ProgressElement(s, args...)
s.adaptive = false
return
}))
return adElPlaceholder
})
}
// ElementPercent shows current percent of progress.
// Optionally can take one or two string arguments.
// First string will be used as value for format float64, default is "%.02f%%".
// Second string will be used when percent can't be calculated, default is "?%"
// In template use as follows: {{percent .}} or {{percent . "%.03f%%"}} or {{percent . "%.03f%%" "?"}}
var ElementPercent ElementFunc = func(state *State, args ...string) string {
argsh := argsHelper(args)
if state.Total() > 0 {
return fmt.Sprintf(
argsh.getNotEmptyOr(0, "%.02f%%"),
float64(state.Value())/(float64(state.Total())/float64(100)),
)
}
return argsh.getOr(1, "?%")
}
// ElementCounters shows current and total values.
// Optionally can take one or two string arguments.
// First string will be used as format value when Total is present (>0). Default is "%s / %s"
// Second string will be used when total <= 0. Default is "%[1]s"
// In template use as follows: {{counters .}} or {{counters . "%s/%s"}} or {{counters . "%s/%s" "%s/?"}}
var ElementCounters ElementFunc = func(state *State, args ...string) string {
var f string
if state.Total() > 0 {
f = argsHelper(args).getNotEmptyOr(0, "%s / %s")
} else {
f = argsHelper(args).getNotEmptyOr(1, "%[1]s")
}
return fmt.Sprintf(f, state.Format(state.Value()), state.Format(state.Total()))
}
type elementKey int
const (
barObj elementKey = iota
speedObj
cycleObj
)
type bar struct {
eb [5][]byte // elements in bytes
cc [5]int // cell counts
buf *bytes.Buffer
}
func (p *bar) write(state *State, eln, width int) int {
repeat := width / p.cc[eln]
remainder := width % p.cc[eln]
for i := 0; i < repeat; i++ {
p.buf.Write(p.eb[eln])
}
if remainder > 0 {
StripStringToBuffer(string(p.eb[eln]), remainder, p.buf)
}
return width
}
func getProgressObj(state *State, args ...string) (p *bar) {
var ok bool
if p, ok = state.Get(barObj).(*bar); !ok {
p = &bar{
buf: bytes.NewBuffer(nil),
}
state.Set(barObj, p)
}
argsH := argsHelper(args)
for i := range p.eb {
arg := argsH.getNotEmptyOr(i, defaultBarEls[i])
if string(p.eb[i]) != arg {
p.cc[i] = CellCount(arg)
p.eb[i] = []byte(arg)
if p.cc[i] == 0 {
p.cc[i] = 1
p.eb[i] = []byte(" ")
}
}
}
return
}
// ElementBar make progress bar view [-->__]
// Optionally can take up to 5 string arguments. Defaults is "[", "-", ">", "_", "]"
// In template use as follows: {{bar . }} or {{bar . "<" "oOo" "|" "~" ">"}}
// Color args: {{bar . (red "[") (green "-") ...
var ElementBar ElementFunc = func(state *State, args ...string) string {
// init
var p = getProgressObj(state, args...)
total, value := state.Total(), state.Value()
if total < 0 {
total = -total
}
if value < 0 {
value = -value
}
// check for overflow
if total != 0 && value > total {
total = value
}
p.buf.Reset()
var widthLeft = state.AdaptiveElWidth()
if widthLeft <= 0 || !state.IsAdaptiveWidth() {
widthLeft = 30
}
// write left border
if p.cc[0] < widthLeft {
widthLeft -= p.write(state, 0, p.cc[0])
} else {
p.write(state, 0, widthLeft)
return p.buf.String()
}
// check right border size
if p.cc[4] < widthLeft {
// write later
widthLeft -= p.cc[4]
} else {
p.write(state, 4, widthLeft)
return p.buf.String()
}
var curCount int
if total > 0 {
// calculate count of currenct space
curCount = int(math.Ceil((float64(value) / float64(total)) * float64(widthLeft)))
}
// write bar
if total == value && state.IsFinished() {
widthLeft -= p.write(state, 1, curCount)
} else if toWrite := curCount - p.cc[2]; toWrite > 0 {
widthLeft -= p.write(state, 1, toWrite)
widthLeft -= p.write(state, 2, p.cc[2])
} else if curCount > 0 {
widthLeft -= p.write(state, 2, curCount)
}
if widthLeft > 0 {
widthLeft -= p.write(state, 3, widthLeft)
}
// write right border
p.write(state, 4, p.cc[4])
// cut result and return string
return p.buf.String()
}
func elapsedTime(state *State) string {
elapsed := state.Time().Sub(state.StartTime())
var precision time.Duration
var ok bool
if precision, ok = state.Get(TimeRound).(time.Duration); !ok {
// default behavior: round to nearest .1s when elapsed < 10s
//
// we compare with 9.95s as opposed to 10s to avoid an annoying
// interaction with the fixed precision display code below,
// where 9.9s would be rounded to 10s but printed as 10.0s, and
// then 10.0s would be rounded to 10s and printed as 10s
if elapsed < 9950*time.Millisecond {
precision = 100 * time.Millisecond
} else {
precision = time.Second
}
}
rounded := elapsed.Round(precision)
if precision < time.Second && rounded >= time.Second {
// special handling to ensure string is shown with the given
// precision, with trailing zeros after the decimal point if
// necessary
reference := (2*time.Second - time.Nanosecond).Truncate(precision).String()
// reference looks like "1.9[...]9s", telling us how many
// decimal digits we need
neededDecimals := len(reference) - 3
s := rounded.String()
dotIndex := strings.LastIndex(s, ".")
if dotIndex != -1 {
// s has the form "[stuff].[decimals]s"
decimals := len(s) - dotIndex - 2
extraZeros := neededDecimals - decimals
return fmt.Sprintf("%s%ss", s[:len(s)-1], strings.Repeat("0", extraZeros))
} else {
// s has the form "[stuff]s"
return fmt.Sprintf("%s.%ss", s[:len(s)-1], strings.Repeat("0", neededDecimals))
}
} else {
return rounded.String()
}
}
// ElementRemainingTime calculates remaining time based on speed (EWMA)
// Optionally can take one or two string arguments.
// First string will be used as value for format time duration string, default is "%s".
// Second string will be used when bar finished and value indicates elapsed time, default is "%s"
// Third string will be used when value not available, default is "?"
// In template use as follows: {{rtime .}} or {{rtime . "%s remain"}} or {{rtime . "%s remain" "%s total" "???"}}
var ElementRemainingTime ElementFunc = func(state *State, args ...string) string {
if state.IsFinished() {
return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state))
}
sp := getSpeedObj(state).value(state)
if sp > 0 {
remain := float64(state.Total() - state.Value())
remainDur := time.Duration(remain/sp) * time.Second
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), remainDur)
}
return argsHelper(args).getOr(2, "?")
}
// ElementElapsedTime shows elapsed time
// Optionally can take one argument - it's format for time string.
// In template use as follows: {{etime .}} or {{etime . "%s elapsed"}}
var ElementElapsedTime ElementFunc = func(state *State, args ...string) string {
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state))
}
// ElementString get value from bar by given key and print them
// bar.Set("myKey", "string to print")
// In template use as follows: {{string . "myKey"}}
var ElementString ElementFunc = func(state *State, args ...string) string {
if len(args) == 0 {
return ""
}
v := state.Get(args[0])
if v == nil {
return ""
}
return fmt.Sprint(v)
}
// ElementCycle return next argument for every call
// In template use as follows: {{cycle . "1" "2" "3"}}
// Or mix width other elements: {{ bar . "" "" (cycle . "↖" "↗" "↘" "↙" )}}
var ElementCycle ElementFunc = func(state *State, args ...string) string {
if len(args) == 0 {
return ""
}
n, _ := state.Get(cycleObj).(int)
if n >= len(args) {
n = 0
}
state.Set(cycleObj, n+1)
return args[n]
}
+13
View File
@@ -0,0 +1,13 @@
module github.com/cheggaaa/pb/v3
require (
github.com/VividCortex/ewma v1.1.1
github.com/fatih/color v1.10.0
github.com/mattn/go-colorable v0.1.8
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.12
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
)
go 1.12
+17
View File
@@ -0,0 +1,17 @@
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+49
View File
@@ -0,0 +1,49 @@
package pb
import (
"io"
)
// Reader it's a wrapper for given reader, but with progress handle
type Reader struct {
io.Reader
bar *ProgressBar
}
// Read reads bytes from wrapped reader and add amount of bytes to progress bar
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
r.bar.Add(n)
return
}
// Close the wrapped reader when it implements io.Closer
func (r *Reader) Close() (err error) {
r.bar.Finish()
if closer, ok := r.Reader.(io.Closer); ok {
return closer.Close()
}
return
}
// Writer it's a wrapper for given writer, but with progress handle
type Writer struct {
io.Writer
bar *ProgressBar
}
// Write writes bytes to wrapped writer and add amount of bytes to progress bar
func (r *Writer) Write(p []byte) (n int, err error) {
n, err = r.Writer.Write(p)
r.bar.Add(n)
return
}
// Close the wrapped reader when it implements io.Closer
func (r *Writer) Close() (err error) {
r.bar.Finish()
if closer, ok := r.Writer.(io.Closer); ok {
return closer.Close()
}
return
}
+588
View File
@@ -0,0 +1,588 @@
package pb
import (
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"text/template"
"time"
"github.com/fatih/color"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/cheggaaa/pb/v3/termutil"
)
// Version of ProgressBar library
const Version = "3.0.8"
type key int
const (
// Bytes means we're working with byte sizes. Numbers will print as Kb, Mb, etc
// bar.Set(pb.Bytes, true)
Bytes key = 1 << iota
// Use SI bytes prefix names (kB, MB, etc) instead of IEC prefix names (KiB, MiB, etc)
SIBytesPrefix
// Terminal means we're will print to terminal and can use ascii sequences
// Also we're will try to use terminal width
Terminal
// Static means progress bar will not update automaticly
Static
// ReturnSymbol - by default in terminal mode it's '\r'
ReturnSymbol
// Color by default is true when output is tty, but you can set to false for disabling colors
Color
// Hide the progress bar when finished, rather than leaving it up. By default it's false.
CleanOnFinish
// Round elapsed time to this precision. Defaults to time.Second.
TimeRound
)
const (
defaultBarWidth = 100
defaultRefreshRate = time.Millisecond * 200
)
// New creates new ProgressBar object
func New(total int) *ProgressBar {
return New64(int64(total))
}
// New64 creates new ProgressBar object using int64 as total
func New64(total int64) *ProgressBar {
pb := new(ProgressBar)
return pb.SetTotal(total)
}
// StartNew starts new ProgressBar with Default template
func StartNew(total int) *ProgressBar {
return New(total).Start()
}
// Start64 starts new ProgressBar with Default template. Using int64 as total.
func Start64(total int64) *ProgressBar {
return New64(total).Start()
}
var (
terminalWidth = termutil.TerminalWidth
isTerminal = isatty.IsTerminal
isCygwinTerminal = isatty.IsCygwinTerminal
)
// ProgressBar is the main object of bar
type ProgressBar struct {
current, total int64
width int
maxWidth int
mu sync.RWMutex
rm sync.Mutex
vars map[interface{}]interface{}
elements map[string]Element
output io.Writer
coutput io.Writer
nocoutput io.Writer
startTime time.Time
refreshRate time.Duration
tmpl *template.Template
state *State
buf *bytes.Buffer
ticker *time.Ticker
finish chan struct{}
finished bool
configured bool
err error
}
func (pb *ProgressBar) configure() {
if pb.configured {
return
}
pb.configured = true
if pb.vars == nil {
pb.vars = make(map[interface{}]interface{})
}
if pb.output == nil {
pb.output = os.Stderr
}
if pb.tmpl == nil {
pb.tmpl, pb.err = getTemplate(string(Default))
if pb.err != nil {
return
}
}
if pb.vars[Terminal] == nil {
if f, ok := pb.output.(*os.File); ok {
if isTerminal(f.Fd()) || isCygwinTerminal(f.Fd()) {
pb.vars[Terminal] = true
}
}
}
if pb.vars[ReturnSymbol] == nil {
if tm, ok := pb.vars[Terminal].(bool); ok && tm {
pb.vars[ReturnSymbol] = "\r"
}
}
if pb.vars[Color] == nil {
if tm, ok := pb.vars[Terminal].(bool); ok && tm {
pb.vars[Color] = true
}
}
if pb.refreshRate == 0 {
pb.refreshRate = defaultRefreshRate
}
if pb.vars[CleanOnFinish] == nil {
pb.vars[CleanOnFinish] = false
}
if f, ok := pb.output.(*os.File); ok {
pb.coutput = colorable.NewColorable(f)
} else {
pb.coutput = pb.output
}
pb.nocoutput = colorable.NewNonColorable(pb.output)
}
// Start starts the bar
func (pb *ProgressBar) Start() *ProgressBar {
pb.mu.Lock()
defer pb.mu.Unlock()
if pb.finish != nil {
return pb
}
pb.configure()
pb.finished = false
pb.state = nil
pb.startTime = time.Now()
if st, ok := pb.vars[Static].(bool); ok && st {
return pb
}
pb.finish = make(chan struct{})
pb.ticker = time.NewTicker(pb.refreshRate)
go pb.writer(pb.finish)
return pb
}
func (pb *ProgressBar) writer(finish chan struct{}) {
for {
select {
case <-pb.ticker.C:
pb.write(false)
case <-finish:
pb.ticker.Stop()
pb.write(true)
finish <- struct{}{}
return
}
}
}
// Write performs write to the output
func (pb *ProgressBar) Write() *ProgressBar {
pb.mu.RLock()
finished := pb.finished
pb.mu.RUnlock()
pb.write(finished)
return pb
}
func (pb *ProgressBar) write(finish bool) {
result, width := pb.render()
if pb.Err() != nil {
return
}
if pb.GetBool(Terminal) {
if r := (width - CellCount(result)); r > 0 {
result += strings.Repeat(" ", r)
}
}
if ret, ok := pb.Get(ReturnSymbol).(string); ok {
result = ret + result
if finish && ret == "\r" {
if pb.GetBool(CleanOnFinish) {
// "Wipe out" progress bar by overwriting one line with blanks
result = "\r" + color.New(color.Reset).Sprintf(strings.Repeat(" ", width)) + "\r"
} else {
result += "\n"
}
}
}
if pb.GetBool(Color) {
pb.coutput.Write([]byte(result))
} else {
pb.nocoutput.Write([]byte(result))
}
}
// Total return current total bar value
func (pb *ProgressBar) Total() int64 {
return atomic.LoadInt64(&pb.total)
}
// SetTotal sets the total bar value
func (pb *ProgressBar) SetTotal(value int64) *ProgressBar {
atomic.StoreInt64(&pb.total, value)
return pb
}
// AddTotal adds to the total bar value
func (pb *ProgressBar) AddTotal(value int64) *ProgressBar {
atomic.AddInt64(&pb.total, value)
return pb
}
// SetCurrent sets the current bar value
func (pb *ProgressBar) SetCurrent(value int64) *ProgressBar {
atomic.StoreInt64(&pb.current, value)
return pb
}
// Current return current bar value
func (pb *ProgressBar) Current() int64 {
return atomic.LoadInt64(&pb.current)
}
// Add adding given int64 value to bar value
func (pb *ProgressBar) Add64(value int64) *ProgressBar {
atomic.AddInt64(&pb.current, value)
return pb
}
// Add adding given int value to bar value
func (pb *ProgressBar) Add(value int) *ProgressBar {
return pb.Add64(int64(value))
}
// Increment atomically increments the progress
func (pb *ProgressBar) Increment() *ProgressBar {
return pb.Add64(1)
}
// Set sets any value by any key
func (pb *ProgressBar) Set(key, value interface{}) *ProgressBar {
pb.mu.Lock()
defer pb.mu.Unlock()
if pb.vars == nil {
pb.vars = make(map[interface{}]interface{})
}
pb.vars[key] = value
return pb
}
// Get return value by key
func (pb *ProgressBar) Get(key interface{}) interface{} {
pb.mu.RLock()
defer pb.mu.RUnlock()
if pb.vars == nil {
return nil
}
return pb.vars[key]
}
// GetBool return value by key and try to convert there to boolean
// If value doesn't set or not boolean - return false
func (pb *ProgressBar) GetBool(key interface{}) bool {
if v, ok := pb.Get(key).(bool); ok {
return v
}
return false
}
// SetWidth sets the bar width
// When given value <= 0 would be using the terminal width (if possible) or default value.
func (pb *ProgressBar) SetWidth(width int) *ProgressBar {
pb.mu.Lock()
pb.width = width
pb.mu.Unlock()
return pb
}
// SetMaxWidth sets the bar maximum width
// When given value <= 0 would be using the terminal width (if possible) or default value.
func (pb *ProgressBar) SetMaxWidth(maxWidth int) *ProgressBar {
pb.mu.Lock()
pb.maxWidth = maxWidth
pb.mu.Unlock()
return pb
}
// Width return the bar width
// It's current terminal width or settled over 'SetWidth' value.
func (pb *ProgressBar) Width() (width int) {
defer func() {
if r := recover(); r != nil {
width = defaultBarWidth
}
}()
pb.mu.RLock()
width = pb.width
maxWidth := pb.maxWidth
pb.mu.RUnlock()
if width <= 0 {
var err error
if width, err = terminalWidth(); err != nil {
return defaultBarWidth
}
}
if maxWidth > 0 && width > maxWidth {
width = maxWidth
}
return
}
func (pb *ProgressBar) SetRefreshRate(dur time.Duration) *ProgressBar {
pb.mu.Lock()
if dur > 0 {
pb.refreshRate = dur
}
pb.mu.Unlock()
return pb
}
// SetWriter sets the io.Writer. Bar will write in this writer
// By default this is os.Stderr
func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar {
pb.mu.Lock()
pb.output = w
pb.configured = false
pb.configure()
pb.mu.Unlock()
return pb
}
// StartTime return the time when bar started
func (pb *ProgressBar) StartTime() time.Time {
pb.mu.RLock()
defer pb.mu.RUnlock()
return pb.startTime
}
// Format convert int64 to string according to the current settings
func (pb *ProgressBar) Format(v int64) string {
if pb.GetBool(Bytes) {
return formatBytes(v, pb.GetBool(SIBytesPrefix))
}
return strconv.FormatInt(v, 10)
}
// Finish stops the bar
func (pb *ProgressBar) Finish() *ProgressBar {
pb.mu.Lock()
if pb.finished {
pb.mu.Unlock()
return pb
}
finishChan := pb.finish
pb.finished = true
pb.mu.Unlock()
if finishChan != nil {
finishChan <- struct{}{}
<-finishChan
pb.mu.Lock()
pb.finish = nil
pb.mu.Unlock()
}
return pb
}
// IsStarted indicates progress bar state
func (pb *ProgressBar) IsStarted() bool {
pb.mu.RLock()
defer pb.mu.RUnlock()
return pb.finish != nil
}
// SetTemplateString sets ProgressBar tempate string and parse it
func (pb *ProgressBar) SetTemplateString(tmpl string) *ProgressBar {
pb.mu.Lock()
defer pb.mu.Unlock()
pb.tmpl, pb.err = getTemplate(tmpl)
return pb
}
// SetTemplateString sets ProgressBarTempate and parse it
func (pb *ProgressBar) SetTemplate(tmpl ProgressBarTemplate) *ProgressBar {
return pb.SetTemplateString(string(tmpl))
}
// NewProxyReader creates a wrapper for given reader, but with progress handle
// Takes io.Reader or io.ReadCloser
// Also, it automatically switches progress bar to handle units as bytes
func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader {
pb.Set(Bytes, true)
return &Reader{r, pb}
}
// NewProxyWriter creates a wrapper for given writer, but with progress handle
// Takes io.Writer or io.WriteCloser
// Also, it automatically switches progress bar to handle units as bytes
func (pb *ProgressBar) NewProxyWriter(r io.Writer) *Writer {
pb.Set(Bytes, true)
return &Writer{r, pb}
}
func (pb *ProgressBar) render() (result string, width int) {
defer func() {
if r := recover(); r != nil {
pb.SetErr(fmt.Errorf("render panic: %v", r))
}
}()
pb.rm.Lock()
defer pb.rm.Unlock()
pb.mu.Lock()
pb.configure()
if pb.state == nil {
pb.state = &State{ProgressBar: pb}
pb.buf = bytes.NewBuffer(nil)
}
if pb.startTime.IsZero() {
pb.startTime = time.Now()
}
pb.state.id++
pb.state.finished = pb.finished
pb.state.time = time.Now()
pb.mu.Unlock()
pb.state.width = pb.Width()
width = pb.state.width
pb.state.total = pb.Total()
pb.state.current = pb.Current()
pb.buf.Reset()
if e := pb.tmpl.Execute(pb.buf, pb.state); e != nil {
pb.SetErr(e)
return "", 0
}
result = pb.buf.String()
aec := len(pb.state.recalc)
if aec == 0 {
// no adaptive elements
return
}
staticWidth := CellCount(result) - (aec * adElPlaceholderLen)
if pb.state.Width()-staticWidth <= 0 {
result = strings.Replace(result, adElPlaceholder, "", -1)
result = StripString(result, pb.state.Width())
} else {
pb.state.adaptiveElWidth = (width - staticWidth) / aec
for _, el := range pb.state.recalc {
result = strings.Replace(result, adElPlaceholder, el.ProgressElement(pb.state), 1)
}
}
pb.state.recalc = pb.state.recalc[:0]
return
}
// SetErr sets error to the ProgressBar
// Error will be available over Err()
func (pb *ProgressBar) SetErr(err error) *ProgressBar {
pb.mu.Lock()
pb.err = err
pb.mu.Unlock()
return pb
}
// Err return possible error
// When all ok - will be nil
// May contain template.Execute errors
func (pb *ProgressBar) Err() error {
pb.mu.RLock()
defer pb.mu.RUnlock()
return pb.err
}
// String return currrent string representation of ProgressBar
func (pb *ProgressBar) String() string {
res, _ := pb.render()
return res
}
// ProgressElement implements Element interface
func (pb *ProgressBar) ProgressElement(s *State, args ...string) string {
if s.IsAdaptiveWidth() {
pb.SetWidth(s.AdaptiveElWidth())
}
return pb.String()
}
// State represents the current state of bar
// Need for bar elements
type State struct {
*ProgressBar
id uint64
total, current int64
width, adaptiveElWidth int
finished, adaptive bool
time time.Time
recalc []Element
}
// Id it's the current state identifier
// - incremental
// - starts with 1
// - resets after finish/start
func (s *State) Id() uint64 {
return s.id
}
// Total it's bar int64 total
func (s *State) Total() int64 {
return s.total
}
// Value it's current value
func (s *State) Value() int64 {
return s.current
}
// Width of bar
func (s *State) Width() int {
return s.width
}
// AdaptiveElWidth - adaptive elements must return string with given cell count (when AdaptiveElWidth > 0)
func (s *State) AdaptiveElWidth() int {
return s.adaptiveElWidth
}
// IsAdaptiveWidth returns true when element must be shown as adaptive
func (s *State) IsAdaptiveWidth() bool {
return s.adaptive
}
// IsFinished return true when bar is finished
func (s *State) IsFinished() bool {
return s.finished
}
// IsFirst return true only in first render
func (s *State) IsFirst() bool {
return s.id == 1
}
// Time when state was created
func (s *State) Time() time.Time {
return s.time
}
+15
View File
@@ -0,0 +1,15 @@
package pb
var (
// Full - preset with all default available elements
// Example: 'Prefix 20/100 [-->______] 20% 1 p/s ETA 1m Suffix'
Full ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }} {{speed . }} {{rtime . "ETA %s"}}{{string . "suffix"}}`
// Default - preset like Full but without elapsed time
// Example: 'Prefix 20/100 [-->______] 20% 1 p/s ETA 1m Suffix'
Default ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }} {{speed . }}{{string . "suffix"}}`
// Simple - preset without speed and any timers. Only counters, bar and percents
// Example: 'Prefix 20/100 [-->______] 20% Suffix'
Simple ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }}{{string . "suffix"}}`
)
+83
View File
@@ -0,0 +1,83 @@
package pb
import (
"fmt"
"math"
"time"
"github.com/VividCortex/ewma"
)
var speedAddLimit = time.Second / 2
type speed struct {
ewma ewma.MovingAverage
lastStateId uint64
prevValue, startValue int64
prevTime, startTime time.Time
}
func (s *speed) value(state *State) float64 {
if s.ewma == nil {
s.ewma = ewma.NewMovingAverage()
}
if state.IsFirst() || state.Id() < s.lastStateId {
s.reset(state)
return 0
}
if state.Id() == s.lastStateId {
return s.ewma.Value()
}
if state.IsFinished() {
return s.absValue(state)
}
dur := state.Time().Sub(s.prevTime)
if dur < speedAddLimit {
return s.ewma.Value()
}
diff := math.Abs(float64(state.Value() - s.prevValue))
lastSpeed := diff / dur.Seconds()
s.prevTime = state.Time()
s.prevValue = state.Value()
s.lastStateId = state.Id()
s.ewma.Add(lastSpeed)
return s.ewma.Value()
}
func (s *speed) reset(state *State) {
s.lastStateId = state.Id()
s.startTime = state.Time()
s.prevTime = state.Time()
s.startValue = state.Value()
s.prevValue = state.Value()
s.ewma = ewma.NewMovingAverage()
}
func (s *speed) absValue(state *State) float64 {
if dur := state.Time().Sub(s.startTime); dur > 0 {
return float64(state.Value()) / dur.Seconds()
}
return 0
}
func getSpeedObj(state *State) (s *speed) {
if sObj, ok := state.Get(speedObj).(*speed); ok {
return sObj
}
s = new(speed)
state.Set(speedObj, s)
return
}
// ElementSpeed calculates current speed by EWMA
// Optionally can take one or two string arguments.
// First string will be used as value for format speed, default is "%s p/s".
// Second string will be used when speed not available, default is "? p/s"
// In template use as follows: {{speed .}} or {{speed . "%s per second"}} or {{speed . "%s ps" "..."}
var ElementSpeed ElementFunc = func(state *State, args ...string) string {
sp := getSpeedObj(state).value(state)
if sp == 0 {
return argsHelper(args).getNotEmptyOr(1, "? p/s")
}
return fmt.Sprintf(argsHelper(args).getNotEmptyOr(0, "%s p/s"), state.Format(int64(round(sp))))
}
+89
View File
@@ -0,0 +1,89 @@
package pb
import (
"math/rand"
"sync"
"text/template"
"github.com/fatih/color"
)
// ProgressBarTemplate that template string
type ProgressBarTemplate string
// New creates new bar from template
func (pbt ProgressBarTemplate) New(total int) *ProgressBar {
return New(total).SetTemplate(pbt)
}
// Start64 create and start new bar with given int64 total value
func (pbt ProgressBarTemplate) Start64(total int64) *ProgressBar {
return New64(total).SetTemplate(pbt).Start()
}
// Start create and start new bar with given int total value
func (pbt ProgressBarTemplate) Start(total int) *ProgressBar {
return pbt.Start64(int64(total))
}
var templateCacheMu sync.Mutex
var templateCache = make(map[string]*template.Template)
var defaultTemplateFuncs = template.FuncMap{
// colors
"black": color.New(color.FgBlack).SprintFunc(),
"red": color.New(color.FgRed).SprintFunc(),
"green": color.New(color.FgGreen).SprintFunc(),
"yellow": color.New(color.FgYellow).SprintFunc(),
"blue": color.New(color.FgBlue).SprintFunc(),
"magenta": color.New(color.FgMagenta).SprintFunc(),
"cyan": color.New(color.FgCyan).SprintFunc(),
"white": color.New(color.FgWhite).SprintFunc(),
"resetcolor": color.New(color.Reset).SprintFunc(),
"rndcolor": rndcolor,
"rnd": rnd,
}
func getTemplate(tmpl string) (t *template.Template, err error) {
templateCacheMu.Lock()
defer templateCacheMu.Unlock()
t = templateCache[tmpl]
if t != nil {
// found in cache
return
}
t = template.New("")
fillTemplateFuncs(t)
_, err = t.Parse(tmpl)
if err != nil {
t = nil
return
}
templateCache[tmpl] = t
return
}
func fillTemplateFuncs(t *template.Template) {
t.Funcs(defaultTemplateFuncs)
emf := make(template.FuncMap)
elementsM.Lock()
for k, v := range elements {
element := v
emf[k] = func(state *State, args ...string) string { return element.ProgressElement(state, args...) }
}
elementsM.Unlock()
t.Funcs(emf)
return
}
func rndcolor(s string) string {
c := rand.Intn(int(color.FgWhite-color.FgBlack)) + int(color.FgBlack)
return color.New(color.Attribute(c)).Sprint(s)
}
func rnd(args ...string) string {
if len(args) == 0 {
return ""
}
return args[rand.Intn(len(args))]
}
+56
View File
@@ -0,0 +1,56 @@
package termutil
import (
"errors"
"os"
"os/signal"
"sync"
)
var echoLocked bool
var echoLockMutex sync.Mutex
var errLocked = errors.New("terminal locked")
// RawModeOn switches terminal to raw mode
func RawModeOn() (quit chan struct{}, err error) {
echoLockMutex.Lock()
defer echoLockMutex.Unlock()
if echoLocked {
err = errLocked
return
}
if err = lockEcho(); err != nil {
return
}
echoLocked = true
quit = make(chan struct{}, 1)
go catchTerminate(quit)
return
}
// RawModeOff restore previous terminal state
func RawModeOff() (err error) {
echoLockMutex.Lock()
defer echoLockMutex.Unlock()
if !echoLocked {
return
}
if err = unlockEcho(); err != nil {
return
}
echoLocked = false
return
}
// listen exit signals and restore terminal state
func catchTerminate(quit chan struct{}) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, unlockSignals...)
defer signal.Stop(sig)
select {
case <-quit:
RawModeOff()
case <-sig:
RawModeOff()
}
}
+11
View File
@@ -0,0 +1,11 @@
// +build appengine
package termutil
import "errors"
// terminalWidth returns width of the terminal, which is not supported
// and should always failed on appengine classic which is a sandboxed PaaS.
func TerminalWidth() (int, error) {
return 0, errors.New("Not supported")
}
+9
View File
@@ -0,0 +1,9 @@
// +build darwin freebsd netbsd openbsd dragonfly
// +build !appengine
package termutil
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA
+7
View File
@@ -0,0 +1,7 @@
// +build linux
// +build !appengine
package termutil
const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS
+8
View File
@@ -0,0 +1,8 @@
// +build linux darwin freebsd netbsd openbsd dragonfly
// +build !appengine
package termutil
import "syscall"
const sysIoctl = syscall.SYS_IOCTL
+50
View File
@@ -0,0 +1,50 @@
package termutil
import (
"errors"
"os"
"syscall"
)
var (
consctl *os.File
// Plan 9 doesn't have syscall.SIGQUIT
unlockSignals = []os.Signal{
os.Interrupt, syscall.SIGTERM, syscall.SIGKILL,
}
)
// TerminalWidth returns width of the terminal.
func TerminalWidth() (int, error) {
return 0, errors.New("Not supported")
}
func lockEcho() error {
if consctl != nil {
return errors.New("consctl already open")
}
var err error
consctl, err = os.OpenFile("/dev/consctl", os.O_WRONLY, 0)
if err != nil {
return err
}
_, err = consctl.WriteString("rawon")
if err != nil {
consctl.Close()
consctl = nil
return err
}
return nil
}
func unlockEcho() error {
if consctl == nil {
return nil
}
if err := consctl.Close(); err != nil {
return err
}
consctl = nil
return nil
}
+8
View File
@@ -0,0 +1,8 @@
// +build solaris
// +build !appengine
package termutil
const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS
const sysIoctl = 54
+155
View File
@@ -0,0 +1,155 @@
// +build windows
package termutil
import (
"fmt"
"os"
"os/exec"
"strconv"
"syscall"
"unsafe"
)
var (
tty = os.Stdin
unlockSignals = []os.Signal{
os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL,
}
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
// GetConsoleScreenBufferInfo retrieves information about the
// specified console screen buffer.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
// GetConsoleMode retrieves the current input mode of a console's
// input buffer or the current output mode of a console screen buffer.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
getConsoleMode = kernel32.NewProc("GetConsoleMode")
// SetConsoleMode sets the input mode of a console's input buffer
// or the output mode of a console screen buffer.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
setConsoleMode = kernel32.NewProc("SetConsoleMode")
// SetConsoleCursorPosition sets the cursor position in the
// specified console screen buffer.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx
setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
mingw = isMingw()
)
type (
// Defines the coordinates of the upper left and lower right corners
// of a rectangle.
// See
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx
smallRect struct {
Left, Top, Right, Bottom int16
}
// Defines the coordinates of a character cell in a console screen
// buffer. The origin of the coordinate system (0,0) is at the top, left cell
// of the buffer.
// See
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx
coordinates struct {
X, Y int16
}
word int16
// Contains information about a console screen buffer.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx
consoleScreenBufferInfo struct {
dwSize coordinates
dwCursorPosition coordinates
wAttributes word
srWindow smallRect
dwMaximumWindowSize coordinates
}
)
// TerminalWidth returns width of the terminal.
func TerminalWidth() (width int, err error) {
if mingw {
return termWidthTPut()
}
return termWidthCmd()
}
func termWidthCmd() (width int, err error) {
var info consoleScreenBufferInfo
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0)
if e != 0 {
return 0, error(e)
}
return int(info.dwSize.X) - 1, nil
}
func isMingw() bool {
return os.Getenv("MINGW_PREFIX") != "" || os.Getenv("MSYSTEM") == "MINGW64"
}
func termWidthTPut() (width int, err error) {
// TODO: maybe anybody knows a better way to get it on mintty...
var res []byte
cmd := exec.Command("tput", "cols")
cmd.Stdin = os.Stdin
if res, err = cmd.CombinedOutput(); err != nil {
return 0, fmt.Errorf("%s: %v", string(res), err)
}
if len(res) > 1 {
res = res[:len(res)-1]
}
return strconv.Atoi(string(res))
}
func getCursorPos() (pos coordinates, err error) {
var info consoleScreenBufferInfo
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0)
if e != 0 {
return info.dwCursorPosition, error(e)
}
return info.dwCursorPosition, nil
}
func setCursorPos(pos coordinates) error {
_, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0)
if e != 0 {
return error(e)
}
return nil
}
var oldState word
func lockEcho() (err error) {
if _, _, e := syscall.Syscall(getConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&oldState)), 0); e != 0 {
err = fmt.Errorf("Can't get terminal settings: %v", e)
return
}
newState := oldState
const ENABLE_ECHO_INPUT = 0x0004
const ENABLE_LINE_INPUT = 0x0002
newState = newState & (^(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT))
if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(newState), 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings: %v", e)
return
}
return
}
func unlockEcho() (err error) {
if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(oldState), 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings")
}
return
}
+76
View File
@@ -0,0 +1,76 @@
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
// +build !appengine
package termutil
import (
"fmt"
"os"
"syscall"
"unsafe"
)
var (
tty *os.File
unlockSignals = []os.Signal{
os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL,
}
)
type window struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
func init() {
var err error
tty, err = os.Open("/dev/tty")
if err != nil {
tty = os.Stdin
}
}
// TerminalWidth returns width of the terminal.
func TerminalWidth() (int, error) {
w := new(window)
res, _, err := syscall.Syscall(sysIoctl,
tty.Fd(),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(w)),
)
if int(res) == -1 {
return 0, err
}
return int(w.Col), nil
}
var oldState syscall.Termios
func lockEcho() (err error) {
fd := tty.Fd()
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't get terminal settings: %v", e)
return
}
newState := oldState
newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings: %v", e)
return
}
return
}
func unlockEcho() (err error) {
fd := tty.Fd()
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings")
}
return
}
+116
View File
@@ -0,0 +1,116 @@
package pb
import (
"bytes"
"fmt"
"github.com/mattn/go-runewidth"
"math"
"regexp"
//"unicode/utf8"
)
const (
_KiB = 1024
_MiB = 1048576
_GiB = 1073741824
_TiB = 1099511627776
_kB = 1e3
_MB = 1e6
_GB = 1e9
_TB = 1e12
)
var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9;]+\x6d")
func CellCount(s string) int {
n := runewidth.StringWidth(s)
for _, sm := range ctrlFinder.FindAllString(s, -1) {
n -= runewidth.StringWidth(sm)
}
return n
}
func StripString(s string, w int) string {
l := CellCount(s)
if l <= w {
return s
}
var buf = bytes.NewBuffer(make([]byte, 0, len(s)))
StripStringToBuffer(s, w, buf)
return buf.String()
}
func StripStringToBuffer(s string, w int, buf *bytes.Buffer) {
var seqs = ctrlFinder.FindAllStringIndex(s, -1)
var maxWidthReached bool
mainloop:
for i, r := range s {
for _, seq := range seqs {
if i >= seq[0] && i < seq[1] {
buf.WriteRune(r)
continue mainloop
}
}
if rw := CellCount(string(r)); rw <= w && !maxWidthReached {
w -= rw
buf.WriteRune(r)
} else {
maxWidthReached = true
}
}
for w > 0 {
buf.WriteByte(' ')
w--
}
return
}
func round(val float64) (newVal float64) {
roundOn := 0.5
places := 0
var round float64
pow := math.Pow(10, float64(places))
digit := pow * val
_, div := math.Modf(digit)
if div >= roundOn {
round = math.Ceil(digit)
} else {
round = math.Floor(digit)
}
newVal = round / pow
return
}
// Convert bytes to human readable string. Like a 2 MiB, 64.2 KiB, or 2 MB, 64.2 kB
// if useSIPrefix is set to true
func formatBytes(i int64, useSIPrefix bool) (result string) {
if !useSIPrefix {
switch {
case i >= _TiB:
result = fmt.Sprintf("%.02f TiB", float64(i)/_TiB)
case i >= _GiB:
result = fmt.Sprintf("%.02f GiB", float64(i)/_GiB)
case i >= _MiB:
result = fmt.Sprintf("%.02f MiB", float64(i)/_MiB)
case i >= _KiB:
result = fmt.Sprintf("%.02f KiB", float64(i)/_KiB)
default:
result = fmt.Sprintf("%d B", i)
}
} else {
switch {
case i >= _TB:
result = fmt.Sprintf("%.02f TB", float64(i)/_TB)
case i >= _GB:
result = fmt.Sprintf("%.02f GB", float64(i)/_GB)
case i >= _MB:
result = fmt.Sprintf("%.02f MB", float64(i)/_MB)
case i >= _kB:
result = fmt.Sprintf("%.02f kB", float64(i)/_kB)
default:
result = fmt.Sprintf("%d B", i)
}
}
return
}
+5
View File
@@ -66,6 +66,8 @@ github.com/RoaringBitmap/roaring
github.com/Shopify/sarama
# github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6
github.com/StackExchange/wmi
# github.com/VividCortex/ewma v1.1.1
github.com/VividCortex/ewma
# github.com/aliyun/alibaba-cloud-sdk-go v1.61.684
github.com/aliyun/alibaba-cloud-sdk-go/sdk
github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth
@@ -206,6 +208,9 @@ github.com/boombuler/barcode/utils
github.com/bradfitz/iter
# github.com/c-bata/go-prompt v0.2.1
github.com/c-bata/go-prompt
# github.com/cheggaaa/pb/v3 v3.0.8
github.com/cheggaaa/pb/v3
github.com/cheggaaa/pb/v3/termutil
# github.com/coredns/coredns v1.3.0
github.com/coredns/coredns/core/dnsserver
github.com/coredns/coredns/coremain