From 8584a3f00d1d66abacfc718b39a95c4d1f147712 Mon Sep 17 00:00:00 2001 From: Jia He Date: Thu, 16 Sep 2021 16:31:28 +0800 Subject: [PATCH] fix(*): refactor code structure --- multi_writer.go | 95 -------- multi_writer_test.go | 70 ------ multipart_reader.go | 128 ++++++++++ ...writer_test.go => multipart_reader_test.go | 53 +++-- response_writer.go | 219 ------------------ transformer.go | 81 +++++++ transformer_test.go | 72 ++++++ 7 files changed, 309 insertions(+), 409 deletions(-) delete mode 100644 multi_writer.go delete mode 100644 multi_writer_test.go create mode 100644 multipart_reader.go rename response_writer_test.go => multipart_reader_test.go (78%) delete mode 100644 response_writer.go create mode 100644 transformer.go create mode 100644 transformer_test.go diff --git a/multi_writer.go b/multi_writer.go deleted file mode 100644 index 842b428..0000000 --- a/multi_writer.go +++ /dev/null @@ -1,95 +0,0 @@ -package multipart - -import ( - "bytes" - "fmt" - "io" -) - -type Writer struct { - src io.ReadSeeker - pr *io.PipeReader - pw *io.PipeWriter - boundary string - parts []*Part -} - -func NewWriter(src io.ReadSeeker, parts []*Part) *Writer { - boundary := randomBoundary() - return NewWriterWithBoundary(src, parts, boundary) -} - -func NewWriterWithBoundary(src io.ReadSeeker, parts []*Part, boundary string) *Writer { - pr, pw := io.Pipe() - return &Writer{ - src: src, - pr: pr, - pw: pw, - boundary: boundary, - parts: parts, - } -} - -func (w *Writer) ContentLength() int64 { - var buf bytes.Buffer - partsBodyLen := int64(0) - - for _, part := range w.parts { - partsBodyLen += part.rangeEndInt - part.rangeStartInt + 1 - w.WritePartHeader(&buf, part) - } - fmt.Fprintf(&buf, "\r\n--%s--", w.boundary) - partsHeaderLen := int64(buf.Len()) - - // the first CRLF is not part of message body - // ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html - return partsHeaderLen + partsBodyLen - 2 -} - -func (w *Writer) SetBoundary(boundary string) { - w.boundary = boundary -} - -func (w *Writer) WritePartHeader(buf io.Writer, part *Part) error { - fmt.Fprintf(buf, "\r\n--%s\r\n", w.boundary) - fmt.Fprint(buf, "Content-Type: application/octet-stream\r\n") - fmt.Fprintf(buf, "Content-Range: bytes %s-%s/%s\r\n", part.rangeStart, part.rangeEnd, part.fileSize) - fmt.Fprint(buf, "\r\n") - - return nil -} - -func (w *Writer) WriteMultiParts() error { - var err error - for _, part := range w.parts { - if err = w.WritePartHeader(w.pw, part); err != nil { - return err - } - if err = writePartBody(w.src, w.pw, part); err != nil { - return err - } - } - - return nil -} - -func (w *Writer) Write(bytes []byte) (n int, err error) { - return w.pw.Write(bytes) -} - -func (w *Writer) Close() error { - var err error - _, err = fmt.Fprintf(w.pw, "\r\n--%s--", w.boundary) - if err != nil { - return err - } - return w.pw.Close() -} - -func (w *Writer) CloseWithError(err error) error { - return w.pw.CloseWithError(err) -} - -func (w *Writer) Read(p []byte) (n int, err error) { - return w.pr.Read(p) -} diff --git a/multi_writer_test.go b/multi_writer_test.go deleted file mode 100644 index 10c43e3..0000000 --- a/multi_writer_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package multipart - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" -) - -func TestWriter(t *testing.T) { - type HttpRange struct { - contentType string - rangeStart string - rangeEnd string - fileSize string - // content string - } - type TestCase struct { - content string - rangeHeader string - // parts []*Part - expectedOut string - } - - sep := "THIS_STRING_SEPARATES" - t.Run("normal cases", func(t *testing.T) { - testCases := []*TestCase{ - &TestCase{ - content: "0123456789", - rangeHeader: "bytes=0-3, 8-8", - expectedOut: "\r\n--THIS_STRING_SEPARATES\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes 0-3/10\r\n\r\n0123\r\n--THIS_STRING_SEPARATES\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes 8-8/10\r\n\r\n8\r\n--THIS_STRING_SEPARATES--", - }, - } - - for _, tc := range testCases { - parts, err := RangeToParts(tc.rangeHeader, "application/octet-stream", fmt.Sprintf("%d", len(tc.content))) - if err != nil { - t.Fatal(err) - } - - r := bytes.NewReader([]byte(tc.content)) - w := NewWriterWithBoundary(r, parts, sep) - c := make(chan bool, 0) - go func() { - respBytes, err := ioutil.ReadAll(w) - if err != nil { - t.Fatal(err) - } - if string(respBytes) != tc.expectedOut { - t.Error("\nnot equal 1.expected 2.got") - t.Error(tc.expectedOut) - t.Error(string(respBytes)) - } - c <- true - }() - - err = w.WriteMultiParts() - if err != nil { - t.Fatal(err) - } - - err = w.Close() - if err != nil { - t.Fatal(err) - } - - <-c - } - }) -} diff --git a/multipart_reader.go b/multipart_reader.go new file mode 100644 index 0000000..a11aeda --- /dev/null +++ b/multipart_reader.go @@ -0,0 +1,128 @@ +package multipart + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/textproto" +) + +var ErrClosed = func(err error) error { + return fmt.Errorf("pipe is closed before writing %s", err) +} + +type MultipartReader struct { + src ReadSeekCloser + outputHeaders bool + contentLen int64 + parts []*Part + boundary string + w *io.PipeWriter + r *io.PipeReader + transformer *Transformer +} + +func NewMultipartReader(src ReadSeekCloser, parts []*Part) (*MultipartReader, error) { + return NewMultipartReaderWithBoudary(src, parts, randomBoundary()) +} + +func NewMultipartReaderWithBoudary(src ReadSeekCloser, parts []*Part, boundary string) (*MultipartReader, error) { + r, w := io.Pipe() + mpReader := &MultipartReader{ + src: src, + parts: parts, + boundary: boundary, + w: w, + r: r, + transformer: NewTransformerWithBoundary(src, parts, boundary), + } + + switch len(parts) { + case 0: + return nil, errors.New("no part to write") + case 1: + // rw.reader, rw.pw = io.Pipe() + mpReader.contentLen = int64(parts[0].rangeEndInt - parts[0].rangeStartInt + 1) + default: + // rw.reader, rw.mw = mw, mw + mpReader.contentLen = mpReader.transformer.ContentLength() + } + + return mpReader, nil +} + +func (mr *MultipartReader) ContentLength() int64 { + return mr.contentLen +} + +func (mr *MultipartReader) SetOutputHeaders(val bool) { + mr.outputHeaders = val +} + +func (mr *MultipartReader) Start() { + var err error + headerBuf := new(bytes.Buffer) + + if len(mr.parts) == 1 { + if mr.outputHeaders { + if err := writeStatus(headerBuf, 206); err != nil { + mr.w.CloseWithError(err) + } + + headers := textproto.MIMEHeader{} + headers.Add("Content-Range", fmt.Sprintf("bytes %s-%s/%s", mr.parts[0].rangeStart, mr.parts[0].rangeEnd, mr.parts[0].fileSize)) + if err = writeHeaders(headerBuf, headers); err != nil { + mr.w.CloseWithError(err) + } + } + + _, err = io.Copy(mr.w, headerBuf) + if err != nil { + mr.w.CloseWithError(err) + return + } + _, err = mr.w.Write([]byte("\r\n")) + if err != nil { + mr.w.CloseWithError(err) + } + if err = writePartBody(mr.src, mr.w, mr.parts[0]); err != nil { + mr.w.CloseWithError(err) + return + } + } else { + if mr.outputHeaders { + if err := writeStatus(headerBuf, 206); err != nil { + mr.w.CloseWithError(err) + } + + headers := textproto.MIMEHeader{} + headers.Add("Content-Type", fmt.Sprintf("multipart/byteranges; boundary=%s", mr.boundary)) + if err = writeHeaders(headerBuf, headers); err != nil { + mr.w.CloseWithError(err) + } + } + + _, err = io.Copy(mr.w, headerBuf) + if err != nil { + mr.w.CloseWithError(err) + return + } + + if err = mr.transformer.WriteMultiParts(mr.w); err != nil { + mr.w.CloseWithError(err) + return + } + } + + // source file should be closed by user + mr.w.CloseWithError(nil) // TODO: log error +} + +func (mr *MultipartReader) Read(p []byte) (n int, err error) { + return mr.r.Read(p) +} + +func (mr *MultipartReader) Close() error { + return mr.r.Close() +} diff --git a/response_writer_test.go b/multipart_reader_test.go similarity index 78% rename from response_writer_test.go rename to multipart_reader_test.go index 9274d76..12c0691 100644 --- a/response_writer_test.go +++ b/multipart_reader_test.go @@ -1,16 +1,17 @@ package multipart import ( + "bytes" "fmt" "io/ioutil" "strings" "testing" ) -func TestWriteMultipartResponse(t *testing.T) { +func TestMultipartReader(t *testing.T) { fileName := "download.jpg" ctype := "application/pdf" - boundary := "THIS_STRING_SEPARATES" + boundary := "BOUNDARY" type testCase struct { src string @@ -31,29 +32,29 @@ func TestWriteMultipartResponse(t *testing.T) { fileSize: fmt.Sprintf("%d", len("10110")), ranges: "bytes=1-2, 3-3, -2, 2-", expectOut: `HTTP/1.1 206 Partial Content -Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES +Content-Type: multipart/byteranges; boundary=BOUNDARY ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes 1-2/5 01 ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes 3-3/5 1 ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes -2/5 10 ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes 2-/5 110 ---THIS_STRING_SEPARATES--`, +--BOUNDARY--`, }, &testCase{ // unknown file size cases @@ -63,36 +64,37 @@ Content-Range: bytes 2-/5 fileSize: "*", ranges: "bytes=0-1, 3-3", expectOut: `HTTP/1.1 206 Partial Content -Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES +Content-Type: multipart/byteranges; boundary=BOUNDARY ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes 0-1/* 10 ---THIS_STRING_SEPARATES +--BOUNDARY Content-Type: application/octet-stream Content-Range: bytes 3-3/* 1 ---THIS_STRING_SEPARATES--`, +--BOUNDARY--`, }, } for _, tc := range testCases { - reader := strings.NewReader(tc.src) + reader := NewMockReadSeekCloser(bytes.NewReader([]byte(tc.src))) parts, err := RangeToParts(tc.ranges, ctype, tc.fileSize) if err != nil { t.Fatal(err) } // pr, pw := io.Pipe() - w, contentLen, err := NewResponseWriterWithBoudary(reader, parts, boundary, true) + w, err := NewMultipartReaderWithBoudary(reader, parts, boundary) if err != nil { t.Fatal(err) } + w.SetOutputHeaders(true) - go w.Write() + go w.Start() // go WriteResponseWithBoundary(reader, pw, fileName, parts, boundary) expectOut := strings.ReplaceAll(tc.expectOut, "\n", "\r\n") @@ -112,16 +114,16 @@ Content-Range: bytes 3-3/* headBody := strings.SplitN(body, "\r\n\r\n", 2) expectHeadBody := strings.SplitN(expectOut, "\r\n\r\n", 2) - if contentLen != int64(len([]byte(expectHeadBody[1]))) { - t.Errorf("content length incorrect: expect(%d) got(%d)", len([]byte(expectHeadBody[1])), contentLen) + if w.ContentLength() != int64(len([]byte(expectHeadBody[1]))) { + t.Errorf("content length incorrect: expect(%d) got(%d)", len([]byte(expectHeadBody[1])), w.ContentLength()) } - if contentLen != int64(len([]byte(headBody[1]))) { - t.Errorf("content length & body length unmatch: cLen(%d) contentLen(%d)", contentLen, len([]byte(headBody[1]))) + if w.ContentLength() != int64(len([]byte(headBody[1]))) { + t.Errorf("content length & body length unmatch: cLen(%d) w.ContentLength()(%d)", w.ContentLength(), len([]byte(headBody[1]))) } } }) - t.Run("single parts", func(t *testing.T) { + t.Run("single part", func(t *testing.T) { testCases := []*testCase{ &testCase{ // normal case, single byte, bytes from end, from specific byte to end @@ -171,19 +173,20 @@ Content-Range: bytes 1-2/* } for _, tc := range testCases { - reader := strings.NewReader(tc.src) + reader := NewMockReadSeekCloser(bytes.NewReader([]byte(tc.src))) parts, err := RangeToParts(tc.ranges, ctype, tc.fileSize) if err != nil { t.Fatal(err) } // pr, pw := io.Pipe() - w, contentLen, err := NewResponseWriterWithBoudary(reader, parts, boundary, true) + w, err := NewMultipartReaderWithBoudary(reader, parts, boundary) if err != nil { t.Fatal(err) } + w.SetOutputHeaders(true) - go w.Write() + go w.Start() // go WriteResponseWithBoundary(reader, pw, fileName, parts, boundary) expectOut := strings.ReplaceAll(tc.expectOut, "\n", "\r\n") @@ -201,8 +204,8 @@ Content-Range: bytes 1-2/* } headBody := strings.Split(body, "\r\n\r\n") - if contentLen != int64(len([]byte(headBody[1]))) { - t.Errorf("content length incorrect: expect(%d) got(%d)", len([]byte(expectOut)), contentLen) + if w.ContentLength() != int64(len([]byte(headBody[1]))) { + t.Errorf("content length incorrect: expect(%d) got(%d)", len([]byte(expectOut)), w.ContentLength()) } } }) diff --git a/response_writer.go b/response_writer.go deleted file mode 100644 index 987b3b2..0000000 --- a/response_writer.go +++ /dev/null @@ -1,219 +0,0 @@ -package multipart - -import ( - "bytes" - "errors" - "fmt" - "io" - "net/textproto" -) - -var ErrClosed = func(err error) error { - return fmt.Errorf("pipe is closed before writing %s", err) -} - -// type CountablePipeWriter struct { -// *io.PipeWriter -// wrote int64 -// } - -// func (w *CountablePipeWriter) Write(b []byte) (int, error) { -// wrote, err := w.PipeWriter.Write(b) -// w.wrote += int64(wrote) -// return wrote, err -// } - -// func (w *CountablePipeWriter) Wrote() int64 { -// return w.wrote -// } - -// type IPipeWriter interface { -// Close() error -// CloseWithError(err error) error -// Write(data []byte) (n int, err error) -// } - -type ResponseWriter struct { - src io.ReadSeeker - headerBuf *bytes.Buffer - parts []*Part - reader io.Reader - pw *io.PipeWriter - mw *Writer - // contentLength int64 - // fileName string - // boundary string - // mw *Writer - // pp *CountablePipeWriter -} - -func NewResponseWriter(src io.ReadSeeker, parts []*Part, outputHeaders bool) (*ResponseWriter, int64, error) { - return NewResponseWriterWithBoudary(src, parts, randomBoundary(), outputHeaders) -} - -func NewResponseWriterWithBoudary(src io.ReadSeeker, parts []*Part, boundary string, outputHeaders bool) (*ResponseWriter, int64, error) { - var ( - err error - contentLength int64 - ) - rw := &ResponseWriter{ - src: src, - headerBuf: new(bytes.Buffer), - parts: parts, - } - - switch len(parts) { - case 0: - return nil, 0, errors.New("no part to write") - case 1: - if outputHeaders { - if err := writeStatus(rw.headerBuf, 206); err != nil { - return nil, 0, err - } - - headers := textproto.MIMEHeader{} - headers.Add("Content-Range", fmt.Sprintf("bytes %s-%s/%s", parts[0].rangeStart, parts[0].rangeEnd, parts[0].fileSize)) - if err = writeHeaders(rw.headerBuf, headers); err != nil { - return nil, 0, err - } - } - - rw.reader, rw.pw = io.Pipe() - contentLength = int64(parts[0].rangeEndInt - parts[0].rangeStartInt + 1) - default: - if outputHeaders { - if err := writeStatus(rw.headerBuf, 206); err != nil { - return nil, 0, err - } - - headers := textproto.MIMEHeader{} - headers.Add("Content-Type", fmt.Sprintf("multipart/byteranges; boundary=%s", boundary)) - if err = writeHeaders(rw.headerBuf, headers); err != nil { - return nil, 0, err - } - } - - mw := NewWriterWithBoundary(src, parts, boundary) - rw.reader, rw.mw = mw, mw - contentLength = mw.ContentLength() - } - - return rw, contentLength, nil -} - -// func (w *ResponseWriter) Write(fileName string) { -// w.WriteWithBoundary(fileName, randomBoundary()) -// } - -func (w *ResponseWriter) Write() { - var err error - if len(w.parts) == 1 { - _, err = io.Copy(w.pw, w.headerBuf) - if err != nil { - w.pw.CloseWithError(err) - return - } - _, err = w.pw.Write([]byte("\r\n")) - if err != nil { - w.pw.CloseWithError(err) - } - if err = writePartBody(w.src, w.pw, w.parts[0]); err != nil { - w.pw.CloseWithError(err) - return - } - w.pw.Close() - } else { - _, err = io.Copy(w.mw, w.headerBuf) - if err != nil { - w.pw.CloseWithError(err) - return - } - // for _, part := range w.parts { - // if err = w.mw.CreatePart(part.contentType, part.rangeStart, part.rangeEnd, part.fileSize); err != nil { - // w.mw.CloseWithError(err) - // return - // } - // if err = writePartBody(w.src, w.mw, part); err != nil { - // w.mw.CloseWithError(err) - // return - // } - // } - - if err = w.mw.WriteMultiParts(); err != nil { - w.mw.CloseWithError(err) - return - } - w.mw.Close() - } -} - -// if err != io.ErrClosedPipe { -// w.pw.CloseWithError(err) -// return false -// } -// panic(ErrClosed(err)) - -func (w *ResponseWriter) Read(p []byte) (n int, err error) { - if len(w.parts) == 1 { - return w.reader.Read(p) - } - return w.mw.Read(p) -} - -// func writeMultiPart(src io.ReadSeeker, mw *Writer, fileName string, parts []*Part, contentLen *int64) { -// for _, part := range parts { -// if !mw.CreatePart(part.contentType, part.rangeStart, part.rangeEnd, part.fileSize) { -// return -// } -// if !writePartBody(src, mw, part) { -// return -// } -// } - -// mw.Close() -// } - -// func WriteResponse(src io.ReadSeeker, dst *io.PipeWriter, fileName string, parts []*Part, contentLen *int64) { -// writer := &CountablePipeWriter{ -// boundary: boundary, -// PipeWriter: dst, -// wrote: 0, -// } -// WriteResponseWithBoundary(src, writer, fileName, parts, contentLen, randomBoundary()) -// } - -// func WriteResponseWithBoundary(src io.ReadSeeker, dst *CountablePipeWriter, fileName string, parts []*Part, contentLen *int64, boundary string) { -// if len(parts) == 0 { -// writeErrResponse(dst, 401, errors.New("no part to write")) -// return -// } else if len(parts) == 1 { -// writeSinglePart(src, dst, fileName, parts[0], contentLen) -// } - -// mw, ok := NewWriterWithBoundary(dst, boundary) -// if !ok { -// return -// } - -// writeMultiPart(src, mw, fileName, parts, contentLen) -// } - -// TODO: Add header: Content-Length -// func writeSinglePart(src io.ReadSeeker, writer *CountablePipeWriter, fileName string, part *Part, contentLen *int64) { -// if !writeStatus(writer, 206) { -// return -// } - -// headers := textproto.MIMEHeader{} -// headers.Add("Content-Range", fmt.Sprintf("bytes %s-%s/%s", part.rangeStart, part.rangeEnd, part.fileSize)) -// if !writeHeaders(writer, headers) { -// return -// } - -// *contentLen = writer.Wrote() + part.rangeEndInt - part.rangeStartInt + 1 -// if !writePartBody(src, writer, part) { -// return -// } - -// writer.Close() -// } diff --git a/transformer.go b/transformer.go new file mode 100644 index 0000000..e1a7dba --- /dev/null +++ b/transformer.go @@ -0,0 +1,81 @@ +package multipart + +import ( + "bytes" + "fmt" + "io" +) + +// TODO: this is introduced in Go 1.16 +type ReadSeekCloser interface { + io.Reader + io.Seeker + io.Closer +} + +type Transformer struct { + src ReadSeekCloser + boundary string + parts []*Part +} + +func NewTransformer(src ReadSeekCloser, parts []*Part) *Transformer { + boundary := randomBoundary() + return NewTransformerWithBoundary(src, parts, boundary) +} + +func NewTransformerWithBoundary(src ReadSeekCloser, parts []*Part, boundary string) *Transformer { + return &Transformer{ + src: src, + boundary: boundary, + parts: parts, + } +} + +func (tfm *Transformer) SetBoundary(boundary string) { + tfm.boundary = boundary +} + +func (tfm *Transformer) ContentLength() int64 { + var buf bytes.Buffer + partsBodyLen := int64(0) + + for _, part := range tfm.parts { + partsBodyLen += part.rangeEndInt - part.rangeStartInt + 1 + tfm.WritePartHeader(&buf, part) + } + fmt.Fprintf(&buf, "\r\n--%s--", tfm.boundary) + partsHeaderLen := int64(buf.Len()) + + // the first CRLF is not part of message body + // ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html + return partsHeaderLen + partsBodyLen - 2 +} + +func (tfm *Transformer) WritePartHeader(buf io.Writer, part *Part) error { + fmt.Fprintf(buf, "\r\n--%s\r\n", tfm.boundary) + fmt.Fprint(buf, "Content-Type: application/octet-stream\r\n") + fmt.Fprintf(buf, "Content-Range: bytes %s-%s/%s\r\n", part.rangeStart, part.rangeEnd, part.fileSize) + fmt.Fprint(buf, "\r\n") + + return nil +} + +func (tfm *Transformer) WriteMultiParts(wt io.Writer) error { + var err error + for _, part := range tfm.parts { + if err = tfm.WritePartHeader(wt, part); err != nil { + return err + } + if err = writePartBody(tfm.src, wt, part); err != nil { + return err + } + } + + _, err = fmt.Fprintf(wt, "\r\n--%s--", tfm.boundary) + if err != nil { + return err + } + + return nil +} diff --git a/transformer_test.go b/transformer_test.go new file mode 100644 index 0000000..c297fc8 --- /dev/null +++ b/transformer_test.go @@ -0,0 +1,72 @@ +package multipart + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" +) + +type MockReadSeekCloser struct { + *bytes.Reader +} + +func NewMockReadSeekCloser(reader *bytes.Reader) *MockReadSeekCloser { + return &MockReadSeekCloser{Reader: reader} +} +func (rc *MockReadSeekCloser) Close() error { + return nil +} + +func TestTransformer(t *testing.T) { + type HttpRange struct { + contentType string + rangeStart string + rangeEnd string + fileSize string + // content string + } + type TestCase struct { + content string + rangeHeader string + // parts []*Part + expectedOut string + } + + sep := "BOUNDARY" + t.Run("normal cases", func(t *testing.T) { + testCases := []*TestCase{ + &TestCase{ + content: "0123456789", + rangeHeader: "bytes=0-3, 8-8", + expectedOut: "\r\n--BOUNDARY\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes 0-3/10\r\n\r\n0123\r\n--BOUNDARY\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes 8-8/10\r\n\r\n8\r\n--BOUNDARY--", + }, + } + + for _, tc := range testCases { + parts, err := RangeToParts(tc.rangeHeader, "application/octet-stream", fmt.Sprintf("%d", len(tc.content))) + if err != nil { + t.Fatal(err) + } + + mockFd := &MockReadSeekCloser{bytes.NewReader([]byte(tc.content))} + w := NewTransformerWithBoundary(mockFd, parts, sep) + + buf := bytes.NewBuffer([]byte{}) + err = w.WriteMultiParts(buf) + if err != nil { + t.Fatal(err) + } + + respBytes, err := ioutil.ReadAll(buf) + if err != nil { + t.Fatal(err) + } + if string(respBytes) != tc.expectedOut { + t.Errorf("\nnot equal 1.expected(%d) 2.got(%d)", len(tc.expectedOut), len(string(respBytes))) + t.Error(tc.expectedOut) + t.Error(string(respBytes)) + } + } + }) +}