diff --git a/frontend/src/organisms/connection/collection/dialogs/export.svelte b/frontend/src/organisms/connection/collection/dialogs/export.svelte index c18e301..b314f7b 100644 --- a/frontend/src/organisms/connection/collection/dialogs/export.svelte +++ b/frontend/src/organisms/connection/collection/dialogs/export.svelte @@ -30,9 +30,10 @@ diff --git a/internal/app/collection_find_export.go b/internal/app/collection_find_export.go index 078a355..bb7064b 100644 --- a/internal/app/collection_find_export.go +++ b/internal/app/collection_find_export.go @@ -1,10 +1,14 @@ package app import ( + "archive/zip" + _ "embed" "encoding/csv" "encoding/json" "fmt" + "io" "os" + "strings" "github.com/wailsapp/wails/v2/pkg/runtime" "go.mongodb.org/mongo-driver/bson" @@ -23,6 +27,24 @@ const ( ExportFormatJsonArray ExportFormat = "jsonarray" ExportFormatNdJson ExportFormat = "ndjson" ExportFormatCsv ExportFormat = "csv" + ExportFormatExcel ExportFormat = "excel" +) + +var ( + //go:embed collection_find_export_excel/app.xml + excelAppXml string + //go:embed collection_find_export_excel/core.xml + excelCoreXml string + //go:embed collection_find_export_excel/rels.xml + excelRelsXml string + //go:embed collection_find_export_excel/styles.xml + excelStylesXml string + //go:embed collection_find_export_excel/theme.xml + excelThemeXml string + //go:embed collection_find_export_excel/contenttypes.xml + excelContentTypesXml string + + alphabet = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") ) type ExportSettings struct { @@ -39,6 +61,29 @@ func getptr[T any](v T) *T { return &v } +func excelColIndex(idx int) string { + str := make([]rune, 0) + + for idx > 0 { + rem := idx % 26 + if rem == 0 { + str = append(str, 'Z') + idx = (idx / 26) - 1 + } else { + + str = append(str, alphabet[rem-1]) + idx = (idx / 26) + } + } + + // Reverse string + for i, j := 0, len(str)-1; i < j; i, j = i+1, j-1 { + str[i], str[j] = str[j], str[i] + } + + return string(str) +} + func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bool { runtime.LogInfof(a.ctx, "Export started for %s/%s/%s. Settings: %s", hostKey, dbKey, collKey, settingsJson) @@ -90,18 +135,27 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo DisplayName: "CSV files (*.csv)", Pattern: "*.csv", } + case ExportFormatJsonArray: defaultFilename = "export.json" fileFilter = runtime.FileFilter{ DisplayName: "JSON files (*.json)", Pattern: "*.json", } + case ExportFormatNdJson: defaultFilename = "export.ndjson" fileFilter = runtime.FileFilter{ DisplayName: "Newline delimited JSON files (*.ndjson)", Pattern: "*.ndjson", } + + case ExportFormatExcel: + defaultFilename = "export.xlsx" + fileFilter = runtime.FileFilter{ + DisplayName: "Microsoft Excel Workbook (*.xlsx)", + Pattern: "*.xlsx", + } } settings.OutFile, err = runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ @@ -123,12 +177,11 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo if settings.OutFile == "" { runtime.LogDebug(a.ctx, "Export: no destination specified") runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Message: "Please specify ab export destination.", + Message: "Please specify an export destination.", Type: runtime.ErrorDialog, }) return false } - if _, err := os.Stat(settings.OutFile); err == nil { runtime.LogDebugf(a.ctx, "Export: destination %s already exists", settings.OutFile) runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ @@ -201,9 +254,14 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo } defer file.Close() - var csvWriter *csv.Writer - var csvColumnKeys []string var index uint = 0 + var columnKeys []string + + var csvWriter *csv.Writer + + var excelZipWriter *zip.Writer + var excelSheetWriter io.Writer + var excelStrings = make([]string, 0) switch settings.Format { case ExportFormatJsonArray: @@ -211,11 +269,78 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo case ExportFormatCsv: csvWriter = csv.NewWriter(file) + + case ExportFormatExcel: + excelZipWriter = zip.NewWriter(file) + + files := map[string]string{ + "docProps/app.xml": excelAppXml, + "docProps/core.xml": strings.Replace(excelCoreXml, "{TITLE}", fmt.Sprintf("%s.%s", dbKey, collKey), 1), + "xl/theme/theme1.xml": excelThemeXml, + "xl/rels/workbook.xml.rels": excelRelsXml, + "xl/styles.xml": excelStylesXml, + "[Content-Types].xml": excelContentTypesXml, + } + + for fname, body := range files { + f, err := excelZipWriter.Create(fname) + if err != nil { + runtime.LogErrorf(a.ctx, "Export: Excel zip.Create error: %s", err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: "ZIP error!", + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + return false + } + + _, err = f.Write([]byte(body)) + if err != nil { + runtime.LogErrorf(a.ctx, "Export: Excel zip.Create.Write error: %s", err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: "ZIP error!", + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + return false + } + } + + excelZipWriter.Create("_rels/") + + excelSheetWriter, err = excelZipWriter.Create("xl/worksheets/sheet1.xml") + if err != nil { + runtime.LogErrorf(a.ctx, "Export: Excel ZIP error creating worksheet: %s", err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: "ZIP error!", + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + return false + } + + excelSheetWriter.Write([]byte(` + + + + + + + + + `)) } for cur.Next(ctx) { - switch settings.Format { - case ExportFormatCsv: + if settings.ViewKey == "list" && columnKeys == nil { + columnKeys = make([]string, 0) els, err := cur.Current.Elements() if err != nil { runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ @@ -225,48 +350,59 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo }) } + for _, el := range els { + if el.Key() == "" { + continue + } + + switch el.Value().Type { + case bsontype.Boolean, + bsontype.Decimal128, + bsontype.Double, + bsontype.Int32, + bsontype.Int64, + bsontype.Null, + bsontype.ObjectID, + bsontype.Regex, + bsontype.String, + bsontype.Symbol, + bsontype.Timestamp, + bsontype.Undefined: + columnKeys = append(columnKeys, el.Key()) + } + } + + runtime.LogDebugf(a.ctx, "Export column keys: %v", columnKeys) + + switch settings.Format { + case ExportFormatCsv: + if err := csvWriter.Write(columnKeys); err != nil { + runtime.LogInfof(a.ctx, "Unable to write item %d to CSV while exporting: %s", index, err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: fmt.Sprintf("Unable to write item %d to CSV", index), + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + } + + case ExportFormatExcel: + excelSheetWriter.Write([]byte(fmt.Sprintf(``, len(columnKeys)))) + for idx, key := range columnKeys { + // excelStringsWriter.Write([]byte(fmt.Sprintf("%s", key))) + excelStrings = append(excelStrings, key) + excelSheetWriter.Write([]byte(fmt.Sprintf(`%d`, excelColIndex(idx+1), len(excelStrings)))) + } + excelSheetWriter.Write([]byte("")) + } + } + + switch settings.Format { + case ExportFormatCsv: csvItem := make([]string, 0) switch settings.ViewKey { case "list": - if csvColumnKeys == nil { - csvColumnKeys = make([]string, 0) - - for _, el := range els { - if el.Key() == "" { - continue - } - - switch el.Value().Type { - case bsontype.Boolean, - bsontype.Decimal128, - bsontype.Double, - bsontype.Int32, - bsontype.Int64, - bsontype.Null, - bsontype.ObjectID, - bsontype.Regex, - bsontype.String, - bsontype.Symbol, - bsontype.Timestamp, - bsontype.Undefined: - csvColumnKeys = append(csvColumnKeys, el.Key()) - } - } - - runtime.LogDebugf(a.ctx, "Export csvColumnKeys: %v", csvColumnKeys) - - if err := csvWriter.Write(csvColumnKeys); err != nil { - runtime.LogInfof(a.ctx, "Unable to write item %d to CSV while exporting: %s", index, err.Error()) - runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Title: fmt.Sprintf("Unable to write item %d to CSV", index), - Message: err.Error(), - Type: runtime.ErrorDialog, - }) - } - } - - for _, k := range csvColumnKeys { + for _, k := range columnKeys { r, err := cur.Current.LookupErr(k) if err != nil { csvItem = append(csvItem, "") @@ -322,14 +458,75 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo if settings.Format == ExportFormatNdJson { file.WriteString("\n") } + + case ExportFormatExcel: + excelRow := make([]string, 0) + + for _, k := range columnKeys { + r, err := cur.Current.LookupErr(k) + if err != nil { + excelRow = append(excelRow, "") + continue + } + + var v any + if err := r.Unmarshal(&v); err != nil { + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: fmt.Sprintf("Unable to unmarshal field %s", k), + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + excelRow = append(excelRow, "") + continue + } + + excelRow = append(excelRow, fmt.Sprintf("%v", v)) + } + + excelSheetWriter.Write([]byte(fmt.Sprintf(``, index+2, len(columnKeys)))) + for idx, val := range excelRow { + // excelStringsWriter.Write([]byte(fmt.Sprintf("%s", val))) + excelStrings = append(excelStrings, val) + excelSheetWriter.Write([]byte(fmt.Sprintf(`%d`, excelColIndex(idx+1), index+2, len(excelStrings)))) + } + excelSheetWriter.Write([]byte("")) } index++ - } - if settings.Format == ExportFormatJsonArray { + switch settings.Format { + case ExportFormatJsonArray: file.WriteString("]\n") + + case ExportFormatExcel: + excelSheetWriter.Write([]byte(``)) + + excelStringsWriter, err := excelZipWriter.Create("xl/sharedStrings.xml") + if err != nil { + runtime.LogErrorf(a.ctx, "Export: Excel ZIP error creating shared strings: %s", err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: "ZIP error!", + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + return false + } + + excelStringsWriter.Write([]byte(fmt.Sprintf(`%s`, len(excelStrings), len(excelStrings), "\r\n"))) + for _, str := range excelStrings { + excelStringsWriter.Write([]byte(fmt.Sprintf("%s\r\n", str))) + } + excelStringsWriter.Write([]byte("")) + + if err := excelZipWriter.Close(); err != nil { + runtime.LogErrorf(a.ctx, "Export: Excel ZIP error while closing: %s", err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Title: "ZIP error!", + Message: err.Error(), + Type: runtime.ErrorDialog, + }) + } } a.ui.Reveal(settings.OutFile) diff --git a/internal/app/collection_find_export_excel/app.xml b/internal/app/collection_find_export_excel/app.xml new file mode 100644 index 0000000..52ba16f --- /dev/null +++ b/internal/app/collection_find_export_excel/app.xml @@ -0,0 +1,31 @@ + + + RolensExport + 0 + false + + + + Sheets + + + 1 + + + + + + RolensExport + + + + + false + false + + false + 16.0300 + diff --git a/internal/app/collection_find_export_excel/contenttypes.xml b/internal/app/collection_find_export_excel/contenttypes.xml new file mode 100644 index 0000000..2312f21 --- /dev/null +++ b/internal/app/collection_find_export_excel/contenttypes.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/internal/app/collection_find_export_excel/core.xml b/internal/app/collection_find_export_excel/core.xml new file mode 100644 index 0000000..bb61633 --- /dev/null +++ b/internal/app/collection_find_export_excel/core.xml @@ -0,0 +1,18 @@ + + + {TITLE} + + Rolens + + + + 2020-01-01T00:00:00Z + 2020-01-01T00:00:00Z + + diff --git a/internal/app/collection_find_export_excel/rels.xml b/internal/app/collection_find_export_excel/rels.xml new file mode 100644 index 0000000..310c413 --- /dev/null +++ b/internal/app/collection_find_export_excel/rels.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/internal/app/collection_find_export_excel/styles.xml b/internal/app/collection_find_export_excel/styles.xml new file mode 100644 index 0000000..9df2015 --- /dev/null +++ b/internal/app/collection_find_export_excel/styles.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/internal/app/collection_find_export_excel/theme.xml b/internal/app/collection_find_export_excel/theme.xml new file mode 100644 index 0000000..264e5d5 --- /dev/null +++ b/internal/app/collection_find_export_excel/theme.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +