From 9b9eadfe92cbea4e8277e3b31d68b815cb561330 Mon Sep 17 00:00:00 2001 From: nxshock Date: Sat, 30 Dec 2023 21:14:42 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20UI=20=D0=BD=D0=B0=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BE=D1=82=D0=B5=D0=BA=D1=83=20LCL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + README.md | 33 ++-- config.go | 116 +++++++++++++ consts.go | 15 ++ doc/main-window.png | Bin 1291 -> 6131 bytes export.go | 7 +- export_csv.go | 41 ++--- export_csvzip.go | 74 ++++++++ go.mod | 25 +-- go.sum | 41 ++--- icons.go | 22 +++ img/bullet_go.png | Bin 0 -> 405 bytes img/change_password.png | Bin 0 -> 667 bytes img/information.png | Bin 0 -> 784 bytes kernel.go | 77 +++++---- main.go | 197 +-------------------- make.bat | 2 +- servers.go | 68 -------- slog.go | 97 ----------- ui_config_editior.go | 80 +++++++++ ui_main_form.go | 372 ++++++++++++++++++++++++++++++++++++++++ 21 files changed, 788 insertions(+), 481 deletions(-) create mode 100644 config.go create mode 100644 export_csvzip.go create mode 100644 icons.go create mode 100644 img/bullet_go.png create mode 100644 img/change_password.png create mode 100644 img/information.png delete mode 100644 servers.go delete mode 100644 slog.go create mode 100644 ui_config_editior.go create mode 100644 ui_main_form.go 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 928c3f24cd68faae5482ba25eb3cd4d5e6fce0a0..b315dfe6a3fb49da04808f0e337db7f4eead6c49 100644 GIT binary patch literal 6131 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 literal 0 HcmV?d00001 diff --git a/img/change_password.png b/img/change_password.png new file mode 100644 index 0000000000000000000000000000000000000000..3fa3cd7d4f0636e5d0bdf1c1be488bd62b7bb7e6 GIT binary patch literal 667 zcmV;M0%ZM(P)QM7cMF=LTHgf(Z=#tNeojN#3)ELj(M%gDeRv)&j0_XMb0pz zBR#uw_}=$?-+RsxtYP5RiCSfuca>7P$}&-w<-@X?E89JNBtayIJ4$Qqn>P0zyVw-l zTl(f>>}d~?uwHkvoK!j5H1Y?%cVICs$Sund5nYriRp{EKXUVIg;DaIja?w7S>`FWL^P zGFK}rv;BzlU68tsvyUoislN$8*s$T7^3&bjO-6bKPNx&4RHr>&Yy{pgF`i21cEODg zr)g>|V@Y}eSYH0a$nX#@m#Y}?)HgIBgvd#WiX)2ypiIKTLCe@BDyrbS{EBG{F+LIG z)b8tKWoJ`VxcADzLnY{Xh-v>6c5@|+oO=$y$YVEo&U^s+pApOJ=*|n6rh$}_!hQRZ zQW6LRFbreXt}Kh;o>342gdop(xi~6yyWJj#!+{ z?QFp3FFpV!yq^*}`|nGP5+_`H`o_O8j-hdd48#OnCK^fLo?4Nms&DMnKl-aAOGp7Znl zD)%dod9vv#7BsIszk_23o?~aM`Qpl59wnP6R>udXLkK7UBQiBQbeLNUDc@dSX8zFz zqXYX{{^}TcfPYpuxOC$ow-!=HhYn*zrrOF)&)JEw;gfyL{+)2UoUo%{Xk;FxnzxH1 zO{dcU+%6|Lrx-rj$Jc&h3^=d6_vd=|7P+&S(&;R*HN@8N>D2{Z|M*u5&JbBc!5KQ8 z<<4SC@7^Nb`*Th8NK{T56s$riAq2?sz#pHy0l-_I{f5)h>;cvX%1Hx3A^;@wDb+S^a(Gh84t6Pk+B~gi>wcv~* zwwjJeQ>oPO_39)or|#={|NF&L$6uqXE8=P9Id!1O=|iP0%MM+BKx{SEDAIJ3Yd_s% z?Ynnh3L%sYP|lWaj?B#Ji|-tNmF}*Hnfa6-@72fyXo2p6#aYFAy2;G!pJYonM}Tr$ z0YLZ8qaP0H7v8*h5~S3&95-``x5!`FC(?;p>B%t;F#sJ3NqkMqA%WD~?vVntE_ O0000 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) +}