2019-12-29 16:02:31 +08:00
|
|
|
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
|
2018-09-14 00:44:23 +08:00
|
|
|
// this source code is governed by a BSD-style license that can be found in
|
|
|
|
// the LICENSE file.
|
2018-12-05 00:27:19 +08:00
|
|
|
|
2018-09-14 00:44:23 +08:00
|
|
|
// Package excelize providing a set of functions that allow you to write to
|
|
|
|
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
|
|
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
2019-08-11 00:36:14 +08:00
|
|
|
// charts of XLSX. This library needs Go version 1.10 or later.
|
2018-09-14 00:58:48 +08:00
|
|
|
//
|
|
|
|
// See https://xuri.me/excelize for more information about this package.
|
2016-08-30 11:51:31 +08:00
|
|
|
package excelize
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
2016-09-04 19:25:31 +08:00
|
|
|
"bytes"
|
2016-08-30 11:51:31 +08:00
|
|
|
"encoding/xml"
|
2019-06-27 21:58:14 +08:00
|
|
|
"errors"
|
2019-04-15 11:22:57 +08:00
|
|
|
"fmt"
|
2017-02-17 02:25:55 +08:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2019-07-21 12:56:36 +08:00
|
|
|
"path"
|
2016-08-30 11:51:31 +08:00
|
|
|
"strconv"
|
2019-07-21 12:56:36 +08:00
|
|
|
"strings"
|
2019-12-20 00:30:48 +08:00
|
|
|
|
|
|
|
"golang.org/x/net/html/charset"
|
2016-08-30 11:51:31 +08:00
|
|
|
)
|
|
|
|
|
2017-01-18 16:05:01 +08:00
|
|
|
// File define a populated XLSX file struct.
|
2016-09-05 16:37:15 +08:00
|
|
|
type File struct {
|
2019-02-25 00:29:58 +08:00
|
|
|
checked map[string]bool
|
|
|
|
sheetMap map[string]string
|
|
|
|
CalcChain *xlsxCalcChain
|
|
|
|
Comments map[string]*xlsxComments
|
|
|
|
ContentTypes *xlsxTypes
|
2019-02-25 22:14:34 +08:00
|
|
|
Drawings map[string]*xlsxWsDr
|
2019-02-25 00:29:58 +08:00
|
|
|
Path string
|
|
|
|
SharedStrings *xlsxSST
|
|
|
|
Sheet map[string]*xlsxWorksheet
|
|
|
|
SheetCount int
|
|
|
|
Styles *xlsxStyleSheet
|
|
|
|
Theme *xlsxTheme
|
|
|
|
DecodeVMLDrawing map[string]*decodeVmlDrawing
|
|
|
|
VMLDrawing map[string]*vmlDrawing
|
|
|
|
WorkBook *xlsxWorkbook
|
2019-09-16 01:17:35 +08:00
|
|
|
Relationships map[string]*xlsxRelationships
|
2019-02-25 00:29:58 +08:00
|
|
|
XLSX map[string][]byte
|
2019-12-20 00:30:48 +08:00
|
|
|
CharsetReader charsetTranscoderFn
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2019-12-20 00:30:48 +08:00
|
|
|
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
|
|
|
|
|
2017-01-18 16:05:01 +08:00
|
|
|
// OpenFile take the name of an XLSX file and returns a populated XLSX file
|
|
|
|
// struct for it.
|
2016-09-06 21:20:24 +08:00
|
|
|
func OpenFile(filename string) (*File, error) {
|
2017-02-17 02:25:55 +08:00
|
|
|
file, err := os.Open(filename)
|
2016-09-06 21:20:24 +08:00
|
|
|
if err != nil {
|
2017-02-17 02:25:55 +08:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-02-17 02:30:05 +08:00
|
|
|
defer file.Close()
|
2017-02-17 02:38:57 +08:00
|
|
|
f, err := OpenReader(file)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
f.Path = filename
|
|
|
|
return f, nil
|
2017-02-17 02:30:05 +08:00
|
|
|
}
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// newFile is object builder
|
2019-12-20 00:30:48 +08:00
|
|
|
func newFile() *File {
|
|
|
|
return &File{
|
|
|
|
checked: make(map[string]bool),
|
|
|
|
sheetMap: make(map[string]string),
|
|
|
|
Comments: make(map[string]*xlsxComments),
|
|
|
|
Drawings: make(map[string]*xlsxWsDr),
|
|
|
|
Sheet: make(map[string]*xlsxWorksheet),
|
|
|
|
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
|
|
|
|
VMLDrawing: make(map[string]*vmlDrawing),
|
|
|
|
Relationships: make(map[string]*xlsxRelationships),
|
|
|
|
CharsetReader: charset.NewReaderLabel,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-17 02:30:05 +08:00
|
|
|
// OpenReader take an io.Reader and return a populated XLSX file.
|
|
|
|
func OpenReader(r io.Reader) (*File, error) {
|
|
|
|
b, err := ioutil.ReadAll(r)
|
2017-02-17 02:25:55 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-02-17 02:30:05 +08:00
|
|
|
zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
|
2017-02-17 02:25:55 +08:00
|
|
|
if err != nil {
|
2019-06-27 21:58:14 +08:00
|
|
|
identifier := []byte{
|
|
|
|
// checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier
|
|
|
|
0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00,
|
|
|
|
0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00,
|
|
|
|
0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00,
|
|
|
|
0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00,
|
|
|
|
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
|
|
|
}
|
|
|
|
if bytes.Contains(b, identifier) {
|
|
|
|
return nil, errors.New("not support encrypted file currently")
|
|
|
|
}
|
2017-02-17 02:25:55 +08:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-02-17 02:30:05 +08:00
|
|
|
|
2017-02-17 02:25:55 +08:00
|
|
|
file, sheetCount, err := ReadZipReader(zr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
2019-12-20 00:30:48 +08:00
|
|
|
f := newFile()
|
|
|
|
f.SheetCount, f.XLSX = sheetCount, file
|
2019-02-25 00:29:58 +08:00
|
|
|
f.CalcChain = f.calcChainReader()
|
2017-10-27 22:05:00 +08:00
|
|
|
f.sheetMap = f.getSheetMap()
|
|
|
|
f.Styles = f.stylesReader()
|
2018-07-07 15:59:48 +08:00
|
|
|
f.Theme = f.themeReader()
|
2017-10-27 22:05:00 +08:00
|
|
|
return f, nil
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// CharsetTranscoder Set user defined codepage transcoder function for open
|
|
|
|
// XLSX from non UTF-8 encoding.
|
2019-12-20 00:30:48 +08:00
|
|
|
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// Creates new XML decoder with charset reader.
|
2019-12-20 00:30:48 +08:00
|
|
|
func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
|
|
|
|
ret = xml.NewDecoder(rdr)
|
|
|
|
ret.CharsetReader = f.CharsetReader
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// setDefaultTimeStyle provides a function to set default numbers format for
|
2018-01-05 09:39:31 +08:00
|
|
|
// time.Time type cell value by given worksheet name, cell coordinates and
|
|
|
|
// number format code.
|
2019-03-23 20:08:06 +08:00
|
|
|
func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error {
|
|
|
|
s, err := f.GetCellStyle(sheet, axis)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if s == 0 {
|
2018-01-05 09:39:31 +08:00
|
|
|
style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`)
|
2019-12-24 01:09:28 +08:00
|
|
|
_ = f.SetCellStyle(sheet, axis, axis, style)
|
2017-06-23 21:07:30 +08:00
|
|
|
}
|
2019-03-23 20:08:06 +08:00
|
|
|
return err
|
2017-06-23 21:07:30 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// workSheetReader provides a function to get the pointer to the structure
|
|
|
|
// after deserialization by given worksheet name.
|
2019-12-20 00:30:48 +08:00
|
|
|
func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) {
|
|
|
|
var (
|
|
|
|
name string
|
|
|
|
ok bool
|
|
|
|
)
|
|
|
|
|
|
|
|
if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok {
|
|
|
|
err = fmt.Errorf("sheet %s is not exist", sheet)
|
|
|
|
return
|
2017-09-13 22:00:33 +08:00
|
|
|
}
|
2019-12-20 00:30:48 +08:00
|
|
|
if xlsx = f.Sheet[name]; f.Sheet[name] == nil {
|
|
|
|
xlsx = new(xlsxWorksheet)
|
|
|
|
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
|
|
|
|
Decode(xlsx); err != nil && err != io.EOF {
|
|
|
|
err = fmt.Errorf("xml decode error: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = nil
|
2017-03-12 20:38:26 +08:00
|
|
|
if f.checked == nil {
|
|
|
|
f.checked = make(map[string]bool)
|
|
|
|
}
|
2019-12-20 00:30:48 +08:00
|
|
|
if ok = f.checked[name]; !ok {
|
|
|
|
checkSheet(xlsx)
|
|
|
|
if err = checkRow(xlsx); err != nil {
|
|
|
|
return
|
|
|
|
}
|
2017-03-12 20:38:26 +08:00
|
|
|
f.checked[name] = true
|
|
|
|
}
|
2019-12-20 00:30:48 +08:00
|
|
|
f.Sheet[name] = xlsx
|
2017-02-12 19:03:24 +08:00
|
|
|
}
|
2019-12-20 00:30:48 +08:00
|
|
|
|
|
|
|
return
|
2017-03-12 20:38:26 +08:00
|
|
|
}
|
2017-02-12 19:03:24 +08:00
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// checkSheet provides a function to fill each row element and make that is
|
2017-03-31 17:14:35 +08:00
|
|
|
// continuous in a worksheet of XML.
|
|
|
|
func checkSheet(xlsx *xlsxWorksheet) {
|
|
|
|
row := len(xlsx.SheetData.Row)
|
2017-05-24 14:17:35 +08:00
|
|
|
if row >= 1 {
|
2017-03-31 17:14:35 +08:00
|
|
|
lastRow := xlsx.SheetData.Row[row-1].R
|
|
|
|
if lastRow >= row {
|
|
|
|
row = lastRow
|
|
|
|
}
|
|
|
|
}
|
2019-10-28 23:34:21 +08:00
|
|
|
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
|
|
|
|
for _, r := range xlsx.SheetData.Row {
|
|
|
|
sheetData.Row[r.R-1] = r
|
2016-12-20 14:40:36 +08:00
|
|
|
}
|
2019-10-28 23:34:21 +08:00
|
|
|
for i := 1; i <= row; i++ {
|
|
|
|
sheetData.Row[i-1].R = i
|
2016-12-20 14:40:36 +08:00
|
|
|
}
|
|
|
|
xlsx.SheetData = sheetData
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2019-09-16 01:17:35 +08:00
|
|
|
// addRels provides a function to add relationships by given XML path,
|
|
|
|
// relationship type, target and target mode.
|
|
|
|
func (f *File) addRels(relPath, relType, target, targetMode string) int {
|
|
|
|
rels := f.relsReader(relPath)
|
|
|
|
if rels == nil {
|
|
|
|
rels = &xlsxRelationships{}
|
|
|
|
}
|
2019-12-23 00:07:40 +08:00
|
|
|
var rID int
|
|
|
|
for _, rel := range rels.Relationships {
|
|
|
|
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
|
|
|
if ID > rID {
|
|
|
|
rID = ID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rID++
|
2019-09-16 01:17:35 +08:00
|
|
|
var ID bytes.Buffer
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
|
|
|
rels.Relationships = append(rels.Relationships, xlsxRelationship{
|
|
|
|
ID: ID.String(),
|
|
|
|
Type: relType,
|
|
|
|
Target: target,
|
|
|
|
TargetMode: targetMode,
|
|
|
|
})
|
|
|
|
f.Relationships[relPath] = rels
|
|
|
|
return rID
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace
|
2017-03-31 17:14:35 +08:00
|
|
|
// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft
|
2017-01-18 16:05:01 +08:00
|
|
|
// Office Excel 2007.
|
2018-05-07 16:12:51 +08:00
|
|
|
func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
|
|
|
|
var oldXmlns = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
2019-11-30 00:06:36 +08:00
|
|
|
var newXmlns = []byte(`<worksheet` + templateNamespaceIDMap)
|
2018-05-07 16:12:51 +08:00
|
|
|
workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
|
|
|
|
return workbookMarshal
|
|
|
|
}
|
|
|
|
|
2019-08-04 20:24:59 +08:00
|
|
|
// replaceStyleRelationshipsNameSpaceBytes provides a function to replace
|
|
|
|
// xl/styles.xml XML tags to self-closing for compatible Microsoft Office
|
|
|
|
// Excel 2007.
|
|
|
|
func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte {
|
|
|
|
var oldXmlns = []byte(`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
2019-11-30 00:06:36 +08:00
|
|
|
var newXmlns = []byte(`<styleSheet` + templateNamespaceIDMap)
|
2019-08-04 20:24:59 +08:00
|
|
|
contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1)
|
|
|
|
return contentMarshal
|
|
|
|
}
|
|
|
|
|
2016-09-07 20:09:02 +08:00
|
|
|
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
|
|
|
|
// Office Excel 2007 and 2010. This function will be remove value tag when met a
|
2017-01-18 16:05:01 +08:00
|
|
|
// cell have a linked value. Reference
|
2019-01-06 14:12:31 +08:00
|
|
|
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating
|
2016-09-06 21:20:24 +08:00
|
|
|
//
|
|
|
|
// Notice: after open XLSX file Excel will be update linked value and generate
|
|
|
|
// new value and will prompt save file or not.
|
|
|
|
//
|
|
|
|
// For example:
|
|
|
|
//
|
2016-09-09 19:39:41 +08:00
|
|
|
// <row r="19" spans="2:2">
|
|
|
|
// <c r="B19">
|
|
|
|
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
|
|
|
// <v>100</v>
|
|
|
|
// </c>
|
|
|
|
// </row>
|
2016-09-06 21:20:24 +08:00
|
|
|
//
|
|
|
|
// to
|
|
|
|
//
|
2016-09-09 19:39:41 +08:00
|
|
|
// <row r="19" spans="2:2">
|
|
|
|
// <c r="B19">
|
|
|
|
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
|
|
|
// </c>
|
|
|
|
// </row>
|
2017-01-17 19:06:42 +08:00
|
|
|
//
|
2019-04-15 11:22:57 +08:00
|
|
|
func (f *File) UpdateLinkedValue() error {
|
2020-03-09 00:08:47 +08:00
|
|
|
wb := f.workbookReader()
|
|
|
|
// recalculate formulas
|
|
|
|
wb.CalcPr = nil
|
2017-09-13 22:00:33 +08:00
|
|
|
for _, name := range f.GetSheetMap() {
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, err := f.workSheetReader(name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-10-16 10:42:43 +08:00
|
|
|
for indexR := range xlsx.SheetData.Row {
|
|
|
|
for indexC, col := range xlsx.SheetData.Row[indexR].C {
|
2017-01-18 14:47:23 +08:00
|
|
|
if col.F != nil && col.V != "" {
|
2017-01-17 19:06:42 +08:00
|
|
|
xlsx.SheetData.Row[indexR].C[indexC].V = ""
|
|
|
|
xlsx.SheetData.Row[indexR].C[indexC].T = ""
|
2016-09-06 21:20:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-15 11:22:57 +08:00
|
|
|
return nil
|
2016-09-06 21:20:24 +08:00
|
|
|
}
|
2019-07-21 12:56:36 +08:00
|
|
|
|
|
|
|
// AddVBAProject provides the method to add vbaProject.bin file which contains
|
|
|
|
// functions and/or macros. The file extension should be .xlsm. For example:
|
|
|
|
//
|
2019-12-31 01:01:16 +08:00
|
|
|
// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil {
|
2020-02-19 00:08:10 +08:00
|
|
|
// fmt.Println(err)
|
2019-07-21 12:56:36 +08:00
|
|
|
// }
|
2019-12-31 01:01:16 +08:00
|
|
|
// if err := f.AddVBAProject("vbaProject.bin"); err != nil {
|
2020-02-19 00:08:10 +08:00
|
|
|
// fmt.Println(err)
|
2019-07-21 12:56:36 +08:00
|
|
|
// }
|
2019-12-31 01:01:16 +08:00
|
|
|
// if err := f.SaveAs("macros.xlsm"); err != nil {
|
2020-02-19 00:08:10 +08:00
|
|
|
// fmt.Println(err)
|
2019-07-21 12:56:36 +08:00
|
|
|
// }
|
|
|
|
//
|
|
|
|
func (f *File) AddVBAProject(bin string) error {
|
|
|
|
var err error
|
|
|
|
// Check vbaProject.bin exists first.
|
|
|
|
if _, err = os.Stat(bin); os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if path.Ext(bin) != ".bin" {
|
|
|
|
return errors.New("unsupported VBA project extension")
|
|
|
|
}
|
|
|
|
f.setContentTypePartVBAProjectExtensions()
|
2019-09-16 01:17:35 +08:00
|
|
|
wb := f.relsReader("xl/_rels/workbook.xml.rels")
|
2019-07-21 12:56:36 +08:00
|
|
|
var rID int
|
|
|
|
var ok bool
|
|
|
|
for _, rel := range wb.Relationships {
|
|
|
|
if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject {
|
|
|
|
ok = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
t, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
|
|
|
if t > rID {
|
|
|
|
rID = t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rID++
|
|
|
|
if !ok {
|
2019-09-16 01:17:35 +08:00
|
|
|
wb.Relationships = append(wb.Relationships, xlsxRelationship{
|
2019-07-21 12:56:36 +08:00
|
|
|
ID: "rId" + strconv.Itoa(rID),
|
|
|
|
Target: "vbaProject.bin",
|
|
|
|
Type: SourceRelationshipVBAProject,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
file, _ := ioutil.ReadFile(bin)
|
|
|
|
f.XLSX["xl/vbaProject.bin"] = file
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// setContentTypePartVBAProjectExtensions provides a function to set the
|
|
|
|
// content type for relationship parts and the main document part.
|
|
|
|
func (f *File) setContentTypePartVBAProjectExtensions() {
|
|
|
|
var ok bool
|
|
|
|
content := f.contentTypesReader()
|
|
|
|
for _, v := range content.Defaults {
|
|
|
|
if v.Extension == "bin" {
|
|
|
|
ok = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for idx, o := range content.Overrides {
|
|
|
|
if o.PartName == "/xl/workbook.xml" {
|
|
|
|
content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
content.Defaults = append(content.Defaults, xlsxDefault{
|
|
|
|
Extension: "bin",
|
|
|
|
ContentType: "application/vnd.ms-office.vbaProject",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|