168 lines
5.3 KiB
Go
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
|
|
}
|