This closes #833, closes #845, and closes #1022, breaking changes

- Close spreadsheet and row's iterator required
- New options `WorksheetUnzipMemLimit` have been added
- Improve streaming reading performance, memory usage decrease about 93.7%
This commit is contained in:
xuri 2021-09-18 23:20:24 +08:00
parent 2add938798
commit 790c363cce
24 changed files with 322 additions and 36 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ test/Test*.xlsm
test/BadEncrypt.xlsx
test/BadWorkbook.SaveAsEmptyStruct.xlsx
test/*.png
test/excelize-*
*.out
*.test
.idea

View File

@ -96,6 +96,10 @@ func main() {
}
fmt.Println()
}
// Close the spreadsheet.
if err = f.Close(); err != nil {
fmt.Println(err)
}
}
```
@ -203,6 +207,10 @@ func main() {
if err = f.Save(); err != nil {
fmt.Println(err)
}
// Close the spreadsheet.
if err = f.Close(); err != nil {
fmt.Println(err)
}
}
```

View File

@ -96,6 +96,10 @@ func main() {
}
fmt.Println()
}
// 关闭工作簿
if err = f.Close(); err != nil {
fmt.Println(err)
}
}
```
@ -199,10 +203,14 @@ func main() {
}`); err != nil {
fmt.Println(err)
}
// 保存文件
// 保存工作簿
if err = f.Save(); err != nil {
fmt.Println(err)
}
// 关闭工作簿
if err = f.Close(); err != nil {
fmt.Println(err)
}
}
```

View File

@ -72,6 +72,7 @@ func TestConcurrency(t *testing.T) {
}
assert.Equal(t, "1", val)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx")))
assert.NoError(t, f.Close())
}
func TestCheckCellInArea(t *testing.T) {
@ -325,6 +326,7 @@ func TestOverflowNumericCell(t *testing.T) {
assert.NoError(t, err)
// GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648"
assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225")
assert.NoError(t, f.Close())
}
func TestSetCellFormula(t *testing.T) {
@ -340,6 +342,7 @@ func TestSetCellFormula(t *testing.T) {
assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx")))
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx"))
if !assert.NoError(t, err) {
@ -351,6 +354,7 @@ func TestSetCellFormula(t *testing.T) {
// Test remove all cell formula.
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", ""))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx")))
assert.NoError(t, f.Close())
// Test set shared formula for the cells.
f = NewFile()

View File

@ -206,6 +206,7 @@ func TestAddChart(t *testing.T) {
assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`, ""), "unexpected end of JSON input")
// Test add combo chart with unsupported chart type
assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown")
assert.NoError(t, f.Close())
}
func TestAddChartSheet(t *testing.T) {
@ -254,6 +255,7 @@ func TestDeleteChart(t *testing.T) {
assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`)
// Test delete chart on no chart worksheet.
assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1"))
assert.NoError(t, f.Close())
}
func TestChartWithLogarithmicBase(t *testing.T) {

2
col.go
View File

@ -209,7 +209,7 @@ func (f *File) Cols(sheet string) (*Cols, error) {
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
}
var colIterator columnXMLIterator
colIterator.cols.sheetXML = f.readXML(name)
colIterator.cols.sheetXML = f.readBytes(name)
decoder := f.xmlNewDecoder(bytes.NewReader(colIterator.cols.sheetXML))
for {
token, _ := decoder.Token()

View File

@ -39,6 +39,7 @@ func TestCols(t *testing.T) {
if !assert.Equal(t, collectedRows, returnedColumns) {
t.FailNow()
}
assert.NoError(t, f.Close())
f = NewFile()
cells := []string{"C2", "C3", "C4"}
@ -75,6 +76,7 @@ func TestColumnsIterator(t *testing.T) {
require.True(t, colCount <= expectedNumCol, "colCount is greater than expected")
}
assert.Equal(t, expectedNumCol, colCount)
assert.NoError(t, f.Close())
f = NewFile()
cells := []string{"C2", "C3", "C4", "D2", "D3", "D4"}
@ -99,6 +101,7 @@ func TestColsError(t *testing.T) {
}
_, err = f.Cols("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
assert.NoError(t, f.Close())
}
func TestGetColsError(t *testing.T) {
@ -108,6 +111,7 @@ func TestGetColsError(t *testing.T) {
}
_, err = f.GetCols("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
assert.NoError(t, f.Close())
f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml")
@ -283,6 +287,7 @@ func TestOutlineLevel(t *testing.T) {
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2))
assert.NoError(t, f.Close())
}
func TestSetColStyle(t *testing.T) {

View File

@ -22,6 +22,7 @@ func TestEncrypt(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"})
assert.NoError(t, err)
assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently")
assert.NoError(t, f.Close())
}
func TestEncryptionMechanism(t *testing.T) {

View File

@ -44,6 +44,7 @@ func TestSetDocProps(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
f.Pkg.Store("docProps/core.xml", nil)
assert.NoError(t, f.SetDocProps(&DocProperties{}))
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()
@ -62,6 +63,7 @@ func TestGetDocProps(t *testing.T) {
f.Pkg.Store("docProps/core.xml", nil)
_, err = f.GetDocProps()
assert.NoError(t, err)
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()

View File

@ -131,4 +131,7 @@ var (
// ErrCellCharsLength defined the error message for receiving a cell
// characters length that exceeds the limit.
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
// ErrOptionsUnzipSizeLimit defined the error message for receiving
// invalid UnzipSizeLimit and WorksheetUnzipMemLimit.
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to WorksheetUnzipMemLimit")
)

View File

@ -37,6 +37,7 @@ type File struct {
checked map[string]bool
sheetMap map[string]string
streams map[string]*StreamWriter
tempFiles sync.Map
CalcChain *xlsxCalcChain
Comments map[string]*xlsxComments
ContentTypes *xlsxTypes
@ -58,13 +59,26 @@ type File struct {
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
// Options define the options for open and reading spreadsheet. RawCellValue
// specify if apply the number format for the cell value or get the raw
// value.
// Options define the options for open and reading spreadsheet.
//
// Password specifies the password of the spreadsheet in plain text.
//
// RawCellValue specifies if apply the number format for the cell value or get
// the raw value.
//
// UnzipSizeLimit specifies the unzip size limit in bytes on open the
// spreadsheet, this value should be greater than or equal to
// WorksheetUnzipMemLimit, the default size limit is 16GB.
//
// WorksheetUnzipMemLimit specifies the memory limit on unzipping worksheet in
// bytes, worksheet XML will be extracted to system temporary directory when
// the file size is over this value, this value should be less than or equal
// to UnzipSizeLimit, the default value is 16MB.
type Options struct {
Password string
RawCellValue bool
UnzipSizeLimit int64
Password string
RawCellValue bool
UnzipSizeLimit int64
WorksheetUnzipMemLimit int64
}
// OpenFile take the name of an spreadsheet file and returns a populated
@ -78,10 +92,8 @@ type Options struct {
//
// Note that the excelize just support decrypt and not support encrypt
// currently, the spreadsheet saved by Save and SaveAs will be without
// password unprotected.
//
// UnzipSizeLimit specified the unzip size limit in bytes on open the
// spreadsheet, the default size limit is 16GB.
// password unprotected. Close the file by Close after opening the
// spreadsheet.
func OpenFile(filename string, opt ...Options) (*File, error) {
file, err := os.Open(filepath.Clean(filename))
if err != nil {
@ -99,10 +111,11 @@ func OpenFile(filename string, opt ...Options) (*File, error) {
// newFile is object builder
func newFile() *File {
return &File{
options: &Options{UnzipSizeLimit: UnzipSizeLimit},
options: &Options{UnzipSizeLimit: UnzipSizeLimit, WorksheetUnzipMemLimit: StreamChunkSize},
xmlAttr: make(map[string][]xml.Attr),
checked: make(map[string]bool),
sheetMap: make(map[string]string),
tempFiles: sync.Map{},
Comments: make(map[string]*xlsxComments),
Drawings: sync.Map{},
sharedStringsMap: make(map[string]int),
@ -125,6 +138,18 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
f.options = parseOptions(opt...)
if f.options.UnzipSizeLimit == 0 {
f.options.UnzipSizeLimit = UnzipSizeLimit
if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit {
f.options.UnzipSizeLimit = f.options.WorksheetUnzipMemLimit
}
}
if f.options.WorksheetUnzipMemLimit == 0 {
f.options.WorksheetUnzipMemLimit = StreamChunkSize
if f.options.UnzipSizeLimit < f.options.WorksheetUnzipMemLimit {
f.options.WorksheetUnzipMemLimit = f.options.UnzipSizeLimit
}
}
if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit {
return nil, ErrOptionsUnzipSizeLimit
}
if bytes.Contains(b, oleIdentifier) {
b, err = Decrypt(b, f.options)
@ -136,7 +161,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
if err != nil {
return nil, err
}
file, sheetCount, err := ReadZipReader(zr, f.options)
file, sheetCount, err := f.ReadZipReader(zr)
if err != nil {
return nil, err
}
@ -210,10 +235,10 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
}
ws = new(xlsxWorksheet)
if _, ok := f.xmlAttr[name]; !ok {
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name))))
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name))))
f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...)
}
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))).
Decode(ws); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return

View File

@ -167,6 +167,7 @@ func TestOpenFile(t *testing.T) {
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx")))
assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), ErrMaxFileNameLength.Error())
assert.NoError(t, f.Close())
}
func TestSaveFile(t *testing.T) {
@ -175,11 +176,13 @@ func TestSaveFile(t *testing.T) {
t.FailNow()
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsx")))
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, f.Save())
assert.NoError(t, f.Close())
}
func TestSaveAsWrongPath(t *testing.T) {
@ -187,6 +190,7 @@ func TestSaveAsWrongPath(t *testing.T) {
assert.NoError(t, err)
// Test write file to not exist directory.
assert.EqualError(t, f.SaveAs(""), "open .: is a directory")
assert.NoError(t, f.Close())
}
func TestCharsetTranscoder(t *testing.T) {
@ -197,7 +201,7 @@ func TestCharsetTranscoder(t *testing.T) {
func TestOpenReader(t *testing.T) {
_, err := OpenReader(strings.NewReader(""))
assert.EqualError(t, err, "zip: not a valid zip file")
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password"})
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", WorksheetUnzipMemLimit: UnzipSizeLimit + 1})
assert.EqualError(t, err, "decrypted file failed")
// Test open spreadsheet with unzip size limit.
@ -210,6 +214,7 @@ func TestOpenReader(t *testing.T) {
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "SECRET", val)
assert.NoError(t, f.Close())
// Test open password protected spreadsheet created by LibreOffice 7.0.0.3.
f, err = OpenFile(filepath.Join("test", "encryptAES.xlsx"), Options{Password: "password"})
@ -217,6 +222,11 @@ func TestOpenReader(t *testing.T) {
val, err = f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "SECRET", val)
assert.NoError(t, f.Close())
// Test open spreadsheet with invalid optioins.
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, WorksheetUnzipMemLimit: 2})
assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error())
// Test unexpected EOF.
var b bytes.Buffer
@ -266,6 +276,7 @@ func TestBrokenFile(t *testing.T) {
f3.GetActiveSheetIndex()
f3.SetActiveSheet(1)
assert.NoError(t, err)
assert.NoError(t, f3.Close())
})
t.Run("OpenNotExistsFile", func(t *testing.T) {
@ -340,6 +351,7 @@ func TestSetCellHyperLink(t *testing.T) {
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx")))
assert.NoError(t, f.Close())
f = NewFile()
_, err = f.workSheetReader("Sheet1")
@ -377,6 +389,7 @@ func TestGetCellHyperLink(t *testing.T) {
link, target, err = f.GetCellHyperLink("Sheet3", "H3")
assert.EqualError(t, err, "sheet Sheet3 is not exist")
t.Log(link, target)
assert.NoError(t, f.Close())
f = NewFile()
_, err = f.workSheetReader("Sheet1")
@ -398,7 +411,6 @@ func TestGetCellHyperLink(t *testing.T) {
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.Equal(t, link, false)
assert.Equal(t, target, "")
}
func TestSetSheetBackground(t *testing.T) {
@ -418,6 +430,7 @@ func TestSetSheetBackground(t *testing.T) {
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx")))
assert.NoError(t, f.Close())
}
func TestSetSheetBackgroundErrors(t *testing.T) {
@ -433,6 +446,7 @@ func TestSetSheetBackgroundErrors(t *testing.T) {
err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx"))
assert.EqualError(t, err, ErrImgExt.Error())
assert.NoError(t, f.Close())
}
// TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function
@ -1052,6 +1066,7 @@ func TestConditionalFormat(t *testing.T) {
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, f.Close())
}
func TestConditionalFormatError(t *testing.T) {
@ -1082,6 +1097,7 @@ func TestSharedStrings(t *testing.T) {
t.FailNow()
}
assert.Equal(t, "Test Weight (Kgs)", rows[0][0])
assert.NoError(t, f.Close())
}
func TestSetSheetRow(t *testing.T) {
@ -1098,6 +1114,7 @@ func TestSetSheetRow(t *testing.T) {
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error())
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), ErrParameterInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx")))
assert.NoError(t, f.Close())
}
func TestHSL(t *testing.T) {
@ -1155,6 +1172,7 @@ func TestUnprotectSheet(t *testing.T) {
assert.NoError(t, f.UnprotectSheet("Sheet1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx")))
assert.NoError(t, f.Close())
}
func TestSetDefaultTimeStyle(t *testing.T) {
@ -1323,7 +1341,11 @@ func fillCells(f *File, sheet string, colCount, rowCount int) {
func BenchmarkOpenFile(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if err != nil {
b.Error(err)
}
if err := f.Close(); err != nil {
b.Error(err)
}
}

12
file.go
View File

@ -82,6 +82,18 @@ func (f *File) SaveAs(name string, opt ...Options) error {
return f.Write(file)
}
// Close closes and cleanup the open temporary file for the spreadsheet.
func (f *File) Close() error {
var err error
f.tempFiles.Range(func(k, v interface{}) bool {
if err = os.Remove(v.(string)); err != nil {
return false
}
return true
})
return err
}
// Write provides a function to write to an io.Writer.
func (f *File) Write(w io.Writer) error {
_, err := f.WriteTo(w)

View File

@ -9,6 +9,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func BenchmarkWrite(b *testing.B) {
@ -62,3 +63,9 @@ func TestWriteTo(t *testing.T) {
assert.Nil(t, err)
}
}
func TestClose(t *testing.T) {
f := NewFile()
f.tempFiles.Store("/d/", "/d/")
require.Error(t, f.Close())
}

71
lib.go
View File

@ -18,14 +18,15 @@ import (
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
)
// ReadZipReader can be used to read the spreadsheet in memory without touching the
// filesystem.
func ReadZipReader(r *zip.Reader, o *Options) (map[string][]byte, int, error) {
// ReadZipReader extract spreadsheet with given options.
func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
var (
err error
docPart = map[string]string{
@ -37,25 +38,49 @@ func ReadZipReader(r *zip.Reader, o *Options) (map[string][]byte, int, error) {
unzipSize int64
)
for _, v := range r.File {
unzipSize += v.FileInfo().Size()
if unzipSize > o.UnzipSizeLimit {
return fileList, worksheets, newUnzipSizeLimitError(o.UnzipSizeLimit)
fileSize := v.FileInfo().Size()
unzipSize += fileSize
if unzipSize > f.options.UnzipSizeLimit {
return fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit)
}
fileName := strings.Replace(v.Name, "\\", "/", -1)
if partName, ok := docPart[strings.ToLower(fileName)]; ok {
fileName = partName
}
if fileList[fileName], err = readFile(v); err != nil {
return nil, 0, err
}
if strings.HasPrefix(fileName, "xl/worksheets/sheet") {
worksheets++
if fileSize > f.options.WorksheetUnzipMemLimit && !v.FileInfo().IsDir() {
if tempFile, err := f.unzipToTemp(v); err == nil {
f.tempFiles.Store(fileName, tempFile)
continue
}
}
}
if fileList[fileName], err = readFile(v); err != nil {
return nil, 0, err
}
}
return fileList, worksheets, nil
}
// readXML provides a function to read XML content as string.
// unzipToTemp unzip the zip entity to the system temporary directory and
// returned the unzipped file path.
func (f *File) unzipToTemp(zipFile *zip.File) (string, error) {
tmp, err := ioutil.TempFile(os.TempDir(), "excelize-")
if err != nil {
return "", err
}
rc, err := zipFile.Open()
if err != nil {
return tmp.Name(), err
}
_, err = io.Copy(tmp, rc)
rc.Close()
tmp.Close()
return tmp.Name(), err
}
// readXML provides a function to read XML content as bytes.
func (f *File) readXML(name string) []byte {
if content, _ := f.Pkg.Load(name); content != nil {
return content.([]byte)
@ -66,6 +91,32 @@ func (f *File) readXML(name string) []byte {
return []byte{}
}
// readBytes read file as bytes by given path.
func (f *File) readBytes(name string) []byte {
content := f.readXML(name)
if len(content) != 0 {
return content
}
file, err := f.readTemp(name)
if err != nil {
return content
}
content, _ = ioutil.ReadAll(file)
f.Pkg.Store(name, content)
file.Close()
return content
}
// readTemp read file from system temporary directory by given path.
func (f *File) readTemp(name string) (file *os.File, err error) {
path, ok := f.tempFiles.Load(name)
if !ok {
return
}
file, err = os.Open(path.(string))
return
}
// saveFileList provides a function to update given file content in file list
// of spreadsheet.
func (f *File) saveFileList(name string, content []byte) {

View File

@ -1,13 +1,18 @@
package excelize
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"os"
"strconv"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var validColumns = []struct {
@ -296,3 +301,41 @@ func TestBstrMarshal(t *testing.T) {
assert.Equal(t, expected, bstrMarshal(bstr))
}
}
func TestReadBytes(t *testing.T) {
f := &File{tempFiles: sync.Map{}}
sheet := "xl/worksheets/sheet1.xml"
f.tempFiles.Store(sheet, "/d/")
assert.Equal(t, []byte{}, f.readBytes(sheet))
}
func TestUnzipToTemp(t *testing.T) {
os.Setenv("TMPDIR", "test")
defer os.Unsetenv("TMPDIR")
assert.NoError(t, os.Chmod(os.TempDir(), 0444))
f := NewFile()
data := []byte("PK\x03\x040000000PK\x01\x0200000" +
"0000000000000000000\x00" +
"\x00\x00\x00\x00\x00000000000000PK\x01" +
"\x020000000000000000000" +
"00000\v\x00\x00\x00\x00\x00000000000" +
"00000000000000PK\x01\x0200" +
"00000000000000000000" +
"00\v\x00\x00\x00\x00\x00000000000000" +
"00000000000PK\x01\x020000<" +
"0\x00\x0000000000000000\v\x00\v" +
"\x00\x00\x00\x00\x0000000000\x00\x00\x00\x00000" +
"00000000PK\x01\x0200000000" +
"0000000000000000\v\x00\x00\x00" +
"\x00\x0000PK\x05\x06000000\x05\x000000" +
"\v\x00\x00\x00\x00\x00")
z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
_, err = f.unzipToTemp(z.File[0])
require.Error(t, err)
assert.NoError(t, os.Chmod(os.TempDir(), 0755))
_, err = f.unzipToTemp(z.File[0])
assert.EqualError(t, err, "EOF")
}

View File

@ -68,6 +68,7 @@ func TestMergeCell(t *testing.T) {
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist")
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))
assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
@ -93,6 +94,7 @@ func TestMergeCellOverlap(t *testing.T) {
assert.Equal(t, "A1", mc[0].GetStartAxis())
assert.Equal(t, "D3", mc[0].GetEndAxis())
assert.Equal(t, "", mc[0].GetCellValue())
assert.NoError(t, f.Close())
}
func TestGetMergeCells(t *testing.T) {
@ -139,6 +141,7 @@ func TestGetMergeCells(t *testing.T) {
// Test get merged cells on not exists worksheet.
_, err = f.GetMergeCells("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
assert.NoError(t, f.Close())
}
func TestUnmergeCell(t *testing.T) {
@ -162,6 +165,7 @@ func TestUnmergeCell(t *testing.T) {
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx")))
assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))

View File

@ -497,6 +497,9 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
// if err := ioutil.WriteFile(file, raw, 0644); err != nil {
// fmt.Println(err)
// }
// if err = f.Close(); err != nil {
// fmt.Println(err)
// }
//
func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
col, row, err := CellNameToCoordinates(cell)

View File

@ -68,6 +68,7 @@ func TestAddPicture(t *testing.T) {
// Test write file to given path.
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx")))
assert.NoError(t, f.Close())
}
func TestAddPictureErrors(t *testing.T) {
@ -90,6 +91,7 @@ func TestAddPictureErrors(t *testing.T) {
// Test add picture to worksheet with invalid file data.
err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1))
assert.EqualError(t, err, "image: unknown format")
assert.NoError(t, f.Close())
}
func TestGetPicture(t *testing.T) {
@ -137,7 +139,6 @@ func TestGetPicture(t *testing.T) {
assert.NoError(t, err)
if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) ||
!assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) {
t.FailNow()
}
@ -146,6 +147,7 @@ func TestGetPicture(t *testing.T) {
assert.NoError(t, err)
assert.Empty(t, file)
assert.Empty(t, raw)
assert.NoError(t, f.Close())
// Test get picture from none drawing worksheet.
f = NewFile()
@ -194,6 +196,7 @@ func TestDeletePicture(t *testing.T) {
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist")
// Test delete picture with invalid coordinates.
assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`)
assert.NoError(t, f.Close())
// Test delete picture on no chart worksheet.
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))
}

43
rows.go
View File

@ -18,6 +18,7 @@ import (
"io"
"log"
"math"
"os"
"strconv"
"github.com/mohae/deepcopy"
@ -60,7 +61,7 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {
max = cur
}
}
return results[:max], nil
return results[:max], rows.Close()
}
// Rows defines an iterator to a sheet.
@ -70,6 +71,7 @@ type Rows struct {
rawCellValue bool
sheet string
f *File
tempFile *os.File
decoder *xml.Decoder
}
@ -84,6 +86,15 @@ func (rows *Rows) Error() error {
return rows.err
}
// Close closes the open worksheet XML file in the system temporary
// directory.
func (rows *Rows) Close() error {
if rows.tempFile != nil {
return rows.tempFile.Close()
}
return nil
}
// Columns return the current row's column values.
func (rows *Rows) Columns(opts ...Options) ([]string, error) {
var rowIterator rowXMLIterator
@ -196,6 +207,9 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, ra
// }
// fmt.Println()
// }
// if err = rows.Close(); err != nil {
// fmt.Println(err)
// }
//
func (f *File) Rows(sheet string) (*Rows, error) {
name, ok := f.sheetMap[trimSheetName(sheet)]
@ -215,8 +229,13 @@ func (f *File) Rows(sheet string) (*Rows, error) {
inElement string
row int
rows Rows
needClose bool
decoder *xml.Decoder
tempFile *os.File
)
decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
if needClose, decoder, tempFile, err = f.sheetDecoder(name); needClose && err == nil {
defer tempFile.Close()
}
for {
token, _ := decoder.Token()
if token == nil {
@ -241,15 +260,29 @@ func (f *File) Rows(sheet string) (*Rows, error) {
if xmlElement.Name.Local == "sheetData" {
rows.f = f
rows.sheet = name
rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
return &rows, nil
_, rows.decoder, rows.tempFile, err = f.sheetDecoder(name)
return &rows, err
}
default:
}
}
return &rows, nil
}
// sheetDecoder creates XML decoder by given path in the zip from memory data
// or system temporary file.
func (f *File) sheetDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
var (
content []byte
err error
tempFile *os.File
)
if content = f.readXML(name); len(content) > 0 {
return false, f.xmlNewDecoder(bytes.NewReader(content)), tempFile, err
}
tempFile, err = f.readTemp(name)
return true, f.xmlNewDecoder(tempFile), tempFile, err
}
// SetRowHeight provides a function to set the height of a single row. For
// example, set the height of the first row in Sheet1:
//

View File

@ -32,6 +32,7 @@ func TestRows(t *testing.T) {
if !assert.NoError(t, rows.Error()) {
t.FailNow()
}
assert.NoError(t, rows.Close())
returnedRows, err := f.GetRows(sheet2)
assert.NoError(t, err)
@ -41,6 +42,7 @@ func TestRows(t *testing.T) {
if !assert.Equal(t, collectedRows, returnedRows) {
t.FailNow()
}
assert.NoError(t, f.Close())
f = NewFile()
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="1"><c r="A1" t="s"><v>1</v></c></row><row r="A"><c r="2" t="str"><v>B</v></c></row></sheetData></worksheet>`))
@ -52,6 +54,14 @@ func TestRows(t *testing.T) {
f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
_, err = f.Rows("Sheet1")
assert.NoError(t, err)
// Test reload the file to memory from system temporary directory.
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{WorksheetUnzipMemLimit: 1024})
assert.NoError(t, err)
value, err := f.GetCellValue("Sheet1", "A19")
assert.NoError(t, err)
assert.Equal(t, "Total:", value)
assert.NoError(t, f.Close())
}
func TestRowsIterator(t *testing.T) {
@ -70,6 +80,8 @@ func TestRowsIterator(t *testing.T) {
require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected")
}
assert.Equal(t, expectedNumRow, rowCount)
assert.NoError(t, rows.Close())
assert.NoError(t, f.Close())
// Valued cell sparse distribution test
f = NewFile()
@ -94,6 +106,7 @@ func TestRowsError(t *testing.T) {
}
_, err = f.Rows("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
assert.NoError(t, f.Close())
}
func TestRowHeight(t *testing.T) {
@ -871,6 +884,11 @@ func TestGetValueFromNumber(t *testing.T) {
}
}
func TestRoundPrecision(t *testing.T) {
_, err := roundPrecision("")
assert.EqualError(t, err, "strconv.ParseFloat: parsing \"\": invalid syntax")
}
func TestErrSheetNotExistError(t *testing.T) {
err := ErrSheetNotExist{SheetName: "Sheet1"}
assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist")
@ -920,6 +938,7 @@ func TestNumberFormats(t *testing.T) {
cells = append(cells, col)
}
assert.Equal(t, []string{"", "200", "450", "200", "510", "315", "127", "89", "348", "53", "37"}, cells[3])
assert.NoError(t, f.Close())
}
func BenchmarkRows(b *testing.B) {
@ -934,6 +953,12 @@ func BenchmarkRows(b *testing.B) {
}
}
}
if err := rows.Close(); err != nil {
b.Error(err)
}
}
if err := f.Close(); err != nil {
b.Error(err)
}
}

View File

@ -419,6 +419,9 @@ func (f *File) GetSheetIndex(name string) int {
// for index, name := range f.GetSheetMap() {
// fmt.Println(index, name)
// }
// if err = f.Close(); err != nil {
// fmt.Println(err)
// }
//
func (f *File) GetSheetMap() map[int]string {
wb := f.workbookReader()
@ -462,6 +465,9 @@ func (f *File) getSheetMap() map[string]string {
if _, ok := f.Pkg.Load(path); ok {
maps[v.Name] = path
}
if _, ok := f.tempFiles.Load(path); ok {
maps[v.Name] = path
}
}
}
}
@ -858,7 +864,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
)
d = f.sharedStringsReader()
decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name)))
for {
var token xml.Token
token, err = decoder.Token()

View File

@ -189,6 +189,7 @@ func TestSearchSheet(t *testing.T) {
result, err = f.SearchSheet("Sheet1", "[0-9]", true)
assert.NoError(t, err)
assert.EqualValues(t, expected, result)
assert.NoError(t, f.Close())
// Test search worksheet data after set cell value
f = NewFile()
@ -327,6 +328,7 @@ func TestGetSheetName(t *testing.T) {
assert.Equal(t, "Sheet2", f.GetSheetName(1))
assert.Equal(t, "", f.GetSheetName(-1))
assert.Equal(t, "", f.GetSheetName(2))
assert.NoError(t, f.Close())
}
func TestGetSheetMap(t *testing.T) {
@ -341,6 +343,7 @@ func TestGetSheetMap(t *testing.T) {
assert.Equal(t, expectedMap[idx], name)
}
assert.Equal(t, len(sheetMap), 2)
assert.NoError(t, f.Close())
}
func TestSetActiveSheet(t *testing.T) {

View File

@ -115,6 +115,21 @@ func TestStreamWriter(t *testing.T) {
cellValue, err := file.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "Data", cellValue)
// Test stream reader for a worksheet with huge amounts of data.
file, err = OpenFile(filepath.Join("test", "TestStreamWriter.xlsx"))
assert.NoError(t, err)
rows, err := file.Rows("Sheet1")
assert.NoError(t, err)
cells := 0
for rows.Next() {
row, err := rows.Columns()
assert.NoError(t, err)
cells += len(row)
}
assert.NoError(t, rows.Close())
assert.Equal(t, 2559558, cells)
assert.NoError(t, file.Close())
}
func TestStreamSetColWidth(t *testing.T) {