03918b1e58
Проверено по шагам мастера, что bj берёт из DEPO сам: подготовка базы, префикс счетов (шаг 3), реквизиты (шаг 4), готовность (шаг 10), ручной ввод, подбор клиента для портфеля ЛК. - Реквизиты (Det01): счёт НРД ссылается на исходную версию лица, а ИНН НРД бывает только в новых (в исходной — ИНН НДЦ) — лицо по любой версии (ID или SELF_ID; ИНН НРД или MNEM «НДЦ»); номер счёта — и «счёт/раздел», не только «счёт/LEI/код/раздел». Счёт НРД — тот, с которым записан выбранный раздел (пара «раздел + счёт» — по ней процедуры находят место хранения), а не самый частый. Для MSSQL — только шире: прежнее находится. - Поиск по ФИО, договору и счёту — без учёта регистра, как в MSSQL (Cyrillic_General_CI_AS): в базе Fancy с кодировкой NONE регистр кириллицы различается — Firebird-варианты сравнивают в WIN1251 (подбор клиента для портфеля ЛК — запросы вынесены в fsql/lkportfolio.go, поиск и список договоров). - На копии базы: шаг 4 — счёт, код, разделы, ИНН и наименования организации; поиск «бр-1147», «бубликов» находит; FBLivePrepare — 103 из 103. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
535 lines
19 KiB
Go
535 lines
19 KiB
Go
package lkgateway
|
||
|
||
// Fancy DEPO на Firebird (27.10.2026). Fancy работает и на Firebird 2.5 — те же таблицы и процедуры ядра, что в
|
||
// MSSQL-версии (MSSQL-версия Fancy — перевод Firebird-версии). Весь SQL bj к DEPO написан на T-SQL; чтобы не
|
||
// держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn):
|
||
// - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL);
|
||
// - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE,
|
||
// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, YEAR(x) — EXTRACT(YEAR FROM x), AS float — AS DOUBLE
|
||
// PRECISION (float T-SQL — 8 байт, FLOAT Firebird — 4), N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя;
|
||
// - шаблоны (fmt, {ACC}) с тем, что механически не переводится, выбираются до подстановки — depoSQL;
|
||
// - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте;
|
||
// - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре,
|
||
// uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T.
|
||
// База Fancy на Firebird обычно в диалекте 1 и кодировке NONE с данными в WIN1251 — подключение в WIN1251.
|
||
// Таблицы, процедуры и права для Firebird ставит depo_setup_fb.go; DDL сюда не попадает.
|
||
|
||
import (
|
||
"context"
|
||
"database/sql/driver"
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"net/url"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
||
"github.com/nakagami/firebirdsql"
|
||
)
|
||
|
||
// fbDSN — строка подключения драйвера firebirdsql: путь к файлу базы на сервере Firebird или псевдоним.
|
||
func fbDSN(s FancyMSSQLSettings, db string) string {
|
||
q := url.Values{}
|
||
q.Set("charset", "WIN1251")
|
||
u := &url.URL{
|
||
Scheme: "firebird",
|
||
User: url.UserPassword(s.User, s.Password),
|
||
Host: fmt.Sprintf("%s:%d", s.Server, s.DefaultPort()),
|
||
Path: "/" + db,
|
||
RawQuery: q.Encode(),
|
||
}
|
||
return u.String()
|
||
}
|
||
|
||
// validFBPath — путь к файлу базы Firebird на сервере (D:\Fancy\DEPO.GDB, /var/db/depo.fdb) или псевдоним
|
||
// из aliases.conf: без кавычек, точки с запятой и управляющих символов (путь уходит в строку подключения).
|
||
func validFBPath(s string) bool {
|
||
if strings.TrimSpace(s) == "" || len(s) > 255 {
|
||
return false
|
||
}
|
||
for _, c := range s {
|
||
if c < ' ' || strings.ContainsRune(`'";?#`, c) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// fbIsLoginFailure — Firebird отверг логин или пароль.
|
||
func fbIsLoginFailure(err error) bool {
|
||
var fe *firebirdsql.FbError
|
||
if !errors.As(err, &fe) {
|
||
return false
|
||
}
|
||
for _, c := range fe.GDSCodes {
|
||
if c == firebirdsql.ISCLogin || c == firebirdsql.ISCLoginError {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// fbConnector — подключения к Firebird с переводом запросов.
|
||
type fbConnector struct{ base driver.Connector }
|
||
|
||
func (c fbConnector) Driver() driver.Driver { return c.base.Driver() }
|
||
|
||
func (c fbConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||
conn, err := c.base.Connect(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &fbConn{c: conn}, nil
|
||
}
|
||
|
||
// fbConn — подключение драйвера firebirdsql, которому database/sql отдаёт T-SQL bj: текст и параметры
|
||
// переводятся в fbRewrite. Остальные методы — как у драйвера.
|
||
type fbConn struct{ c driver.Conn }
|
||
|
||
func (f *fbConn) Prepare(query string) (driver.Stmt, error) {
|
||
return f.PrepareContext(context.Background(), query)
|
||
}
|
||
|
||
func (f *fbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||
q, names := query, []string(nil)
|
||
if !fbNative(query) {
|
||
q, names = fbParams(fbSQL(query))
|
||
}
|
||
var st driver.Stmt
|
||
var err error
|
||
if pc, ok := f.c.(driver.ConnPrepareContext); ok {
|
||
st, err = pc.PrepareContext(ctx, q)
|
||
} else {
|
||
st, err = f.c.Prepare(q)
|
||
}
|
||
if err != nil {
|
||
return nil, fbErr(query, q, err)
|
||
}
|
||
return &fbStmt{st: st, names: names}, nil
|
||
}
|
||
|
||
func (f *fbConn) Close() error { return f.c.Close() }
|
||
|
||
func (f *fbConn) Begin() (driver.Tx, error) {
|
||
return f.BeginTx(context.Background(), driver.TxOptions{})
|
||
}
|
||
|
||
func (f *fbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||
if bt, ok := f.c.(driver.ConnBeginTx); ok {
|
||
return bt.BeginTx(ctx, opts)
|
||
}
|
||
return f.c.Begin() //nolint:staticcheck // у драйвера без ConnBeginTx другого способа нет
|
||
}
|
||
|
||
func (f *fbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||
ex, ok := f.c.(driver.ExecerContext)
|
||
if !ok {
|
||
return nil, driver.ErrSkip
|
||
}
|
||
q, a, err := fbRewrite(query, args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
res, err := ex.ExecContext(ctx, q, a)
|
||
return res, fbErr(query, q, err)
|
||
}
|
||
|
||
func (f *fbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||
qr, ok := f.c.(driver.QueryerContext)
|
||
if !ok {
|
||
return nil, driver.ErrSkip
|
||
}
|
||
q, a, err := fbRewrite(query, args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rows, err := qr.QueryContext(ctx, q, a)
|
||
return rows, fbErr(query, q, err)
|
||
}
|
||
|
||
func (f *fbConn) Ping(ctx context.Context) error {
|
||
if p, ok := f.c.(driver.Pinger); ok {
|
||
return p.Ping(ctx)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (f *fbConn) CheckNamedValue(nv *driver.NamedValue) error {
|
||
if c, ok := f.c.(driver.NamedValueChecker); ok {
|
||
return c.CheckNamedValue(nv)
|
||
}
|
||
return driver.ErrSkip
|
||
}
|
||
|
||
func (f *fbConn) IsValid() bool {
|
||
if v, ok := f.c.(driver.Validator); ok {
|
||
return v.IsValid()
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (f *fbConn) ResetSession(ctx context.Context) error {
|
||
if r, ok := f.c.(driver.SessionResetter); ok {
|
||
return r.ResetSession(ctx)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// fbStmt — подготовленный запрос: параметры при выполнении расставляются по именам из текста.
|
||
type fbStmt struct {
|
||
st driver.Stmt
|
||
names []string
|
||
}
|
||
|
||
func (s *fbStmt) Close() error { return s.st.Close() }
|
||
func (s *fbStmt) NumInput() int { return -1 }
|
||
|
||
func (s *fbStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||
return s.ExecContext(context.Background(), fbValues(args))
|
||
}
|
||
|
||
func (s *fbStmt) Query(args []driver.Value) (driver.Rows, error) {
|
||
return s.QueryContext(context.Background(), fbValues(args))
|
||
}
|
||
|
||
func (s *fbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||
a, err := fbArgs(s.names, args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if ex, ok := s.st.(driver.StmtExecContext); ok {
|
||
return ex.ExecContext(ctx, a)
|
||
}
|
||
return s.st.Exec(fbPlain(a)) //nolint:staticcheck // драйвер без StmtExecContext
|
||
}
|
||
|
||
func (s *fbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||
a, err := fbArgs(s.names, args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if q, ok := s.st.(driver.StmtQueryContext); ok {
|
||
return q.QueryContext(ctx, a)
|
||
}
|
||
return s.st.Query(fbPlain(a)) //nolint:staticcheck // драйвер без StmtQueryContext
|
||
}
|
||
|
||
func fbValues(v []driver.Value) []driver.NamedValue {
|
||
out := make([]driver.NamedValue, len(v))
|
||
for i, x := range v {
|
||
out[i] = driver.NamedValue{Ordinal: i + 1, Value: x}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func fbPlain(a []driver.NamedValue) []driver.Value {
|
||
out := make([]driver.Value, len(a))
|
||
for i, x := range a {
|
||
out[i] = x.Value
|
||
}
|
||
return out
|
||
}
|
||
|
||
// fbRewrite — текст и параметры запроса T-SQL для Firebird. DDL и блоки PSQL (установка, depo_setup_fb.go)
|
||
// написаны для Firebird — уходят как есть.
|
||
func fbRewrite(query string, args []driver.NamedValue) (string, []driver.NamedValue, error) {
|
||
if fbNative(query) {
|
||
return query, args, nil
|
||
}
|
||
q, names := fbParams(fbSQL(query))
|
||
a, err := fbArgs(names, args)
|
||
return q, a, err
|
||
}
|
||
|
||
var fbNativeRe = regexp.MustCompile(`(?i)^\s*(CREATE|ALTER|RECREATE|DROP|GRANT|REVOKE|EXECUTE\s+BLOCK|COMMENT\s+ON)\b`)
|
||
|
||
// fbNative — команда уже на Firebird (установка таблиц, процедур, прав).
|
||
func fbNative(q string) bool { return fbNativeRe.MatchString(fbStripComments(q)) }
|
||
|
||
var (
|
||
fbExecRe = regexp.MustCompile(`(?is)^\s*exec(?:ute)?\s+(?:\[?dbo\]?\.)?\[?([A-Za-z0-9_]+)\]?\s*;?\s*$`)
|
||
fbObjectIDRe = regexp.MustCompile(`(?i)^SELECT OBJECT_ID\('(?:dbo\.)?([A-Za-z0-9_]+)'\)$`)
|
||
fbDboRe = regexp.MustCompile(`(?i)\[?\bdbo\]?\.`)
|
||
fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`)
|
||
fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`)
|
||
fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`)
|
||
fbDatePartRe = regexp.MustCompile(`(?i)\b(YEAR|MONTH|DAY)\s*\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)`)
|
||
fbFloatRe = regexp.MustCompile(`(?i)\bAS\s+float\b`)
|
||
fbTopRe = regexp.MustCompile(`(?i)\bSELECT(\s+DISTINCT)?\s+TOP\s*(\(\s*@?\w+\s*\)|\d+)`)
|
||
fbGUIDRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
||
fbISOTimeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$`)
|
||
)
|
||
|
||
// depoSQL — текст запроса-шаблона для СУБД базы DEPO: для Firebird — перевод из fsql.FB, если он есть (шаблон с
|
||
// fmt-подстановкой или {ACC} подключение к Firebird по тексту уже не узнает). Остальное переводит fbConn.
|
||
func depoSQL(f FancyMSSQLSettings, q string) string {
|
||
if f.IsFirebird() {
|
||
if t, ok := fsql.FB[q]; ok {
|
||
return t
|
||
}
|
||
}
|
||
return q
|
||
}
|
||
|
||
// fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический.
|
||
func fbSQL(q string) string {
|
||
if t, ok := fsql.FB[q]; ok {
|
||
return t
|
||
}
|
||
if m := fbExecRe.FindStringSubmatch(q); m != nil {
|
||
return "EXECUTE PROCEDURE " + m[1]
|
||
}
|
||
if m := fbObjectIDRe.FindStringSubmatch(q); m != nil { // depoTableExists: NULL — таблицы нет
|
||
return "SELECT (SELECT FIRST 1 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = '" + strings.ToUpper(m[1]) + "') FROM RDB$DATABASE"
|
||
}
|
||
return fbOutsideLiterals(q, func(code string) string {
|
||
code = fbDboRe.ReplaceAllString(code, "")
|
||
code = fbIsNullRe.ReplaceAllString(code, "COALESCE(")
|
||
code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP")
|
||
code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(")
|
||
code = fbDatePartRe.ReplaceAllString(code, "EXTRACT($1 FROM $2)")
|
||
code = fbFloatRe.ReplaceAllString(code, "AS DOUBLE PRECISION") // float T-SQL — 8 байт, FLOAT Firebird — 4
|
||
return fbTopRe.ReplaceAllStringFunc(code, func(s string) string {
|
||
m := fbTopRe.FindStringSubmatch(s)
|
||
return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT
|
||
})
|
||
})
|
||
}
|
||
|
||
// fbOutsideLiterals — замены только в коде: строки, имена в кавычках и комментарии не трогаются; N перед
|
||
// строкой (N'…' — национальная строка T-SQL) убирается.
|
||
func fbOutsideLiterals(q string, fix func(string) string) string {
|
||
var b, code strings.Builder
|
||
emit := func(lit string) {
|
||
c := code.String()
|
||
code.Reset()
|
||
if strings.HasPrefix(lit, "'") && (strings.HasSuffix(c, "N") || strings.HasSuffix(c, "n")) &&
|
||
(len(c) == 1 || !fbIdentChar(c[len(c)-2])) {
|
||
c = c[:len(c)-1]
|
||
}
|
||
b.WriteString(fix(c))
|
||
b.WriteString(lit)
|
||
}
|
||
for i := 0; i < len(q); i++ {
|
||
c := q[i]
|
||
switch {
|
||
case c == '\'' || c == '"':
|
||
j := i + 1
|
||
for j < len(q) {
|
||
if q[j] == c {
|
||
if j+1 < len(q) && q[j+1] == c {
|
||
j += 2
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
j++
|
||
}
|
||
if j >= len(q) {
|
||
j = len(q) - 1
|
||
}
|
||
emit(q[i : j+1])
|
||
i = j
|
||
case c == '-' && i+1 < len(q) && q[i+1] == '-':
|
||
j := strings.IndexByte(q[i:], '\n')
|
||
if j < 0 {
|
||
j = len(q) - i
|
||
}
|
||
emit(q[i : i+j])
|
||
i += j - 1
|
||
case c == '/' && i+1 < len(q) && q[i+1] == '*':
|
||
j := strings.Index(q[i+2:], "*/")
|
||
end := len(q)
|
||
if j >= 0 {
|
||
end = i + 2 + j + 2
|
||
}
|
||
emit(q[i:end])
|
||
i = end - 1
|
||
default:
|
||
code.WriteByte(c)
|
||
}
|
||
}
|
||
b.WriteString(fix(code.String()))
|
||
return b.String()
|
||
}
|
||
|
||
// fbParams — @имя вне строк и комментариев заменяются на ?; возвращает имена по порядку (повторы — повторно).
|
||
func fbParams(q string) (string, []string) {
|
||
if !strings.Contains(q, "@") {
|
||
return q, nil
|
||
}
|
||
var b strings.Builder
|
||
var names []string
|
||
for i := 0; i < len(q); i++ {
|
||
c := q[i]
|
||
switch {
|
||
case c == '\'' || c == '"': // строка или имя в кавычках — как есть до закрывающей
|
||
j := i + 1
|
||
for j < len(q) {
|
||
if q[j] == c {
|
||
if j+1 < len(q) && q[j+1] == c {
|
||
j += 2
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
j++
|
||
}
|
||
if j >= len(q) {
|
||
j = len(q) - 1
|
||
}
|
||
b.WriteString(q[i : j+1])
|
||
i = j
|
||
case c == '-' && i+1 < len(q) && q[i+1] == '-':
|
||
j := strings.IndexByte(q[i:], '\n')
|
||
if j < 0 {
|
||
j = len(q) - i
|
||
}
|
||
b.WriteString(q[i : i+j])
|
||
i += j - 1
|
||
case c == '/' && i+1 < len(q) && q[i+1] == '*':
|
||
j := strings.Index(q[i+2:], "*/")
|
||
end := len(q)
|
||
if j >= 0 {
|
||
end = i + 2 + j + 2
|
||
}
|
||
b.WriteString(q[i:end])
|
||
i = end - 1
|
||
case c == '@' && i+1 < len(q) && fbIdentStart(q[i+1]):
|
||
j := i + 1
|
||
for j < len(q) && fbIdentChar(q[j]) {
|
||
j++
|
||
}
|
||
names = append(names, q[i+1:j])
|
||
b.WriteByte('?')
|
||
i = j - 1
|
||
default:
|
||
b.WriteByte(c)
|
||
}
|
||
}
|
||
return b.String(), names
|
||
}
|
||
|
||
func fbIdentStart(c byte) bool { return c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
|
||
func fbIdentChar(c byte) bool { return fbIdentStart(c) || c >= '0' && c <= '9' }
|
||
|
||
// fbArgs — значения для ? по именам из текста: sql.Named — по имени, @p1…@pN без имени — по номеру.
|
||
// Без имён в тексте параметры передаются как есть (запрос уже с ?).
|
||
func fbArgs(names []string, args []driver.NamedValue) ([]driver.NamedValue, error) {
|
||
if len(names) == 0 {
|
||
out := make([]driver.NamedValue, len(args))
|
||
for i, a := range args {
|
||
out[i] = driver.NamedValue{Ordinal: i + 1, Value: fbValue(a.Value)}
|
||
}
|
||
return out, nil
|
||
}
|
||
out := make([]driver.NamedValue, 0, len(names))
|
||
for _, n := range names {
|
||
v, ok := fbArg(n, args)
|
||
if !ok {
|
||
return nil, fmt.Errorf("параметр @%s не передан", n)
|
||
}
|
||
out = append(out, driver.NamedValue{Ordinal: len(out) + 1, Value: fbValue(v)})
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func fbArg(name string, args []driver.NamedValue) (driver.Value, bool) {
|
||
for _, a := range args {
|
||
if a.Name != "" && strings.EqualFold(a.Name, name) {
|
||
return a.Value, true
|
||
}
|
||
}
|
||
if len(name) > 1 && (name[0] == 'p' || name[0] == 'P') {
|
||
if n, err := strconv.Atoi(name[1:]); err == nil && n >= 1 {
|
||
for _, a := range args {
|
||
if a.Name == "" && a.Ordinal == n {
|
||
return a.Value, true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
// fbValue — GUID в верхнем регистре, время ISO 8601 — с пробелом вместо T (Firebird «T» не разбирает).
|
||
func fbValue(v driver.Value) driver.Value {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return v
|
||
}
|
||
switch {
|
||
case fbGUIDRe.MatchString(s):
|
||
return strings.ToUpper(s)
|
||
case fbISOTimeRe.MatchString(s):
|
||
return strings.Replace(s, "T", " ", 1)
|
||
}
|
||
return v
|
||
}
|
||
|
||
var fbErrLogged sync.Map
|
||
|
||
// fbErr — ошибка Firebird; для разбора в журнал (раз на запрос) — исходный текст T-SQL и что ушло в Firebird.
|
||
func fbErr(orig, sent string, err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
var fe *firebirdsql.FbError
|
||
if errors.As(err, &fe) && fe.SQLCode == -104 { // синтаксис: перевод запроса не подошёл
|
||
if _, seen := fbErrLogged.LoadOrStore(orig, true); !seen {
|
||
log.Printf("depo-fb: запрос не принят Firebird: %v\n T-SQL: %s\n Firebird: %s", err, depoStmtHead(orig), sent)
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
var (
|
||
fbDriveRe = regexp.MustCompile(`^[A-Za-z]:[\\/]`)
|
||
fbHostRe = regexp.MustCompile(`^([A-Za-z0-9._\-]{2,}|\[[0-9A-Fa-f:]+\])(?:/(\d{1,5}))?:(.+)$`)
|
||
fbUNCRe = regexp.MustCompile(`^\\\\([^\\/]+)[\\/](.+)$`)
|
||
fbAdmShRe = regexp.MustCompile(`^([A-Za-z])\$[\\/](.*)$`)
|
||
fbInetURLRe = regexp.MustCompile(`(?i)^inet[46]?://([^/:]+)(?::(\d{1,5}))?/(.+)$`)
|
||
)
|
||
|
||
// fbSplitConn — строка подключения Fancy к базе Firebird целиком: «сервер:путь», «сервер/порт:путь»,
|
||
// «\\сервер\D:\путь», «\\сервер\D$\путь», «inet://сервер:порт/путь». База обычно на другом компьютере — на
|
||
// том, где работает сервер Firebird; bj подключается к нему по сети, а путь — на диске этого компьютера.
|
||
// host="" — указан только путь (сервер — в отдельном поле). Сетевую папку (\\сервер\папка\…) Firebird не
|
||
// открывает — ошибка с подсказкой.
|
||
func fbSplitConn(s string) (host string, port int, path string, err error) {
|
||
s = strings.TrimSpace(s)
|
||
switch {
|
||
case fbDriveRe.MatchString(s) || strings.HasPrefix(s, "/") || !strings.ContainsAny(s, `:\/`):
|
||
return "", 0, s, nil // путь на сервере или псевдоним
|
||
case fbInetURLRe.MatchString(s):
|
||
m := fbInetURLRe.FindStringSubmatch(s)
|
||
port, _ = strconv.Atoi(m[2])
|
||
path = m[3]
|
||
if !fbDriveRe.MatchString(path) && !strings.ContainsAny(path, `\/`) { // inet://host/alias
|
||
return m[1], port, path, nil
|
||
}
|
||
if !fbDriveRe.MatchString(path) {
|
||
path = "/" + path
|
||
}
|
||
return m[1], port, path, nil
|
||
case fbUNCRe.MatchString(s):
|
||
m := fbUNCRe.FindStringSubmatch(s)
|
||
rest := m[2]
|
||
if fbDriveRe.MatchString(rest) { // \\сервер\D:\путь — так Fancy пишет подключение по именованным каналам
|
||
return m[1], 0, rest, nil
|
||
}
|
||
if a := fbAdmShRe.FindStringSubmatch(rest); a != nil { // \\сервер\D$\путь — административный общий ресурс
|
||
return m[1], 0, strings.ToUpper(a[1]) + `:\` + a[2], nil
|
||
}
|
||
return "", 0, "", fmt.Errorf("%s — сетевая папка: Firebird не открывает базы по сети как файл. Укажите компьютер %s в поле «Адрес сервера» (на нём должен работать сервер Firebird), а путь — на его диске, например D:\\Fancy\\DEPO.GDB, — как в подключении Fancy", s, m[1])
|
||
case fbHostRe.MatchString(s):
|
||
m := fbHostRe.FindStringSubmatch(s)
|
||
port, _ = strconv.Atoi(m[2])
|
||
return strings.Trim(m[1], "[]"), port, m[3], nil
|
||
}
|
||
return "", 0, s, nil
|
||
}
|