Compare commits

..

15 Commits
v0.0.1 ... main

Author SHA1 Message Date
677f5047a9 Fix dummy trasformer 2024-03-10 12:45:02 +05:00
ffd232ad61 Update screenshot 2024-03-09 22:16:42 +05:00
05ec7868a9 Make UTF-8 default encoding 2024-03-09 21:59:01 +05:00
f9c1b89608 Add CSV+Zstandart export format 2024-03-09 21:54:26 +05:00
1be8583ae6 Remove unused vars 2024-03-05 21:46:13 +05:00
fdb66f92f5 Add export path picker 2024-03-05 21:41:51 +05:00
7c6ca4c81b Add buffered disk writes 2024-03-05 21:41:27 +05:00
9b9eadfe92 Миграция UI на библиотеку LCL 2023-12-30 21:14:42 +05:00
e08ba58bb0 Update deps 2023-11-21 21:15:34 +05:00
4c3004e113 Display progress in UI 2023-11-20 20:34:09 +05:00
00a57489b5 Fix word definitions 2023-11-18 21:32:01 +05:00
e27672796d Change module name 2023-11-18 12:17:39 +05:00
935c4164fc Add main window screenshot 2023-11-17 21:53:26 +05:00
efa1eaa47d Add main window screenshot 2023-11-17 21:52:32 +05:00
4f39415439 Fix module path 2023-11-17 21:42:51 +05:00
23 changed files with 942 additions and 346 deletions

2
.gitignore vendored
View File

@ -25,8 +25,10 @@ _cgo_export.*
_testmain.go
*.exe
*.dll
*.test
*.prof
*.zip
*coverage.out
coverage.all

View File

@ -1,35 +1,50 @@
# omc
# omq
Oracle Multi Querier (omc) - программа для выгрузки результатов SQL-запроса с нескольких серверов Oracle.
Oracle Multi Querier (omq) - программа для выгрузки результатов SQL-запроса с нескольких серверов Oracle.
![Скриншот главного окна](doc/main-window.png)
## SQL-скрипты
1. Скрипты должны лежать в папке `sql` с расширением `.sql`.
2. Скрипт может начинаться со строки с комментарием `-- 1`, где `1` - номер колонки, значение которой будет заменяться наименованием филиала (нумерация с единицы). Если комментарий не обнаружен, будет использовано значение `1`.
2. Скрипт может начинаться со строки с комментарием `-- 1`, где `1` - номер колонки, значение которой будет заменяться наименованием сервера (нумерация с единицы). Если комментарий не обнаружен, будет использовано значение `1`.
## Параметры подключения к серверам
Список серверов должен быть указан в файле с расширением `.ini` со следующей структурой:
Список серверов должен быть указан в файле с расширением `.toml`, который лежит в папке `db`, со следующей структурой:
```ini
[<HOST>/<SERVICE>]
Login = <LOGIN>
Password = <PASSWORD>
Name = <NAME>
[Servers]
Name = "<NAME>"
Login = "<LOGIN>"
Password = "<PASSWORD>"
Hosts = ["<HOST1>", "<HOST2>"]
Service = "<SERVICE>"
```
где:
* `<NAME>` - наименование филиала
* `<HOST>` - адрес сервера
* `<SERVICE>` - наименование сервиса
* `<LOGIN>` - логин
* `<PASSWORD>` - пароль
* `<NAME>` - наименование филиала
* `<HOST1>`, `<HOST2>` - список хостов БД, которые будут перебираться в порядке указания
* `<SERVICE>` - наименование сервиса
например:
```ini
[start-kr.mrk.vt.ru/STARTW]
Login = MRF_SOTNIKOV_AV
Password = p@$$w0rd
Name = Киров
[Servers]
Name = "Основной сервер"
Login = "User1"
Password = "p@$$w0rd1"
Hosts = ["db.server1.com", "db.server2.com"]
Service = "mydb"
[Servers]
Name = "Второй сервер сервер"
Login = "User2"
Password = "p@$$w0rd2"
Hosts = ["db.server3.com", "db.server4.com"]
Service = "mydb"
```

113
config.go Normal file
View File

