| ... | ... |
@@ -202,7 +202,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
| 202 | 202 |
// FIXME: ProgressReader shouldn't be this annoying to use |
| 203 | 203 |
if context != nil {
|
| 204 | 204 |
sf := utils.NewStreamFormatter(false) |
| 205 |
- body = utils.ProgressReader(ioutil.NopCloser(context), 0, cli.err, sf.FormatProgress("", "Uploading context", "%v bytes%0.0s%0.0s"), sf, true)
|
|
| 205 |
+ body = utils.ProgressReader(ioutil.NopCloser(context), 0, cli.err, sf, true, "", "Uploading context") |
|
| 206 | 206 |
} |
| 207 | 207 |
// Upload the build context |
| 208 | 208 |
v := &url.Values{}
|
| ... | ... |
@@ -205,7 +205,7 @@ func (graph *Graph) TempLayerArchive(id string, compression archive.Compression, |
| 205 | 205 |
if err != nil {
|
| 206 | 206 |
return nil, err |
| 207 | 207 |
} |
| 208 |
- return archive.NewTempArchive(utils.ProgressReader(ioutil.NopCloser(a), 0, output, sf.FormatProgress("", "Buffering to disk", "%v/%v (%v)"), sf, true), tmp)
|
|
| 208 |
+ return archive.NewTempArchive(utils.ProgressReader(ioutil.NopCloser(a), 0, output, sf, true, "", "Buffering to disk"), tmp) |
|
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 | 211 |
// Mktemp creates a temporary sub-directory inside the graph's filesystem. |
| ... | ... |
@@ -451,7 +451,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer, sf *utils. |
| 451 | 451 |
return err |
| 452 | 452 |
} |
| 453 | 453 |
|
| 454 |
- if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, sf.FormatProgress("", "Downloading", "%8v/%v (%v)"), sf, false), path); err != nil {
|
|
| 454 |
+ if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, sf, false, "", "Downloading"), path); err != nil {
|
|
| 455 | 455 |
return err |
| 456 | 456 |
} |
| 457 | 457 |
// FIXME: Handle custom repo, tag comment, author |
| ... | ... |
@@ -761,7 +761,7 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgID, endpoin |
| 761 | 761 |
if err != nil {
|
| 762 | 762 |
return err |
| 763 | 763 |
} |
| 764 |
- out.Write(sf.FormatProgress(utils.TruncateID(imgID), "Pulling", "dependent layers")) |
|
| 764 |
+ out.Write(sf.FormatProgress(utils.TruncateID(imgID), "Pulling dependent layers", nil)) |
|
| 765 | 765 |
// FIXME: Try to stream the images? |
| 766 | 766 |
// FIXME: Launch the getRemoteImage() in goroutines |
| 767 | 767 |
|
| ... | ... |
@@ -776,33 +776,33 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgID, endpoin |
| 776 | 776 |
defer srv.poolRemove("pull", "layer:"+id)
|
| 777 | 777 |
|
| 778 | 778 |
if !srv.runtime.graph.Exists(id) {
|
| 779 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Pulling", "metadata")) |
|
| 779 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Pulling metadata", nil)) |
|
| 780 | 780 |
imgJSON, imgSize, err := r.GetRemoteImageJSON(id, endpoint, token) |
| 781 | 781 |
if err != nil {
|
| 782 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Error", "pulling dependent layers")) |
|
| 782 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil)) |
|
| 783 | 783 |
// FIXME: Keep going in case of error? |
| 784 | 784 |
return err |
| 785 | 785 |
} |
| 786 | 786 |
img, err := NewImgJSON(imgJSON) |
| 787 | 787 |
if err != nil {
|
| 788 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Error", "pulling dependent layers")) |
|
| 788 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil)) |
|
| 789 | 789 |
return fmt.Errorf("Failed to parse json: %s", err)
|
| 790 | 790 |
} |
| 791 | 791 |
|
| 792 | 792 |
// Get the layer |
| 793 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Pulling", "fs layer")) |
|
| 793 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Pulling fs layer", nil)) |
|
| 794 | 794 |
layer, err := r.GetRemoteImageLayer(img.ID, endpoint, token) |
| 795 | 795 |
if err != nil {
|
| 796 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Error", "pulling dependent layers")) |
|
| 796 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil)) |
|
| 797 | 797 |
return err |
| 798 | 798 |
} |
| 799 | 799 |
defer layer.Close() |
| 800 |
- if err := srv.runtime.graph.Register(imgJSON, utils.ProgressReader(layer, imgSize, out, sf.FormatProgress(utils.TruncateID(id), "Downloading", "%8v/%v (%v)"), sf, false), img); err != nil {
|
|
| 801 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Error", "downloading dependent layers")) |
|
| 800 |
+ if err := srv.runtime.graph.Register(imgJSON, utils.ProgressReader(layer, imgSize, out, sf, false, utils.TruncateID(id), "Downloading"), img); err != nil {
|
|
| 801 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Error downloading dependent layers", nil)) |
|
| 802 | 802 |
return err |
| 803 | 803 |
} |
| 804 | 804 |
} |
| 805 |
- out.Write(sf.FormatProgress(utils.TruncateID(id), "Download", "complete")) |
|
| 805 |
+ out.Write(sf.FormatProgress(utils.TruncateID(id), "Download complete", nil)) |
|
| 806 | 806 |
|
| 807 | 807 |
} |
| 808 | 808 |
return nil |
| ... | ... |
@@ -875,29 +875,29 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, localName |
| 875 | 875 |
} |
| 876 | 876 |
defer srv.poolRemove("pull", "img:"+img.ID)
|
| 877 | 877 |
|
| 878 |
- out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Pulling", fmt.Sprintf("image (%s) from %s", img.Tag, localName)))
|
|
| 878 |
+ out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, localName), nil))
|
|
| 879 | 879 |
success := false |
| 880 | 880 |
var lastErr error |
| 881 | 881 |
for _, ep := range repoData.Endpoints {
|
| 882 |
- out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Pulling", fmt.Sprintf("image (%s) from %s, endpoint: %s", img.Tag, localName, ep)))
|
|
| 882 |
+ out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, localName, ep), nil))
|
|
| 883 | 883 |
if err := srv.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
|
| 884 | 884 |
// Its not ideal that only the last error is returned, it would be better to concatenate the errors. |
| 885 | 885 |
// As the error is also given to the output stream the user will see the error. |
| 886 | 886 |
lastErr = err |
| 887 |
- out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Error pulling", fmt.Sprintf("image (%s) from %s, endpoint: %s, %s", img.Tag, localName, ep, err)))
|
|
| 887 |
+ out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, localName, ep, err), nil))
|
|
| 888 | 888 |
continue |
| 889 | 889 |
} |
| 890 | 890 |
success = true |
| 891 | 891 |
break |
| 892 | 892 |
} |
| 893 | 893 |
if !success {
|
| 894 |
- out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Error pulling", fmt.Sprintf("image (%s) from %s, %s", img.Tag, localName, lastErr)))
|
|
| 894 |
+ out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, %s", img.Tag, localName, lastErr), nil))
|
|
| 895 | 895 |
if parallel {
|
| 896 | 896 |
errors <- fmt.Errorf("Could not find repository on any of the indexed registries.")
|
| 897 | 897 |
return |
| 898 | 898 |
} |
| 899 | 899 |
} |
| 900 |
- out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download", "complete")) |
|
| 900 |
+ out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil)) |
|
| 901 | 901 |
|
| 902 | 902 |
if parallel {
|
| 903 | 903 |
errors <- nil |
| ... | ... |
@@ -1171,7 +1171,7 @@ func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgID, |
| 1171 | 1171 |
defer os.RemoveAll(layerData.Name()) |
| 1172 | 1172 |
|
| 1173 | 1173 |
// Send the layer |
| 1174 |
- checksum, err = r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf.FormatProgress("", "Pushing", "%8v/%v (%v)"), sf, false), ep, token, jsonRaw)
|
|
| 1174 |
+ checksum, err = r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf, false, "", "Pushing"), ep, token, jsonRaw) |
|
| 1175 | 1175 |
if err != nil {
|
| 1176 | 1176 |
return "", err |
| 1177 | 1177 |
} |
| ... | ... |
@@ -1251,7 +1251,7 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write |
| 1251 | 1251 |
if err != nil {
|
| 1252 | 1252 |
return err |
| 1253 | 1253 |
} |
| 1254 |
- archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, sf.FormatProgress("", "Importing", "%8v/%v (%v)"), sf, true)
|
|
| 1254 |
+ archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, sf, true, "", "Importing") |
|
| 1255 | 1255 |
} |
| 1256 | 1256 |
img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil) |
| 1257 | 1257 |
if err != nil {
|
| 1258 | 1258 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,118 @@ |
| 0 |
+package utils |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "time" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+type JSONError struct {
|
|
| 10 |
+ Code int `json:"code,omitempty"` |
|
| 11 |
+ Message string `json:"message,omitempty"` |
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+func (e *JSONError) Error() string {
|
|
| 15 |
+ return e.Message |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+type JSONProgress struct {
|
|
| 19 |
+ Current int `json:"current,omitempty"` |
|
| 20 |
+ Total int `json:"total,omitempty"` |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+func (p *JSONProgress) String() string {
|
|
| 24 |
+ if p.Current == 0 && p.Total == 0 {
|
|
| 25 |
+ return "" |
|
| 26 |
+ } |
|
| 27 |
+ current := HumanSize(int64(p.Current)) |
|
| 28 |
+ if p.Total == 0 {
|
|
| 29 |
+ return fmt.Sprintf("%8v/?", current)
|
|
| 30 |
+ } |
|
| 31 |
+ total := HumanSize(int64(p.Total)) |
|
| 32 |
+ percentage := float64(p.Current) / float64(p.Total) * 100 |
|
| 33 |
+ return fmt.Sprintf("%8v/%v (%.0f%%)", current, total, percentage)
|
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+type JSONMessage struct {
|
|
| 37 |
+ Status string `json:"status,omitempty"` |
|
| 38 |
+ Progress *JSONProgress `json:"progressDetail,omitempty"` |
|
| 39 |
+ ProgressMessage string `json:"progress,omitempty"` //deprecated |
|
| 40 |
+ ID string `json:"id,omitempty"` |
|
| 41 |
+ From string `json:"from,omitempty"` |
|
| 42 |
+ Time int64 `json:"time,omitempty"` |
|
| 43 |
+ Error *JSONError `json:"errorDetail,omitempty"` |
|
| 44 |
+ ErrorMessage string `json:"error,omitempty"` //deprecated |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
|
|
| 48 |
+ if jm.Error != nil {
|
|
| 49 |
+ if jm.Error.Code == 401 {
|
|
| 50 |
+ return fmt.Errorf("Authentication is required.")
|
|
| 51 |
+ } |
|
| 52 |
+ return jm.Error |
|
| 53 |
+ } |
|
| 54 |
+ endl := "" |
|
| 55 |
+ if isTerminal {
|
|
| 56 |
+ // <ESC>[2K = erase entire current line |
|
| 57 |
+ fmt.Fprintf(out, "%c[2K\r", 27) |
|
| 58 |
+ endl = "\r" |
|
| 59 |
+ } |
|
| 60 |
+ if jm.Time != 0 {
|
|
| 61 |
+ fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0)) |
|
| 62 |
+ } |
|
| 63 |
+ if jm.ID != "" {
|
|
| 64 |
+ fmt.Fprintf(out, "%s: ", jm.ID) |
|
| 65 |
+ } |
|
| 66 |
+ if jm.From != "" {
|
|
| 67 |
+ fmt.Fprintf(out, "(from %s) ", jm.From) |
|
| 68 |
+ } |
|
| 69 |
+ if jm.Progress != nil {
|
|
| 70 |
+ fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) |
|
| 71 |
+ } else if jm.ProgressMessage != "" { //deprecated
|
|
| 72 |
+ fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) |
|
| 73 |
+ } else {
|
|
| 74 |
+ fmt.Fprintf(out, "%s%s\n", jm.Status, endl) |
|
| 75 |
+ } |
|
| 76 |
+ return nil |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+func DisplayJSONMessagesStream(in io.Reader, out io.Writer, isTerminal bool) error {
|
|
| 80 |
+ dec := json.NewDecoder(in) |
|
| 81 |
+ ids := make(map[string]int) |
|
| 82 |
+ diff := 0 |
|
| 83 |
+ for {
|
|
| 84 |
+ jm := JSONMessage{}
|
|
| 85 |
+ if err := dec.Decode(&jm); err == io.EOF {
|
|
| 86 |
+ break |
|
| 87 |
+ } else if err != nil {
|
|
| 88 |
+ return err |
|
| 89 |
+ } |
|
| 90 |
+ if (jm.Progress != nil || jm.ProgressMessage != "") && jm.ID != "" {
|
|
| 91 |
+ line, ok := ids[jm.ID] |
|
| 92 |
+ if !ok {
|
|
| 93 |
+ line = len(ids) |
|
| 94 |
+ ids[jm.ID] = line |
|
| 95 |
+ fmt.Fprintf(out, "\n") |
|
| 96 |
+ diff = 0 |
|
| 97 |
+ } else {
|
|
| 98 |
+ diff = len(ids) - line |
|
| 99 |
+ } |
|
| 100 |
+ if isTerminal {
|
|
| 101 |
+ // <ESC>[{diff}A = move cursor up diff rows
|
|
| 102 |
+ fmt.Fprintf(out, "%c[%dA", 27, diff) |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+ err := jm.Display(out, isTerminal) |
|
| 106 |
+ if jm.ID != "" {
|
|
| 107 |
+ if isTerminal {
|
|
| 108 |
+ // <ESC>[{diff}B = move cursor down diff rows
|
|
| 109 |
+ fmt.Fprintf(out, "%c[%dB", 27, diff) |
|
| 110 |
+ } |
|
| 111 |
+ } |
|
| 112 |
+ if err != nil {
|
|
| 113 |
+ return err |
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ return nil |
|
| 117 |
+} |
| 0 | 118 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,55 @@ |
| 0 |
+package utils |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// Reader with progress bar |
|
| 7 |
+type progressReader struct {
|
|
| 8 |
+ reader io.ReadCloser // Stream to read from |
|
| 9 |
+ output io.Writer // Where to send progress bar to |
|
| 10 |
+ progress JSONProgress |
|
| 11 |
+ // readTotal int // Expected stream length (bytes) |
|
| 12 |
+ // readProgress int // How much has been read so far (bytes) |
|
| 13 |
+ lastUpdate int // How many bytes read at least update |
|
| 14 |
+ ID string |
|
| 15 |
+ action string |
|
| 16 |
+ // template string // Template to print. Default "%v/%v (%v)" |
|
| 17 |
+ sf *StreamFormatter |
|
| 18 |
+ newLine bool |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+func (r *progressReader) Read(p []byte) (n int, err error) {
|
|
| 22 |
+ read, err := io.ReadCloser(r.reader).Read(p) |
|
| 23 |
+ r.progress.Current += read |
|
| 24 |
+ updateEvery := 1024 * 512 //512kB |
|
| 25 |
+ if r.progress.Total > 0 {
|
|
| 26 |
+ // Update progress for every 1% read if 1% < 512kB |
|
| 27 |
+ if increment := int(0.01 * float64(r.progress.Total)); increment < updateEvery {
|
|
| 28 |
+ updateEvery = increment |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ if r.progress.Current-r.lastUpdate > updateEvery || err != nil {
|
|
| 32 |
+ r.output.Write(r.sf.FormatProgress(r.ID, r.action, &r.progress)) |
|
| 33 |
+ r.lastUpdate = r.progress.Current |
|
| 34 |
+ } |
|
| 35 |
+ // Send newline when complete |
|
| 36 |
+ if r.newLine && err != nil {
|
|
| 37 |
+ r.output.Write(r.sf.FormatStatus("", ""))
|
|
| 38 |
+ } |
|
| 39 |
+ return read, err |
|
| 40 |
+} |
|
| 41 |
+func (r *progressReader) Close() error {
|
|
| 42 |
+ return io.ReadCloser(r.reader).Close() |
|
| 43 |
+} |
|
| 44 |
+func ProgressReader(r io.ReadCloser, size int, output io.Writer, sf *StreamFormatter, newline bool, ID, action string) *progressReader {
|
|
| 45 |
+ return &progressReader{
|
|
| 46 |
+ reader: r, |
|
| 47 |
+ output: NewWriteFlusher(output), |
|
| 48 |
+ ID: ID, |
|
| 49 |
+ action: action, |
|
| 50 |
+ progress: JSONProgress{Total: size},
|
|
| 51 |
+ sf: sf, |
|
| 52 |
+ newLine: newline, |
|
| 53 |
+ } |
|
| 54 |
+} |
| 0 | 55 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,68 @@ |
| 0 |
+package utils |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+type StreamFormatter struct {
|
|
| 8 |
+ json bool |
|
| 9 |
+ used bool |
|
| 10 |
+} |
|
| 11 |
+ |
|
| 12 |
+func NewStreamFormatter(json bool) *StreamFormatter {
|
|
| 13 |
+ return &StreamFormatter{json, false}
|
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func (sf *StreamFormatter) FormatStatus(id, format string, a ...interface{}) []byte {
|
|
| 17 |
+ sf.used = true |
|
| 18 |
+ str := fmt.Sprintf(format, a...) |
|
| 19 |
+ if sf.json {
|
|
| 20 |
+ b, err := json.Marshal(&JSONMessage{ID: id, Status: str})
|
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return sf.FormatError(err) |
|
| 23 |
+ } |
|
| 24 |
+ return b |
|
| 25 |
+ } |
|
| 26 |
+ return []byte(str + "\r\n") |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func (sf *StreamFormatter) FormatError(err error) []byte {
|
|
| 30 |
+ sf.used = true |
|
| 31 |
+ if sf.json {
|
|
| 32 |
+ jsonError, ok := err.(*JSONError) |
|
| 33 |
+ if !ok {
|
|
| 34 |
+ jsonError = &JSONError{Message: err.Error()}
|
|
| 35 |
+ } |
|
| 36 |
+ if b, err := json.Marshal(&JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
|
|
| 37 |
+ return b |
|
| 38 |
+ } |
|
| 39 |
+ return []byte("{\"error\":\"format error\"}")
|
|
| 40 |
+ } |
|
| 41 |
+ return []byte("Error: " + err.Error() + "\r\n")
|
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+func (sf *StreamFormatter) FormatProgress(id, action string, progress *JSONProgress) []byte {
|
|
| 45 |
+ if progress == nil {
|
|
| 46 |
+ progress = &JSONProgress{}
|
|
| 47 |
+ } |
|
| 48 |
+ sf.used = true |
|
| 49 |
+ if sf.json {
|
|
| 50 |
+ |
|
| 51 |
+ b, err := json.Marshal(&JSONMessage{
|
|
| 52 |
+ Status: action, |
|
| 53 |
+ ProgressMessage: progress.String(), |
|
| 54 |
+ Progress: progress, |
|
| 55 |
+ ID: id, |
|
| 56 |
+ }) |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return nil |
|
| 59 |
+ } |
|
| 60 |
+ return b |
|
| 61 |
+ } |
|
| 62 |
+ return []byte(action + " " + progress.String() + "\r") |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 65 |
+func (sf *StreamFormatter) Used() bool {
|
|
| 66 |
+ return sf.used |
|
| 67 |
+} |
| ... | ... |
@@ -94,56 +94,6 @@ func Errorf(format string, a ...interface{}) {
|
| 94 | 94 |
logf("error", format, a...)
|
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 |
-// Reader with progress bar |
|
| 98 |
-type progressReader struct {
|
|
| 99 |
- reader io.ReadCloser // Stream to read from |
|
| 100 |
- output io.Writer // Where to send progress bar to |
|
| 101 |
- readTotal int // Expected stream length (bytes) |
|
| 102 |
- readProgress int // How much has been read so far (bytes) |
|
| 103 |
- lastUpdate int // How many bytes read at least update |
|
| 104 |
- template string // Template to print. Default "%v/%v (%v)" |
|
| 105 |
- sf *StreamFormatter |
|
| 106 |
- newLine bool |
|
| 107 |
-} |
|
| 108 |
- |
|
| 109 |
-func (r *progressReader) Read(p []byte) (n int, err error) {
|
|
| 110 |
- read, err := io.ReadCloser(r.reader).Read(p) |
|
| 111 |
- r.readProgress += read |
|
| 112 |
- updateEvery := 1024 * 512 //512kB |
|
| 113 |
- if r.readTotal > 0 {
|
|
| 114 |
- // Update progress for every 1% read if 1% < 512kB |
|
| 115 |
- if increment := int(0.01 * float64(r.readTotal)); increment < updateEvery {
|
|
| 116 |
- updateEvery = increment |
|
| 117 |
- } |
|
| 118 |
- } |
|
| 119 |
- if r.readProgress-r.lastUpdate > updateEvery || err != nil {
|
|
| 120 |
- if r.readTotal > 0 {
|
|
| 121 |
- fmt.Fprintf(r.output, r.template, HumanSize(int64(r.readProgress)), HumanSize(int64(r.readTotal)), fmt.Sprintf("%.0f%%", float64(r.readProgress)/float64(r.readTotal)*100))
|
|
| 122 |
- } else {
|
|
| 123 |
- fmt.Fprintf(r.output, r.template, r.readProgress, "?", "n/a") |
|
| 124 |
- } |
|
| 125 |
- r.lastUpdate = r.readProgress |
|
| 126 |
- } |
|
| 127 |
- // Send newline when complete |
|
| 128 |
- if r.newLine && err != nil {
|
|
| 129 |
- r.output.Write(r.sf.FormatStatus("", ""))
|
|
| 130 |
- } |
|
| 131 |
- return read, err |
|
| 132 |
-} |
|
| 133 |
-func (r *progressReader) Close() error {
|
|
| 134 |
- return io.ReadCloser(r.reader).Close() |
|
| 135 |
-} |
|
| 136 |
-func ProgressReader(r io.ReadCloser, size int, output io.Writer, tpl []byte, sf *StreamFormatter, newline bool) *progressReader {
|
|
| 137 |
- return &progressReader{
|
|
| 138 |
- reader: r, |
|
| 139 |
- output: NewWriteFlusher(output), |
|
| 140 |
- readTotal: size, |
|
| 141 |
- template: string(tpl), |
|
| 142 |
- sf: sf, |
|
| 143 |
- newLine: newline, |
|
| 144 |
- } |
|
| 145 |
-} |
|
| 146 |
- |
|
| 147 | 97 |
// HumanDuration returns a human-readable approximation of a duration |
| 148 | 98 |
// (eg. "About a minute", "4 hours ago", etc.) |
| 149 | 99 |
func HumanDuration(d time.Duration) string {
|
| ... | ... |
@@ -754,25 +704,6 @@ func NewWriteFlusher(w io.Writer) *WriteFlusher {
|
| 754 | 754 |
return &WriteFlusher{w: w, flusher: flusher}
|
| 755 | 755 |
} |
| 756 | 756 |
|
| 757 |
-type JSONError struct {
|
|
| 758 |
- Code int `json:"code,omitempty"` |
|
| 759 |
- Message string `json:"message,omitempty"` |
|
| 760 |
-} |
|
| 761 |
- |
|
| 762 |
-type JSONMessage struct {
|
|
| 763 |
- Status string `json:"status,omitempty"` |
|
| 764 |
- Progress string `json:"progress,omitempty"` |
|
| 765 |
- ErrorMessage string `json:"error,omitempty"` //deprecated |
|
| 766 |
- ID string `json:"id,omitempty"` |
|
| 767 |
- From string `json:"from,omitempty"` |
|
| 768 |
- Time int64 `json:"time,omitempty"` |
|
| 769 |
- Error *JSONError `json:"errorDetail,omitempty"` |
|
| 770 |
-} |
|
| 771 |
- |
|
| 772 |
-func (e *JSONError) Error() string {
|
|
| 773 |
- return e.Message |
|
| 774 |
-} |
|
| 775 |
- |
|
| 776 | 757 |
func NewHTTPRequestError(msg string, res *http.Response) error {
|
| 777 | 758 |
return &JSONError{
|
| 778 | 759 |
Message: msg, |
| ... | ... |
@@ -780,129 +711,6 @@ func NewHTTPRequestError(msg string, res *http.Response) error {
|
| 780 | 780 |
} |
| 781 | 781 |
} |
| 782 | 782 |
|
| 783 |
-func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
|
|
| 784 |
- if jm.Error != nil {
|
|
| 785 |
- if jm.Error.Code == 401 {
|
|
| 786 |
- return fmt.Errorf("Authentication is required.")
|
|
| 787 |
- } |
|
| 788 |
- return jm.Error |
|
| 789 |
- } |
|
| 790 |
- endl := "" |
|
| 791 |
- if isTerminal {
|
|
| 792 |
- // <ESC>[2K = erase entire current line |
|
| 793 |
- fmt.Fprintf(out, "%c[2K\r", 27) |
|
| 794 |
- endl = "\r" |
|
| 795 |
- } |
|
| 796 |
- if jm.Time != 0 {
|
|
| 797 |
- fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0)) |
|
| 798 |
- } |
|
| 799 |
- if jm.ID != "" {
|
|
| 800 |
- fmt.Fprintf(out, "%s: ", jm.ID) |
|
| 801 |
- } |
|
| 802 |
- if jm.From != "" {
|
|
| 803 |
- fmt.Fprintf(out, "(from %s) ", jm.From) |
|
| 804 |
- } |
|
| 805 |
- if jm.Progress != "" {
|
|
| 806 |
- fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress, endl) |
|
| 807 |
- } else {
|
|
| 808 |
- fmt.Fprintf(out, "%s%s\n", jm.Status, endl) |
|
| 809 |
- } |
|
| 810 |
- return nil |
|
| 811 |
-} |
|
| 812 |
- |
|
| 813 |
-func DisplayJSONMessagesStream(in io.Reader, out io.Writer, isTerminal bool) error {
|
|
| 814 |
- dec := json.NewDecoder(in) |
|
| 815 |
- ids := make(map[string]int) |
|
| 816 |
- diff := 0 |
|
| 817 |
- for {
|
|
| 818 |
- jm := JSONMessage{}
|
|
| 819 |
- if err := dec.Decode(&jm); err == io.EOF {
|
|
| 820 |
- break |
|
| 821 |
- } else if err != nil {
|
|
| 822 |
- return err |
|
| 823 |
- } |
|
| 824 |
- if jm.Progress != "" && jm.ID != "" {
|
|
| 825 |
- line, ok := ids[jm.ID] |
|
| 826 |
- if !ok {
|
|
| 827 |
- line = len(ids) |
|
| 828 |
- ids[jm.ID] = line |
|
| 829 |
- fmt.Fprintf(out, "\n") |
|
| 830 |
- diff = 0 |
|
| 831 |
- } else {
|
|
| 832 |
- diff = len(ids) - line |
|
| 833 |
- } |
|
| 834 |
- if isTerminal {
|
|
| 835 |
- // <ESC>[{diff}A = move cursor up diff rows
|
|
| 836 |
- fmt.Fprintf(out, "%c[%dA", 27, diff) |
|
| 837 |
- } |
|
| 838 |
- } |
|
| 839 |
- err := jm.Display(out, isTerminal) |
|
| 840 |
- if jm.ID != "" {
|
|
| 841 |
- if isTerminal {
|
|
| 842 |
- // <ESC>[{diff}B = move cursor down diff rows
|
|
| 843 |
- fmt.Fprintf(out, "%c[%dB", 27, diff) |
|
| 844 |
- } |
|
| 845 |
- } |
|
| 846 |
- if err != nil {
|
|
| 847 |
- return err |
|
| 848 |
- } |
|
| 849 |
- } |
|
| 850 |
- return nil |
|
| 851 |
-} |
|
| 852 |
- |
|
| 853 |
-type StreamFormatter struct {
|
|
| 854 |
- json bool |
|
| 855 |
- used bool |
|
| 856 |
-} |
|
| 857 |
- |
|
| 858 |
-func NewStreamFormatter(json bool) *StreamFormatter {
|
|
| 859 |
- return &StreamFormatter{json, false}
|
|
| 860 |
-} |
|
| 861 |
- |
|
| 862 |
-func (sf *StreamFormatter) FormatStatus(id, format string, a ...interface{}) []byte {
|
|
| 863 |
- sf.used = true |
|
| 864 |
- str := fmt.Sprintf(format, a...) |
|
| 865 |
- if sf.json {
|
|
| 866 |
- b, err := json.Marshal(&JSONMessage{ID: id, Status: str})
|
|
| 867 |
- if err != nil {
|
|
| 868 |
- return sf.FormatError(err) |
|
| 869 |
- } |
|
| 870 |
- return b |
|
| 871 |
- } |
|
| 872 |
- return []byte(str + "\r\n") |
|
| 873 |
-} |
|
| 874 |
- |
|
| 875 |
-func (sf *StreamFormatter) FormatError(err error) []byte {
|
|
| 876 |
- sf.used = true |
|
| 877 |
- if sf.json {
|
|
| 878 |
- jsonError, ok := err.(*JSONError) |
|
| 879 |
- if !ok {
|
|
| 880 |
- jsonError = &JSONError{Message: err.Error()}
|
|
| 881 |
- } |
|
| 882 |
- if b, err := json.Marshal(&JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
|
|
| 883 |
- return b |
|
| 884 |
- } |
|
| 885 |
- return []byte("{\"error\":\"format error\"}")
|
|
| 886 |
- } |
|
| 887 |
- return []byte("Error: " + err.Error() + "\r\n")
|
|
| 888 |
-} |
|
| 889 |
- |
|
| 890 |
-func (sf *StreamFormatter) FormatProgress(id, action, progress string) []byte {
|
|
| 891 |
- sf.used = true |
|
| 892 |
- if sf.json {
|
|
| 893 |
- b, err := json.Marshal(&JSONMessage{Status: action, Progress: progress, ID: id})
|
|
| 894 |
- if err != nil {
|
|
| 895 |
- return nil |
|
| 896 |
- } |
|
| 897 |
- return b |
|
| 898 |
- } |
|
| 899 |
- return []byte(action + " " + progress + "\r") |
|
| 900 |
-} |
|
| 901 |
- |
|
| 902 |
-func (sf *StreamFormatter) Used() bool {
|
|
| 903 |
- return sf.used |
|
| 904 |
-} |
|
| 905 |
- |
|
| 906 | 783 |
func IsURL(str string) bool {
|
| 907 | 784 |
return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://") |
| 908 | 785 |
} |