diff --git a/.gitignore b/.gitignore index f1e636c..389e9de 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,7 @@ # Local History for Visual Studio Code .history/ -convert/* -upload/* - +log/* # Built Visual Studio Code Extensions *.vsix diff --git a/cmd/apisirius/main.go b/cmd/apisirius/main.go index 9f4cd8e..feacc79 100644 --- a/cmd/apisirius/main.go +++ b/cmd/apisirius/main.go @@ -1,24 +1,25 @@ package main import ( - "os" + "flag" + "fmt" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "gitstore.ru/tolikproh/sirius/internal/model" "gitstore.ru/tolikproh/sirius/internal/server" ) func main() { - log.SetFormatter(&log.JSONFormatter{}) - log.SetOutput(os.Stdout) - log.SetLevel(log.WarnLevel) + flag.Parse() - if err := model.Init(); err != nil { - log.Fatalf("%s", err.Error()) + cfg, err := initConfig() + if err != nil { + fmt.Printf("Error initializing configs file (configs set default): %s\n", err.Error()) } - app := server.NewApp(viper.GetString("port")) + log := NewLogger(cfg) + + app := server.New(cfg, log) + + fmt.Printf("Start server on lister port: %s\n", cfg.Srv.Port) if err := app.Run(); err != nil { log.Fatalf("%s", err.Error()) diff --git a/config/config.yml b/config/config.yml deleted file mode 100644 index f4d4aac..0000000 --- a/config/config.yml +++ /dev/null @@ -1 +0,0 @@ -port: 8188 \ No newline at end of file diff --git a/configs/config.yml b/configs/config.yml new file mode 100644 index 0000000..fb09d7b --- /dev/null +++ b/configs/config.yml @@ -0,0 +1,17 @@ +srv: + hostname: "gitstore.ru" + port: 8188 + mode: release #Running in "release, debug, test" mode Switch to "release" mode in production. + + # Logger log level: + # PanicLevel = 0 + # FatalLevel = 1 + # ErrorLevel = 2 + # WarnLevel = 3 + # InfoLevel = 4 + # DebugLevel = 5 + + # TraceLevel = 6 + loglevel: 6 + logpath: "./log" + maxsizefile: 1 # Max size file for upload [MegaByte] \ No newline at end of file diff --git a/go.mod b/go.mod index 345da84..5f17722 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( golang.org/x/sys v0.7.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -45,7 +46,9 @@ require ( github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect github.com/sirupsen/logrus v1.9.0 + github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d github.com/spf13/viper v1.15.0 + github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect golang.org/x/crypto v0.8.0 // indirect diff --git a/go.sum b/go.sum index 003f2cc..3e25708 100644 --- a/go.sum +++ b/go.sum @@ -201,6 +201,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d h1:4660u5vJtsyrn3QwJNfESwCws+TM1CMhRn123xjVyQ8= +github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d/go.mod h1:ZLVe3VfhAuMYLYWliGEydMBoRnfib8EFSqkBYu1ck9E= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -226,6 +228,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY= +github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= @@ -568,6 +572,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/controller/handler/convert.go b/internal/controller/handler/convert.go new file mode 100644 index 0000000..3a90695 --- /dev/null +++ b/internal/controller/handler/convert.go @@ -0,0 +1,103 @@ +package handler + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/xuri/excelize/v2" + "gitstore.ru/tolikproh/sirius/internal/model" +) + +var ( + TEXT_TYPES = map[string]interface{}{ + "text/plain; charset=utf-8": nil, + } +) + +func (s *Handler) GinConvert(c *gin.Context) { + s.log.Debug("start convert") + + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, s.cfg.Srv.MaxSizeFile<<20) + + file, fileHeader, err := c.Request.FormFile("file") + if err != nil { + s.log.Errorf("error code: 001. %s", err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + defer file.Close() + + buffer := make([]byte, fileHeader.Size) + file.Read(buffer) + + buffer = bytes.TrimPrefix(buffer, []byte("\xef\xbb\xbf")) + + fileType := http.DetectContentType(buffer) + + // Validate File Type + if _, ex := TEXT_TYPES[fileType]; !ex { + s.log.Errorf("error code: 002. formated data not text/plain") + c.JSON(http.StatusBadRequest, gin.H{ + "error": errors.New("formated data not text/plain").Error(), + }) + return + } + + if !json.Valid(buffer) { + s.log.Errorf("error code: 003. formated data not json") + c.JSON(http.StatusBadRequest, gin.H{ + "error": errors.New("formated data not json").Error(), + }) + return + } + + var sirius model.Sirius + + err = json.Unmarshal(buffer, &sirius) + if err != nil { + s.log.Errorf("error code: 004. %s", err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "error": errors.New("Error unmarshal file").Error(), + }) + return + } + + buff, err := SaveToExel(sirius.NewBolid().ZoneInfo()) + if err != nil { + s.log.Errorf("error code: 005. %s", err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "error": errors.New("Error create file").Error(), + }) + return + } + + c.Data(200, "application/vnd.ms-excel", buff.Bytes()) + + s.log.Printf("converted: ok. filename: %s; file size: %d bytes", fileHeader.Filename, fileHeader.Size) + +} + +func SaveToExel(data []model.ZoneInfo) (*bytes.Buffer, error) { + f := excelize.NewFile() + defer f.Close() + + // Create a new sheet. + f.SetCellValue("Sheet1", "A2", "№ зоны") + f.SetCellValue("Sheet1", "B2", "Описание зоны") + f.SetCellValue("Sheet1", "C2", "Адреса в зоне") + i := 3 + for _, d := range data { + f.SetCellValue("Sheet1", "A"+strconv.Itoa(i), d.ZoneNum) + f.SetCellValue("Sheet1", "B"+strconv.Itoa(i), d.ZoneName) + f.SetCellValue("Sheet1", "C"+strconv.Itoa(i), d.InputString()) + i++ + } + + return f.WriteToBuffer() +} diff --git a/internal/controller/handler/handler.go b/internal/controller/handler/handler.go index 70e622a..fd5e53c 100644 --- a/internal/controller/handler/handler.go +++ b/internal/controller/handler/handler.go @@ -1,104 +1,16 @@ package handler import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/xuri/excelize/v2" + "github.com/sirupsen/logrus" "gitstore.ru/tolikproh/sirius/internal/model" ) -func GinConvert(c *gin.Context) { - file, err := c.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) - } - - fileUuid := uuid.New().String() - fileUpload := "./upload/" + fileUuid + "_" + file.Filename - // сохраняем загруженный файл в /tmp - err = c.SaveUploadedFile(file, fileUpload) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) - return - } - - var sirius model.Sirius - siriusJson, err := ioutil.ReadFile(fileUpload) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": errors.New("No read file").Error(), - }) - return - } - - siriusJson = bytes.TrimPrefix(siriusJson, []byte("\xef\xbb\xbf")) - - if !json.Valid(siriusJson) { - c.JSON(http.StatusBadRequest, gin.H{ - "error": errors.New("Formated file not supported").Error(), - }) - return - } - - err = json.Unmarshal(siriusJson, &sirius) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": errors.New("Error unmarshal file").Error(), - }) - return - } - - fileConverted := "./convert/" + fileUuid + ".xlsx" - if err := SaveToExel(fileConverted, sirius.NewBolid().ZoneInfo()); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": errors.New("Error create file").Error(), - }) - return - } - - siriusExel, err := ioutil.ReadFile(fileConverted) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": errors.New("No read file exel").Error(), - }) - return - } - c.Data(200, "application/vnd.ms-excel", siriusExel) - +type Handler struct { + log *logrus.Logger + cfg *model.Config } -func SaveToExel(filename string, data []model.ZoneInfo) error { - f := excelize.NewFile() - defer func() { - if err := f.Close(); err != nil { - return - } - }() - // Create a new sheet. - f.SetCellValue("Sheet1", "A2", "№ зоны") - f.SetCellValue("Sheet1", "B2", "Описание зоны") - f.SetCellValue("Sheet1", "C2", "Адреса в зоне") - i := 3 - for _, d := range data { - f.SetCellValue("Sheet1", "A"+strconv.Itoa(i), d.ZoneNum) - f.SetCellValue("Sheet1", "B"+strconv.Itoa(i), d.ZoneName) - f.SetCellValue("Sheet1", "C"+strconv.Itoa(i), d.InputString()) - i++ - } - // Save spreadsheet by the given path. - if err := f.SaveAs(filename); err != nil { - return err - } - return nil +func New(cfg *model.Config, log *logrus.Logger) *Handler { + // HTTP Server + return &Handler{cfg: cfg, log: log} } diff --git a/internal/model/config.go b/internal/model/config.go index 2295caf..846a5dc 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -1,12 +1,57 @@ package model import ( - "github.com/spf13/viper" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) -func Init() error { - viper.AddConfigPath("./config") - viper.SetConfigName("config") - - return viper.ReadInConfig() +type Config struct { + Srv struct { + HostName string `yaml:"hostname"` + Port string `yaml:"port"` + Mode string `yaml:"mode"` + LogLevel int `yaml:"loglevel"` + LogPath string `yaml:"logpath"` + MaxSizeFile int64 `yaml:"maxsizefile"` + } `yaml:"srv"` +} + +// NewConfig Set Default +func NewConfig() *Config { + var cnf Config + + cnf.Srv.HostName = "example.com" + cnf.Srv.Port = "8080" + cnf.Srv.Mode = gin.DebugMode + cnf.Srv.LogLevel = int(logrus.DebugLevel) + cnf.Srv.LogPath = "./log" + cnf.Srv.MaxSizeFile = 2 //2 Mb + return &cnf +} + +func ValidLogLevel(cnf *Config) logrus.Level { + switch cnf.Srv.LogLevel { + case int(logrus.PanicLevel): + return logrus.PanicLevel //PanicLevel = 0 + + case int(logrus.FatalLevel): + return logrus.FatalLevel //FatalLevel = 1 + + case int(logrus.ErrorLevel): + return logrus.ErrorLevel //ErrorLevel = 2 + + case int(logrus.WarnLevel): + return logrus.WarnLevel //WarnLevel = 3 + + case int(logrus.InfoLevel): + return logrus.InfoLevel //InfoLevel = 4 + + case int(logrus.DebugLevel): + return logrus.DebugLevel //DebugLevel = 5 + + case int(logrus.TraceLevel): + return logrus.TraceLevel //TraceLevel = 6 + } + + return logrus.DebugLevel } diff --git a/internal/server/app.go b/internal/server/app.go index 5ae83f4..c975723 100644 --- a/internal/server/app.go +++ b/internal/server/app.go @@ -4,36 +4,44 @@ import ( "context" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + ginlogrus "github.com/toorop/gin-logrus" "github.com/gin-contrib/cors" "gitstore.ru/tolikproh/sirius/internal/controller/handler" + "gitstore.ru/tolikproh/sirius/internal/model" - "log" "net/http" "os" "os/signal" "time" ) -type App struct { +type Server struct { httpServer *http.Server + log *logrus.Logger + cfg *model.Config } -func NewApp(port string) *App { +func New(cfg *model.Config, log *logrus.Logger) *Server { // Initiate an S3 compatible client - return &App{ + return &Server{ httpServer: &http.Server{ - Addr: ":" + port, + Addr: ":" + cfg.Srv.Port, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, - }} + }, + cfg: cfg, log: log} } -func (a *App) Run() error { - // Init gin handler - router := gin.Default() +func (a *Server) Run() error { + a.log.Debug("start server") + + gin.SetMode(validMode(a.cfg.Srv.Mode)) + + router := gin.New() router.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, @@ -45,19 +53,22 @@ func (a *App) Run() error { router.Use( gin.Recovery(), - gin.Logger(), + ginlogrus.Logger(a.log), ) + h := handler.New(a.cfg, a.log) + // API endpoints api := router.Group("/api") - api.POST("/sirius", handler.GinConvert) + api.POST("/sirius", h.GinConvert) // HTTP Server a.httpServer.Handler = router go func() { if err := a.httpServer.ListenAndServe(); err != nil { - log.Fatalf("Failed to listen and serve: %+v", err) + a.log.Fatalf("Failed to listen and serve: %+v", err) + return } }() @@ -65,9 +76,31 @@ func (a *App) Run() error { signal.Notify(quit, os.Interrupt, os.Interrupt) <-quit + a.log.Println("Shutdown Server ...") - ctx, shutdown := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdown() + ctx, cansel := context.WithTimeout(context.Background(), 5*time.Second) + defer cansel() + + select { + case <-ctx.Done(): + a.log.Println("timeout of 5 seconds.") + } return a.httpServer.Shutdown(ctx) } + +func validMode(mode string) string { + switch mode { + case gin.ReleaseMode: + return gin.ReleaseMode + + case gin.DebugMode: + return gin.DebugMode + + case gin.TestMode: + return gin.TestMode + } + + return gin.TestMode + +} diff --git a/pub/index.html b/pub/index.html index e7e47fd..d238fd1 100644 --- a/pub/index.html +++ b/pub/index.html @@ -1,7 +1,7 @@
-