Files
2026-10-01 09:50:59 +07:00

168 lines
5.3 KiB
Go

package lexicomai
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"sort"
"strings"
)
// repoFile — элемент листинга каталога SFTPGo web-client (/dirs).
type repoFile struct {
Name string `json:"name"`
Type int `json:"type"` // 1 = папка, 2 = файл
Size int64 `json:"size"`
DirPath string `json:"dir_path"`
}
// repoClient — аутентифицированный клиент web-API репозитория.
//
// Доступ — только web-client (REST /api/v2 и SFTP снаружи закрыты):
//
// POST {base}/sftpgo/web/client/login (form) → Set-Cookie: jwt
//
// Креды — из конфига (${VAR}), никогда из кода. Отсутствие кред — fail-closed.
func (m *Manager) repoClient(ctx context.Context, cfg *Config) (*http.Client, error) {
base := strings.TrimRight(cfg.Repo.Base, "/")
if base == "" || cfg.Repo.User == "" || cfg.Repo.Pass == "" {
return nil, fmt.Errorf("repo not configured (repo.base/user/pass)")
}
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar}
loginBody := "username=" + url.QueryEscape(cfg.Repo.User) + "&password=" + url.QueryEscape(cfg.Repo.Pass)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/sftpgo/web/client/login", strings.NewReader(loginBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("repo login: %w", err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return client, nil
}
// repoList возвращает имена файлов в /repos/<product>/<branch> (только .sh).
func (m *Manager) repoList(ctx context.Context, client *http.Client, base, product, branch string) ([]string, error) {
p := fmt.Sprintf("/repos/%s/%s", product, branch)
listURL := base + "/sftpgo/web/client/dirs?path=" + url.QueryEscape(p)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, listURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list %s: http %d", p, resp.StatusCode)
}
var items []repoFile
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
return nil, fmt.Errorf("decode %s: %w", p, err)
}
var names []string
for _, it := range items {
if it.Type == 2 && strings.HasSuffix(it.Name, ".sh") {
names = append(names, it.Name)
}
}
return names, nil
}
// RepoCatalog возвращает версии продукта в ветках release/beta из репозитория.
func (m *Manager) RepoCatalog(ctx context.Context, product string) (string, error) {
cfg, err := m.resolve()
if err != nil {
return "", err
}
client, err := m.repoClient(ctx, cfg)
if err != nil {
return "", err
}
base := strings.TrimRight(cfg.Repo.Base, "/")
var b strings.Builder
for _, branch := range []string{"release", "beta"} {
files, err := m.repoList(ctx, client, base, product, branch)
if err != nil {
fmt.Fprintf(&b, "%s: %v\n", branch, err)
continue
}
sort.Strings(files)
if len(files) == 0 {
fmt.Fprintf(&b, "%s: (empty)\n", branch)
continue
}
fmt.Fprintf(&b, "%s:\n", branch)
for _, f := range files {
b.WriteString(" " + f + "\n")
}
}
return strings.TrimRight(b.String(), "\n"), nil
}
// RepoResolve находит имя файла дистрибутива для продукта/версии/ветки,
// обращаясь к репозиторию. Пустая версия → последняя в ветке.
func (m *Manager) RepoResolve(ctx context.Context, product, version, branch string) (string, error) {
branch, err := normalizeBranch(branch)
if err != nil {
return "", err
}
cfg, err := m.resolve()
if err != nil {
return "", err
}
client, err := m.repoClient(ctx, cfg)
if err != nil {
return "", err
}
base := strings.TrimRight(cfg.Repo.Base, "/")
names, err := m.repoList(ctx, client, base, product, branch)
if err != nil {
return "", err
}
return ResolveDistro(names, product, version, branch)
}
// RepoStream открывает поток скачивания дистрибутива (web-client download=1).
// Вызывающий обязан закрыть поток. Стриминг — без буферизации целиком
// (файлы могут быть до ~1.2 ГБ).
func (m *Manager) RepoStream(ctx context.Context, product, branch, name string) (io.ReadCloser, error) {
cfg, err := m.resolve()
if err != nil {
return nil, err
}
client, err := m.repoClient(ctx, cfg)
if err != nil {
return nil, err
}
base := strings.TrimRight(cfg.Repo.Base, "/")
p := fmt.Sprintf("/repos/%s/%s/%s", product, branch, name)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/sftpgo/web/client/files?path="+url.QueryEscape(p)+"&download=1", nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("download %s: http %d", p, resp.StatusCode)
}
return resp.Body, nil
}