excelize icon indicating copy to clipboard operation
excelize copied to clipboard

Function SaveTo to save file to writer

Open Donaldas opened this issue 4 years ago • 0 comments

Hi, it would be great to have possibility to pass unlocked file to io.Writer interface.

Use case: Web service downloads excel files from cloud storage and processes it. In case excel file is password protected, service should unlock it with provided password and upload unlocked file back to cloud storage.

Description

Current implementation allows to save unlocked filed to the file on file system via: func (f *File) SaveAs(name string, opt ...Options) error

New function SaveTo would allow to upload unlocked file without persisting it on file system. func (f *File) SaveTo(w io.Writer, opt ...Options) (int64, error)

Other option would be to allow reset options in order to save unlocked file using:

func (f *File) Write(w io.Writer) error
func (f *File) WriteTo(w io.Writer) (int64, error)
func (f *File) WriteToBuffer() (*bytes.Buffer, error)

Thanks!

Donaldas avatar Dec 11 '20 18:12 Donaldas

Any update on this from the contributors?

ronna-s avatar Sep 06 '22 12:09 ronna-s

This library has added the WriteTo function since version 1.4.1, which can be used to save the file to the writer. I will add options support for it recently.

xuri avatar Sep 06 '22 12:09 xuri

Thanks for the quick reply @xuri I see the functions but they don't seem to set the contentType using the file extension as SaveAs does and that functionality is not exported (setContentTypePartProjectExtensions). I have a case for a file upload that doesn't need local storage.

ronna-s avatar Sep 06 '22 12:09 ronna-s

Okay, thanks for your feedback. I will add support for it at this week.

xuri avatar Sep 06 '22 12:09 xuri

I can create a proposal PR. Against v2 or master?

ronna-s avatar Sep 06 '22 12:09 ronna-s

Thank you. Contributions are welcome. Please create PR base on the master branch. Here are some changes already have made for your reference:

diff --git a/crypt_test.go b/crypt_test.go
index f7c465e..95b6f52 100644
--- a/crypt_test.go
+++ b/crypt_test.go
@@ -48,6 +48,8 @@ func TestEncrypt(t *testing.T) {
 	cell, err = f.GetCellValue("Sheet1", "A1")
 	assert.NoError(t, err)
 	assert.Equal(t, "SECRET", cell)
+	// Test remove password by save workbook with options
+	assert.NoError(t, f.Save(Options{Password: ""}))
 	assert.NoError(t, f.Close())
 }
 
