From 01b185436c6213ffbb3601102a6e23cf90830f63 Mon Sep 17 00:00:00 2001 From: mengzhongyuan Date: Fri, 11 Jul 2025 22:56:57 +0800 Subject: [PATCH 1/3] refactor: replace StreamWriter.rawData with a TepFile abstraction to support more storage Change-Id: Ibaca66b80a3ed8793396e1386bf7c0b2db7c21c0 Signed-off-by: mengzhongyuan --- excelize.go | 9 ++++++++- file_test.go | 10 +++++++--- lib.go | 10 +++++++++- stream.go | 51 +++++++++++++++++++++++++++++++++++++++++--------- stream_test.go | 38 +++++++++++++++++++++++-------------- 5 files changed, 90 insertions(+), 28 deletions(-) diff --git a/excelize.go b/excelize.go index ea8926a3b2..6449f03892 100644 --- a/excelize.go +++ b/excelize.go @@ -101,17 +101,24 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // // CultureInfo specifies the country code for applying built-in language number // format code these effect by the system's local language settings. +// +// TmpDir specifies the temporary directory for creating temporary files, if the +// value is empty, the system default temporary directory will be used. +// +// StreamingTmpFile specifies the temporary file for streaming writing streaming writer temporary file, +// if the value is nil, the system default temporary file will be used to write streaming data. type Options struct { MaxCalcIterations uint Password string RawCellValue bool UnzipSizeLimit int64 UnzipXMLSizeLimit int64 - TmpDir string ShortDatePattern string LongDatePattern string LongTimePattern string CultureInfo CultureName + TmpDir string + StreamingTmpFile *TmpFile } // OpenFile take the name of a spreadsheet file and returns a populated diff --git a/file_test.go b/file_test.go index 58c9e4a265..b6a0fe862b 100644 --- a/file_test.go +++ b/file_test.go @@ -61,9 +61,13 @@ func TestWriteTo(t *testing.T) { f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} f.Pkg.Store("s", nil) f.streams = make(map[string]*StreamWriter) - file, _ := os.Open("123") - f.streams["s"] = &StreamWriter{rawData: bufferedWriter{tmp: file}} - _, err := f.WriteTo(bufio.NewWriter(&buf)) + file, err := os.Open("123") + assert.Error(t, err) + + rawData := newBufferedWriter(f.options.TmpDir, nil) + rawData.tmp = file + f.streams["s"] = &StreamWriter{rawData: rawData} + _, err = f.WriteTo(bufio.NewWriter(&buf)) assert.Nil(t, err) } // Test write with temporary file diff --git a/lib.go b/lib.go index 7c5cdfd78a..bd038e410a 100644 --- a/lib.go +++ b/lib.go @@ -103,7 +103,15 @@ func (f *File) readXML(name string) []byte { return content.([]byte) } if content, ok := f.streams[name]; ok { - return content.rawData.buf.Bytes() + rawDataReader, err := content.rawData.Reader() + if err != nil { + return []byte{} + } + rawDataContent, err := io.ReadAll(rawDataReader) + if err != nil { + return []byte{} + } + return rawDataContent } return []byte{} } diff --git a/stream.go b/stream.go index e4844b6958..c064ee6fae 100644 --- a/stream.go +++ b/stream.go @@ -30,7 +30,7 @@ type StreamWriter struct { SheetID int sheetWritten bool worksheet *xlsxWorksheet - rawData bufferedWriter + rawData TmpFile rows int mergeCellsCount int mergeCells strings.Builder @@ -119,11 +119,17 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { if sheetID == -1 { return nil, ErrSheetNotExist{sheet} } + + rawData := TmpFile(newBufferedWriter(f.options.TmpDir, nil)) + if f.options.StreamingTmpFile != nil { + rawData = *f.options.StreamingTmpFile + } + sw := &StreamWriter{ file: f, Sheet: sheet, SheetID: sheetID, - rawData: bufferedWriter{tmpDir: f.options.TmpDir}, + rawData: rawData, } var err error sw.worksheet, err = f.workSheetReader(sheet) @@ -138,7 +144,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { f.streams[sheetXMLPath] = sw _, _ = sw.rawData.WriteString(xml.Header + ``) return err } - writeCell(&sw.rawData, c) + writeCell(sw.rawData, c) } _, _ = sw.rawData.WriteString(``) return sw.rawData.Sync() @@ -602,7 +608,7 @@ func setCellIntFunc(c *xlsxC, val interface{}) { } // writeCell constructs a cell XML and writes it to the buffer. -func writeCell(buf *bufferedWriter, c xlsxC) { +func writeCell(buf TmpFile, c xlsxC) { _, _ = buf.WriteString(`") for _, col := range sw.worksheet.Cols.Col { @@ -695,7 +701,7 @@ func (sw *StreamWriter) writeSheetData() { func (sw *StreamWriter) Flush() error { sw.writeSheetData() _, _ = sw.rawData.WriteString(``) - bulkAppendFields(&sw.rawData, sw.worksheet, 9, 16) + bulkAppendFields(sw.rawData, sw.worksheet, 9, 16) mergeCells := strings.Builder{} if sw.mergeCellsCount > 0 { _, _ = mergeCells.WriteString(`") + bw.buf.WriteString("") _, err = sw.getRowValues(1, 1, 1) assert.NoError(t, err) - sw.rawData.buf.Reset() + bw.buf.Reset() // Test getRowValues with illegal cell reference - sw.rawData.buf.WriteString("") + bw.buf.WriteString("") _, err = sw.getRowValues(1, 1, 1) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) - sw.rawData.buf.Reset() + bw.buf.Reset() // Test getRowValues with invalid c element characters - sw.rawData.buf.WriteString("") + bw.buf.WriteString("") _, err = sw.getRowValues(1, 1, 1) assert.EqualError(t, err, "XML syntax error on line 1: element closed by ") - sw.rawData.buf.Reset() + bw.buf.Reset() } func TestStreamWriterGetRowElement(t *testing.T) { From 1f4ad5751da6959f5fc40aeec601bf8ecc63a2f6 Mon Sep 17 00:00:00 2001 From: mengzhongyuan Date: Sun, 8 Feb 2026 18:38:56 +0800 Subject: [PATCH 2/3] test: fix failed test because of nil File.options Change-Id: I7f207e3de005574f79163b3a7b8c68c20ca9adad Signed-off-by: mengzhongyuan --- file_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/file_test.go b/file_test.go index 3ff7922787..9a362d54d8 100644 --- a/file_test.go +++ b/file_test.go @@ -62,7 +62,9 @@ func TestWriteTo(t *testing.T) { } // Test StreamsWriter err { - f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} + f, buf := File{Pkg: sync.Map{}, options: &Options{ + TmpDir: "/tmp", + }}, bytes.Buffer{} f.SetZipWriter(func(w io.Writer) ZipWriter { return zip.NewWriter(w) }) f.Pkg.Store("s", nil) f.streams = make(map[string]*StreamWriter) From 3651ed247d28ec10e6eaf02231169a74501b8020 Mon Sep 17 00:00:00 2001 From: mengzhongyuan Date: Mon, 9 Feb 2026 23:55:28 +0800 Subject: [PATCH 3/3] test: streaming writer abstract file impl writer and reader test Change-Id: I04fc04bedb438b3cb692b43c8114a56cc114bb2c Signed-off-by: mengzhongyuan --- excelize.go | 2 +- stream.go | 2 +- stream_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/excelize.go b/excelize.go index 9b9eeb8bfb..7f164a2ed3 100644 --- a/excelize.go +++ b/excelize.go @@ -127,7 +127,7 @@ type Options struct { LongTimePattern string CultureInfo CultureName TmpDir string - StreamingTmpFile *TmpFile + StreamingTmpFile TmpFile } // OpenFile take the name of a spreadsheet file and returns a populated diff --git a/stream.go b/stream.go index 0a82d6ccc4..b9ed5c8fca 100644 --- a/stream.go +++ b/stream.go @@ -122,7 +122,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { rawData := TmpFile(newBufferedWriter(f.options.TmpDir, nil)) if f.options.StreamingTmpFile != nil { - rawData = *f.options.StreamingTmpFile + rawData = f.options.StreamingTmpFile } sw := &StreamWriter{ diff --git a/stream_test.go b/stream_test.go index e8315e1b16..f5c389f7a7 100644 --- a/stream_test.go +++ b/stream_test.go @@ -1,6 +1,7 @@ package excelize import ( + "bytes" "encoding/xml" "fmt" "io" @@ -154,6 +155,69 @@ func TestStreamWriter(t *testing.T) { // Save spreadsheet with password. assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"})) assert.NoError(t, file.Close()) + + // Test stream writer with target file impl + { + file = NewFile(Options{ + StreamingTmpFile: newTestFileImpl(), + }) + defer func() { + assert.NoError(t, file.Close()) + }() + + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{Cell{StyleID: styleID, Value: "Data"}})) + assert.NoError(t, streamWriter.Flush()) + + // then read from the file impl + rows, err = file.Rows("Sheet1") + assert.NoError(t, err) + assert.True(t, rows.Next()) + rowColumns, err := rows.Columns() + assert.NoError(t, err) + assert.Equal(t, "Data", rowColumns[0]) + + cellValue, err = file.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "Data", cellValue) + } +} + +func newTestFileImpl() *testFileImpl { + return &testFileImpl{ + data: []byte{}, + } +} + +type testFileImpl struct { + data []byte +} + +func (t *testFileImpl) Close() error { + return nil +} + +func (t *testFileImpl) Reader() (io.Reader, error) { + return bytes.NewReader(t.data), nil +} + +func (t *testFileImpl) Sync() error { + return nil +} + +func (t *testFileImpl) Write(p []byte) (n int, err error) { + t.data = append(t.data, p...) + return len(p), nil +} + +func (t *testFileImpl) WriteString(s string) (n int, err error) { + t.data = append(t.data, s...) + return len(s), nil +} + +func (t *testFileImpl) Flush() error { + return nil } func TestStreamSetColVisible(t *testing.T) {