@ -0,0 +1,113 @@
package main
import (
"bytes"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/BurntSushi/toml"
go_ora "github.com/sijms/go-ora/v2"
)
type Config struct {
// Список серверов
Servers []*Server
// Кол-во prefetch строк
PrefetchRows int
// Таймаут ожидания ответа на запрос
Timeout int
}
type Server struct {
// Имя сервера
Name string
// Логин БД Oracle
Login string
// Пароль БД Oracle
Password string
// Список хостов БД Oracle
Hosts []string
// Наименование сервиса БД Oracle
Service string
// Статус работы с сервером
status string
// Ошибка работы с сервером
err error
config *Config
}
func loadConfig(filePath string) (*Config, error) {
b, err := readFileIgnoreBOM(filePath)
if err != nil {
return nil, err
}
config := new(Config)
_, err = toml.NewDecoder(bytes.NewReader(b)).Decode(&config)
if err != nil {
return nil, err
}
if config.PrefetchRows <= 0 {
config.PrefetchRows = 1000
}
if config.Timeout < 0 {
config.Timeout = 0
}
for i := range config.Servers {
config.Servers[i].config = config
}
return config, nil
}
func (s *Server) Url() (string, error) {
urlOptions := make(map[string]string)
if len(s.Hosts) == 0 {
return "", errors.New("hostname is not specified")
}
host, portStr, err := net.SplitHostPort(s.Hosts[0])
if err != nil {
host = s.Hosts[0]
portStr = "1521"
}
port, err := strconv.Atoi(portStr)
if err != nil {
return "", err
}
additionalHosts := make([]string, 0)
if len(s.Hosts) > 1 {
for _, v := range s.Hosts[1:] {
h, p, err := net.SplitHostPort(v)
if err != nil {
h = v
p = "1521"
}
additionalHosts = append(additionalHosts, fmt.Sprintf("%s:%s", h, p))
}
urlOptions["server"] = strings.Join(additionalHosts, ",") // TODO: должен добавляться порт
}
urlOptions["TIMEOUT"] = strconv.Itoa(s.config.Timeout)
urlOptions["PREFETCH_ROWS"] = strconv.Itoa(s.config.PrefetchRows)
return go_ora.BuildUrl(host, port, s.Service, s.Login, s.Password, urlOptions), nil
}

View File

@ -1,5 +1,20 @@
package main
import "time"
const (
// Путь к папке с SQL-скриптами
SQL_FILES_DIR = "sql"
// Расширение файлов SQL-скриптов
SQL_FILE_EXT = "sql"
// Путь к папке с настройками БД
CONFIG_FILES_DIR = "db"
// Расширение файлов с настройками БД
CONFIG_FILE_EXT = "toml"
// Период обновления графического интерфейса
UI_UPDATE_PERIOD = time.Second / 2
)

BIN
doc/main-window.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -30,6 +30,7 @@ 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
return len(src), len(src), nil
}
func (e *DummyTransformer) Reset() {}

View File

