diff --git a/.gitignore b/.gitignore index 53365ed..a8da4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,10 @@ _cgo_export.* _testmain.go *.exe +*.dll *.test *.prof +*.zip *coverage.out coverage.all diff --git a/README.md b/README.md index f951350..750af0e 100644 --- a/README.md +++ b/README.md @@ -11,27 +11,40 @@ Oracle Multi Querier (omq) - программа для выгрузки резу ## Параметры подключения к серверам -Список серверов должен быть указан в файле с расширением `.ini` со следующей структурой: +Список серверов должен быть указан в файле с расширением `.toml`, который лежит в папке `db`, со следующей структурой: ```ini -[/] -Login = -Password = -Name = +[Servers] +Name = "" +Login = "" +Password = "" +Hosts = ["", ""] +Service = "" ``` где: +* `` - наименование филиала * `` - адрес сервера * `` - наименование сервиса * `` - логин * `` - пароль -* `` - наименование филиала +* ``, `` - список хостов БД, которые будут перебираться в порядке указания +* `` - наименование сервиса например: ```ini -[myserver.example.com/MYSERVICE] -Login = User1 -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" ``` diff --git a/config.go b/config.go new file mode 100644 index 0000000..4ae3b67 --- /dev/null +++ b/config.go @@ -0,0 +1,116 @@ +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 + + // Флаг завершения работы + finished bool + + 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 +} diff --git a/consts.go b/consts.go index 5f0be28..1bce63a 100644 --- a/consts.go +++ b/consts.go @@ -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 ) diff --git a/doc/main-window.png b/doc/main-window.png index 928c3f2..b315dfe 100644 Binary files a/doc/main-window.png and b/doc/main-window.png differ diff --git a/export.go b/export.go index b17388b..a904827 100644 --- a/export.go +++ b/export.go @@ -7,8 +7,9 @@ import ( type ExportFormat string const ( - ExportFormatExcel ExportFormat = "XLSX" - ExportFormatCsv ExportFormat = "CSV" + ExportFormatExcel ExportFormat = "XLSX" + ExportFormatCsv ExportFormat = "CSV" + ExportFormatCsvZip ExportFormat = "CSV+ZIP" ) // Exporter - интерфейс экспорта @@ -22,6 +23,8 @@ 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 } return nil, fmt.Errorf("unknown format: %s", string(e)) diff --git a/export_csv.go b/export_csv.go index ea71b66..6a097d6 100644 --- a/export_csv.go +++ b/export_csv.go @@ -1,21 +1,19 @@ package main import ( - "archive/zip" "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 +22,17 @@ 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 - }*/ - enc, err := c.Encoding.Encoder() if err != nil { return err } - w := csv.NewWriter(enc.Writer(zw)) + w := csv.NewWriter(enc.Writer(f)) 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 +46,17 @@ func (c *CsvExporter) Convert(filePath string, rows chan []any) error { } w.Flush() - - if w.Error() != nil { - f.Close() - return w.Error() - } - - err = z.Close() - if err != nil { + if err = w.Error(); err != nil { f.Close() return err } - return f.Close() + err = f.Close() + if err != nil { + return err + } + + return nil } func toCsvField(a any) string { diff --git a/export_csvzip.go b/export_csvzip.go new file mode 100644 index 0000000..1acbad8 --- /dev/null +++ b/export_csvzip.go @@ -0,0 +1,74 @@ +package main + +import ( + "archive/zip" + "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 + } + + z := zip.NewWriter(f) + + 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 = f.Close() + if err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 47fdabb..da376c2 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,22 @@ -module 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/gdamore/tcell/v2 v2.6.0 - github.com/rivo/tview v0.0.0-20231115183240-7c9e464bac02 - github.com/sijms/go-ora/v2 v2.7.22 + github.com/sijms/go-ora/v2 v2.8.4 github.com/xuri/excelize/v2 v2.8.0 + github.com/ying32/govcl v2.2.3+incompatible golang.org/x/text v0.14.0 - gopkg.in/ini.v1 v1.67.0 ) require ( - github.com/gdamore/encoding v1.0.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-runewidth v0.0.15 // 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.4 // indirect - github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect - github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // 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 ) diff --git a/go.sum b/go.sum index ac6fdf2..39eb6ff 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,10 @@ +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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/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= @@ -21,33 +14,26 @@ 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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sijms/go-ora/v2 v2.7.22 h1:B2mSLhDDWTgsjdM0d/O7iJn041cBy5z7fA8sR082vCg= -github.com/sijms/go-ora/v2 v2.7.22/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= 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/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= -github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/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/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= -github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/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= +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/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 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= @@ -57,9 +43,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 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= @@ -71,15 +56,11 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc 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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= 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= @@ -94,8 +75,6 @@ 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/icons.go b/icons.go new file mode 100644 index 0000000..1df8353 --- /dev/null +++ b/icons.go @@ -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() +} diff --git a/img/bullet_go.png b/img/bullet_go.png new file mode 100644 index 0000000..22c97be Binary files /dev/null and b/img/bullet_go.png differ diff --git a/img/change_password.png b/img/change_password.png new file mode 100644 index 0000000..3fa3cd7 Binary files /dev/null and b/img/change_password.png differ diff --git a/img/information.png b/img/information.png new file mode 100644 index 0000000..0ab640d Binary files /dev/null and b/img/information.png differ diff --git a/kernel.go b/kernel.go index 41e9aa5..2940ee3 100644 --- a/kernel.go +++ b/kernel.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "io" - "log/slog" "os" "path/filepath" "strconv" @@ -27,36 +26,35 @@ type Job struct { exportFileFormat ExportFormat encoding Encoding - servers []Server - status string + config Config - isFinished bool + status string + err error + isFinished bool + exportedRows int } func (j *Job) init() error { j.status = "Чтение списка серверов..." - servers, err := loadConfig(j.configFilePath) + config, err := loadConfig(j.configFilePath) if err != nil { return err } - j.servers = servers + j.config = *config return nil } func (j *Job) launch() error { - j.status = "Чтение файла SQL-скрипта..." + 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 := j.iterateServers(string(sqlBytes), branchFieldNum) @@ -84,10 +82,9 @@ func (j *Job) export(inputRows chan Row) error { gotHeader := false rowsCache := make([][]any, 0) - rowCount := -1 for row := range inputRows { - rowCount += 1 + j.exportedRows++ if gotHeader { outputRows <- row.data @@ -107,7 +104,7 @@ func (j *Job) export(inputRows chan Row) error { }() err = converter.Convert(fileName, outputRows) - j.status = "ЗАВЕРШЕНО." + j.status = "Выгрузка завершена." j.isFinished = true if err != nil { return err @@ -125,36 +122,42 @@ func (j *Job) iterateServers(sqlStr string, branchFieldNum int) chan Row { }() wg := new(sync.WaitGroup) - wg.Add(len(j.servers)) + wg.Add(len(j.config.Servers)) - for i, server := range j.servers { + for i, server := range j.config.Servers { i := i server := server go func() { defer wg.Done() - j.servers[i].Status = "Подключение к серверу" - db := sql.OpenDB(go_ora.NewConnector(server.Url)) - defer db.Close() - - err := db.Ping() + serverUrl, err := server.Url() if err != nil { - j.servers[i].Error = err + j.config.Servers[i].err = err return } - j.servers[i].Status = "Выполнение SQL-запроса" + 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 { - j.servers[i].Error = err + server.err = err return } defer rows.Close() cols, err := rows.Columns() if err != nil { - j.servers[i].Error = err + server.err = err return } @@ -163,8 +166,8 @@ func (j *Job) iterateServers(sqlStr string, branchFieldNum int) chan Row { } rowNum := 0 + server.status = fmt.Sprintf("Выгружено %d строк...", rowNum) for rows.Next() { - j.servers[i].Status = fmt.Sprintf("Выгружено %d строк", rowNum) pointers := make([]any, len(cols)) container := make([]any, len(cols)) @@ -174,13 +177,22 @@ func (j *Job) iterateServers(sqlStr string, branchFieldNum int) chan Row { err = rows.Scan(pointers...) if err != nil { - j.servers[i].Error = err + server.err = err break } - rowsChan <- Row{isHeader: false, data: append(append(container[:branchFieldNum-1], server.Name), container[branchFieldNum:]...)} // Добавление имени сервера + + resultRow := make([]any, 0) + 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) } - j.servers[i].Status = fmt.Sprintf("ЗАВЕРШЕНО: Выгружено %d строк.", rowNum) + server.status = fmt.Sprintf("Выгружено %d строк.", rowNum) }() } j.status = "Ожидание завершения работы с серверами" @@ -199,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 { @@ -220,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 diff --git a/main.go b/main.go index a7d1c11..ccf2ad6 100644 --- a/main.go +++ b/main.go @@ -1,201 +1,10 @@ package main import ( - "log/slog" - "os" - "path/filepath" - "strings" - "time" - - "github.com/gdamore/tcell/v2" - "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() { - renderUI() -} - -func renderUI() { - // SQL-files list - files, _ := filepath.Glob(filepath.Join(SQL_FILES_DIR, "*.sql")) - for i := range files { - files[i] = filepath.Base(files[i]) - } - - // Servers file list - 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() - - job := &Job{ - configFilePath: configFilePath, - scriptFilePath: filepath.Join(SQL_FILES_DIR, scriptFilePath), - exportFileFormat: ExportFormat(exportFileFormatStr), - encoding: Encoding(encodingStr)} - - err := job.init() - if err != nil { - showError(app, err) - return - } - - renderProgressUI(app, job) - }), 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 { - panic(err) - } -} - -func renderProgressUI(app *tview.Application, job *Job) { - statusTextViews := make([]*tview.TextView, 0) - maxServerNameLen := 0 - for i := range job.servers { - if len(job.servers[i].Name) > maxServerNameLen { - maxServerNameLen = len([]rune(job.servers[i].Name)) + 2 - } - } - rows := make([]int, len(job.servers)+3) - for i := range rows { - rows[i] = 1 - } - - grid := tview.NewGrid().SetRows(rows...).SetColumns(1, maxServerNameLen+2) - for i, server := range job.servers { - grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(server.Name), i+1, 1, 1, 1, 0, 0, false) - - p := tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText("...") - statusTextViews = append(statusTextViews, p) - grid.AddItem(p, i+1, 2, 1, 1, 0, 0, false) - } - - grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(strings.Repeat("-", maxServerNameLen)), len(job.servers)+1, 1, 1, 1, 0, 0, false) - grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft), len(job.servers)+1, 2, 1, 1, 0, 0, false) - - statusTextView := tview.NewTextView() - grid.AddItem(statusTextView, len(job.servers)+2, 2, 1, 1, 0, 0, false) - - // Last empty line - grid.AddItem(tview.NewTextView(), len(job.servers)+3, 1, 1, 1, 0, 0, false) - grid.AddItem(tview.NewTextView(), len(job.servers)+3, 2, 1, 1, 0, 0, false) - - go func() { - oneMoreUpdate := false - for !job.isFinished || oneMoreUpdate { - if !oneMoreUpdate { - time.Sleep(time.Second) - } - - for i, server := range job.servers { - s := []string{server.Status} - if server.Error != nil { - s = append(s, server.Error.Error()) - } - statusTextViews[i].SetText(strings.Join(s, ": ")) - - if server.Error != nil { - statusTextViews[i].SetTextColor(tcell.ColorRed) - } else if strings.HasPrefix(server.Status, "ЗАВЕРШЕНО: ") { - statusTextViews[i].SetTextColor(tcell.ColorGreen) - } - } - - statusTextView.SetText(job.status) - if strings.HasPrefix(job.status, "ЗАВЕРШЕНО") { - statusTextView.SetTextColor(tcell.ColorGreen) - } - - app.ForceDraw() - if job.isFinished { - if !oneMoreUpdate { - oneMoreUpdate = true - } else { - break - } - } - } - // TODO: - /*finishButton := tview.NewButton("OK").SetSelectedFunc(func() { app.Stop() }).SetBackgroundColor(tcell.ColorDarkGreen) - grid.AddItem(finishButton, len(job.servers)+2, 1, 1, 1, 0, 0, false) - app.ForceDraw().SetFocus(finishButton)*/ - showMessage(app, "Выгрузка завершена.") - }() - - go func() { - err := job.launch() - if err != nil { - showError(app, err) - } - }() - - if err := app.SetRoot(grid, true).SetFocus(grid).Run(); err != nil { - panic(err) - } -} - -func showError(app *tview.Application, err error) { - modal := tview.NewModal(). - SetText(err.Error()). - AddButtons([]string{"OK"}). - SetBackgroundColor(tcell.ColorRed). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - os.Exit(1) - }) - app.SetRoot(modal, true).SetFocus(modal) -} - -func showMessage(app *tview.Application, text string) { - modal := tview.NewModal(). - SetText(text). - AddButtons([]string{"OK"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - os.Exit(0) - }) - app.SetRoot(modal, true).SetFocus(modal).ForceDraw() + vcl.RunApp(&mainForm) } diff --git a/make.bat b/make.bat index ab71261..5db2360 100644 --- a/make.bat +++ b/make.bat @@ -1 +1 @@ -go build -trimpath -buildmode=pie -ldflags "-s -w" +go build -trimpath -buildmode=pie -ldflags "-s -w -H=windowsgui" diff --git a/servers.go b/servers.go deleted file mode 100644 index 2e80ec0..0000000 --- a/servers.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "strings" - - go_ora "github.com/sijms/go-ora/v2" - "gopkg.in/ini.v1" -) - -// Server - экземпляр сервера -type Server struct { - // Полная ссылка на БД, вкючая логин/пароль - Url string - - // Наименование филиала - Name string - - // Статус работы с сервером - Status string - - // Ошибка работы с сервером - Error error -} - -// 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(), map[string]string{"TIMEOUT": "0", "PREFETCH_ROWS": "1000"}) - - server := Server{ - Url: dbUrl, - Name: nameKey.String()} - - servers = append(servers, server) - } - - return servers, nil -} diff --git a/slog.go b/slog.go deleted file mode 100644 index 490e9e9..0000000 --- a/slog.go +++ /dev/null @@ -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 -} diff --git a/ui_config_editior.go b/ui_config_editior.go new file mode 100644 index 0000000..b873e39 --- /dev/null +++ b/ui_config_editior.go @@ -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) +} diff --git a/ui_main_form.go b/ui_main_form.go new file mode 100644 index 0000000..eaebdac --- /dev/null +++ b/ui_main_form.go @@ -0,0 +1,372 @@ +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 + 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.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)} { + f.ExportFormatComboBox.Items().Add(v) + } + f.ExportFormatComboBox.SetItemIndex(0) + + // Charsets ---------------------------------------------------------------- + for _, v := range []string{string(EncodingWin1251), string(EncodingUtf8)} { + 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())} + + 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)}, 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) +}