Убраны файлы и добавлен лог файл
This commit is contained in:
parent
2592598ce3
commit
20d0a2bddd
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,9 +8,7 @@
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
convert/*
|
||||
upload/*
|
||||
|
||||
log/*
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
@ -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())
|
||||
|
@ -1 +0,0 @@
|
||||
port: 8188
|
17
configs/config.yml
Normal file
17
configs/config.yml
Normal file
@ -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]
|
3
go.mod
3
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
|
||||
|
6
go.sum
6
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=
|
||||
|
103
internal/controller/handler/convert.go
Normal file
103
internal/controller/handler/convert.go
Normal file
@ -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()
|
||||
}
|
@ -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}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Конвертор ППКУП Сириус</title>
|
||||
<title>Конвертер ППКУП Сириус</title>
|
||||
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="description" content="Онлайн конвертер конфураций ППКУП Сириус v1.03.022 (Болид)">
|
||||
|
Loading…
Reference in New Issue
Block a user