@@ -4,9 +4,12 @@ import (
44 gotar "archive/tar"
55 "bufio"
66 "compress/gzip"
7+ "encoding/json"
78 "errors"
89 "fmt"
910 "io"
11+ "net/http"
12+ "net/url"
1013 "os"
1114 gopath "path"
1215 "path/filepath"
@@ -15,11 +18,13 @@ import (
1518 "github.com/ipfs/kubo/core/commands/cmdenv"
1619 "github.com/ipfs/kubo/core/commands/cmdutils"
1720 "github.com/ipfs/kubo/core/commands/e"
21+ fsrepo "github.com/ipfs/kubo/repo/fsrepo"
1822
1923 "github.com/cheggaaa/pb"
2024 "github.com/ipfs/boxo/files"
2125 "github.com/ipfs/boxo/tar"
2226 cmds "github.com/ipfs/go-ipfs-cmds"
27+ manet "github.com/multiformats/go-multiaddr/net"
2328)
2429
2530var ErrInvalidCompressionLevel = errors .New ("compression level must be between 1 and 9" )
@@ -29,6 +34,7 @@ const (
2934 archiveOptionName = "archive"
3035 compressOptionName = "compress"
3136 compressionLevelOptionName = "compression-level"
37+ getTotalBlocksKey = "_getTotalBlocks"
3238)
3339
3440var GetCmd = & cmds.Command {
@@ -61,8 +67,29 @@ may also specify the level of compression by specifying '-l=<1-9>'.
6167 cmds .BoolOption (progressOptionName , "p" , "Stream progress data." ).WithDefault (true ),
6268 },
6369 PreRun : func (req * cmds.Request , env cmds.Environment ) error {
64- _ , err := getCompressOptions (req )
65- return err
70+ if _ , err := getCompressOptions (req ); err != nil {
71+ return err
72+ }
73+
74+ progress , _ := req .Options [progressOptionName ].(bool )
75+ if ! progress {
76+ return nil
77+ }
78+
79+ baseURL , err := getAPIBaseURL (env )
80+ if err != nil {
81+ return nil
82+ }
83+
84+ rootCID , err := resolveRootCID (baseURL , req .Arguments [0 ])
85+ if err != nil || rootCID == "" {
86+ return nil
87+ }
88+
89+ totalBlocks , files := fetchStatInfo (baseURL , rootCID )
90+ req .Options [getTotalBlocksKey ] = totalBlocks
91+ printGetProgress (os .Stderr , rootCID , files , totalBlocks )
92+ return nil
6693 },
6794 Run : func (req * cmds.Request , res cmds.ResponseEmitter , env cmds.Environment ) error {
6895 ctx := req .Context
@@ -141,6 +168,10 @@ may also specify the level of compression by specifying '-l=<1-9>'.
141168
142169 archive , _ := req .Options [archiveOptionName ].(bool )
143170 progress , _ := req .Options [progressOptionName ].(bool )
171+ totalBlocks , _ := req .Options [getTotalBlocksKey ].(int )
172+ if totalBlocks <= 0 {
173+ totalBlocks = 1
174+ }
144175
145176 gw := getWriter {
146177 Out : os .Stdout ,
@@ -149,13 +180,93 @@ may also specify the level of compression by specifying '-l=<1-9>'.
149180 Compression : cmplvl ,
150181 Size : int64 (res .Length ()),
151182 Progress : progress ,
183+ TotalBlocks : totalBlocks ,
152184 }
153185
154186 return gw .Write (outReader , outPath )
155187 },
156188 },
157189}
158190
191+ func getAPIBaseURL (env cmds.Environment ) (string , error ) {
192+ configRoot , err := cmdenv .GetConfigRoot (env )
193+ if err != nil {
194+ return "" , err
195+ }
196+ apiAddr , err := fsrepo .APIAddr (configRoot )
197+ if err != nil {
198+ return "" , err
199+ }
200+ network , host , err := manet .DialArgs (apiAddr )
201+ if err != nil || (network != "tcp" && network != "tcp4" && network != "tcp6" ) {
202+ return "" , errors .New ("unsupported network" )
203+ }
204+ return "http://" + host , nil
205+ }
206+
207+ func resolveRootCID (baseURL , pathStr string ) (string , error ) {
208+ resp , err := http .Post (baseURL + "/api/v0/dag/resolve?arg=" + url .QueryEscape (pathStr ), "" , nil )
209+ if err != nil {
210+ return "" , err
211+ }
212+ defer resp .Body .Close ()
213+ var out struct {
214+ Cid interface {} `json:"Cid"`
215+ }
216+ if err := json .NewDecoder (resp .Body ).Decode (& out ); err != nil {
217+ return "" , err
218+ }
219+ switch v := out .Cid .(type ) {
220+ case string :
221+ return v , nil
222+ case map [string ]interface {}:
223+ cid , _ := v ["/" ].(string )
224+ return cid , nil
225+ }
226+ return "" , nil
227+ }
228+
229+ // fetchStatInfo calls dag/stat and returns totalBlocks and file count directly from NumFiles.
230+ func fetchStatInfo (baseURL , rootCID string ) (totalBlocks , numFiles int ) {
231+ resp , err := http .Post (
232+ baseURL + "/api/v0/dag/stat?arg=" + url .QueryEscape ("/ipfs/" + rootCID )+ "&progress=false" ,
233+ "" , nil ,
234+ )
235+ if err != nil || resp .StatusCode != http .StatusOK {
236+ return 1 , 1
237+ }
238+ defer resp .Body .Close ()
239+
240+ var out struct {
241+ UniqueBlocks int `json:"UniqueBlocks"`
242+ DagStats []struct {
243+ NumBlocks int64 `json:"NumBlocks"`
244+ NumFiles int64 `json:"NumFiles"`
245+ } `json:"DagStats"`
246+ }
247+ if err := json .NewDecoder (resp .Body ).Decode (& out ); err == nil && len (out .DagStats ) > 0 {
248+ ds := out .DagStats [0 ]
249+ totalBlocks = int (ds .NumBlocks )
250+ numFiles = int (ds .NumFiles )
251+ }
252+ if totalBlocks <= 0 {
253+ totalBlocks = max (out .UniqueBlocks , 1 )
254+ }
255+ if numFiles <= 0 {
256+ numFiles = 1
257+ }
258+ return
259+ }
260+
261+ func printGetProgress (w io.Writer , cidStr string , numFiles , totalBlocks int ) {
262+ fmt .Fprintf (w , "\n Fetching CID: %s\n " , cidStr )
263+ if numFiles == 1 {
264+ fmt .Fprintf (w , "Items: 1 file | Blocks: %d\n \n " , totalBlocks )
265+ } else {
266+ fmt .Fprintf (w , "Items: %d files | Blocks: %d\n \n " , numFiles , totalBlocks )
267+ }
268+ }
269+
159270type clearlineReader struct {
160271 io.Reader
161272 out io.Writer
@@ -164,8 +275,7 @@ type clearlineReader struct {
164275func (r * clearlineReader ) Read (p []byte ) (n int , err error ) {
165276 n , err = r .Reader .Read (p )
166277 if err == io .EOF {
167- // callback
168- fmt .Fprintf (r .out , "\033 [2K\r " ) // clear progress bar line on EOF
278+ fmt .Fprintf (r .out , "\033 [2K\r " )
169279 }
170280 return
171281}
@@ -210,6 +320,7 @@ type getWriter struct {
210320 Compression int
211321 Size int64
212322 Progress bool
323+ TotalBlocks int
213324}
214325
215326func (gw * getWriter ) Write (r io.Reader , fpath string ) error {
@@ -258,10 +369,24 @@ func (gw *getWriter) writeExtracted(r io.Reader, fpath string) error {
258369 var progressCb func (int64 ) int64
259370 if gw .Progress {
260371 bar := makeProgressBar (gw .Err , gw .Size )
372+ totalBlocks := gw .TotalBlocks
373+ fmt .Fprintf (gw .Err , "Blocks: 0 / %d\n " , totalBlocks )
261374 bar .Start ()
262375 defer bar .Finish ()
263- defer bar .Set64 (gw .Size )
264- progressCb = bar .Add64
376+ defer func () {
377+ bar .Set64 (gw .Size )
378+ fmt .Fprintf (gw .Err , "\033 [A\033 [2K\r Blocks: %d / %d\033 [B" , totalBlocks , totalBlocks )
379+ }()
380+ size := gw .Size
381+ progressCb = func (delta int64 ) int64 {
382+ total := bar .Add64 (delta )
383+ blocksEst := totalBlocks
384+ if total < size && size > 0 {
385+ blocksEst = int (int64 (totalBlocks ) * total / size )
386+ }
387+ fmt .Fprintf (gw .Err , "\033 [A\033 [2K\r Blocks: %d / %d\033 [B" , blocksEst , totalBlocks )
388+ return total
389+ }
265390 }
266391
267392 extractor := & tar.Extractor {Path : fpath , Progress : progressCb }
0 commit comments