Skip to content

Commit 12a1f8e

Browse files
committed
feat: add progress bar for large uploads
1 parent 6abc953 commit 12a1f8e

File tree

4 files changed

+111
-21
lines changed

4 files changed

+111
-21
lines changed

cmd/client/cmd/progress.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"golang.org/x/term"
11+
)
12+
13+
type ProgressReader struct {
14+
r io.Reader
15+
size int64
16+
read int64
17+
start time.Time
18+
lastDraw time.Time
19+
lastLen int
20+
tty bool
21+
done bool
22+
}
23+
24+
func NewProgressReader(r io.Reader, size int64) *ProgressReader {
25+
return &ProgressReader{
26+
r: r,
27+
size: size,
28+
tty: term.IsTerminal(int(os.Stderr.Fd())),
29+
}
30+
}
31+
32+
func (pr *ProgressReader) Read(buf []byte) (n int, err error) {
33+
if pr.start.IsZero() {
34+
pr.start = time.Now()
35+
}
36+
n, err = pr.r.Read(buf)
37+
pr.read += int64(n)
38+
if pr.tty && time.Since(pr.lastDraw) >= 100*time.Millisecond {
39+
pr.Render()
40+
pr.lastDraw = time.Now()
41+
}
42+
return n, err
43+
}
44+
45+
func (pr *ProgressReader) Render() {
46+
const barWidth = 20
47+
elapsed := time.Since(pr.start)
48+
49+
var rate float64
50+
if elapsed >= time.Millisecond {
51+
rate = float64(pr.read) / elapsed.Seconds()
52+
}
53+
rateStr := formatBytes(int64(rate)) + "/s"
54+
55+
var line string
56+
if pr.size > 0 {
57+
pct := float64(pr.read) / float64(pr.size) * 100
58+
filled := int(pct / 100 * barWidth)
59+
if filled > barWidth {
60+
filled = barWidth
61+
}
62+
bar := strings.Repeat("#", filled)
63+
if filled < barWidth {
64+
bar += strings.Repeat(" ", barWidth-filled-1)
65+
}
66+
line = fmt.Sprintf("%s / %s [%s] %.0f%% at %s",
67+
formatBytes(pr.read), formatBytes(pr.size), bar, pct, rateStr)
68+
} else {
69+
line = fmt.Sprintf("%s at %s", formatBytes(pr.read), rateStr)
70+
}
71+
72+
_, _ = fmt.Fprintf(os.Stderr, "\r%s", line)
73+
pr.lastLen = len(line)
74+
}
75+
76+
func formatBytes(n int64) string {
77+
const unit = 1024
78+
if n < unit {
79+
return fmt.Sprintf("%d B", n)
80+
}
81+
div, exp := int64(unit), 0
82+
for v := n / unit; v >= unit; v /= unit {
83+
div *= unit
84+
exp++
85+
}
86+
return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
87+
}
88+
89+
func (pr *ProgressReader) Done() {
90+
if pr.tty && !pr.done {
91+
_, _ = fmt.Fprintf(os.Stderr, "\r%s\r", strings.Repeat(" ", pr.lastLen))
92+
pr.done = true
93+
}
94+
}

cmd/client/cmd/root.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/gen2brain/beeep"
2121
"github.com/spf13/cobra"
2222
"github.com/spf13/viper"
23+
"golang.org/x/term"
2324
)
2425

2526
var RootCmd = &cobra.Command{
@@ -35,6 +36,7 @@ cat main.go | clip -l go`,
3536
RunE: func(cmd *cobra.Command, args []string) error {
3637
var r io.Reader
3738
var filename, source string
39+
var size int64 = -1
3840

3941
switch {
4042
case len(args) > 0 && args[0] != "-":
@@ -45,32 +47,35 @@ cat main.go | clip -l go`,
4547
defer func(f *os.File) {
4648
_ = f.Close()
4749
}(f)
50+
if info, err := f.Stat(); err == nil {
51+
size = info.Size()
52+
}
4853
r = f
4954
filename = args[0]
5055
source = filepath.Base(args[0])
5156
case len(args) > 0 && args[0] == "-":
5257
r = os.Stdin
5358
source = "stdin"
5459
default:
55-
stat, _ := os.Stdin.Stat()
56-
if (stat.Mode() & os.ModeCharDevice) == 0 {
60+
if !term.IsTerminal(int(os.Stdin.Fd())) {
5761
r = os.Stdin
5862
source = "stdin"
5963
} else {
6064
text, err := clipboard.ReadAll()
6165
if err != nil {
6266
return err
6367
}
68+
size = int64(len(text))
6469
r = strings.NewReader(text)
6570
source = "clipboard"
6671
}
6772
}
6873

69-
return upload(cmd, r, filename, source)
74+
return upload(cmd, r, filename, source, size)
7075
},
7176
}
7277

73-
func upload(cmd *cobra.Command, r io.Reader, filename, source string) error {
78+
func upload(cmd *cobra.Command, r io.Reader, filename, source string, size int64) error {
7479
contentType := "application/octet-stream"
7580
if ext := filepath.Ext(filename); ext != "" {
7681
if t := mime.TypeByExtension(ext); t != "" {
@@ -86,7 +91,9 @@ func upload(cmd *cobra.Command, r io.Reader, filename, source string) error {
8691
}
8792
r = io.MultiReader(bytes.NewReader(head), r)
8893
}
89-
body := r
94+
pr := NewProgressReader(r, size)
95+
defer pr.Done()
96+
body := pr
9097

9198
serverURL := viper.GetString("url")
9299
if serverURL == "" {
@@ -133,6 +140,7 @@ func upload(cmd *cobra.Command, r io.Reader, filename, source string) error {
133140
return err
134141
}
135142
uploadedURL := strings.TrimSpace(string(resBody))
143+
pr.Done()
136144
fmt.Println(uploadedURL)
137145

138146
if viper.GetBool("history") {

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/gen2brain/beeep v0.11.2
99
github.com/spf13/cobra v1.10.2
1010
github.com/spf13/viper v1.21.0
11+
golang.org/x/term v0.40.0
1112
)
1213

1314
require (
@@ -27,7 +28,6 @@ require (
2728
github.com/sagikazarmark/locafero v0.12.0 // indirect
2829
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
2930
github.com/sergeymakinen/go-ico v1.0.0 // indirect
30-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
3131
github.com/spf13/afero v1.15.0 // indirect
3232
github.com/spf13/cast v1.10.0 // indirect
3333
github.com/spf13/pflag v1.0.10 // indirect
@@ -36,4 +36,5 @@ require (
3636
go.yaml.in/yaml/v3 v3.0.4 // indirect
3737
golang.org/x/sys v0.41.0 // indirect
3838
golang.org/x/text v0.34.0 // indirect
39+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
3940
)

go.sum

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
44
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
55
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
66
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
7-
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
87
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
98
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
109
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
1110
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1211
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1312
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14-
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
1513
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
1614
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
1715
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
@@ -25,11 +23,8 @@ github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWD
2523
github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
2624
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
2725
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
28-
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
29-
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
3026
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
3127
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
32-
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
3328
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
3429
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
3530
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
@@ -53,18 +48,12 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
5348
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
5449
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
5550
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
56-
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
57-
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
5851
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
5952
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
6053
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
6154
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
62-
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
63-
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
6455
github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80=
6556
github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
66-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
67-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
6857
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
6958
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
7059
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -92,12 +81,10 @@ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+a
9281
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
9382
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
9483
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
96-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
9784
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
9885
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
99-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
100-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
86+
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
87+
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
10188
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
10289
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
10390
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

0 commit comments

Comments
 (0)