diff --git a/excelize.go b/excelize.go
index f1269fe..bb4bde0 100644
--- a/excelize.go
+++ b/excelize.go
@@ -136,13 +136,13 @@ func newFile() *File {
 
 // OpenReader read data stream from io.Reader and return a populated
 // spreadsheet file.
-func OpenReader(r io.Reader, opt ...Options) (*File, error) {
+func OpenReader(r io.Reader, opts ...Options) (*File, error) {
 	b, err := ioutil.ReadAll(r)
 	if err != nil {
 		return nil, err
 	}
 	f := newFile()
-	f.options = parseOptions(opt...)
+	f.options = parseOptions(opts...)
 	if f.options.UnzipSizeLimit == 0 {
 		f.options.UnzipSizeLimit = UnzipSizeLimit
 		if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
diff --git a/excelize_test.go b/excelize_test.go
index 19aba7e..93cd2bf 100644
--- a/excelize_test.go
+++ b/excelize_test.go
@@ -186,18 +186,15 @@ func TestOpenFile(t *testing.T) {
 
 func TestSaveFile(t *testing.T) {
 	f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
-	if !assert.NoError(t, err) {
-		t.FailNow()
-	}
+	assert.NoError(t, err)
 	assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookFileFormat.Error())
 	for _, ext := range []string{".xlam", ".xlsm", ".xlsx", ".xltm", ".xltx"} {
 		assert.NoError(t, f.SaveAs(filepath.Join("test", fmt.Sprintf("TestSaveFile%s", ext))))
 	}
 	assert.NoError(t, f.Close())
+
 	f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
-	if !assert.NoError(t, err) {
-		t.FailNow()
-	}
+	assert.NoError(t, err)
 	assert.NoError(t, f.Save())
 	assert.NoError(t, f.Close())
 }
diff --git a/file.go b/file.go
index 065e7c5..c83d17e 100644
--- a/file.go
+++ b/file.go
@@ -55,44 +55,32 @@ func NewFile() *File {
 }
 
 // Save provides a function to override the spreadsheet with origin path.
-func (f *File) Save() error {
+func (f *File) Save(opts ...Options) error {
 	if f.Path == "" {
 		return ErrSave
 	}
-	if f.options != nil {
-		return f.SaveAs(f.Path, *f.options)
+	for i := range opts {
+		f.options = &opts[i]
 	}
-	return f.SaveAs(f.Path)
+	return f.SaveAs(f.Path, *f.options)
 }
 
 // SaveAs provides a function to create or update to a spreadsheet at the
 // provided path.
-func (f *File) SaveAs(name string, opt ...Options) error {
+func (f *File) SaveAs(name string, opts ...Options) error {
 	if len(name) > MaxFilePathLength {
 		return ErrMaxFilePathLength
 	}
 	f.Path = name
-	contentType, ok := map[string]string{
-		".xlam": ContentTypeAddinMacro,
-		".xlsm": ContentTypeMacro,
-		".xlsx": ContentTypeSheetML,
-		".xltm": ContentTypeTemplateMacro,
-		".xltx": ContentTypeTemplate,
-	}[filepath.Ext(f.Path)]
-	if !ok {
+	if _, ok := supportedContentType[filepath.Ext(f.Path)]; !ok {
 		return ErrWorkbookFileFormat
 	}
-	f.setContentTypePartProjectExtensions(contentType)
 	file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
 	if err != nil {
 		return err
 	}
 	defer file.Close()
-	f.options = nil
-	for i := range opt {
-		f.options = &opt[i]
-	}
-	return f.Write(file)
+	return f.Write(file, opts...)
 }
 
 // Close closes and cleanup the open temporary file for the spreadsheet.
@@ -113,13 +101,23 @@ func (f *File) Close() error {
 }
 
 // Write provides a function to write to an io.Writer.
-func (f *File) Write(w io.Writer) error {
-	_, err := f.WriteTo(w)
+func (f *File) Write(w io.Writer, opts ...Options) error {
+	_, err := f.WriteTo(w, opts...)
 	return err
 }
 
 // WriteTo implements io.WriterTo to write the file.
-func (f *File) WriteTo(w io.Writer) (int64, error) {
+func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) {
+	for i := range opts {
+		f.options = &opts[i]
+	}
+	if len(f.Path) != 0 {
+		contentType, ok := supportedContentType[filepath.Ext(f.Path)]
+		if !ok {
+			return 0, ErrWorkbookFileFormat
+		}
+		f.setContentTypePartProjectExtensions(contentType)
+	}
 	if f.options != nil && f.options.Password != "" {
 		buf, err := f.WriteToBuffer()
 		if err != nil {
diff --git a/file_test.go b/file_test.go
index 8e65c5d..83a9b78 100644
--- a/file_test.go
+++ b/file_test.go
@@ -71,6 +71,14 @@ func TestWriteTo(t *testing.T) {
 		_, err := f.WriteTo(bufio.NewWriter(&buf))
 		assert.EqualError(t, err, "zip: FileHeader.Name too long")
 	}
+	// Test write with unsupported workbook file format
+	{
+		f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
+		f.Pkg.Store("/d", []byte("s"))
+		f.Path = "Book1.xls"
+		_, err := f.WriteTo(bufio.NewWriter(&buf))
+		assert.EqualError(t, err, ErrWorkbookFileFormat.Error())
+	}
 }
 
 func TestClose(t *testing.T) {
diff --git a/xmlDrawing.go b/xmlDrawing.go
index 34c9858..fc8dee5 100644
--- a/xmlDrawing.go
+++ b/xmlDrawing.go
@@ -144,6 +144,15 @@ const (
 // supportedImageTypes defined supported image types.
 var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"}
 
+// supportedContentType defined supported file format types.
+var supportedContentType = map[string]string{
+	".xlam": ContentTypeAddinMacro,
+	".xlsm": ContentTypeMacro,
+	".xlsx": ContentTypeSheetML,
+	".xltm": ContentTypeTemplateMacro,
+	".xltx": ContentTypeTemplate,
+}
+
 // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
 // element specifies non-visual canvas properties. This allows for additional
 // information that does not affect the appearance of the picture to be stored.

xuri avatar Sep 06 '22 13:09 xuri

I have fixed this issue. Now the Save, Write and WriteTo function accept saving options, and here is an example for handle a workbook without a touch file system. Please upgrade to the master branch code, and this patch will be released in the next version.

xuri avatar Sep 08 '22 14:09 xuri