commit db23e5c41d8d280f5a1b57e240ceaf2aec420494 Author: nxshock Date: Fri Nov 17 20:34:20 2023 +0500 Upload code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53365ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# IntelliJ +.idea +# Goland's output filename can not be set manually +/go_build_* + +# MS VSCode +.vscode +__debug_bin + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +*coverage.out +coverage.all +cpu.out + +/modules/migration/bindata.go +/modules/migration/bindata.go.hash +/modules/options/bindata.go +/modules/options/bindata.go.hash +/modules/public/bindata.go +/modules/public/bindata.go.hash +/modules/templates/bindata.go +/modules/templates/bindata.go.hash + +*.db +*.log +*.log.*.gz + +/gitea +/gitea-vet +/debug +/integrations.test + +/bin +/dist +/custom/* +!/custom/conf/app.example.ini +/data +/indexers +/log +/public/img/avatar +/tests/integration/gitea-integration-* +/tests/integration/indexers-* +/tests/e2e/gitea-e2e-* +/tests/e2e/indexers-* +/tests/e2e/reports +/tests/e2e/test-artifacts +/tests/e2e/test-snapshots +/tests/*.ini +/tests/**/*.git/**/*.sample +/node_modules +/.venv +/yarn.lock +/yarn-error.log +/npm-debug.log* +/public/assets/js +/public/assets/css +/public/assets/fonts +/public/assets/licenses.txt +/public/assets/img/webpack +/vendor +/web_src/fomantic/node_modules +/web_src/fomantic/build/* +!/web_src/fomantic/build/semantic.js +!/web_src/fomantic/build/semantic.css +!/web_src/fomantic/build/themes +/web_src/fomantic/build/themes/* +!/web_src/fomantic/build/themes/default +/web_src/fomantic/build/themes/default/assets/* +!/web_src/fomantic/build/themes/default/assets/fonts +/web_src/fomantic/build/themes/default/assets/fonts/* +!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2 +!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2 +/VERSION +/.air +/.go-licenses + +# Snapcraft +/gitea_a*.txt +snap/.snapcraft/ +parts/ +stage/ +prime/ +*.snap +*.snap-build +*_source.tar.bz2 +.DS_Store + +# Make evidence files +/.make_evidence + +# Manpage +/man diff --git a/README.md b/README.md new file mode 100644 index 0000000..8098e64 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# omc + +Oracle Multi Querier (omc) - программа для выгрузки результатов SQL-запроса с нескольких серверов Oracle. + +## SQL-скрипты + +1. Скрипты должны лежать в папке `sql` с расширением `.sql`. +2. Скрипт может начинаться со строки с комментарием `-- 1`, где `1` - номер колонки, значение которой будет заменяться наименованием филиала (нумерация с единицы). Если комментарий не обнаружен, будет использовано значение `1`. + +## Параметры подключения к серверам + +Список серверов должен быть указан в файле с расширением `.ini` со следующей структурой: + +```ini +[/] +Login = +Password = +Name = +``` + +где: +* `` - адрес сервера +* `` - наименование сервиса +* `` - логин +* `` - пароль +* `` - наименование филиала + +например: + +```ini +[start-kr.mrk.vt.ru/STARTW] +Login = MRF_SOTNIKOV_AV +Password = p@$$w0rd +Name = Киров +``` diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..5f0be28 --- /dev/null +++ b/consts.go @@ -0,0 +1,5 @@ +package main + +const ( + SQL_FILES_DIR = "sql" +) diff --git a/encodings.go b/encodings.go new file mode 100644 index 0000000..fbaac61 --- /dev/null +++ b/encodings.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" +) + +type Encoding string + +const ( + EncodingUtf8 Encoding = "UTF-8" + EncodingWin1251 Encoding = "Win-1251" +) + +func (e Encoding) Encoder() (*encoding.Encoder, error) { + switch e { + case EncodingWin1251: + return charmap.Windows1251.NewEncoder(), nil + case EncodingUtf8: + return &encoding.Encoder{Transformer: new(DummyTransformer)}, nil + } + + return nil, fmt.Errorf("unknown encoding: %s", string(e)) +} + +type DummyTransformer struct{} + +func (e *DummyTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + copy(dst, src) + + return len(dst), len(src), nil +} +func (e *DummyTransformer) Reset() {} diff --git a/export.go b/export.go new file mode 100644 index 0000000..b17388b --- /dev/null +++ b/export.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" +) + +type ExportFormat string + +const ( + ExportFormatExcel ExportFormat = "XLSX" + ExportFormatCsv ExportFormat = "CSV" +) + +// Exporter - интерфейс экспорта +type Exporter interface { + Convert(filePath string, rows chan []any) error +} + +func (e ExportFormat) GetExporter(encoding Encoding) (Exporter, error) { + switch e { + case ExportFormatExcel: + return new(XlsxExporter), nil + case ExportFormatCsv: + return &CsvExporter{Encoding: encoding}, nil + } + + return nil, fmt.Errorf("unknown format: %s", string(e)) +} diff --git a/export_csv.go b/export_csv.go new file mode 100644 index 0000000..ea71b66 --- /dev/null +++ b/export_csv.go @@ -0,0 +1,90 @@ +package main + +import ( + "archive/zip" + "encoding/csv" + "fmt" + "os" + "path/filepath" + "time" +) + +// Экспорт в CSV, сжатый в ZIP-архив +type CsvExporter struct { + Encoding Encoding +} + +func (c *CsvExporter) FileExt() string { + return ".zip" +} + +func (c *CsvExporter) Convert(filePath string, rows chan []any) error { + f, err := os.Create(filePath + c.FileExt()) + if err != nil { + return err + } + + z := zip.NewWriter(f) + + zw, err := z.Create(filepath.Base(filePath) + ".csv") + if err != nil { + f.Close() + return err + } + + /*var enc io.Writer + if c.Encoding == EncodingWndows1251 { + enc = charmap.Windows1251.NewEncoder().Writer(zw) + } else { + enc = zw + }*/ + + enc, err := c.Encoding.Encoder() + if err != nil { + return err + } + + w := csv.NewWriter(enc.Writer(zw)) + w.Comma = ';' + + for row := range rows { + rowsStr := make([]string, len(row)) + for i := range row { + rowsStr[i] = toCsvField(row[i]) + } + + err = w.Write(rowsStr) + if err != nil { + f.Close() + return err + } + } + + w.Flush() + + if w.Error() != nil { + f.Close() + return w.Error() + } + + err = z.Close() + if err != nil { + f.Close() + return err + } + + return f.Close() +} + +func toCsvField(a any) string { + if a == nil { + return "" + } + + switch v := a.(type) { + case time.Time: + return v.Format("02.01.2006 15:04:05") + } + + return fmt.Sprint(a) +} diff --git a/export_excel.go b/export_excel.go new file mode 100644 index 0000000..261fffc --- /dev/null +++ b/export_excel.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/xuri/excelize/v2" +) + +// Экспорт в Excel +type XlsxExporter struct{} + +func (x *XlsxExporter) FileExt() string { + return ".xlsx" +} + +func (x *XlsxExporter) Convert(filePath string, rows chan []any) error { + excelFile := excelize.NewFile() + defer excelFile.Close() + + excelFile.Path = filePath + x.FileExt() + + stream, err := excelFile.NewStreamWriter("Sheet1") + if err != nil { + return err + } + + rowNum := 0 + for row := range rows { + rowNum += 1 + cell, err := excelize.CoordinatesToCellName(1, rowNum) + if err != nil { + return err + } + if err := stream.SetRow(cell, row); err != nil { + return err + } + } + + err = stream.Flush() + if err != nil { + return err + } + + return excelFile.Save() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2b4d553 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module git.nxshock.me/omc + +go 1.21.3 + +require ( + github.com/dimchansky/utfbom v1.1.1 + github.com/rivo/tview v0.0.0-20231115183240-7c9e464bac02 + github.com/sijms/go-ora/v2 v2.7.21 + github.com/xuri/excelize/v2 v2.8.0 + golang.org/x/text v0.12.0 + gopkg.in/ini.v1 v1.67.0 +) + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca // indirect + github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..98d9724 --- /dev/null +++ b/go.sum @@ -0,0 +1,92 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= +github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/tview v0.0.0-20231115183240-7c9e464bac02 h1:UkSrnoeeuKdeNFe4ghSjZmp7tA5B1CQKnvV1By9FSYw= +github.com/rivo/tview v0.0.0-20231115183240-7c9e464bac02/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sijms/go-ora/v2 v2.7.21 h1:BbfkcgoRYanmQkHklvRFJ7v/Cil8gPSxfG6ExZrHHlY= +github.com/sijms/go-ora/v2 v2.7.21/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.8.0 h1:Vd4Qy809fupgp1v7X+nCS/MioeQmYVVzi495UCTqB7U= +github.com/xuri/excelize/v2 v2.8.0/go.mod h1:6iA2edBTKxKbZAa7X5bDhcCg51xdOn1Ar5sfoXRGrQg= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kernel.go b/kernel.go new file mode 100644 index 0000000..93baa2f --- /dev/null +++ b/kernel.go @@ -0,0 +1,253 @@ +package main + +import ( + "bytes" + "database/sql" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/dimchansky/utfbom" + go_ora "github.com/sijms/go-ora/v2" + "gopkg.in/ini.v1" +) + +type Row struct { + isHeader bool + data []any +} + +func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding) error { + sqlBytes, err := readFileIgnoreBOM(scriptFilePath) + if err != nil { + return err + } + + servers, err := loadConfig(configFilePath) + if err != nil { + return err + } + + branchFieldNum := getBranchFieldNumber(string(sqlBytes)) + if branchFieldNum <= 0 { + return fmt.Errorf("Некорректное значение номера поля филиала: %v", branchFieldNum) + } + + rowsChan := iterateServers(servers, string(sqlBytes), branchFieldNum) + + err = export(scriptFilePath, exportFileFormat, encoding, rowsChan) + if err != nil { + return err + } + + return nil +} + +func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding, inputRows chan Row) error { + converter, err := exportFileFormat.GetExporter(encoding) + if err != nil { + return err + } + + fileName := filepath.Base(scriptFilePath) + fileName = strings.TrimSuffix(fileName, filepath.Ext(fileName)) + + outputRows := make(chan []any) + + go func() { + defer close(outputRows) + + gotHeader := false + rowsCache := make([][]any, 0) + rowCount := -1 + + for row := range inputRows { + rowCount += 1 + + if gotHeader { + outputRows <- row.data + continue + } + + if row.isHeader { + gotHeader = true + outputRows <- row.data + for _, cachedRow := range rowsCache { + outputRows <- cachedRow + } + } else { + rowsCache = append(rowsCache, row.data) + } + } + }() + + return converter.Convert(fileName, outputRows) +} + +func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Row { + rowsChan := make(chan Row) + + slog.Info("Выгрузка начата...") + + go func() { + defer func() { + close(rowsChan) + }() + + wg := new(sync.WaitGroup) + wg.Add(len(servers)) + + for i, server := range servers { + i := i + server := server + + go func() { + defer wg.Done() + + db, err := sql.Open("oracle", server.Url) + if err != nil { + slog.Error("Ошибка подключения к серверу", slog.String("server", server.Url), slog.Any("err", err)) + return + } + + rows, err := db.Query(sqlStr) + if err != nil { + slog.Error("Ошибка выполнения запроса", slog.String("server", server.Url), slog.Any("err", err)) + return + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + slog.Error("Ошибка получения списка колонок", slog.String("server", server.Url), slog.Any("err", err)) + return + } + + if i == 0 { + rowsChan <- Row{isHeader: true, data: sliceToAnySlice[string](cols)} // Добавление заголовков + } + + rowNum := 0 + for rows.Next() { + pointers := make([]any, len(cols)) + container := make([]any, len(cols)) + for i := range pointers { + pointers[i] = &container[i] + } + + err = rows.Scan(pointers...) + if err != nil { + slog.Error("Ошибка получения строки", slog.String("server", server.Url), slog.Any("err", err)) + break + } + rowsChan <- Row{isHeader: false, data: append(append(container[:branchFieldNum-1], server.Name), container[branchFieldNum:]...)} // Добавление имени сервера + rowNum += 1 + } + slog.Info("Получение строк завершено", slog.String("server", server.Name), slog.Int("rowCount", rowNum)) + }() + } + wg.Wait() + }() + + return rowsChan +} + +// loadConfig считывает конфиг и возвращает список параметров серверов +func loadConfig(filePath string) ([]Server, error) { + servers := make([]Server, 0) + + iniBytes, err := readFileIgnoreBOM(filePath) + if err != nil { + return nil, err + } + + cfg, err := ini.Load(iniBytes) + if err != nil { + return nil, err + } + + cfg.DeleteSection("DEFAULT") + + for _, server := range cfg.SectionStrings() { + loginKey, err := cfg.Section(server).GetKey("Login") + if err != nil { + return nil, err + } + passwordKey, err := cfg.Section(server).GetKey("Password") + if err != nil { + return nil, err + } + + nameKey, err := cfg.Section(server).GetKey("Name") + if err != nil { + return nil, err + } + + serv := strings.Split(server, "/")[0] + service := strings.Split(server, "/")[1] + dbUrl := go_ora.BuildUrl(serv, 1521, service, loginKey.String(), passwordKey.String(), nil) + + server := Server{ + Url: dbUrl, + Name: nameKey.String()} + + servers = append(servers, server) + } + + return servers, nil +} + +func sliceToAnySlice[T string | any](slice []T) []any { + result := make([]any, len(slice)) + for i := range result { + result[i] = slice[i] + } + + return result +} + +func readFileIgnoreBOM(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + b, err := io.ReadAll(utfbom.SkipOnly(f)) + if err != nil { + return nil, err + } + + return bytes.ReplaceAll(b, []byte("\r"), []byte{}), nil +} + +// getBranchFieldNumber осуществляет определение номера колонки для подстановки +// наименования филиала. В первой строке SQL-скрипта должен быть комментарий, +// начинающийся на `// ` и содержащий только номер колонки (нумерация с 1). +func getBranchFieldNumber(sqlStr string) int { + lines := strings.Split(sqlStr, "\n") + if len(lines) == 0 { + return 1 + } + + line := lines[0] + + if !strings.HasPrefix(line, "-- ") { + slog.Warn("Не указан номер колонки для вывода филиала, будет перезаписан первый столбец!") + return 1 + } + + line = strings.TrimPrefix(line, "-- ") + fieldNum, err := strconv.Atoi(line) + if err != nil { + slog.Warn("Неверно указан номер колонки для вывода филиала, будет перезаписан первый столбец!") + return 1 + } + + return fieldNum +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f6144dc --- /dev/null +++ b/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "log" + "log/slog" + "os" + "path/filepath" + + "github.com/rivo/tview" +) + +func init() { + logger := slog.New(&Handler{os.Stderr, slog.LevelInfo}) + slog.SetDefault(logger) +} + +func main() { + configFilePath, scriptFilePath, exportFileFormat, encoding, err := getReportParams() + if err != nil { + log.Fatalln(err) + } + + err = launch(configFilePath, scriptFilePath, exportFileFormat, encoding) + if err != nil { + slog.Error("Ошибка при выполнении", slog.Any("err", err)) + fmt.Scanln() + return + } +} + +func getReportParams() (configFilePath, scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding, err error) { + exportFileFormatStr := "" + + // Список файлов с SQL-скриптами + files, _ := filepath.Glob(filepath.Join(SQL_FILES_DIR, "*.sql")) + for i := range files { + files[i] = filepath.Base(files[i]) + } + // Список файлов с настройками подключения к БД + configs, _ := filepath.Glob("*.ini") + + configFileDropDown := tview.NewDropDown(). + SetOptions(configs, nil). + SetLabel("Файл настроек серверов"). + SetCurrentOption(0) + sqlFileDropDown := tview.NewDropDown(). + SetOptions(files, nil). + SetLabel("Файл SQL-скрипта"). + SetCurrentOption(0) + encodingDropDown := tview.NewDropDown(). + SetOptions([]string{string(EncodingWin1251), string(EncodingUtf8)}, nil). + SetLabel("Кодировка CSV-файла") + encodingDropDown.SetDisabled(true) + encodingDropDown.SetCurrentOption(0) + + formatDropDown := tview.NewDropDown(). + SetOptions([]string{string(ExportFormatExcel), string(ExportFormatCsv)}, func(text string, index int) { + if text == string(ExportFormatCsv) { + encodingDropDown.SetDisabled(false) + return + } + + encodingDropDown.SetDisabled(true) + }). + SetLabel("Формат выгрузки").SetCurrentOption(0) + + form := tview.NewForm(). + AddFormItem(configFileDropDown). + AddFormItem(sqlFileDropDown). + AddFormItem(formatDropDown). + AddFormItem(encodingDropDown) + form.SetTitle("Параметры").SetBorder(true) + + app := tview.NewApplication() + grid := tview.NewGrid().SetRows(-1, 1). + AddItem(form, 0, 0, 1, 1, 0, 0, true). + AddItem(tview.NewButton("Запуск").SetSelectedFunc(func() { + _, scriptFilePath = sqlFileDropDown.GetCurrentOption() + _, exportFileFormatStr = formatDropDown.GetCurrentOption() + _, configFilePath = configFileDropDown.GetCurrentOption() + _, encodingStr := encodingDropDown.GetCurrentOption() + encoding = Encoding(encodingStr) + app.Stop() + }), 1, 0, 1, 1, 0, 0, false) + grid.SetBorderPadding(0, 1, 1, 1) + + form.SetTitle("Параметры выгрузки").SetTitleAlign(tview.AlignLeft) + if err := app.SetRoot(grid, true).EnableMouse(true).Run(); err != nil { + return "", "", "", "", err + } + + return configFilePath, filepath.Join(SQL_FILES_DIR, scriptFilePath), ExportFormat(exportFileFormatStr), encoding, nil +} diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..ab71261 --- /dev/null +++ b/make.bat @@ -0,0 +1 @@ +go build -trimpath -buildmode=pie -ldflags "-s -w" diff --git a/servers.go b/servers.go new file mode 100644 index 0000000..38b9c42 --- /dev/null +++ b/servers.go @@ -0,0 +1,9 @@ +package main + +// Server - экземпляр сервера +type Server struct { + // Полная ссылка на БД, вкючая логин/пароль + Url string + // Наименование филиала + Name string +} diff --git a/slog.go b/slog.go new file mode 100644 index 0000000..490e9e9 --- /dev/null +++ b/slog.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "fmt" + "io" + "log/slog" + "sync" +) + +// Мьютекс для предотвращения наложения строк из горутин +var mu = new(sync.Mutex) + +type Handler struct { + w io.Writer + level slog.Level +} + +func (h *Handler) Enabled(c context.Context, l slog.Level) bool { + return l >= h.level +} + +func (h *Handler) Handle(c context.Context, r slog.Record) error { + mu.Lock() + defer mu.Unlock() + + switch r.Level { + case slog.LevelError: + fmt.Fprintf(h.w, "• ОШИБКА: %v\n", r.Message) + + r.Attrs(func(a slog.Attr) bool { + s := fmt.Sprintf(" %v=%v\n", a.Key, a.Value) + fmt.Fprint(h.w, s) + return true + }) + default: + fmt.Fprintf(h.w, "• %v ", r.Message) + + r.Attrs(func(a slog.Attr) bool { + s := fmt.Sprintf("%v=%v ", a.Key, a.Value) + fmt.Fprint(h.w, s) + return true + }) + fmt.Fprint(h.w, "\n") + } + + return nil +} + +/*func (h *Handler) HandleColor(c context.Context, r slog.Record) error { + var yellow = color.New(color.FgYellow).SprintFunc() + + switch r.Level { + case slog.LevelError: + redHi := color.New(color.FgHiRed).SprintFunc() + fmt.Fprintf(h.w, redHi("• %v\n"), r.Message) + + red := color.New(color.FgRed).SprintFunc() + r.Attrs(func(a slog.Attr) bool { + s := fmt.Sprintf(" %v=%v\n", red(a.Key), a.Value) + fmt.Fprint(h.w, s) + return true + }) + case slog.LevelWarn: + fmt.Fprintf(h.w, yellow("• %v\n"), r.Message) + + red := color.New(color.FgRed).SprintFunc() + r.Attrs(func(a slog.Attr) bool { + s := fmt.Sprintf(" %v=%v\n", red(a.Key), yellow(a.Value)) + fmt.Fprint(h.w, s) + return true + }) + default: + fmt.Fprintf(h.w, "• %v ", r.Message) + + r.Attrs(func(a slog.Attr) bool { + s := fmt.Sprintf("%v=%v ", yellow(a.Key), a.Value) + fmt.Fprint(h.w, s) + return true + }) + fmt.Fprint(h.w, "\n") + } + + return nil +}*/ + +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + return h +} + +func (h *Handler) WithGroup(name string) slog.Handler { + return h +} + +func (h *Handler) SetLevel(level slog.Level) { + h.level = level +}