-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprogress.go
232 lines (204 loc) · 5.91 KB
/
progress.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Streamy
// For the full copyright and license information, please view the LICENSE.txt file.
package streamy
import (
"errors"
"math"
"sync"
"time"
)
// ErrProgressStopped means that a progress stopped (i.e. by calling Stop method).
var ErrProgressStopped = errors.New("stopped")
// Progress implements the io.Writer interface for tracking bytes.
type Progress struct {
bytesWritten int64
totalBytes int64
statsEnabled bool
statsMode ProgressStatsMode
statsBytes [][]int64
statsBytesLimit int
statsFrom int64
statsTo int64
controlsEnabled bool
controlsClosedCh <-chan struct{}
controlsStopCh chan struct{}
rwMu sync.RWMutex
stopped bool
}
// Write implements the io.Writer interface.
func (progress *Progress) Write(p []byte) (n int, err error) {
// Init vars
n = len(p)
ni := int64(n)
progress.rwMu.RLock()
defer progress.rwMu.RUnlock()
// Update written bytes
progress.bytesWritten += ni
// Update stats
if progress.statsEnabled {
unixNano := time.Now().UnixNano()
if progress.statsFrom == 0 {
// Note that initial start time (from writer) might be earlier.
progress.statsFrom = unixNano
}
// Shift bytes
if len(progress.statsBytes) >= progress.statsBytesLimit {
progress.statsBytes = append(progress.statsBytes[1:progress.statsBytesLimit], []int64{unixNano, ni})
} else {
progress.statsBytes = append(progress.statsBytes, []int64{unixNano, ni})
}
}
// Check controls
if progress.controlsEnabled {
select {
case <-progress.controlsClosedCh:
case _, ok := <-progress.controlsStopCh:
if !ok {
progress.stopped = true
return 0, ErrProgressStopped
}
}
}
// Update stats
if progress.statsEnabled {
// Any delay in this block should be added to the stats.
progress.statsTo = time.Now().UnixNano()
}
return n, nil
}
// BytesWritten returns the number of bytes written.
func (progress *Progress) BytesWritten() int64 {
return progress.bytesWritten
}
// SetTotalSize sets the total size by the given size and binary unit.
func (progress *Progress) SetTotalSize(size int64, unit BinaryUnit) {
progress.totalBytes = size * int64(unit)
}
// TotalBytes returns the total number of bytes.
func (progress *Progress) TotalBytes() int64 {
return progress.totalBytes
}
// EnableStats enables the progress stats.
func (progress *Progress) EnableStats(mode ProgressStatsMode) error {
progress.rwMu.Lock()
defer progress.rwMu.Unlock()
if !progress.statsEnabled {
switch mode {
case ProgressStatsModeSimple:
progress.statsMode = ProgressStatsModeSimple
progress.statsBytesLimit = 10
default:
return errors.New("invalid mode")
}
}
progress.statsEnabled = true
return nil
}
// DisableStats disables the progress stats.
func (progress *Progress) DisableStats() {
progress.rwMu.Lock()
defer progress.rwMu.Unlock()
if progress.statsEnabled {
progress.statsBytes = nil
progress.statsBytesLimit = 0
progress.statsFrom = 0
progress.statsTo = 0
}
progress.statsEnabled = false
}
// EnableControls enables the progress controls such as pause and stop.
func (progress *Progress) EnableControls() error {
progress.rwMu.Lock()
defer progress.rwMu.Unlock()
if !progress.controlsEnabled {
// Ref: https://go.dev/ref/spec#Receive_operator
// https://go.dev/ref/spec#Send_statements
// https://go.dev/ref/spec#Close
ch := make(chan struct{})
close(ch)
progress.controlsClosedCh = ch
progress.controlsStopCh = make(chan struct{})
}
progress.controlsEnabled = true
return nil
}
// Stop stops the the progress writer.
func (progress *Progress) Stop() {
progress.rwMu.Lock()
defer progress.rwMu.Unlock()
if !progress.controlsEnabled || progress.stopped {
return
}
close(progress.controlsStopCh)
}
// Stats returns the progress stats.
func (progress *Progress) Stats() ProgressStats {
if !progress.statsEnabled {
// Do not return anything to avoid confusion
return ProgressStats{}
}
// Init stats
stats := ProgressStats{
BytesWritten: progress.bytesWritten,
TotalBytes: progress.totalBytes,
Took: time.Duration(progress.statsTo - progress.statsFrom),
}
// Calculate the percentage
// Note that totalBytes is given by the user and it can be 0 (see progress.SetTotalSize).
if progress.totalBytes > 0 {
if progress.totalBytes == progress.bytesWritten {
stats.Percentage = 100
} else {
stats.Percentage = int((float64(progress.bytesWritten) / float64(progress.totalBytes)) * 100)
}
}
// Calculate the bytes per second
switch progress.statsMode {
case ProgressStatsModeSimple:
var first int64 = 0
var last int64 = 0
var total int64 = 0
for i, l := 0, len(progress.statsBytes); i < l; i++ {
if i == 0 {
first = progress.statsBytes[i][0]
}
if i+1 == l { // The length might be 1 so don't use else.
last = progress.statsBytes[i][0]
}
total += progress.statsBytes[i][1]
}
diff := last - first
if stats.Took.Seconds() > 1 {
stats.BytesPerSecond = int64(math.Round(float64(total) / time.Duration(diff).Seconds()))
} else {
stats.BytesPerSecond = stats.BytesWritten
}
}
// Calculate remaining seconds
// Note that totalBytes is given by the user and it can be 0 (see progress.SetTotalSize).
if progress.totalBytes > 0 {
if progress.totalBytes == progress.bytesWritten {
stats.Remaining = 0
} else if stats.BytesPerSecond > 0 {
stats.Remaining = time.Duration(math.Ceil(float64(progress.totalBytes-progress.bytesWritten)/float64(stats.BytesPerSecond))) * time.Second
}
}
return stats
}
// ProgressStats represents the progress stats.
type ProgressStats struct {
TotalBytes int64
BytesWritten int64
BytesPerSecond int64
Took time.Duration
Remaining time.Duration
Percentage int
}
// ProgressStatsMode represents a progress stats mode.
type ProgressStatsMode struct {
mode uint8
}
var (
// ProgressStatsModeSimple represents the simple progress stats mode.
ProgressStatsModeSimple = ProgressStatsMode{mode: 0}
)