Skip to content

Commit

Permalink
support export to tsv file
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Oct 29, 2023
1 parent 429e270 commit 55d7994
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 295 deletions.
17 changes: 15 additions & 2 deletions cmd/user_data.go
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"os"

Expand Down Expand Up @@ -231,7 +232,7 @@ var UserData = &cli.Command{
},
{
Name: "transaction-export",
Usage: "Export user all transactions to csv file",
Usage: "Export user all transactions to file",
Action: exportUserTransaction,
Flags: []cli.Flag{
&cli.StringFlag{
Expand All @@ -246,6 +247,12 @@ var UserData = &cli.Command{
Required: true,
Usage: "Specific exported file path (e.g. transaction.csv)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Required: false,
Usage: "Export file type, support csv or tsv, default is csv",
},
},
},
},
Expand Down Expand Up @@ -555,6 +562,12 @@ func exportUserTransaction(c *cli.Context) error {

username := c.String("username")
filePath := c.String("file")
fileType := c.String("type")

if fileType != "" && fileType != "csv" && fileType != "tsv" {
log.BootErrorf("[user_data.exportUserTransaction] export file type is not supported")
return errs.ErrNotSupported
}

if filePath == "" {
log.BootErrorf("[user_data.exportUserTransaction] export file path is not specified")
Expand All @@ -570,7 +583,7 @@ func exportUserTransaction(c *cli.Context) error {

log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)

content, err := clis.UserData.ExportTransaction(c, username)
content, err := clis.UserData.ExportTransaction(c, username, fileType)

if err != nil {
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
Expand Down
16 changes: 15 additions & 1 deletion cmd/webserver.go
Expand Up @@ -256,7 +256,8 @@ func startWebServer(c *cli.Context) error {
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))

if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler))
}

// Accounts
Expand Down Expand Up @@ -376,6 +377,19 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}

func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, fileName, err := fn(c)

if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, "text/tab-separated-values", fileName, result)
}
}
}

func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
Expand Down
199 changes: 109 additions & 90 deletions pkg/api/data_managements.go
Expand Up @@ -19,103 +19,38 @@ const pageCountForDataExport = 1000

// DataManagementsApi represents data management api
type DataManagementsApi struct {
exporter *converters.EzBookKeepingCSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
}

// Initialize a data management api singleton instance
var (
DataManagements = &DataManagementsApi{
exporter: &converters.EzBookKeepingCSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
}
)

// ExportDataHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}

timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()

if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}

uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)

if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}

return nil, "", errs.ErrUserNotFound
}

accounts, err := a.accounts.GetAllAccountsByUid(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

tags, err := a.tags.GetAllTagsByUid(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)

allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}

fileName := a.getFileName(user, timezone)
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "csv")
}

return result, fileName, nil
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "tsv")
}

// DataStatisticsHandler returns user data statistics
Expand Down Expand Up @@ -209,11 +144,95 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
return true, nil
}

func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location) string {
func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}

timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()

if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}

uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)

if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}

return nil, "", errs.ErrUserNotFound
}

accounts, err := a.accounts.GetAllAccountsByUid(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

tags, err := a.tags.GetAllTagsByUid(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)

allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}

var dataExporter converters.DataConverter

if fileType == "tsv" {
dataExporter = a.ezBookKeepingTsvExporter
} else {
dataExporter = a.ezBookKeepingCsvExporter
}

result, err := dataExporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)

if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}

fileName := a.getFileName(user, timezone, fileType)

return result, fileName, nil
}

func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1)
currentTime = strings.Replace(currentTime, ":", "_", -1)

return fmt.Sprintf("%s_%s.csv", user.Username, currentTime)
return fmt.Sprintf("%s_%s.%s", user.Username, currentTime, fileExtension)
}
14 changes: 12 additions & 2 deletions pkg/cli/user_data.go
Expand Up @@ -20,6 +20,7 @@ const pageCountForDataExport = 1000
// UserDataCli represents user data cli
type UserDataCli struct {
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
Expand All @@ -34,6 +35,7 @@ type UserDataCli struct {
var (
UserData = &UserDataCli{
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
Expand Down Expand Up @@ -537,7 +539,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
}

// ExportTransaction returns csv file content according user all transactions
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte, error) {
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileType string) ([]byte, error) {
if username == "" {
log.BootErrorf("[user_data.ExportTransaction] user name is empty")
return nil, errs.ErrUsernameIsEmpty
Expand All @@ -564,7 +566,15 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte
return nil, err
}

result, err := l.ezBookKeepingCsvExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
var dataExporter converters.DataConverter

if fileType == "tsv" {
dataExporter = l.ezBookKeepingTsvExporter
} else {
dataExporter = l.ezBookKeepingCsvExporter
}

result, err := dataExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)

if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"%s\", because %s", username, err.Error())
Expand Down

0 comments on commit 55d7994

Please sign in to comment.