@ -7,8 +7,10 @@ import (
type ExportFormat string
const (
ExportFormatExcel ExportFormat = "XLSX"
ExportFormatCsv ExportFormat = "CSV"
ExportFormatExcel ExportFormat = "XLSX"
ExportFormatCsv ExportFormat = "CSV"
ExportFormatCsvZip ExportFormat = "CSV+ZIP"
ExportFormatCsvZst ExportFormat = "CSV+ZSTD"
)
// Exporter - интерфейс экспорта
@ -22,6 +24,10 @@ func (e ExportFormat) GetExporter(encoding Encoding) (Exporter, error) {
return new(XlsxExporter), nil
case ExportFormatCsv:
return &CsvExporter{Encoding: encoding}, nil
case ExportFormatCsvZip:
return &CsvZipExporter{Encoding: encoding}, nil
case ExportFormatCsvZst:
return &CsvZstExporter{Encoding: encoding}, nil
}
return nil, fmt.Errorf("unknown format: %s", string(e))

View File

@ -1,21 +1,20 @@
package main
import (
"archive/zip"
"bufio"
"encoding/csv"
"fmt"
"os"
"path/filepath"
"time"
)
// Экспорт в CSV, сжатый в ZIP-архив
// Экспорт в CSV
type CsvExporter struct {
Encoding Encoding
}
func (c *CsvExporter) FileExt() string {
return ".zip"
return ".csv"
}
func (c *CsvExporter) Convert(filePath string, rows chan []any) error {
@ -24,30 +23,19 @@ func (c *CsvExporter) Convert(filePath string, rows chan []any) error {
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
}*/
buf := bufio.NewWriterSize(f, 4*1024*1024)
enc, err := c.Encoding.Encoder()
if err != nil {
return err
}
w := csv.NewWriter(enc.Writer(zw))
w := csv.NewWriter(enc.Writer(buf))
w.Comma = ';'
rowNum := 0
for row := range rows {
rowNum++
rowsStr := make([]string, len(row))
for i := range row {
rowsStr[i] = toCsvField(row[i])
@ -61,19 +49,23 @@ func (c *CsvExporter) Convert(filePath string, rows chan []any) error {
}
w.Flush()
if w.Error() != nil {
if err = w.Error(); err != nil {
f.Close()
return w.Error()
return err
}
err = z.Close()
err = buf.Flush()
if err != nil {
f.Close()
return err
}
return f.Close()
err = f.Close()
if err != nil {
return err
}
return nil
}
func toCsvField(a any) string {

83
export_csvzip.go Normal file
View File

@ -0,0 +1,83 @@
package main
import (
"archive/zip"
"bufio"
"encoding/csv"
"os"
"path/filepath"
)
// Экспорт в CSV, сжатый в ZIP-архив
type CsvZipExporter struct {
Encoding Encoding
}
func (c *CsvZipExporter) FileExt() string {
return ".zip"
}
func (c *CsvZipExporter) Convert(filePath string, rows chan []any) error {
f, err := os.Create(filePath + c.FileExt())
if err != nil {
return err
}
buf := bufio.NewWriterSize(f, 4*1024*1024)
z := zip.NewWriter(buf)
zw, err := z.Create(filepath.Base(filePath) + ".csv")
if err != nil {
f.Close()
return err
}
enc, err := c.Encoding.Encoder()
if err != nil {
return err
}
w := csv.NewWriter(enc.Writer(zw))
w.Comma = ';'
rowNum := 0
for row := range rows {
rowNum++
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 err = w.Error(); err != nil {
f.Close()
return err
}
err = z.Close()
if err != nil {
f.Close()
return err
}
err = buf.Flush()
if err != nil {
f.Close()
return err
}
err = f.Close()
if err != nil {
return err
}
return nil
}

81
export_csvzst.go Normal file
View File

@ -0,0 +1,81 @@
package main
import (
"bufio"
"encoding/csv"
"os"
"github.com/klauspost/compress/zstd"
)
// Экспорт в CSV, сжатый в ZIP-архив
type CsvZstExporter struct {
Encoding Encoding
}
func (c *CsvZstExporter) FileExt() string {
return ".csv.zst"
}
func (c *CsvZstExporter) Convert(filePath string, rows chan []any) error {
f, err := os.Create(filePath + c.FileExt())
if err != nil {
return err
}
buf := bufio.NewWriterSize(f, 4*1024*1024)
z, err := zstd.NewWriter(buf)
if err != nil {
f.Close()
return err
}
enc, err := c.Encoding.Encoder()
if err != nil {
return err
}
w := csv.NewWriter(enc.Writer(z))
w.Comma = ';'
rowNum := 0
for row := range rows {
rowNum++
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 err = w.Error(); err != nil {
f.Close()
return err
}
err = z.Close()
if err != nil {
f.Close()
return err
}
err = buf.Flush()
if err != nil {
f.Close()
return err
}
err = f.Close()
if err != nil {
return err
}
return nil
}

20
go.mod
View File

@ -1,29 +1,23 @@
module git.nxshock.me/omc
module omq
go 1.21.3
go 1.21
require (
github.com/BurntSushi/toml v1.3.2
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/sijms/go-ora/v2 v2.8.4
github.com/xuri/excelize/v2 v2.8.0
golang.org/x/text v0.12.0
gopkg.in/ini.v1 v1.67.0
github.com/ying32/govcl v2.2.3+incompatible
golang.org/x/text v0.14.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/klauspost/compress v1.17.7 // 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
)

30
go.sum
View File

@ -1,16 +1,12 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
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/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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=
@ -20,13 +16,8 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
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/sijms/go-ora/v2 v2.8.4 h1:6VmkqKg/8bELn+k3mr2wI9+inJEiNhNx1p2IOrnx7T4=
github.com/sijms/go-ora/v2 v2.8.4/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=
@ -38,6 +29,8 @@ github.com/xuri/excelize/v2 v2.8.0 h1:Vd4Qy809fupgp1v7X+nCS/MioeQmYVVzi495UCTqB7
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/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw=
github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8=
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=
@ -64,29 +57,26 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=

22
icons.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"embed"
"github.com/ying32/govcl/vcl"
)
//go:embed img/*
var images embed.FS
func getImageBitmap(path string) *vcl.TBitmap {
b, err := images.ReadFile(path)
if err != nil {
panic(err)
}
pic := vcl.NewPicture()
pic.LoadFromBytes(b)
return pic.Bitmap()
}

BIN
img/bullet_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

BIN
img/change_password.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

BIN
img/information.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

157
kernel.go
View File

@ -5,7 +5,6 @@ import (
"database/sql"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strconv"
@ -14,7 +13,6 @@ import (
"github.com/dimchansky/utfbom"
go_ora "github.com/sijms/go-ora/v2"
"gopkg.in/ini.v1"
)
type Row struct {
@ -22,25 +20,45 @@ type Row struct {
data []any
}
func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding) error {
sqlBytes, err := readFileIgnoreBOM(scriptFilePath)
type Job struct {
configFilePath string
scriptFilePath string
exportFileFormat ExportFormat
encoding Encoding
exportPath string
config Config
status string
isFinished bool
exportedRows int
}
func (j *Job) init() error {
j.status = "Чтение списка серверов..."
config, err := loadConfig(j.configFilePath)
if err != nil {
return err
}
servers, err := loadConfig(configFilePath)
j.config = *config
return nil
}
func (j *Job) launch() error {
j.status = "Чтение файла SQL-скрипта"
sqlBytes, err := readFileIgnoreBOM(j.scriptFilePath)
if err != nil {
return err
}
branchFieldNum := getBranchFieldNumber(string(sqlBytes))
if branchFieldNum <= 0 {
return fmt.Errorf("Некорректное значение номера поля филиала: %v", branchFieldNum)
}
rowsChan := iterateServers(servers, string(sqlBytes), branchFieldNum)
rowsChan := j.iterateServers(string(sqlBytes), branchFieldNum)
err = export(scriptFilePath, exportFileFormat, encoding, rowsChan)
err = j.export(rowsChan)
if err != nil {
return err
}
@ -48,13 +66,13 @@ func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat
return nil
}
func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding, inputRows chan Row) error {
converter, err := exportFileFormat.GetExporter(encoding)
func (j *Job) export(inputRows chan Row) error {
converter, err := j.exportFileFormat.GetExporter(j.encoding)
if err != nil {
return err
}
fileName := filepath.Base(scriptFilePath)
fileName := filepath.Join(j.exportPath, filepath.Base(j.scriptFilePath))
fileName = strings.TrimSuffix(fileName, filepath.Ext(fileName))
outputRows := make(chan []any)
@ -64,10 +82,9 @@ func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encod
gotHeader := false
rowsCache := make([][]any, 0)
rowCount := -1
for row := range inputRows {
rowCount += 1
j.exportedRows++
if gotHeader {
outputRows <- row.data
@ -81,50 +98,66 @@ func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encod
outputRows <- cachedRow
}
} else {
rowsCache = append(rowsCache, row.data)
rowsCache = append(rowsCache, row.data) // store data row in cache until got a header row
}
}
}()
return converter.Convert(fileName, outputRows)
err = converter.Convert(fileName, outputRows)
j.status = "Выгрузка завершена."
j.isFinished = true
if err != nil {
return err
}
return nil
}
func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Row {
func (j *Job) iterateServers(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))
wg.Add(len(j.config.Servers))
for i, server := range servers {
for i, server := range j.config.Servers {
i := i
server := server
go func() {
defer wg.Done()
db, err := sql.Open("oracle", server.Url)
serverUrl, err := server.Url()
if err != nil {
slog.Error("Ошибка подключения к серверу", slog.String("server", server.Url), slog.Any("err", err))
j.config.Servers[i].err = err
return
}
server.status = "Подключение к серверу..."
db := sql.OpenDB(go_ora.NewConnector(serverUrl))
defer db.Close()
err = db.Ping()
if err != nil {
server.err = err
return
}
server.status = "Выполнение SQL-запроса..."
rows, err := db.Query(sqlStr)
if err != nil {
slog.Error("Ошибка выполнения запроса", slog.String("server", server.Url), slog.Any("err", err))
server.err = err
return
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
slog.Error("Ошибка получения списка колонок", slog.String("server", server.Url), slog.Any("err", err))
server.err = err
return
}
@ -133,7 +166,9 @@ func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Ro
}
rowNum := 0
server.status = fmt.Sprintf("Выгружено %d строк...", rowNum)
for rows.Next() {
pointers := make([]any, len(cols))
container := make([]any, len(cols))
for i := range pointers {
@ -142,66 +177,31 @@ func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Ro
err = rows.Scan(pointers...)
if err != nil {
slog.Error("Ошибка получения строки", slog.String("server", server.Url), slog.Any("err", err))
server.err = err
break
}
rowsChan <- Row{isHeader: false, data: append(append(container[:branchFieldNum-1], server.Name), container[branchFieldNum:]...)} // Добавление имени сервера
var resultRow []any
if branchFieldNum != 0 {
// Подстановка имени сервера
resultRow = append(append(container[:branchFieldNum-1], server.Name), container[branchFieldNum:]...)
} else {
resultRow = container
}
rowsChan <- Row{isHeader: false, data: resultRow}
rowNum += 1
server.status = fmt.Sprintf("Выгружено %d строк...", rowNum)
}
slog.Info("Получение строк завершено", slog.String("server", server.Name), slog.Int("rowCount", rowNum))
server.status = fmt.Sprintf("Выгружено %d строк.", rowNum)
}()
}
j.status = "Ожидание завершения работы с серверами"
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 {
@ -211,6 +211,7 @@ func sliceToAnySlice[T string | any](slice []T) []any {
return result
}
// readFileIgnoreBOM возвращает содержимое файла без BOM
func readFileIgnoreBOM(filePath string) ([]byte, error) {
f, err := os.Open(filePath)
if err != nil {
@ -232,21 +233,19 @@ func readFileIgnoreBOM(filePath string) ([]byte, error) {
func getBranchFieldNumber(sqlStr string) int {
lines := strings.Split(sqlStr, "\n")
if len(lines) == 0 {
return 1
return 0
}
line := lines[0]
if !strings.HasPrefix(line, "-- ") {
slog.Warn("Не указан номер колонки для вывода филиала, будет перезаписан первый столбец!")
return 1
return 0
}
line = strings.TrimPrefix(line, "-- ")
fieldNum, err := strconv.Atoi(line)
if err != nil {
slog.Warn("Неверно указан номер колонки для вывода филиала, будет перезаписан первый столбец!")
return 1
return 0
}
return fieldNum

90
main.go
View File

@ -1,94 +1,10 @@
package main
import (
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"github.com/rivo/tview"
_ "github.com/ying32/govcl/pkgs/winappres"
"github.com/ying32/govcl/vcl"
)
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
vcl.RunApp(&mainForm)
}

View File

@ -1 +1 @@
go build -trimpath -buildmode=pie -ldflags "-s -w"
go build -trimpath -buildmode=pie -ldflags "-s -w -H=windowsgui"

View File

@ -1,9 +0,0 @@
package main
// Server - экземпляр сервера
type Server struct {
// Полная ссылка на БД, вкючая логин/пароль
Url string
// Наименование филиала
Name string
}

97
slog.go
View File

@ -1,97 +0,0 @@
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
}

80
ui_config_editior.go Normal file
View File

@ -0,0 +1,80 @@
package main
import (
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type TConfigEditorForm struct {
*vcl.TForm
loginLabel *vcl.TLabel
passwordLabel *vcl.TLabel
loginEdit *vcl.TEdit
passwordEdit *vcl.TEdit
buttonPanel *vcl.TPanel
confirmButton *vcl.TButton
}
func (f *TConfigEditorForm) OnFormCreate(sender vcl.IObject) {
f.SetWidth(640)
f.SetHeight(160)
f.Constraints().SetMinHeight(160) // to prevent wrong ordering of widgets for small windows sizes
f.SetPosition(types.PoOwnerFormCenter)
f.SetCaption("Настройки серверов")
f.SetDoubleBuffered(true)
f.SetBorderWidth(8)
f.loginLabel = vcl.NewLabel(f)
f.loginLabel.SetParent(f)
f.loginLabel.SetCaption("Логин:")
f.loginLabel.SetAlign(types.AlTop)
f.loginLabel.SetTop(0)
f.loginEdit = vcl.NewEdit(f)
f.loginEdit.SetParent(f)
f.loginEdit.SetAlign(types.AlTop)
f.loginEdit.SetTop(100)
f.passwordLabel = vcl.NewLabel(f)
f.passwordLabel.SetParent(f)
f.passwordLabel.SetCaption("Пароль:")
f.passwordLabel.SetAlign(types.AlTop)
f.passwordLabel.SetTop(200)
f.passwordLabel.BorderSpacing().SetTop(8)
f.passwordEdit = vcl.NewEdit(f)
f.passwordEdit.SetParent(f)
f.passwordEdit.SetAlign(types.AlTop)
f.passwordEdit.SetPasswordChar(uint16('*'))
f.passwordEdit.SetTop(300)
f.buttonPanel = vcl.NewPanel(f)
f.buttonPanel.SetParent(f)
f.buttonPanel.SetAlign(types.AlBottom)
f.buttonPanel.SetHeight(32)
f.buttonPanel.SetBevelOuter(types.BvNone)
f.confirmButton = vcl.NewButton(f)
f.confirmButton.SetParent(f.buttonPanel)
f.confirmButton.SetAlign(types.AlRight)
f.confirmButton.SetWidth(96)
f.confirmButton.SetCaption("Сохранить")
f.confirmButton.SetOnClick(f.OnConfirmButtonClick)
}
func (f *TConfigEditorForm) OnConfirmButtonClick(sender vcl.IObject) {
if f.loginEdit.Text() == "" {
vcl.MessageDlg("Логин не может быть пустым", types.MtError)
return
}
if f.passwordEdit.Text() == "" {
vcl.MessageDlg("Пароль не может быть пустым", types.MtError)
return
}
f.SetModalResult(types.IdOK)
}

393
ui_main_form.go Normal file
View File

@ -0,0 +1,393 @@
package main
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type TMainForm struct {
*vcl.TForm
MainMenu *vcl.TMainMenu
ChangePasswordMenuItem *vcl.TMenuItem
AboutMenuItem *vcl.TMenuItem
PageControl *vcl.TPageControl
TabSheet1 *vcl.TTabSheet
GroupBox *vcl.TGroupBox
ConfigLabel *vcl.TLabel
ConfigComboBox *vcl.TComboBox
SqlFileLabel *vcl.TLabel
SqlFileComboBox *vcl.TComboBox
ExportFormatLabel *vcl.TLabel
ExportFormatComboBox *vcl.TComboBox
CharsetLabel *vcl.TLabel
CharsetComboBox *vcl.TComboBox
ExportPathLabel *vcl.TLabel
ExportPathPicker *vcl.TDirectoryEdit
BottomPanel *vcl.TPanel
LaunchButton *vcl.TBitBtn
TabSheet2 *vcl.TTabSheet
ServerListView *vcl.TListView
c1 *vcl.TListColumn
c2 *vcl.TListColumn
StatusBar *vcl.TStatusBar
p1 *vcl.TStatusPanel
p2 *vcl.TStatusPanel
}
var mainForm *TMainForm
func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
f.SetWidth(800)
f.SetHeight(400)
f.Constraints().SetMinHeight(400) // to prevent wrong ordering of widgets for small windows sizes
f.SetPosition(types.PoDesktopCenter)
f.SetCaption("OMQ")
f.SetDoubleBuffered(true)
f.MainMenu = vcl.NewMainMenu(f)
f.ChangePasswordMenuItem = vcl.NewMenuItem(f)
f.MainMenu.Items().Add(f.ChangePasswordMenuItem)
f.ChangePasswordMenuItem.SetCaption("Изменить логин/пароль")
f.ChangePasswordMenuItem.SetBitmap(getImageBitmap("img/change_password.png"))
f.ChangePasswordMenuItem.SetOnClick(f.changePassword)
f.AboutMenuItem = vcl.NewMenuItem(f)
f.MainMenu.Items().Add(f.AboutMenuItem)
f.AboutMenuItem.SetCaption("О программе")
f.AboutMenuItem.SetBitmap(getImageBitmap("img/information.png"))
f.AboutMenuItem.SetOnClick(func(sender vcl.IObject) {
vcl.MessageDlg("Программа выгрузки результатов SQL-запросов с нескольких серверов баз данных Oracle.", types.MtInformation)
})
f.PageControl = vcl.NewPageControl(f)
f.PageControl.SetParent(f)
f.PageControl.SetAlign(types.AlClient)
f.TabSheet1 = vcl.NewTabSheet(f.PageControl)
f.TabSheet1.SetParent(f.PageControl)
f.TabSheet1.SetTabVisible(false)
f.GroupBox = vcl.NewGroupBox(f)
f.GroupBox.SetParent(f.TabSheet1)
f.GroupBox.SetAlign(types.AlClient)
f.GroupBox.SetCaption("Параметры запроса")
// TODO: добавить AutoSize
f.ConfigLabel = vcl.NewLabel(f)
f.ConfigLabel.SetParent(f.GroupBox)
f.ConfigLabel.SetCaption("Настройки серверов")
f.ConfigLabel.SetTop(0)
f.ConfigLabel.BorderSpacing().SetTop(8)
f.ConfigLabel.BorderSpacing().SetLeft(8)
f.ConfigLabel.BorderSpacing().SetRight(8)
f.ConfigLabel.SetAlign(types.AlTop)
f.ConfigComboBox = vcl.NewComboBox(f)
f.ConfigComboBox.SetParent(f.GroupBox)
f.ConfigComboBox.SetAlign(types.AlTop)
f.ConfigComboBox.BorderSpacing().SetTop(2)
f.ConfigComboBox.BorderSpacing().SetLeft(8)
f.ConfigComboBox.BorderSpacing().SetRight(8)
f.ConfigComboBox.SetTop(100)
f.ConfigComboBox.SetStyle(types.CsDropDownList)
f.SqlFileLabel = vcl.NewLabel(f)
f.SqlFileLabel.SetParent(f.GroupBox)
f.SqlFileLabel.SetCaption("SQL-скрипт")
f.SqlFileLabel.SetAlign(types.AlTop)
f.SqlFileLabel.BorderSpacing().SetTop(8)
f.SqlFileLabel.SetTop(200)
f.SqlFileLabel.BorderSpacing().SetLeft(8)
f.SqlFileLabel.BorderSpacing().SetRight(8)
f.SqlFileComboBox = vcl.NewComboBox(f)
f.SqlFileComboBox.SetParent(f.GroupBox)
f.SqlFileComboBox.SetAlign(types.AlTop)
f.SqlFileComboBox.BorderSpacing().SetTop(2)
f.SqlFileComboBox.BorderSpacing().SetLeft(8)
f.SqlFileComboBox.BorderSpacing().SetRight(8)
f.SqlFileComboBox.SetTop(300)
f.SqlFileComboBox.SetStyle(types.CsDropDownList)
f.ExportFormatLabel = vcl.NewLabel(f)
f.ExportFormatLabel.SetParent(f.GroupBox)
f.ExportFormatLabel.SetCaption("Формат выгрузки")
f.ExportFormatLabel.SetAlign(types.AlTop)
f.ExportFormatLabel.SetTop(400)
f.ExportFormatLabel.BorderSpacing().SetTop(8)
f.ExportFormatLabel.BorderSpacing().SetLeft(8)
f.ExportFormatLabel.BorderSpacing().SetRight(8)
f.ExportFormatComboBox = vcl.NewComboBox(f)
f.ExportFormatComboBox.SetParent(f.GroupBox)
f.ExportFormatComboBox.SetAlign(types.AlTop)
f.ExportFormatComboBox.BorderSpacing().SetTop(2)
f.ExportFormatComboBox.BorderSpacing().SetLeft(8)
f.ExportFormatComboBox.BorderSpacing().SetRight(8)
f.ExportFormatComboBox.SetTop(500)
f.ExportFormatComboBox.SetStyle(types.CsDropDownList)
f.ExportFormatComboBox.SetOnChange(f.OnExportFormatComboBoxChange)
f.CharsetLabel = vcl.NewLabel(f)
f.CharsetLabel.SetParent(f.GroupBox)
f.CharsetLabel.SetCaption("Кодировка CSV-файла")
f.CharsetLabel.SetAlign(types.AlTop)
f.CharsetLabel.SetTop(600)
f.CharsetLabel.BorderSpacing().SetTop(8)
f.CharsetLabel.BorderSpacing().SetLeft(8)
f.CharsetLabel.BorderSpacing().SetRight(8)
f.CharsetComboBox = vcl.NewComboBox(f)
f.CharsetComboBox.SetParent(f.GroupBox)
f.CharsetComboBox.SetAlign(types.AlTop)
f.CharsetComboBox.BorderSpacing().SetTop(2)
f.CharsetComboBox.BorderSpacing().SetLeft(8)
f.CharsetComboBox.BorderSpacing().SetRight(8)
f.CharsetComboBox.SetTop(700)
f.CharsetComboBox.SetStyle(types.CsDropDownList)
f.ExportPathLabel = vcl.NewLabel(f)
f.ExportPathLabel.SetParent(f.GroupBox)
f.ExportPathLabel.SetCaption("Путь сохранения результата")
f.ExportPathLabel.SetAlign(types.AlTop)
f.ExportPathLabel.SetTop(800)
f.ExportPathLabel.BorderSpacing().SetTop(8)
f.ExportPathLabel.BorderSpacing().SetLeft(8)
f.ExportPathLabel.BorderSpacing().SetRight(8)
f.ExportPathPicker = vcl.NewDirectoryEdit(f)
f.ExportPathPicker.SetParent(f.GroupBox)
f.ExportPathPicker.SetAlign(types.AlTop)
f.ExportPathPicker.BorderSpacing().SetTop(2)
f.ExportPathPicker.BorderSpacing().SetLeft(8)
f.ExportPathPicker.BorderSpacing().SetRight(8)
f.ExportPathPicker.SetTop(900)
f.ExportPathPicker.SetFlat(true)
f.BottomPanel = vcl.NewPanel(f)
f.BottomPanel.SetParent(f.TabSheet1)
f.BottomPanel.SetAlign(types.AlBottom)
f.BottomPanel.SetHeight(48)
f.BottomPanel.SetBevelOuter(types.BvNone)
f.LaunchButton = vcl.NewBitBtn(f)
f.LaunchButton.SetParent(f.BottomPanel)
f.LaunchButton.SetCaption("Запуск")
f.LaunchButton.SetAlign(types.AlRight)
f.LaunchButton.SetWidth(96)
f.LaunchButton.BorderSpacing().SetAround(8)
f.LaunchButton.SetOnClick(f.OnLaunchButtonClick)
f.LaunchButton.SetGlyph(getImageBitmap("img/bullet_go.png"))
f.TabSheet2 = vcl.NewTabSheet(f.PageControl)
f.TabSheet2.SetParent(f.PageControl)
f.TabSheet2.SetTabVisible(false)
f.ServerListView = vcl.NewListView(f)
f.ServerListView.SetParent(f.TabSheet2)
f.ServerListView.SetAlign(types.AlClient)
f.ServerListView.SetViewStyle(types.VsReport)
f.ServerListView.SetBorderStyle(types.BsNone)
f.ServerListView.SetReadOnly(true)
f.ServerListView.SetRowSelect(true)
f.c1 = f.ServerListView.Columns().Add()
f.c1.SetCaption("Сервер")
f.c1.SetAutoSize(true)
f.c2 = f.ServerListView.Columns().Add()
f.c2.SetCaption("Статус")
f.StatusBar = vcl.NewStatusBar(f)
f.StatusBar.SetParent(f)
f.StatusBar.SetSimplePanel(false)
f.p1 = f.StatusBar.Panels().Add()
f.p2 = f.StatusBar.Panels().Add()
f.SetOnResize(f.OnFormResize)
// Servers file list -------------------------------------------------------
configs, _ := filepath.Glob(filepath.Join(CONFIG_FILES_DIR, fmt.Sprintf("*.%s", CONFIG_FILE_EXT)))
for i := range configs {
configs[i] = strings.TrimSuffix(filepath.Base(configs[i]), "."+CONFIG_FILE_EXT)
}
for _, v := range configs {
f.ConfigComboBox.Items().Add(v)
}
if len(configs) > 0 {
f.ConfigComboBox.SetItemIndex(0)
}
// -------------------------------------------------------------------------
// SQL-files list ----------------------------------------------------------
files, _ := filepath.Glob(filepath.Join(SQL_FILES_DIR, fmt.Sprintf("*.%s", SQL_FILE_EXT)))
for i := range files {
files[i] = strings.TrimSuffix(filepath.Base(files[i]), "."+SQL_FILE_EXT)
}
for _, v := range files {
f.SqlFileComboBox.Items().Add(v)
}
if len(configs) > 0 {
f.SqlFileComboBox.SetItemIndex(0)
}
// -------------------------------------------------------------------------
// File formats ------------------------------------------------------------
for _, v := range []string{string(ExportFormatExcel), string(ExportFormatCsv), string(ExportFormatCsvZip), string(ExportFormatCsvZst)} {
f.ExportFormatComboBox.Items().Add(v)
}
f.ExportFormatComboBox.SetItemIndex(0)
// Charsets ----------------------------------------------------------------
for _, v := range []string{string(EncodingUtf8), string(EncodingWin1251)} {
f.CharsetComboBox.Items().Add(v)
}
f.CharsetComboBox.SetItemIndex(0)
f.OnExportFormatComboBoxChange(nil)
// -------------------------------------------------------------------------
}
func (f *TMainForm) OnFormResize(sender vcl.IObject) {
if f.Width() < 320 {
f.StatusBar.Panels().Items(0).SetWidth(f.Width() / 2)
} else {
f.StatusBar.Panels().Items(0).SetWidth(f.Width() - 160)
}
f.ServerListView.Columns().Items(1).SetWidth(
f.ServerListView.Width() - f.ServerListView.Columns().Items(0).Width())
}
func (f *TMainForm) OnLaunchButtonClick(sender vcl.IObject) {
f.MainMenu.Free()
job := &Job{
configFilePath: filepath.Join(CONFIG_FILES_DIR, f.ConfigComboBox.Text()) + "." + CONFIG_FILE_EXT,
scriptFilePath: filepath.Join(SQL_FILES_DIR, f.SqlFileComboBox.Text()) + "." + SQL_FILE_EXT,
exportFileFormat: ExportFormat(f.ExportFormatComboBox.Text()),
encoding: Encoding(f.CharsetComboBox.Text()),
exportPath: f.ExportPathPicker.Directory()}
err := job.init()
if err != nil {
vcl.MessageDlg(err.Error(), types.MtError)
return
}
for _, server := range job.config.Servers {
item := f.ServerListView.Items().Add()
item.SetCaption(server.Name)
item.SubItems().Add("")
}
f.PageControl.SetPageIndex(1)
go func() {
for !job.isFinished {
f.UpdateStatus(job)
time.Sleep(UI_UPDATE_PERIOD)
}
}()
go func() {
err = job.launch()
f.UpdateStatus(job)
if err != nil {
vcl.ThreadSync(func() {
vcl.MessageDlg(err.Error(), types.MtError)
f.Close()
})
} else {
vcl.ThreadSync(func() {
vcl.MessageDlg("Завершено.", types.MtInformation)
f.Close()
})
}
}()
}
func (f *TMainForm) OnExportFormatComboBoxChange(sender vcl.IObject) {
if slices.Contains([]string{string(ExportFormatCsv), string(ExportFormatCsvZip), string(ExportFormatCsvZst)}, f.ExportFormatComboBox.Text()) {
f.CharsetComboBox.SetEnabled(true)
} else {
f.CharsetComboBox.SetEnabled(false)
}
}
func (f *TMainForm) UpdateStatus(job *Job) {
vcl.ThreadSync(func() {
f.OnFormResize(nil)
f.StatusBar.Panels().Items(0).SetText(job.status)
f.StatusBar.Panels().Items(1).SetText(fmt.Sprintf("%d строк", job.exportedRows))
for i, v := range job.config.Servers {
s := []string{v.status}
if v.err != nil {
s = append(s, v.err.Error())
}
f.ServerListView.Items().Item(int32(i)).SubItems().SetText(strings.Join(s, ": "))
}
})
}
func (f *TMainForm) changePassword(sender vcl.IObject) {
changePasswordForm := new(TConfigEditorForm)
vcl.Application.CreateForm(&changePasswordForm)
if changePasswordForm.ShowModal() != types.IdOK {
return
}
newLogin := changePasswordForm.loginEdit.Text()
newPassword := changePasswordForm.passwordEdit.Text()
changePasswordForm.Free()
configFilePath := filepath.Join(CONFIG_FILES_DIR, f.ConfigComboBox.Text()) + "." + CONFIG_FILE_EXT
config, err := loadConfig(configFilePath)
if err != nil {
vcl.MessageDlg(err.Error(), types.MtError)
return
}
for _, server := range config.Servers {
server.Login = newLogin
server.Password = newPassword
}
file, err := os.Create(configFilePath)
if err != nil {
vcl.MessageDlg(err.Error(), types.MtError)
return
}
err = toml.NewEncoder(file).Encode(config)
if err != nil {
vcl.MessageDlg(err.Error(), types.MtError)
file.Close()
return
}
err = file.Close()
if err != nil {
vcl.MessageDlg(err.Error(), types.MtError)
return
}
vcl.MessageDlg("Логин/пароль успешно изменены.", types.MtInformation)
}