valasek/timesheet

View on GitHub
server/api/api.go

Summary

Maintainability
C
1 day
Test Coverage
// Copyright © 2018-2020 Stanislav Valasek <valasek@gmail.com>

package api

import (
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "path/filepath"
    "time"

    "github.com/valasek/timesheet/server/logger"
    "github.com/valasek/timesheet/server/models"
    "github.com/valasek/timesheet/server/version"

    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
)

// API -
type API struct {
    consultants     *models.ConsultantManager
    reportedRecords *models.ReportedRecordManager
    projects        *models.ProjectManager
    rates           *models.RateManager
    holidays        *models.HolidayManager
}

// AppSettings -
type AppSettings struct {
    Version              string  `json:"version"`
    DailyWorkingHours    float64 `json:"dailyWorkingHours"`
    DailyWorkingHoursMin float64 `json:"dailyWorkingHoursMin"`
    DailyWorkingHoursMax float64 `json:"dailyWorkingHoursMax"`
    Vacation             string  `json:"vacation"`
    YearlyVacationDays   int64   `json:"yearlyVacationDays"`
    VacationPersonal     string  `json:"vacationPersonal"`
    YearlyPersonalDays   int64   `json:"yearlyPersonalDays"`
    VacationSick         string  `json:"vacationSick"`
    YearlySickDays       int64   `json:"yearlySickDays"`
    IsWorking            string  `json:"isWorking"`
    IsNonWorking         string  `json:"isNonWorking"`
}

// EntityOverview -
type EntityOverview struct {
    Name     string `json:"name"`
    Total    int    `json:"total"`
    Active   int    `json:"active"`
    Disabled int    `json:"disabled"`
    Deleted  int    `json:"deleted"`
}

// AppSettings returns list of all appliocation and user settings for configuration file
func (api *API) AppSettings(c *gin.Context) {
    settings := AppSettings{
        Version:              version.Version,
        DailyWorkingHours:    viper.GetFloat64("dailyWorkingHours"),
        DailyWorkingHoursMin: viper.GetFloat64("dailyWorkingHoursMin"),
        DailyWorkingHoursMax: viper.GetFloat64("dailyWorkingHoursMax"),
        Vacation:             viper.GetString("vacation"),
        YearlyVacationDays:   viper.GetInt64("yearlyVacationDays"),
        VacationPersonal:     viper.GetString("vacationPersonal"),
        YearlyPersonalDays:   viper.GetInt64("yearlyPersonalDays"),
        VacationSick:         viper.GetString("vacationSick"),
        YearlySickDays:       viper.GetInt64("yearlySickDays"),
        IsWorking:            viper.GetString("isWorking"),
        IsNonWorking:         viper.GetString("isNonWorking"),
    }
    c.JSON(http.StatusOK, settings)
}

// TableStatistics -
func (api *API) TableStatistics(c *gin.Context) {
    entityOverview := []EntityOverview{}
    entityOverview = append(entityOverview, EntityOverview(api.projects.ProjectsGetStatistics()))
    entityOverview = append(entityOverview, EntityOverview(api.reportedRecords.ReportedRecordsGetStatistics()))
    entityOverview = append(entityOverview, EntityOverview(api.rates.RatesGetStatistics()))
    entityOverview = append(entityOverview, EntityOverview(api.consultants.ConsultantsGetStatistics()))
    c.JSON(http.StatusOK, entityOverview)
}

// Download -
func (api *API) Download(c *gin.Context) {
    fileName, err := export()
    if err != nil {
        c.String(http.StatusNotFound, "downloading data failed with error: "+err.Error())
        return
    }

    file, err := os.Open(fileName)
    defer file.Close()
    if err != nil {
        c.String(http.StatusNotFound, "file not found")
        return
    }

    //Get the Content-Type of the file
    //Create a buffer to store the header of the file in
    FileHeader := make([]byte, 512)
    //Copy the headers into the FileHeader buffer
    file.Read(FileHeader)
    //Get content type of file
    FileContentType := http.DetectContentType(FileHeader)

    //Get the file size
    FileStat, _ := file.Stat()  //Get info from file
    FileSize := FileStat.Size() //Get file size

    //Send the headers
    extraHeaders := map[string]string{
        "Content-Disposition": `attachment; filename="timesheet-backup.zip"`,
    }

    //Send the file
    //We read 512 bytes from the file already, so we reset the offset back to 0
    file.Seek(0, 0)
    c.DataFromReader(http.StatusOK, FileSize, FileContentType, file, extraHeaders)
    return
}

