Экспорт QUIK — отдельная служба bj-quikexport; модули, выделенные из ядра, ставятся с обновлением
bj-quikexport (modules/quikexport + cmd/bj-quikexport, 127.0.0.1:8084): управление ARQA quikexport перенесено из bj-server. Настройки у службы в /var/lib/bj/.bj/quik-export/settings.json (при первом запуске переносятся из setup.json), раздел отдаётся фрагментом с индикатором меню, действия отвечают X-BJ-Flash. bj-server встраивает раздел в «Настройку», проксирует формы /admin/setup/quikexport/… и передаёт лицензию (X-BJ-Licensed). Общая проверка служебного ключа — modules/modkit (им пользуется и bj-npr), общее для прокси — lkgateway/modules.go. Выделенные из ядра модули (bj-npr, bj-quikexport: Component.FromCore) при обновлении ставятся и там, где их ещё нет, если входят в лицензию: release.MarkSplitModules, request.json "new", root-установщик ставит и включает unit-файл из комплекта. Если в обновлении меняется сам установщик, модули ставятся после перезапуска bj-server (ensureSplitModules). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package lkgateway
|
||||
|
||||
// Модуль «Экспорт биржевой информации» (ARQA quikexport) работает отдельной службой bj-quikexport
|
||||
// (modules/quikexport, обновляется независимо). Раздел остаётся на странице «Настройка»: bj-server
|
||||
// получает от службы готовый HTML раздела и цвет индикатора меню, а формы раздела
|
||||
// (/admin/setup/quikexport/…) проксирует службе и превращает её ответ X-BJ-Flash в обычное
|
||||
// сообщение страницы настроек. Лицензию проверяет bj-server и передаёт в X-BJ-Licensed.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// quikExportFeature — лицензионный признак модуля.
|
||||
const quikExportFeature = "quik-export"
|
||||
|
||||
// Применение настроек ждёт root-воркер до 3 минут.
|
||||
var quikExportHTTP = &http.Client{Timeout: 4 * time.Minute, CheckRedirect: noRedirect}
|
||||
|
||||
func quikExportURL() string {
|
||||
if u := strings.TrimSpace(os.Getenv("BJ_QUIKEXPORT_URL")); u != "" {
|
||||
return strings.TrimRight(u, "/")
|
||||
}
|
||||
return "http://127.0.0.1:8084"
|
||||
}
|
||||
|
||||
// QuikExportView — раздел «Экспорт QUIK» на странице настроек.
|
||||
type QuikExportView struct {
|
||||
HTML template.HTML
|
||||
Ind string // индикатор в меню: ok | warn | err | пусто
|
||||
}
|
||||
|
||||
func quikExportDo(ctx context.Context, rc *RuntimeConfig, method, uri string, body io.Reader, contentType string) (*http.Response, error) {
|
||||
key, err := moduleKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("служебный ключ модулей: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, quikExportURL()+uri, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
req.Header.Set("X-BJ-Module-Key", key)
|
||||
lic := "0"
|
||||
if moduleAllowed(rc, quikExportFeature) {
|
||||
lic = "1"
|
||||
}
|
||||
req.Header.Set("X-BJ-Licensed", lic)
|
||||
return quikExportHTTP.Do(req)
|
||||
}
|
||||
|
||||
func quikExportView(rc *RuntimeConfig, showRO bool) QuikExportView {
|
||||
uri := "/section"
|
||||
if showRO {
|
||||
uri += "?reveal_ro=1"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
resp, err := quikExportDo(ctx, rc, http.MethodGet, uri, nil, "")
|
||||
if err == nil {
|
||||
b, rerr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
resp.Body.Close()
|
||||
switch {
|
||||
case rerr != nil:
|
||||
err = rerr
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
err = fmt.Errorf("служба ответила %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
||||
default:
|
||||
return QuikExportView{HTML: template.HTML(b), Ind: resp.Header.Get("X-BJ-Indicator")}
|
||||
}
|
||||
}
|
||||
return QuikExportView{Ind: "err", HTML: template.HTML(`<h1>QUIK — Экспорт биржевой информации</h1>`) + moduleMessage(
|
||||
"Служба модуля не отвечает",
|
||||
"Служба bj-quikexport не запущена или ещё не установлена на этом сервере ("+err.Error()+").",
|
||||
"Установка и обновление модуля: Настройка → Обновления. Состояние службы: systemctl status bj-quikexport. Сам экспортёр ARQA от этого не зависит и продолжает выгрузку.")}
|
||||
}
|
||||
|
||||
// registerQuikExportSetup — формы раздела проксируются службе bj-quikexport.
|
||||
func registerQuikExportSetup(mux *http.ServeMux, h *setupHandlers) {
|
||||
proxy := func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 8<<20))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(body) == 0 && r.PostForm != nil && !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
|
||||
body = []byte(r.PostForm.Encode()) // форму уже разобрали раньше
|
||||
}
|
||||
// setupFlash читает return_to/section из формы — возвращаем тело на место.
|
||||
restore := func() { r.Body = io.NopCloser(bytes.NewReader(body)) }
|
||||
resp, err := quikExportDo(r.Context(), h.rc, r.Method, r.URL.RequestURI(), bytes.NewReader(body), r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
if r.Method == http.MethodPost {
|
||||
restore()
|
||||
setupFlash(w, r, "Экспорт QUIK: служба bj-quikexport не отвечает — "+err.Error())
|
||||
return
|
||||
}
|
||||
http.Error(w, "служба bj-quikexport не отвечает: "+err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if f := resp.Header.Get("X-BJ-Flash"); f != "" {
|
||||
msg, uerr := url.PathUnescape(f)
|
||||
if uerr != nil {
|
||||
msg = f
|
||||
}
|
||||
restore()
|
||||
setupFlash(w, r, msg)
|
||||
return
|
||||
}
|
||||
for _, k := range []string{"Content-Type", "Content-Disposition", "Cache-Control"} {
|
||||
if v := resp.Header.Get(k); v != "" {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = io.Copy(w, resp.Body)
|
||||
}
|
||||
mux.HandleFunc("/admin/setup/quikexport", proxy)
|
||||
mux.HandleFunc("/admin/setup/quikexport/", proxy)
|
||||
}
|
||||
Reference in New Issue
Block a user