2015-01-04 17:23:00 +00:00
|
|
|
package restic
|
|
|
|
|
|
|
|
import (
|
2015-01-04 19:11:32 +00:00
|
|
|
"fmt"
|
2016-08-25 20:13:47 +00:00
|
|
|
"os"
|
2017-10-26 06:46:56 +00:00
|
|
|
"strconv"
|
2015-01-04 17:23:00 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
2016-08-28 20:19:48 +00:00
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
2015-01-04 17:23:00 +00:00
|
|
|
)
|
|
|
|
|
2017-10-26 06:46:56 +00:00
|
|
|
// minTickerTime limits how often the progress ticker is updated. It can be
|
|
|
|
// overridden using the RESTIC_PROGRESS_FPS (frames per second) environment
|
|
|
|
// variable.
|
|
|
|
var minTickerTime = time.Second / 60
|
2015-08-18 07:26:10 +00:00
|
|
|
|
2016-08-25 20:13:47 +00:00
|
|
|
var isTerminal = terminal.IsTerminal(int(os.Stdout.Fd()))
|
|
|
|
var forceUpdateProgress = make(chan bool)
|
|
|
|
|
2017-10-26 06:46:56 +00:00
|
|
|
func init() {
|
|
|
|
fps, err := strconv.ParseInt(os.Getenv("RESTIC_PROGRESS_FPS"), 10, 64)
|
|
|
|
if err == nil && fps >= 1 {
|
|
|
|
if fps > 60 {
|
|
|
|
fps = 60
|
|
|
|
}
|
|
|
|
minTickerTime = time.Second / time.Duration(fps)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-25 15:03:48 +00:00
|
|
|
// Progress reports progress on an operation.
|
2015-01-04 17:23:00 +00:00
|
|
|
type Progress struct {
|
2015-03-22 13:19:16 +00:00
|
|
|
OnStart func()
|
2015-02-07 14:27:09 +00:00
|
|
|
OnUpdate ProgressFunc
|
|
|
|
OnDone ProgressFunc
|
|
|
|
fnM sync.Mutex
|
2015-01-04 17:23:00 +00:00
|
|
|
|
2015-08-18 07:26:10 +00:00
|
|
|
cur Stat
|
|
|
|
curM sync.Mutex
|
|
|
|
start time.Time
|
|
|
|
c *time.Ticker
|
|
|
|
cancel chan struct{}
|
2016-05-08 11:04:58 +00:00
|
|
|
o *sync.Once
|
2015-08-18 07:26:10 +00:00
|
|
|
d time.Duration
|
|
|
|
lastUpdate time.Time
|
2015-01-04 17:23:00 +00:00
|
|
|
|
|
|
|
running bool
|
|
|
|
}
|
|
|
|
|
2017-05-25 15:03:48 +00:00
|
|
|
// Stat captures newly done parts of the operation.
|
2015-01-04 17:23:00 +00:00
|
|
|
type Stat struct {
|
2015-04-26 01:54:35 +00:00
|
|
|
Files uint64
|
|
|
|
Dirs uint64
|
|
|
|
Bytes uint64
|
|
|
|
Trees uint64
|
|
|
|
Blobs uint64
|
|
|
|
Errors uint64
|
2015-01-04 17:23:00 +00:00
|
|
|
}
|
|
|
|
|
2017-05-25 15:03:48 +00:00
|
|
|
// ProgressFunc is used to report progress back to the user.
|
2015-01-04 17:23:00 +00:00
|
|
|
type ProgressFunc func(s Stat, runtime time.Duration, ticker bool)
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
// NewProgress returns a new progress reporter. When Start() is called, the
|
2015-03-22 13:19:16 +00:00
|
|
|
// function OnStart is executed once. Afterwards the function OnUpdate is
|
|
|
|
// called when new data arrives or at least every d interval. The function
|
|
|
|
// OnDone is called when Done() is called. Both functions are called
|
|
|
|
// synchronously and can use shared state.
|
2016-08-25 20:13:47 +00:00
|
|
|
func NewProgress() *Progress {
|
|
|
|
var d time.Duration
|
2017-05-25 15:03:48 +00:00
|
|
|
if isTerminal {
|
2016-08-25 20:13:47 +00:00
|
|
|
d = time.Second
|
|
|
|
}
|
2015-01-04 17:23:00 +00:00
|
|
|
return &Progress{d: d}
|
|
|
|
}
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
// Start resets and runs the progress reporter.
|
2015-01-04 17:23:00 +00:00
|
|
|
func (p *Progress) Start() {
|
2015-03-22 13:12:51 +00:00
|
|
|
if p == nil || p.running {
|
2015-01-04 17:23:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-08 11:04:58 +00:00
|
|
|
p.o = &sync.Once{}
|
2015-01-04 17:23:00 +00:00
|
|
|
p.cancel = make(chan struct{})
|
|
|
|
p.running = true
|
|
|
|
p.Reset()
|
|
|
|
p.start = time.Now()
|
2017-05-25 15:03:48 +00:00
|
|
|
p.c = nil
|
|
|
|
if p.d != 0 {
|
|
|
|
p.c = time.NewTicker(p.d)
|
|
|
|
}
|
2015-01-04 17:23:00 +00:00
|
|
|
|
2015-03-22 13:19:16 +00:00
|
|
|
if p.OnStart != nil {
|
|
|
|
p.OnStart()
|
|
|
|
}
|
|
|
|
|
2015-01-04 17:23:00 +00:00
|
|
|
go p.reporter()
|
|
|
|
}
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
// Reset resets all statistic counters to zero.
|
|
|
|
func (p *Progress) Reset() {
|
|
|
|
if p == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !p.running {
|
|
|
|
panic("resetting a non-running Progress")
|
|
|
|
}
|
|
|
|
|
|
|
|
p.curM.Lock()
|
|
|
|
p.cur = Stat{}
|
|
|
|
p.curM.Unlock()
|
|
|
|
}
|
|
|
|
|
2015-01-04 17:23:00 +00:00
|
|
|
// Report adds the statistics from s to the current state and tries to report
|
|
|
|
// the accumulated statistics via the feedback channel.
|
|
|
|
func (p *Progress) Report(s Stat) {
|
|
|
|
if p == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !p.running {
|
|
|
|
panic("reporting in a non-running Progress")
|
|
|
|
}
|
|
|
|
|
|
|
|
p.curM.Lock()
|
|
|
|
p.cur.Add(s)
|
|
|
|
cur := p.cur
|
2015-08-18 07:26:10 +00:00
|
|
|
needUpdate := false
|
2016-08-25 20:13:47 +00:00
|
|
|
if isTerminal && time.Since(p.lastUpdate) > minTickerTime {
|
2015-08-18 07:26:10 +00:00
|
|
|
p.lastUpdate = time.Now()
|
|
|
|
needUpdate = true
|
|
|
|
}
|
2015-01-04 17:23:00 +00:00
|
|
|
p.curM.Unlock()
|
|
|
|
|
2015-08-18 07:26:10 +00:00
|
|
|
if needUpdate {
|
|
|
|
p.updateProgress(cur, false)
|
|
|
|
}
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Progress) updateProgress(cur Stat, ticker bool) {
|
|
|
|
if p.OnUpdate == nil {
|
|
|
|
return
|
2015-01-04 17:23:00 +00:00
|
|
|
}
|
2015-04-30 00:59:06 +00:00
|
|
|
|
|
|
|
p.fnM.Lock()
|
|
|
|
p.OnUpdate(cur, time.Since(p.start), ticker)
|
|
|
|
p.fnM.Unlock()
|
2015-01-04 17:23:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Progress) reporter() {
|
|
|
|
if p == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-08-25 20:13:47 +00:00
|
|
|
updateProgress := func() {
|
|
|
|
p.curM.Lock()
|
|
|
|
cur := p.cur
|
|
|
|
p.curM.Unlock()
|
|
|
|
p.updateProgress(cur, true)
|
|
|
|
}
|
|
|
|
|
2017-05-25 15:03:48 +00:00
|
|
|
var ticker <-chan time.Time
|
|
|
|
if p.c != nil {
|
|
|
|
ticker = p.c.C
|
|
|
|
}
|
|
|
|
|
2015-01-04 17:23:00 +00:00
|
|
|
for {
|
|
|
|
select {
|
2017-05-25 15:03:48 +00:00
|
|
|
case <-ticker:
|
2016-08-25 20:13:47 +00:00
|
|
|
updateProgress()
|
|
|
|
case <-forceUpdateProgress:
|
|
|
|
updateProgress()
|
2015-01-04 17:23:00 +00:00
|
|
|
case <-p.cancel:
|
2017-05-25 15:03:48 +00:00
|
|
|
if p.c != nil {
|
|
|
|
p.c.Stop()
|
|
|
|
}
|
2015-01-04 17:23:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Done closes the progress report.
|
|
|
|
func (p *Progress) Done() {
|
2015-03-22 13:12:51 +00:00
|
|
|
if p == nil || !p.running {
|
2015-01-04 17:23:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
p.running = false
|
|
|
|
p.o.Do(func() {
|
|
|
|
close(p.cancel)
|
|
|
|
})
|
2015-01-04 17:23:00 +00:00
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
cur := p.cur
|
2015-01-04 17:23:00 +00:00
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
if p.OnDone != nil {
|
|
|
|
p.fnM.Lock()
|
2016-08-15 18:15:17 +00:00
|
|
|
p.OnUpdate(cur, time.Since(p.start), false)
|
2015-04-30 00:59:06 +00:00
|
|
|
p.OnDone(cur, time.Since(p.start), false)
|
|
|
|
p.fnM.Unlock()
|
2015-01-04 17:23:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add accumulates other into s.
|
|
|
|
func (s *Stat) Add(other Stat) {
|
|
|
|
s.Bytes += other.Bytes
|
|
|
|
s.Dirs += other.Dirs
|
|
|
|
s.Files += other.Files
|
2015-02-21 14:32:48 +00:00
|
|
|
s.Trees += other.Trees
|
|
|
|
s.Blobs += other.Blobs
|
2015-04-26 01:54:35 +00:00
|
|
|
s.Errors += other.Errors
|
2015-01-04 17:23:00 +00:00
|
|
|
}
|
2015-01-04 19:11:32 +00:00
|
|
|
|
|
|
|
func (s Stat) String() string {
|
|
|
|
b := float64(s.Bytes)
|
|
|
|
var str string
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case s.Bytes > 1<<40:
|
|
|
|
str = fmt.Sprintf("%.3f TiB", b/(1<<40))
|
|
|
|
case s.Bytes > 1<<30:
|
|
|
|
str = fmt.Sprintf("%.3f GiB", b/(1<<30))
|
|
|
|
case s.Bytes > 1<<20:
|
|
|
|
str = fmt.Sprintf("%.3f MiB", b/(1<<20))
|
|
|
|
case s.Bytes > 1<<10:
|
|
|
|
str = fmt.Sprintf("%.3f KiB", b/(1<<10))
|
|
|
|
default:
|
|
|
|
str = fmt.Sprintf("%dB", s.Bytes)
|
|
|
|
}
|
|
|
|
|
2015-04-26 01:54:35 +00:00
|
|
|
return fmt.Sprintf("Stat(%d files, %d dirs, %v trees, %v blobs, %d errors, %v)",
|
|
|
|
s.Files, s.Dirs, s.Trees, s.Blobs, s.Errors, str)
|
2015-01-04 19:11:32 +00:00
|
|
|
}
|