// Upload -
func (api *API) Upload(c *gin.Context) {

    // parse and validate file and post parameters
    form, err := c.MultipartForm()
    if err != nil {
        logger.Log.Error("unable to upload file, INVALID_FILE: ", err)
        c.String(http.StatusBadRequest, "INVALID_FILE: "+err.Error())
        return
    }
    uploadFileName := ""
    for k := range form.File {
        uploadFileName = k
        break
    }
    if uploadFileName == "" {
        logger.Log.Error("unable to upload file, INVALID_FILE: empty filename")
        c.String(http.StatusBadRequest, "INVALID_FILE: empty filename")
        return
    }
    ffile, err := c.FormFile(uploadFileName)
    if err != nil {
        logger.Log.Error("unable to upload file, INVALID_FILE: ", err)
        c.String(http.StatusBadRequest, "INVALID_FILE: "+err.Error())
        return
    }
    file, err := ffile.Open()
    if err != nil {
        logger.Log.Error("unable open file, INVALID_FILE: ", err)
        c.String(http.StatusBadRequest, "INVALID_FILE: "+err.Error())
        return
    }
    defer file.Close()
    fileBytes, err := ioutil.ReadAll(file)
    if err != nil {
        logger.Log.Error("unable to upload file: INVALID_FILE: ", err)
        c.String(http.StatusBadRequest, "INVALID_FILE: "+err.Error())
        return
    }

    // check file type, detectcontenttype only needs the first 512 bytes
    filetype := http.DetectContentType(fileBytes)
    if (filetype != "application/zip") && (filetype != "application/x-zip-compressed") {
        logger.Log.Error("unable to upload file, INVALID_FILE_TYPE (supported: application/zip, application/x-zip-compressed): ", filetype)
        c.String(http.StatusBadRequest, "INVALID_FILE_TYPE: "+filetype)
        return
    }
    fileName := randToken(12) + ".zip"
    uploadPath := viper.GetString("uploadFolder")
    newPath := filepath.Join(uploadPath, fileName)
    // FIXME check file type
    // fmt.Printf("FileType: %s, File: %s\n", fileType, newPath)

    // write file
    newFile, err := os.Create(newPath)
    if err != nil {
        logger.Log.Error("unable to upload file, CANT_WRITE_FILE: ", err)
        c.String(http.StatusInternalServerError, "CANT_WRITE_FILE: "+err.Error())
        return
    }
    defer newFile.Close() // idempotent, okay to call twice
    if _, err := newFile.Write(fileBytes); err != nil || newFile.Close() != nil {
        logger.Log.Error("unable to upload file. CANT_WRITE_FILE: ", err)
        c.String(http.StatusInternalServerError, "CANT_WRITE_FILE: "+err.Error())
        return
    }
    if err := restoreDB(newFile); err != nil {
        logger.Log.Error("unable to restore: ", err)
        c.String(http.StatusInternalServerError, "CANT_RESTORE: "+err.Error())
        return
    }
    err = os.RemoveAll(viper.GetString("uploadFolder"))
    if err != nil {
        logger.Log.Error("unable to restore: ", err)
        c.String(http.StatusInternalServerError, "CANT_RESTORE: "+err.Error())
        return
    }
    err = os.Mkdir(viper.GetString("uploadFolder"), os.ModeDir)
    if err != nil {
        logger.Log.Error("unable to restore: ", err)
        c.String(http.StatusInternalServerError, "CANT_RESTORE: "+err.Error())
        return
    }
    c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", ffile.Filename))
}

// DownloadDocs -
func (api *API) DownloadDocs(c *gin.Context) {

    fileName := filepath.Join("documentation", "documentation.md")
    f, err := os.Open(fileName)
    defer f.Close()
    if err != nil {
        c.String(http.StatusOK, fileName+" does not exist")
        return
    }
    fi, err := f.Stat()
    if err != nil {
        c.String(http.StatusOK, fileName+" cannot get file size")
        return
    }
    if fi.Size() == 0 {
        c.String(http.StatusOK, fileName+" does not exist")
        return
    }
    c.File(fileName)
}

// DownloadLogs -
func (api *API) DownloadLogs(c *gin.Context) {

    logLevel := c.Param("logLevel")
    if len(logLevel) < 1 {
        logger.Log.Error("unable to download log files, param 'logLevel' is missing")
        c.String(http.StatusInternalServerError, "unable to download log files, param 'logLevel' is missing")
        return
    }

    file := ""
    switch logLevel {
    case "0":
        file = "info.log"
    case "1":
        file = "error.log"
    default:
        logger.Log.Error("unable to download log files, unknown logLevel: ", logLevel)
    }
    fileName := filepath.Join(viper.GetString("logFolder"), file)
    f, err := os.Open(fileName)
    defer f.Close()
    if err != nil {
        c.String(http.StatusOK, file+" contains no log entries")
        return
    }
    fi, err := f.Stat()
    if err != nil {
        c.String(http.StatusOK, file+" cannot get file size")
        return
    }
    if fi.Size() == 0 {
        c.String(http.StatusOK, file+" contains no log entries")
        return
    }
    c.File(fileName)
}

