Browse code

Move pkg/streamformatter to api/pkg/streamformatter

Signed-off-by: Derek McGowan <derek@mcg.dev>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Derek McGowan authored on 2025/07/30 05:28:31
Showing 25 changed files
... ...
@@ -5,11 +5,10 @@ go 1.23.0
5 5
 require (
6 6
 	github.com/docker/go-connections v0.5.0
7 7
 	github.com/docker/go-units v0.5.0
8
+	github.com/google/go-cmp v0.5.9
8 9
 	github.com/moby/docker-image-spec v1.3.1
9 10
 	github.com/opencontainers/go-digest v1.0.0
10 11
 	github.com/opencontainers/image-spec v1.1.1
11 12
 	golang.org/x/time v0.11.0
12 13
 	gotest.tools/v3 v3.5.2
13 14
 )
14
-
15
-require github.com/google/go-cmp v0.5.9 // indirect
16 15
new file mode 100644
... ...
@@ -0,0 +1,247 @@
0
+// Package streamformatter provides helper functions to format a stream.
1
+package streamformatter
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"strings"
8
+	"sync"
9
+	"time"
10
+
11
+	"github.com/docker/go-units"
12
+	"github.com/moby/moby/api/pkg/progress"
13
+	"github.com/moby/moby/api/types/jsonstream"
14
+)
15
+
16
+// jsonMessage defines a message struct. It describes
17
+// the created time, where it from, status, ID of the
18
+// message. It's used for docker events.
19
+//
20
+// It is a reduced set of [jsonmessage.JSONMessage].
21
+type jsonMessage struct {
22
+	Stream   string               `json:"stream,omitempty"`
23
+	Status   string               `json:"status,omitempty"`
24
+	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
25
+	ID       string               `json:"id,omitempty"`
26
+	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
27
+	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
28
+
29
+	// ErrorMessage contains errors encountered during the operation.
30
+	//
31
+	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
32
+	ErrorMessage string `json:"error,omitempty"` // deprecated
33
+}
34
+
35
+const streamNewline = "\r\n"
36
+
37
+type jsonProgressFormatter struct{}
38
+
39
+func appendNewline(source []byte) []byte {
40
+	return append(source, []byte(streamNewline)...)
41
+}
42
+
43
+// FormatStatus formats the specified objects according to the specified format (and id).
44
+func FormatStatus(id, format string, a ...interface{}) []byte {
45
+	str := fmt.Sprintf(format, a...)
46
+	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
47
+	if err != nil {
48
+		return FormatError(err)
49
+	}
50
+	return appendNewline(b)
51
+}
52
+
53
+// FormatError formats the error as a JSON object
54
+func FormatError(err error) []byte {
55
+	jsonError, ok := err.(*jsonstream.Error)
56
+	if !ok {
57
+		jsonError = &jsonstream.Error{Message: err.Error()}
58
+	}
59
+	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
60
+		return appendNewline(b)
61
+	}
62
+	return []byte(`{"error":"format error"}` + streamNewline)
63
+}
64
+
65
+func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
66
+	return FormatStatus(id, format, a...)
67
+}
68
+
69
+// formatProgress formats the progress information for a specified action.
70
+func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
71
+	if progress == nil {
72
+		progress = &jsonstream.Progress{}
73
+	}
74
+	var auxJSON *json.RawMessage
75
+	if aux != nil {
76
+		auxJSONBytes, err := json.Marshal(aux)
77
+		if err != nil {
78
+			return nil
79
+		}
80
+		auxJSON = new(json.RawMessage)
81
+		*auxJSON = auxJSONBytes
82
+	}
83
+	b, err := json.Marshal(&jsonMessage{
84
+		Status:   action,
85
+		Progress: progress,
86
+		ID:       id,
87
+		Aux:      auxJSON,
88
+	})
89
+	if err != nil {
90
+		return nil
91
+	}
92
+	return appendNewline(b)
93
+}
94
+
95
+type rawProgressFormatter struct{}
96
+
97
+func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
98
+	return []byte(fmt.Sprintf(format, a...) + streamNewline)
99
+}
100
+
101
+func rawProgressString(p *jsonstream.Progress) string {
102
+	if p == nil || (p.Current <= 0 && p.Total <= 0) {
103
+		return ""
104
+	}
105
+	if p.Total <= 0 {
106
+		switch p.Units {
107
+		case "":
108
+			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
109
+		default:
110
+			return fmt.Sprintf("%d %s", p.Current, p.Units)
111
+		}
112
+	}
113
+
114
+	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
115
+	if percentage > 50 {
116
+		percentage = 50
117
+	}
118
+
119
+	numSpaces := 0
120
+	if 50-percentage > 0 {
121
+		numSpaces = 50 - percentage
122
+	}
123
+	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
124
+
125
+	var numbersBox string
126
+	switch {
127
+	case p.HideCounts:
128
+	case p.Units == "": // no units, use bytes
129
+		current := units.HumanSize(float64(p.Current))
130
+		total := units.HumanSize(float64(p.Total))
131
+
132
+		numbersBox = fmt.Sprintf("%8v/%v", current, total)
133
+
134
+		if p.Current > p.Total {
135
+			// remove total display if the reported current is wonky.
136
+			numbersBox = fmt.Sprintf("%8v", current)
137
+		}
138
+	default:
139
+		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
140
+
141
+		if p.Current > p.Total {
142
+			// remove total display if the reported current is wonky.
143
+			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
144
+		}
145
+	}
146
+
147
+	var timeLeftBox string
148
+	if p.Current > 0 && p.Start > 0 && percentage < 50 {
149
+		fromStart := time.Since(time.Unix(p.Start, 0))
150
+		perEntry := fromStart / time.Duration(p.Current)
151
+		left := time.Duration(p.Total-p.Current) * perEntry
152
+		timeLeftBox = " " + left.Round(time.Second).String()
153
+	}
154
+	return pbBox + numbersBox + timeLeftBox
155
+}
156
+
157
+func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
158
+	if progress == nil {
159
+		progress = &jsonstream.Progress{}
160
+	}
161
+	endl := "\r"
162
+	out := rawProgressString(progress)
163
+	if out == "" {
164
+		endl += "\n"
165
+	}
166
+	return []byte(action + " " + out + endl)
167
+}
168
+
169
+// NewProgressOutput returns a progress.Output object that can be passed to
170
+// progress.NewProgressReader.
171
+func NewProgressOutput(out io.Writer) progress.Output {
172
+	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
173
+}
174
+
175
+// NewJSONProgressOutput returns a progress.Output that formats output
176
+// using JSON objects
177
+func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
178
+	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
179
+}
180
+
181
+type formatProgress interface {
182
+	formatStatus(id, format string, a ...interface{}) []byte
183
+	formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte
184
+}
185
+
186
+type progressOutput struct {
187
+	sf       formatProgress
188
+	out      io.Writer
189
+	newLines bool
190
+	mu       sync.Mutex
191
+}
192
+
193
+// WriteProgress formats progress information from a ProgressReader.
194
+func (out *progressOutput) WriteProgress(prog progress.Progress) error {
195
+	var formatted []byte
196
+	if prog.Message != "" {
197
+		formatted = out.sf.formatStatus(prog.ID, prog.Message)
198
+	} else {
199
+		jsonProgress := jsonstream.Progress{
200
+			Current:    prog.Current,
201
+			Total:      prog.Total,
202
+			HideCounts: prog.HideCounts,
203
+			Units:      prog.Units,
204
+		}
205
+		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
206
+	}
207
+
208
+	out.mu.Lock()
209
+	defer out.mu.Unlock()
210
+	_, err := out.out.Write(formatted)
211
+	if err != nil {
212
+		return err
213
+	}
214
+
215
+	if out.newLines && prog.LastUpdate {
216
+		_, err = out.out.Write(out.sf.formatStatus("", ""))
217
+		return err
218
+	}
219
+
220
+	return nil
221
+}
222
+
223
+// AuxFormatter is a streamFormatter that writes aux progress messages
224
+type AuxFormatter struct {
225
+	io.Writer
226
+}
227
+
228
+// Emit emits the given interface as an aux progress message
229
+func (sf *AuxFormatter) Emit(id string, aux interface{}) error {
230
+	auxJSONBytes, err := json.Marshal(aux)
231
+	if err != nil {
232
+		return err
233
+	}
234
+	auxJSON := new(json.RawMessage)
235
+	*auxJSON = auxJSONBytes
236
+	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
237
+	if err != nil {
238
+		return err
239
+	}
240
+	msgJSON = appendNewline(msgJSON)
241
+	n, err := sf.Writer.Write(msgJSON)
242
+	if n != len(msgJSON) {
243
+		return io.ErrShortWrite
244
+	}
245
+	return err
246
+}
0 247
new file mode 100644
... ...
@@ -0,0 +1,110 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"errors"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/google/go-cmp/cmp"
10
+	"github.com/moby/moby/api/types/jsonstream"
11
+	"gotest.tools/v3/assert"
12
+	is "gotest.tools/v3/assert/cmp"
13
+)
14
+
15
+func TestRawProgressFormatterFormatStatus(t *testing.T) {
16
+	sf := rawProgressFormatter{}
17
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
18
+	assert.Check(t, is.Equal("a1\r\n", string(res)))
19
+}
20
+
21
+func TestRawProgressFormatterFormatProgress(t *testing.T) {
22
+	sf := rawProgressFormatter{}
23
+	jsonProgress := &jsonstream.Progress{
24
+		Current: 15,
25
+		Total:   30,
26
+		Start:   1,
27
+	}
28
+	res := sf.formatProgress("id", "action", jsonProgress, nil)
29
+	out := string(res)
30
+	assert.Check(t, strings.HasPrefix(out, "action [===="))
31
+	assert.Check(t, is.Contains(out, "15B/30B"))
32
+	assert.Check(t, strings.HasSuffix(out, "\r"))
33
+}
34
+
35
+func TestFormatStatus(t *testing.T) {
36
+	res := FormatStatus("ID", "%s%d", "a", 1)
37
+	expected := `{"status":"a1","id":"ID"}` + streamNewline
38
+	assert.Check(t, is.Equal(expected, string(res)))
39
+}
40
+
41
+func TestFormatError(t *testing.T) {
42
+	res := FormatError(errors.New("Error for formatter"))
43
+	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
44
+	assert.Check(t, is.Equal(expected, string(res)))
45
+}
46
+
47
+func TestFormatJSONError(t *testing.T) {
48
+	err := &jsonstream.Error{Code: 50, Message: "Json error"}
49
+	res := FormatError(err)
50
+	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
51
+	assert.Check(t, is.Equal(expected, string(res)))
52
+}
53
+
54
+func TestJsonProgressFormatterFormatProgress(t *testing.T) {
55
+	sf := &jsonProgressFormatter{}
56
+	jsonProgress := &jsonstream.Progress{
57
+		Current: 15,
58
+		Total:   30,
59
+		Start:   1,
60
+	}
61
+	aux := "aux message"
62
+	res := sf.formatProgress("id", "action", jsonProgress, aux)
63
+	msg := &jsonMessage{}
64
+
65
+	assert.NilError(t, json.Unmarshal(res, msg))
66
+
67
+	rawAux := json.RawMessage(`"` + aux + `"`)
68
+	expected := &jsonMessage{
69
+		ID:       "id",
70
+		Status:   "action",
71
+		Aux:      &rawAux,
72
+		Progress: jsonProgress,
73
+	}
74
+	assert.DeepEqual(t, msg, expected, cmpJSONMessageOpt())
75
+}
76
+
77
+func cmpJSONMessageOpt() cmp.Option {
78
+	progressMessagePath := func(path cmp.Path) bool {
79
+		return path.String() == "ProgressMessage"
80
+	}
81
+	return cmp.Options{
82
+		// Ignore deprecated property that is a derivative of Progress
83
+		cmp.FilterPath(progressMessagePath, cmp.Ignore()),
84
+	}
85
+}
86
+
87
+func TestJsonProgressFormatterFormatStatus(t *testing.T) {
88
+	sf := jsonProgressFormatter{}
89
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
90
+	assert.Check(t, is.Equal(`{"status":"a1","id":"ID"}`+streamNewline, string(res)))
91
+}
92
+
93
+func TestNewJSONProgressOutput(t *testing.T) {
94
+	b := bytes.Buffer{}
95
+	b.Write(FormatStatus("id", "Downloading"))
96
+	_ = NewJSONProgressOutput(&b, false)
97
+	assert.Check(t, is.Equal(`{"status":"Downloading","id":"id"}`+streamNewline, b.String()))
98
+}
99
+
100
+func TestAuxFormatterEmit(t *testing.T) {
101
+	b := bytes.Buffer{}
102
+	aux := &AuxFormatter{Writer: &b}
103
+	sampleAux := &struct {
104
+		Data string
105
+	}{"Additional data"}
106
+	err := aux.Emit("", sampleAux)
107
+	assert.NilError(t, err)
108
+	assert.Check(t, is.Equal(`{"aux":{"Data":"Additional data"}}`+streamNewline, b.String()))
109
+}
0 110
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+package streamformatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+)
6
+
7
+type streamWriter struct {
8
+	io.Writer
9
+	lineFormat func([]byte) string
10
+}
11
+
12
+func (sw *streamWriter) Write(buf []byte) (int, error) {
13
+	formattedBuf := sw.format(buf)
14
+	n, err := sw.Writer.Write(formattedBuf)
15
+	if n != len(formattedBuf) {
16
+		return n, io.ErrShortWrite
17
+	}
18
+	return len(buf), err
19
+}
20
+
21
+func (sw *streamWriter) format(buf []byte) []byte {
22
+	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
23
+	b, err := json.Marshal(msg)
24
+	if err != nil {
25
+		return FormatError(err)
26
+	}
27
+	return appendNewline(b)
28
+}
29
+
30
+// NewStdoutWriter returns a writer which formats the output as json message
31
+// representing stdout lines
32
+func NewStdoutWriter(out io.Writer) io.Writer {
33
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
34
+		return string(buf)
35
+	}}
36
+}
37
+
38
+// NewStderrWriter returns a writer which formats the output as json message
39
+// representing stderr lines
40
+func NewStderrWriter(out io.Writer) io.Writer {
41
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
42
+		return "\033[91m" + string(buf) + "\033[0m"
43
+	}}
44
+}
0 45
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"gotest.tools/v3/assert"
7
+	is "gotest.tools/v3/assert/cmp"
8
+)
9
+
10
+func TestStreamWriterStdout(t *testing.T) {
11
+	buffer := &bytes.Buffer{}
12
+	content := "content"
13
+	sw := NewStdoutWriter(buffer)
14
+	size, err := sw.Write([]byte(content))
15
+
16
+	assert.NilError(t, err)
17
+	assert.Check(t, is.Equal(len(content), size))
18
+
19
+	expected := `{"stream":"content"}` + streamNewline
20
+	assert.Check(t, is.Equal(expected, buffer.String()))
21
+}
22
+
23
+func TestStreamWriterStderr(t *testing.T) {
24
+	buffer := &bytes.Buffer{}
25
+	content := "content"
26
+	sw := NewStderrWriter(buffer)
27
+	size, err := sw.Write([]byte(content))
28
+
29
+	assert.NilError(t, err)
30
+	assert.Check(t, is.Equal(len(content), size))
31
+
32
+	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
33
+	assert.Check(t, is.Equal(expected, buffer.String()))
34
+}
... ...
@@ -19,10 +19,10 @@ import (
19 19
 	"github.com/docker/docker/daemon/builder/remotecontext/urlutil"
20 20
 	"github.com/docker/docker/daemon/internal/system"
21 21
 	"github.com/docker/docker/pkg/longpath"
22
-	"github.com/docker/docker/pkg/streamformatter"
23 22
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
24 23
 	"github.com/moby/go-archive"
25 24
 	"github.com/moby/moby/api/pkg/progress"
25
+	"github.com/moby/moby/api/pkg/streamformatter"
26 26
 	"github.com/moby/sys/symlink"
27 27
 	"github.com/moby/sys/user"
28 28
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -27,10 +27,10 @@ import (
27 27
 	"github.com/docker/docker/daemon/internal/stringid"
28 28
 	"github.com/docker/docker/daemon/server/backend"
29 29
 	"github.com/docker/docker/errdefs"
30
-	"github.com/docker/docker/pkg/streamformatter"
31 30
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
32 31
 	"github.com/moby/go-archive"
33 32
 	"github.com/moby/moby/api/pkg/progress"
33
+	"github.com/moby/moby/api/pkg/streamformatter"
34 34
 	"github.com/moby/moby/api/types/container"
35 35
 	"github.com/moby/moby/api/types/events"
36 36
 	"github.com/moby/moby/api/types/registry"
... ...
@@ -17,8 +17,8 @@ import (
17 17
 	"github.com/distribution/reference"
18 18
 	"github.com/docker/docker/daemon/images"
19 19
 	"github.com/docker/docker/errdefs"
20
-	"github.com/docker/docker/pkg/streamformatter"
21 20
 	"github.com/moby/go-archive/compression"
21
+	"github.com/moby/moby/api/pkg/streamformatter"
22 22
 	"github.com/moby/moby/api/types/events"
23 23
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24 24
 	"github.com/pkg/errors"
... ...
@@ -20,8 +20,8 @@ import (
20 20
 	"github.com/docker/docker/daemon/internal/metrics"
21 21
 	"github.com/docker/docker/daemon/internal/stringid"
22 22
 	"github.com/docker/docker/errdefs"
23
-	"github.com/docker/docker/pkg/streamformatter"
24 23
 	"github.com/moby/moby/api/pkg/progress"
24
+	"github.com/moby/moby/api/pkg/streamformatter"
25 25
 	"github.com/moby/moby/api/types/events"
26 26
 	registrytypes "github.com/moby/moby/api/types/registry"
27 27
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -19,8 +19,8 @@ import (
19 19
 	"github.com/distribution/reference"
20 20
 	"github.com/docker/docker/daemon/internal/metrics"
21 21
 	"github.com/docker/docker/errdefs"
22
-	"github.com/docker/docker/pkg/streamformatter"
23 22
 	"github.com/moby/moby/api/pkg/progress"
23
+	"github.com/moby/moby/api/pkg/streamformatter"
24 24
 	"github.com/moby/moby/api/types/auxprogress"
25 25
 	"github.com/moby/moby/api/types/events"
26 26
 	"github.com/moby/moby/api/types/registry"
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/docker/docker/daemon/internal/layer"
15 15
 	"github.com/docker/docker/daemon/internal/stringid"
16 16
 	"github.com/docker/docker/daemon/server/backend"
17
-	"github.com/docker/docker/pkg/streamformatter"
18 17
 	"github.com/moby/moby/api/pkg/progress"
18
+	"github.com/moby/moby/api/pkg/streamformatter"
19 19
 	"github.com/moby/moby/api/types/registry"
20 20
 	"github.com/opencontainers/go-digest"
21 21
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	progressutils "github.com/docker/docker/daemon/internal/distribution/utils"
15 15
 	"github.com/docker/docker/daemon/internal/metrics"
16 16
 	"github.com/docker/docker/daemon/server/backend"
17
-	"github.com/docker/docker/pkg/streamformatter"
18 17
 	"github.com/moby/moby/api/pkg/progress"
18
+	"github.com/moby/moby/api/pkg/streamformatter"
19 19
 	"github.com/moby/moby/api/types/registry"
20 20
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21 21
 	"github.com/pkg/errors"
... ...
@@ -22,7 +22,6 @@ import (
22 22
 	"github.com/docker/docker/daemon/pkg/opts"
23 23
 	"github.com/docker/docker/daemon/server/backend"
24 24
 	"github.com/docker/docker/errdefs"
25
-	"github.com/docker/docker/pkg/streamformatter"
26 25
 	controlapi "github.com/moby/buildkit/api/services/control"
27 26
 	"github.com/moby/buildkit/client"
28 27
 	"github.com/moby/buildkit/control"
... ...
@@ -30,6 +29,7 @@ import (
30 30
 	"github.com/moby/buildkit/session"
31 31
 	"github.com/moby/buildkit/util/entitlements"
32 32
 	"github.com/moby/buildkit/util/tracing"
33
+	"github.com/moby/moby/api/pkg/streamformatter"
33 34
 	"github.com/moby/moby/api/types/build"
34 35
 	"github.com/moby/moby/api/types/container"
35 36
 	"github.com/moby/moby/api/types/network"
... ...
@@ -7,8 +7,8 @@ import (
7 7
 	"syscall"
8 8
 
9 9
 	"github.com/containerd/log"
10
-	"github.com/docker/docker/pkg/streamformatter"
11 10
 	"github.com/moby/moby/api/pkg/progress"
11
+	"github.com/moby/moby/api/pkg/streamformatter"
12 12
 )
13 13
 
14 14
 // WriteDistributionProgress is a helper for writing progress from chan to JSON
... ...
@@ -19,10 +19,10 @@ import (
19 19
 	"github.com/docker/docker/daemon/internal/ioutils"
20 20
 	"github.com/docker/docker/daemon/internal/layer"
21 21
 	"github.com/docker/docker/daemon/internal/stringid"
22
-	"github.com/docker/docker/pkg/streamformatter"
23 22
 	"github.com/moby/go-archive/chrootarchive"
24 23
 	"github.com/moby/go-archive/compression"
25 24
 	"github.com/moby/moby/api/pkg/progress"
25
+	"github.com/moby/moby/api/pkg/streamformatter"
26 26
 	"github.com/moby/moby/api/types/events"
27 27
 	"github.com/moby/sys/sequential"
28 28
 	"github.com/moby/sys/symlink"
... ...
@@ -19,8 +19,8 @@ import (
19 19
 	"github.com/docker/docker/daemon/server/backend"
20 20
 	"github.com/docker/docker/daemon/server/httputils"
21 21
 	"github.com/docker/docker/pkg/ioutils"
22
-	"github.com/docker/docker/pkg/streamformatter"
23 22
 	"github.com/moby/moby/api/pkg/progress"
23
+	"github.com/moby/moby/api/pkg/streamformatter"
24 24
 	"github.com/moby/moby/api/types/build"
25 25
 	"github.com/moby/moby/api/types/container"
26 26
 	"github.com/moby/moby/api/types/filters"
... ...
@@ -19,8 +19,8 @@ import (
19 19
 	"github.com/docker/docker/dockerversion"
20 20
 	"github.com/docker/docker/errdefs"
21 21
 	"github.com/docker/docker/pkg/ioutils"
22
-	"github.com/docker/docker/pkg/streamformatter"
23 22
 	"github.com/moby/moby/api/pkg/progress"
23
+	"github.com/moby/moby/api/pkg/streamformatter"
24 24
 	"github.com/moby/moby/api/types/filters"
25 25
 	imagetypes "github.com/moby/moby/api/types/image"
26 26
 	"github.com/moby/moby/api/types/registry"
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"github.com/docker/docker/daemon/server/backend"
11 11
 	"github.com/docker/docker/daemon/server/httputils"
12 12
 	"github.com/docker/docker/pkg/ioutils"
13
-	"github.com/docker/docker/pkg/streamformatter"
13
+	"github.com/moby/moby/api/pkg/streamformatter"
14 14
 	"github.com/moby/moby/api/types"
15 15
 	"github.com/moby/moby/api/types/filters"
16 16
 	"github.com/moby/moby/api/types/registry"
17 17
deleted file mode 100644
... ...
@@ -1,247 +0,0 @@
1
-// Package streamformatter provides helper functions to format a stream.
2
-package streamformatter
3
-
4
-import (
5
-	"encoding/json"
6
-	"fmt"
7
-	"io"
8
-	"strings"
9
-	"sync"
10
-	"time"
11
-
12
-	"github.com/docker/go-units"
13
-	"github.com/moby/moby/api/pkg/progress"
14
-	"github.com/moby/moby/api/types/jsonstream"
15
-)
16
-
17
-// jsonMessage defines a message struct. It describes
18
-// the created time, where it from, status, ID of the
19
-// message. It's used for docker events.
20
-//
21
-// It is a reduced set of [jsonmessage.JSONMessage].
22
-type jsonMessage struct {
23
-	Stream   string               `json:"stream,omitempty"`
24
-	Status   string               `json:"status,omitempty"`
25
-	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
26
-	ID       string               `json:"id,omitempty"`
27
-	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
28
-	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
29
-
30
-	// ErrorMessage contains errors encountered during the operation.
31
-	//
32
-	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
33
-	ErrorMessage string `json:"error,omitempty"` // deprecated
34
-}
35
-
36
-const streamNewline = "\r\n"
37
-
38
-type jsonProgressFormatter struct{}
39
-
40
-func appendNewline(source []byte) []byte {
41
-	return append(source, []byte(streamNewline)...)
42
-}
43
-
44
-// FormatStatus formats the specified objects according to the specified format (and id).
45
-func FormatStatus(id, format string, a ...interface{}) []byte {
46
-	str := fmt.Sprintf(format, a...)
47
-	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
48
-	if err != nil {
49
-		return FormatError(err)
50
-	}
51
-	return appendNewline(b)
52
-}
53
-
54
-// FormatError formats the error as a JSON object
55
-func FormatError(err error) []byte {
56
-	jsonError, ok := err.(*jsonstream.Error)
57
-	if !ok {
58
-		jsonError = &jsonstream.Error{Message: err.Error()}
59
-	}
60
-	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
61
-		return appendNewline(b)
62
-	}
63
-	return []byte(`{"error":"format error"}` + streamNewline)
64
-}
65
-
66
-func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
67
-	return FormatStatus(id, format, a...)
68
-}
69
-
70
-// formatProgress formats the progress information for a specified action.
71
-func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
72
-	if progress == nil {
73
-		progress = &jsonstream.Progress{}
74
-	}
75
-	var auxJSON *json.RawMessage
76
-	if aux != nil {
77
-		auxJSONBytes, err := json.Marshal(aux)
78
-		if err != nil {
79
-			return nil
80
-		}
81
-		auxJSON = new(json.RawMessage)
82
-		*auxJSON = auxJSONBytes
83
-	}
84
-	b, err := json.Marshal(&jsonMessage{
85
-		Status:   action,
86
-		Progress: progress,
87
-		ID:       id,
88
-		Aux:      auxJSON,
89
-	})
90
-	if err != nil {
91
-		return nil
92
-	}
93
-	return appendNewline(b)
94
-}
95
-
96
-type rawProgressFormatter struct{}
97
-
98
-func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
99
-	return []byte(fmt.Sprintf(format, a...) + streamNewline)
100
-}
101
-
102
-func rawProgressString(p *jsonstream.Progress) string {
103
-	if p == nil || (p.Current <= 0 && p.Total <= 0) {
104
-		return ""
105
-	}
106
-	if p.Total <= 0 {
107
-		switch p.Units {
108
-		case "":
109
-			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
110
-		default:
111
-			return fmt.Sprintf("%d %s", p.Current, p.Units)
112
-		}
113
-	}
114
-
115
-	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
116
-	if percentage > 50 {
117
-		percentage = 50
118
-	}
119
-
120
-	numSpaces := 0
121
-	if 50-percentage > 0 {
122
-		numSpaces = 50 - percentage
123
-	}
124
-	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
125
-
126
-	var numbersBox string
127
-	switch {
128
-	case p.HideCounts:
129
-	case p.Units == "": // no units, use bytes
130
-		current := units.HumanSize(float64(p.Current))
131
-		total := units.HumanSize(float64(p.Total))
132
-
133
-		numbersBox = fmt.Sprintf("%8v/%v", current, total)
134
-
135
-		if p.Current > p.Total {
136
-			// remove total display if the reported current is wonky.
137
-			numbersBox = fmt.Sprintf("%8v", current)
138
-		}
139
-	default:
140
-		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
141
-
142
-		if p.Current > p.Total {
143
-			// remove total display if the reported current is wonky.
144
-			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
145
-		}
146
-	}
147
-
148
-	var timeLeftBox string
149
-	if p.Current > 0 && p.Start > 0 && percentage < 50 {
150
-		fromStart := time.Since(time.Unix(p.Start, 0))
151
-		perEntry := fromStart / time.Duration(p.Current)
152
-		left := time.Duration(p.Total-p.Current) * perEntry
153
-		timeLeftBox = " " + left.Round(time.Second).String()
154
-	}
155
-	return pbBox + numbersBox + timeLeftBox
156
-}
157
-
158
-func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
159
-	if progress == nil {
160
-		progress = &jsonstream.Progress{}
161
-	}
162
-	endl := "\r"
163
-	out := rawProgressString(progress)
164
-	if out == "" {
165
-		endl += "\n"
166
-	}
167
-	return []byte(action + " " + out + endl)
168
-}
169
-
170
-// NewProgressOutput returns a progress.Output object that can be passed to
171
-// progress.NewProgressReader.
172
-func NewProgressOutput(out io.Writer) progress.Output {
173
-	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
174
-}
175
-
176
-// NewJSONProgressOutput returns a progress.Output that formats output
177
-// using JSON objects
178
-func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
179
-	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
180
-}
181
-
182
-type formatProgress interface {
183
-	formatStatus(id, format string, a ...interface{}) []byte
184
-	formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte
185
-}
186
-
187
-type progressOutput struct {
188
-	sf       formatProgress
189
-	out      io.Writer
190
-	newLines bool
191
-	mu       sync.Mutex
192
-}
193
-
194
-// WriteProgress formats progress information from a ProgressReader.
195
-func (out *progressOutput) WriteProgress(prog progress.Progress) error {
196
-	var formatted []byte
197
-	if prog.Message != "" {
198
-		formatted = out.sf.formatStatus(prog.ID, prog.Message)
199
-	} else {
200
-		jsonProgress := jsonstream.Progress{
201
-			Current:    prog.Current,
202
-			Total:      prog.Total,
203
-			HideCounts: prog.HideCounts,
204
-			Units:      prog.Units,
205
-		}
206
-		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
207
-	}
208
-
209
-	out.mu.Lock()
210
-	defer out.mu.Unlock()
211
-	_, err := out.out.Write(formatted)
212
-	if err != nil {
213
-		return err
214
-	}
215
-
216
-	if out.newLines && prog.LastUpdate {
217
-		_, err = out.out.Write(out.sf.formatStatus("", ""))
218
-		return err
219
-	}
220
-
221
-	return nil
222
-}
223
-
224
-// AuxFormatter is a streamFormatter that writes aux progress messages
225
-type AuxFormatter struct {
226
-	io.Writer
227
-}
228
-
229
-// Emit emits the given interface as an aux progress message
230
-func (sf *AuxFormatter) Emit(id string, aux interface{}) error {
231
-	auxJSONBytes, err := json.Marshal(aux)
232
-	if err != nil {
233
-		return err
234
-	}
235
-	auxJSON := new(json.RawMessage)
236
-	*auxJSON = auxJSONBytes
237
-	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
238
-	if err != nil {
239
-		return err
240
-	}
241
-	msgJSON = appendNewline(msgJSON)
242
-	n, err := sf.Writer.Write(msgJSON)
243
-	if n != len(msgJSON) {
244
-		return io.ErrShortWrite
245
-	}
246
-	return err
247
-}
248 1
deleted file mode 100644
... ...
@@ -1,110 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/json"
6
-	"errors"
7
-	"strings"
8
-	"testing"
9
-
10
-	"github.com/google/go-cmp/cmp"
11
-	"github.com/moby/moby/api/types/jsonstream"
12
-	"gotest.tools/v3/assert"
13
-	is "gotest.tools/v3/assert/cmp"
14
-)
15
-
16
-func TestRawProgressFormatterFormatStatus(t *testing.T) {
17
-	sf := rawProgressFormatter{}
18
-	res := sf.formatStatus("ID", "%s%d", "a", 1)
19
-	assert.Check(t, is.Equal("a1\r\n", string(res)))
20
-}
21
-
22
-func TestRawProgressFormatterFormatProgress(t *testing.T) {
23
-	sf := rawProgressFormatter{}
24
-	jsonProgress := &jsonstream.Progress{
25
-		Current: 15,
26
-		Total:   30,
27
-		Start:   1,
28
-	}
29
-	res := sf.formatProgress("id", "action", jsonProgress, nil)
30
-	out := string(res)
31
-	assert.Check(t, strings.HasPrefix(out, "action [===="))
32
-	assert.Check(t, is.Contains(out, "15B/30B"))
33
-	assert.Check(t, strings.HasSuffix(out, "\r"))
34
-}
35
-
36
-func TestFormatStatus(t *testing.T) {
37
-	res := FormatStatus("ID", "%s%d", "a", 1)
38
-	expected := `{"status":"a1","id":"ID"}` + streamNewline
39
-	assert.Check(t, is.Equal(expected, string(res)))
40
-}
41
-
42
-func TestFormatError(t *testing.T) {
43
-	res := FormatError(errors.New("Error for formatter"))
44
-	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
45
-	assert.Check(t, is.Equal(expected, string(res)))
46
-}
47
-
48
-func TestFormatJSONError(t *testing.T) {
49
-	err := &jsonstream.Error{Code: 50, Message: "Json error"}
50
-	res := FormatError(err)
51
-	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
52
-	assert.Check(t, is.Equal(expected, string(res)))
53
-}
54
-
55
-func TestJsonProgressFormatterFormatProgress(t *testing.T) {
56
-	sf := &jsonProgressFormatter{}
57
-	jsonProgress := &jsonstream.Progress{
58
-		Current: 15,
59
-		Total:   30,
60
-		Start:   1,
61
-	}
62
-	aux := "aux message"
63
-	res := sf.formatProgress("id", "action", jsonProgress, aux)
64
-	msg := &jsonMessage{}
65
-
66
-	assert.NilError(t, json.Unmarshal(res, msg))
67
-
68
-	rawAux := json.RawMessage(`"` + aux + `"`)
69
-	expected := &jsonMessage{
70
-		ID:       "id",
71
-		Status:   "action",
72
-		Aux:      &rawAux,
73
-		Progress: jsonProgress,
74
-	}
75
-	assert.DeepEqual(t, msg, expected, cmpJSONMessageOpt())
76
-}
77
-
78
-func cmpJSONMessageOpt() cmp.Option {
79
-	progressMessagePath := func(path cmp.Path) bool {
80
-		return path.String() == "ProgressMessage"
81
-	}
82
-	return cmp.Options{
83
-		// Ignore deprecated property that is a derivative of Progress
84
-		cmp.FilterPath(progressMessagePath, cmp.Ignore()),
85
-	}
86
-}
87
-
88
-func TestJsonProgressFormatterFormatStatus(t *testing.T) {
89
-	sf := jsonProgressFormatter{}
90
-	res := sf.formatStatus("ID", "%s%d", "a", 1)
91
-	assert.Check(t, is.Equal(`{"status":"a1","id":"ID"}`+streamNewline, string(res)))
92
-}
93
-
94
-func TestNewJSONProgressOutput(t *testing.T) {
95
-	b := bytes.Buffer{}
96
-	b.Write(FormatStatus("id", "Downloading"))
97
-	_ = NewJSONProgressOutput(&b, false)
98
-	assert.Check(t, is.Equal(`{"status":"Downloading","id":"id"}`+streamNewline, b.String()))
99
-}
100
-
101
-func TestAuxFormatterEmit(t *testing.T) {
102
-	b := bytes.Buffer{}
103
-	aux := &AuxFormatter{Writer: &b}
104
-	sampleAux := &struct {
105
-		Data string
106
-	}{"Additional data"}
107
-	err := aux.Emit("", sampleAux)
108
-	assert.NilError(t, err)
109
-	assert.Check(t, is.Equal(`{"aux":{"Data":"Additional data"}}`+streamNewline, b.String()))
110
-}
111 1
deleted file mode 100644
... ...
@@ -1,45 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"encoding/json"
5
-	"io"
6
-)
7
-
8
-type streamWriter struct {
9
-	io.Writer
10
-	lineFormat func([]byte) string
11
-}
12
-
13
-func (sw *streamWriter) Write(buf []byte) (int, error) {
14
-	formattedBuf := sw.format(buf)
15
-	n, err := sw.Writer.Write(formattedBuf)
16
-	if n != len(formattedBuf) {
17
-		return n, io.ErrShortWrite
18
-	}
19
-	return len(buf), err
20
-}
21
-
22
-func (sw *streamWriter) format(buf []byte) []byte {
23
-	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
24
-	b, err := json.Marshal(msg)
25
-	if err != nil {
26
-		return FormatError(err)
27
-	}
28
-	return appendNewline(b)
29
-}
30
-
31
-// NewStdoutWriter returns a writer which formats the output as json message
32
-// representing stdout lines
33
-func NewStdoutWriter(out io.Writer) io.Writer {
34
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
35
-		return string(buf)
36
-	}}
37
-}
38
-
39
-// NewStderrWriter returns a writer which formats the output as json message
40
-// representing stderr lines
41
-func NewStderrWriter(out io.Writer) io.Writer {
42
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
43
-		return "\033[91m" + string(buf) + "\033[0m"
44
-	}}
45
-}
46 1
deleted file mode 100644
... ...
@@ -1,35 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"bytes"
5
-	"testing"
6
-
7
-	"gotest.tools/v3/assert"
8
-	is "gotest.tools/v3/assert/cmp"
9
-)
10
-
11
-func TestStreamWriterStdout(t *testing.T) {
12
-	buffer := &bytes.Buffer{}
13
-	content := "content"
14
-	sw := NewStdoutWriter(buffer)
15
-	size, err := sw.Write([]byte(content))
16
-
17
-	assert.NilError(t, err)
18
-	assert.Check(t, is.Equal(len(content), size))
19
-
20
-	expected := `{"stream":"content"}` + streamNewline
21
-	assert.Check(t, is.Equal(expected, buffer.String()))
22
-}
23
-
24
-func TestStreamWriterStderr(t *testing.T) {
25
-	buffer := &bytes.Buffer{}
26
-	content := "content"
27
-	sw := NewStderrWriter(buffer)
28
-	size, err := sw.Write([]byte(content))
29
-
30
-	assert.NilError(t, err)
31
-	assert.Check(t, is.Equal(len(content), size))
32
-
33
-	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
34
-	assert.Check(t, is.Equal(expected, buffer.String()))
35
-}
36 1
new file mode 100644
... ...
@@ -0,0 +1,247 @@
0
+// Package streamformatter provides helper functions to format a stream.
1
+package streamformatter
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"strings"
8
+	"sync"
9
+	"time"
10
+
11
+	"github.com/docker/go-units"
12
+	"github.com/moby/moby/api/pkg/progress"
13
+	"github.com/moby/moby/api/types/jsonstream"
14
+)
15
+
16
+// jsonMessage defines a message struct. It describes
17
+// the created time, where it from, status, ID of the
18
+// message. It's used for docker events.
19
+//
20
+// It is a reduced set of [jsonmessage.JSONMessage].
21
+type jsonMessage struct {
22
+	Stream   string               `json:"stream,omitempty"`
23
+	Status   string               `json:"status,omitempty"`
24
+	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
25
+	ID       string               `json:"id,omitempty"`
26
+	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
27
+	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
28
+
29
+	// ErrorMessage contains errors encountered during the operation.
30
+	//
31
+	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
32
+	ErrorMessage string `json:"error,omitempty"` // deprecated
33
+}
34
+
35
+const streamNewline = "\r\n"
36
+
37
+type jsonProgressFormatter struct{}
38
+
39
+func appendNewline(source []byte) []byte {
40
+	return append(source, []byte(streamNewline)...)
41
+}
42
+
43
+// FormatStatus formats the specified objects according to the specified format (and id).
44
+func FormatStatus(id, format string, a ...interface{}) []byte {
45
+	str := fmt.Sprintf(format, a...)
46
+	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
47
+	if err != nil {
48
+		return FormatError(err)
49
+	}
50
+	return appendNewline(b)
51
+}
52
+
53
+// FormatError formats the error as a JSON object
54
+func FormatError(err error) []byte {
55
+	jsonError, ok := err.(*jsonstream.Error)
56
+	if !ok {
57
+		jsonError = &jsonstream.Error{Message: err.Error()}
58
+	}
59
+	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
60
+		return appendNewline(b)
61
+	}
62
+	return []byte(`{"error":"format error"}` + streamNewline)
63
+}
64
+
65
+func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
66
+	return FormatStatus(id, format, a...)
67
+}
68
+
69
+// formatProgress formats the progress information for a specified action.
70
+func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
71
+	if progress == nil {
72
+		progress = &jsonstream.Progress{}
73
+	}
74
+	var auxJSON *json.RawMessage
75
+	if aux != nil {
76
+		auxJSONBytes, err := json.Marshal(aux)
77
+		if err != nil {
78
+			return nil
79
+		}
80
+		auxJSON = new(json.RawMessage)
81
+		*auxJSON = auxJSONBytes
82
+	}
83
+	b, err := json.Marshal(&jsonMessage{
84
+		Status:   action,
85
+		Progress: progress,
86
+		ID:       id,
87
+		Aux:      auxJSON,
88
+	})
89
+	if err != nil {
90
+		return nil
91
+	}
92
+	return appendNewline(b)
93
+}
94
+
95
+type rawProgressFormatter struct{}
96
+
97
+func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
98
+	return []byte(fmt.Sprintf(format, a...) + streamNewline)
99
+}
100
+
101
+func rawProgressString(p *jsonstream.Progress) string {
102
+	if p == nil || (p.Current <= 0 && p.Total <= 0) {
103
+		return ""
104
+	}
105
+	if p.Total <= 0 {
106
+		switch p.Units {
107
+		case "":
108
+			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
109
+		default:
110
+			return fmt.Sprintf("%d %s", p.Current, p.Units)
111
+		}
112
+	}
113
+
114
+	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
115
+	if percentage > 50 {
116
+		percentage = 50
117
+	}
118
+
119
+	numSpaces := 0
120
+	if 50-percentage > 0 {
121
+		numSpaces = 50 - percentage
122
+	}
123
+	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
124
+
125
+	var numbersBox string
126
+	switch {
127
+	case p.HideCounts:
128
+	case p.Units == "": // no units, use bytes
129
+		current := units.HumanSize(float64(p.Current))
130
+		total := units.HumanSize(float64(p.Total))
131
+
132
+		numbersBox = fmt.Sprintf("%8v/%v", current, total)
133
+
134
+		if p.Current > p.Total {
135
+			// remove total display if the reported current is wonky.
136
+			numbersBox = fmt.Sprintf("%8v", current)
137
+		}
138
+	default:
139
+		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
140
+
141
+		if p.Current > p.Total {
142
+			// remove total display if the reported current is wonky.
143
+			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
144
+		}
145
+	}
146
+
147
+	var timeLeftBox string
148
+	if p.Current > 0 && p.Start > 0 && percentage < 50 {
149
+		fromStart := time.Since(time.Unix(p.Start, 0))
150
+		perEntry := fromStart / time.Duration(p.Current)
151
+		left := time.Duration(p.Total-p.Current) * perEntry
152
+		timeLeftBox = " " + left.Round(time.Second).String()
153
+	}
154
+	return pbBox + numbersBox + timeLeftBox
155
+}
156
+
157
+func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
158
+	if progress == nil {
159
+		progress = &jsonstream.Progress{}
160
+	}
161
+	endl := "\r"
162
+	out := rawProgressString(progress)
163
+	if out == "" {
164
+		endl += "\n"
165
+	}
166
+	return []byte(action + " " + out + endl)
167
+}
168
+
169
+// NewProgressOutput returns a progress.Output object that can be passed to
170
+// progress.NewProgressReader.
171
+func NewProgressOutput(out io.Writer) progress.Output {
172
+	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
173
+}
174
+
175
+// NewJSONProgressOutput returns a progress.Output that formats output
176
+// using JSON objects
177
+func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
178
+	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
179
+}
180
+
181
+type formatProgress interface {
182
+	formatStatus(id, format string, a ...interface{}) []byte
183
+	formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte
184
+}
185
+
186
+type progressOutput struct {
187
+	sf       formatProgress
188
+	out      io.Writer
189
+	newLines bool
190
+	mu       sync.Mutex
191
+}
192
+
193
+// WriteProgress formats progress information from a ProgressReader.
194
+func (out *progressOutput) WriteProgress(prog progress.Progress) error {
195
+	var formatted []byte
196
+	if prog.Message != "" {
197
+		formatted = out.sf.formatStatus(prog.ID, prog.Message)
198
+	} else {
199
+		jsonProgress := jsonstream.Progress{
200
+			Current:    prog.Current,
201
+			Total:      prog.Total,
202
+			HideCounts: prog.HideCounts,
203
+			Units:      prog.Units,
204
+		}
205
+		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
206
+	}
207
+
208
+	out.mu.Lock()
209
+	defer out.mu.Unlock()
210
+	_, err := out.out.Write(formatted)
211
+	if err != nil {
212
+		return err
213
+	}
214
+
215
+	if out.newLines && prog.LastUpdate {
216
+		_, err = out.out.Write(out.sf.formatStatus("", ""))
217
+		return err
218
+	}
219
+
220
+	return nil
221
+}
222
+
223
+// AuxFormatter is a streamFormatter that writes aux progress messages
224
+type AuxFormatter struct {
225
+	io.Writer
226
+}
227
+
228
+// Emit emits the given interface as an aux progress message
229
+func (sf *AuxFormatter) Emit(id string, aux interface{}) error {
230
+	auxJSONBytes, err := json.Marshal(aux)
231
+	if err != nil {
232
+		return err
233
+	}
234
+	auxJSON := new(json.RawMessage)
235
+	*auxJSON = auxJSONBytes
236
+	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
237
+	if err != nil {
238
+		return err
239
+	}
240
+	msgJSON = appendNewline(msgJSON)
241
+	n, err := sf.Writer.Write(msgJSON)
242
+	if n != len(msgJSON) {
243
+		return io.ErrShortWrite
244
+	}
245
+	return err
246
+}
0 247
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+package streamformatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+)
6
+
7
+type streamWriter struct {
8
+	io.Writer
9
+	lineFormat func([]byte) string
10
+}
11
+
12
+func (sw *streamWriter) Write(buf []byte) (int, error) {
13
+	formattedBuf := sw.format(buf)
14
+	n, err := sw.Writer.Write(formattedBuf)
15
+	if n != len(formattedBuf) {
16
+		return n, io.ErrShortWrite
17
+	}
18
+	return len(buf), err
19
+}
20
+
21
+func (sw *streamWriter) format(buf []byte) []byte {
22
+	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
23
+	b, err := json.Marshal(msg)
24
+	if err != nil {
25
+		return FormatError(err)
26
+	}
27
+	return appendNewline(b)
28
+}
29
+
30
+// NewStdoutWriter returns a writer which formats the output as json message
31
+// representing stdout lines
32
+func NewStdoutWriter(out io.Writer) io.Writer {
33
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
34
+		return string(buf)
35
+	}}
36
+}
37
+
38
+// NewStderrWriter returns a writer which formats the output as json message
39
+// representing stderr lines
40
+func NewStderrWriter(out io.Writer) io.Writer {
41
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
42
+		return "\033[91m" + string(buf) + "\033[0m"
43
+	}}
44
+}
... ...
@@ -941,6 +941,7 @@ github.com/moby/locker
941 941
 github.com/moby/moby/api
942 942
 github.com/moby/moby/api/pkg/progress
943 943
 github.com/moby/moby/api/pkg/stdcopy
944
+github.com/moby/moby/api/pkg/streamformatter
944 945
 github.com/moby/moby/api/types
945 946
 github.com/moby/moby/api/types/auxprogress
946 947
 github.com/moby/moby/api/types/blkiodev