// NewAPI -
func NewAPI(db *models.DB) *API {

    consultantmgr, err := models.NewConsultantManager(db)
    if err != nil {
        logger.Log.Error(err)
    }
    reportedrecordsmgr, err := models.NewReportedRecordManager(db)
    if err != nil {
        logger.Log.Error(err)
    }
    projectsmgr, err := models.NewProjectManager(db)
    if err != nil {
        logger.Log.Error(err)
    }
    ratesmgr, err := models.NewRateManager(db)
    if err != nil {
        logger.Log.Error(err)
    }
    holidaysmgr, err := models.NewHolidayManager(db)
    if err != nil {
        logger.Log.Error(err)
    }

    return &API{
        // users:  usermgr,
        consultants:     consultantmgr,
        reportedRecords: reportedrecordsmgr,
        projects:        projectsmgr,
        rates:           ratesmgr,
        holidays:        holidaysmgr,
    }
}

// ResetAPI - drops and creates all empty tables
func ResetAPI(db *models.DB) {

    // db.DropTableIfExists(&models.Users{})
    db.DropTableIfExists(&models.Consultant{})
    db.DropTableIfExists(&models.ReportedRecord{})
    db.DropTableIfExists(&models.Rate{})
    db.DropTableIfExists(&models.Project{})
    db.DropTableIfExists(&models.Holiday{})

    logger.Log.Info("recreated tables:")
    _, err := models.NewConsultantManager(db)
    if err != nil {
        logger.Log.Error("recreated tables:", err)
    }
    logger.Log.Info("- consultants")
    _, err = models.NewReportedRecordManager(db)
    if err != nil {
        logger.Log.Error("recreated tables:", err)
    }
    logger.Log.Info("- reported_records")
    models.NewProjectManager(db)
    if err != nil {
        logger.Log.Error("recreated tables:", err)
    }
    logger.Log.Info("- projects")
    models.NewRateManager(db)
    if err != nil {
        logger.Log.Error("recreated tables:", err)
    }
    logger.Log.Info("- rates")
    models.NewHolidayManager(db)
    if err != nil {
        logger.Log.Error("recreated tables:", err)
    }
    logger.Log.Info("- holidays")
}

// GenerateAPI - generates demo initial data and saves them into ./data folder
func GenerateAPI(folder string, db *models.DB) {

    api := NewAPI(db)
    var err error

    logger.Log.Infof("generated files (%s):", folder)
    tableNames := []string{"rates", "projects", "reported_records", "consultants", "holidays"}
    for _, baseFileName := range tableNames {

        fileName := baseFileName + ".csv"
        filePath := filepath.Join(folder, fileName)
        n := 0
        switch baseFileName {
        case "projects":
            n, err = api.projects.ProjectGenerate(filePath)
        case "rates":
            n, err = api.rates.RateGenerate(filePath)
        case "consultants":
            n, err = api.consultants.ConsultantGenerate(filePath)
        case "holidays":
            n, err = api.holidays.HolidayGenerate(filePath)
        case "reported_records":
            n, err = api.reportedRecords.ReportedRecordGenerate(filePath)
        }
        if err != nil {
            logger.Log.Error(fmt.Sprintf("generated tables: error during %s generate: %s", baseFileName, err))
        } else {
            logger.Log.Info(fmt.Sprintf("- %s.csv, %d records", baseFileName, n))
        }
    }
}

// SeedAPI - loads initial data into DB
func SeedAPI(db *models.DB, table string, inputFiles map[string]string) error {
    api := NewAPI(db)
    logger.Log.Info("Loaded table, # of records, filename:")
    switch table {
    case "rates", "consultants", "projects", "reported_records", "holidays":
        SeedTable(api, table, inputFiles[table])
    case "all":
        // users
        SeedTable(api, "rates", inputFiles["rates"])
        SeedTable(api, "consultants", inputFiles["consultants"])
        SeedTable(api, "projects", inputFiles["projects"])
        SeedTable(api, "reported_records", inputFiles["reported_records"])
        SeedTable(api, "holidays", inputFiles["holidays"])
    default:
        logger.Log.Error("unable to seed non-existent table: ", table)
        return errors.New("unable to seed non-existent table: " + table)
    }
    return nil
}

// SeedTable -
func SeedTable(api *API, table, file string) (count int) {
    switch table {
    case "rates":
        if api.rates.RateCount() > 0 {
            logger.Log.Warn(fmt.Sprintf("- rates, file %s skipped, table contains %d records", file, api.rates.RateCount()))
            return 0
        }
        count = api.rates.RateSeed(file)
        logger.Log.Info(fmt.Sprintf("- rates, %d records, %s", count, file))
    case "consultants":
        if api.consultants.ConsultantCount() > 0 {
            logger.Log.Warn(fmt.Sprintf("- consultants, file %s skipped, table contains %d records", file, api.consultants.ConsultantCount()))
            return 0
        }
        count = api.consultants.ConsultantSeed(file)
        logger.Log.Info(fmt.Sprintf("- consultants, %d records, %s", count, file))
    case "projects":
        if api.projects.ProjectCount() > 0 {
            logger.Log.Warn(fmt.Sprintf("- projects, file %s skipped, table contains %d records", file, api.projects.ProjectCount()))
            return 0
        }
        count = api.projects.ProjectSeed(file)
        logger.Log.Info(fmt.Sprintf("- projects, %d records, %s", count, file))
    case "reported_records":
        if api.reportedRecords.ReportedRecordCount() > 0 {
            logger.Log.Warn(fmt.Sprintf("- reported_records, file %s skipped, table contains %d records", file, api.reportedRecords.ReportedRecordCount()))
            return 0
        }
        count = api.reportedRecords.ReportedRecordSeed(file)
        logger.Log.Info(fmt.Sprintf("- reported_records, %d records, %s", count, file))
    case "holidays":
        if api.holidays.HolidayCount() > 0 {
            logger.Log.Warn(fmt.Sprintf("- holidays, file %s skipped, table contains %d records", file, api.holidays.HolidayCount()))
            return 0
        }
        count = api.holidays.HolidaySeed(file)
        logger.Log.Info(fmt.Sprintf("- holidays, %d records, %s", count, file))
    default:
        logger.Log.Warn("unknown table to seed: ", table)
    }
    return count
}

// CheckAndInitAPI - loads initial data into DB
func CheckAndInitAPI(db *models.DB) (api *API) {
    logger.Log.Info("checking DB ...")
    // files := FileList()
    // emptyTable := false
    api = NewAPI(db)
    // skip loading data into empty tables
    // if api.rates.RateCount() == 0 {
    //     SeedTable(api, "rates", files["rates"])
    //     emptyTable = true
    // }
    // if api.consultants.ConsultantCount() == 0 {
    //     SeedTable(api, "consultants", files["consultants"])
    //     emptyTable = true
    // }
    // if api.projects.ProjectCount() == 0 {
    //     SeedTable(api, "projects", files["projects"])
    //     emptyTable = true
    // }
    // if api.reportedRecords.ReportedRecordCount() == 0 {
    //     SeedTable(api, "reported_records", files["reported_records"])
    //     emptyTable = true
    // }
    // if api.holidays.HolidayCount() == 0 {
    //     SeedTable(api, "holidays", files["holidays"])
    //     emptyTable = true
    // }
    // if emptyTable {
    //     logger.Log.Info("loaded missing required data (see tables above)")
    // }
    return api
}

// BackupAPI - drops and creates all empty tables
func BackupAPI(rotation int, folder string, db *models.DB) {

    api := NewAPI(db)
    id := time.Now().Format("2006-01-02_150405")

    logger.Log.Info("backuped tables:")
    tableNames := []string{"rates", "projects", "reported_records", "consultants", "holidays"}
    for _, baseFileName := range tableNames {
        err := rotateBackupFile(rotation, folder, baseFileName)
        if err != nil {
            logger.Log.Error(fmt.Sprintf("not able to rotate %s backup files, backups stopped, handle the error: %s", baseFileName, err))
        }

        fileName := baseFileName + "_" + id + ".csv"
        filePath := filepath.Join(folder, fileName)
        n := 0
        switch baseFileName {
        case "projects":
            n, err = api.projects.ProjectBackup(filePath)
        case "rates":
            n, err = api.rates.RateBackup(filePath)
        case "consultants":
            n, err = api.consultants.ConsultantBackup(filePath)
        case "holidays":
            n, err = api.holidays.HolidayBackup(filePath)
        case "reported_records":
            n, err = api.reportedRecords.ReportedRecordBackup(filePath)
        }
        if err != nil {
            logger.Log.Error(fmt.Sprintf("backuped tables: error during %s backup: %s", baseFileName, err))
        } else {
            logger.Log.Info(fmt.Sprintf("- %s, %d records", baseFileName, n))
        }
    }
}