diff --git a/internal/build/build.go b/internal/build/build.go index 8b33905..cdf5eb4 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -32,6 +32,7 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/stats" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" @@ -401,9 +402,19 @@ func (b *Builder) BuildPackage( // We filter so as not to re-build what has already been built at the `installBuildDeps` stage. var filteredDepends []string + + // Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей + currentPackageNames := make(map[string]struct{}) + for _, pkg := range input.packages { + currentPackageNames[pkg] = struct{}{} + } + for _, d := range depends { if _, found := depNames[d]; !found { - filteredDepends = append(filteredDepends, d) + // Исключаем зависимости, которые являются подпакетами текущего мультипакета + if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage { + filteredDepends = append(filteredDepends, d) + } } } @@ -528,6 +539,13 @@ func (b *Builder) InstallALRPackages( if err != nil { return err } + + // Отслеживание установки ALR пакетов + for _, dep := range res { + if stats.ShouldTrackPackage(dep.Name) { + stats.TrackInstallation(ctx, dep.Name, "upgrade") + } + } } return nil @@ -552,11 +570,13 @@ func (b *Builder) BuildALRDeps( repoDeps = notFound // Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез - pkgs := cliutils.FlattenPkgs( + // Для зависимостей указываем isDependency = true + pkgs := cliutils.FlattenPkgsWithContext( ctx, found, "install", input.BuildOpts().Interactive, + true, ) type item struct { pkg *alrsh.Package @@ -691,6 +711,13 @@ func (i *Builder) InstallPkgs( if err != nil { return nil, err } + + // Отслеживание установки локальных пакетов + for _, dep := range builtDeps { + if stats.ShouldTrackPackage(dep.Name) { + stats.TrackInstallation(ctx, dep.Name, "install") + } + } } if len(repoDeps) > 0 { @@ -700,6 +727,13 @@ func (i *Builder) InstallPkgs( if err != nil { return nil, err } + + // Отслеживание установки пакетов из репозитория + for _, pkg := range repoDeps { + if stats.ShouldTrackPackage(pkg) { + stats.TrackInstallation(ctx, pkg, "install") + } + } } return builtDeps, nil diff --git a/internal/cliutils/prompt.go b/internal/cliutils/prompt.go index bc07c39..24cce29 100644 --- a/internal/cliutils/prompt.go +++ b/internal/cliutils/prompt.go @@ -103,22 +103,62 @@ func ShowScript(path, name, style string) error { // FlattenPkgs attempts to flatten the a map of slices of packages into a single slice // of packages by prompting the user if multiple packages match. func FlattenPkgs(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool) []alrsh.Package { + return FlattenPkgsWithContext(ctx, found, verb, interactive, false) +} + +// FlattenPkgsWithContext расширенная версия FlattenPkgs с контекстом обработки зависимостей +func FlattenPkgsWithContext(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool, isDependency bool) []alrsh.Package { var outPkgs []alrsh.Package for _, pkgs := range found { - if len(pkgs) > 1 && interactive { - choice, err := PkgPrompt(ctx, pkgs, verb, interactive) - if err != nil { - slog.Error(gotext.Get("Error prompting for choice of package")) - os.Exit(1) + if len(pkgs) > 1 { + // Проверяем, являются ли пакеты подпакетами одного мультипакета + if isMultiPackage(pkgs) && verb == "install" { + // Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора + // Это правильное поведение как для прямой установки, так и для зависимостей + outPkgs = append(outPkgs, pkgs...) + } else if interactive { + // Для разных пакетов с одинаковым именем - показываем меню выбора + choice, err := PkgPrompt(ctx, pkgs, verb, interactive) + if err != nil { + slog.Error(gotext.Get("Error prompting for choice of package")) + os.Exit(1) + } + outPkgs = append(outPkgs, choice) + } else { + // Если не интерактивный режим - берем первый + outPkgs = append(outPkgs, pkgs[0]) } - outPkgs = append(outPkgs, choice) - } else if len(pkgs) == 1 || !interactive { + } else { + // Если только один пакет - берем его outPkgs = append(outPkgs, pkgs[0]) } } return outPkgs } +// isMultiPackage проверяет, являются ли пакеты подпакетами одного мультипакета +func isMultiPackage(pkgs []alrsh.Package) bool { + if len(pkgs) <= 1 { + return false + } + + // Проверяем, что у всех пакетов одинаковый BasePkgName и Repository + firstBasePkg := pkgs[0].BasePkgName + firstRepo := pkgs[0].Repository + + if firstBasePkg == "" { + return false // Не мультипакет + } + + for _, pkg := range pkgs[1:] { + if pkg.BasePkgName != firstBasePkg || pkg.Repository != firstRepo { + return false + } + } + + return true +} + // PkgPrompt asks the user to choose between multiple packages. func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) { if !interactive { diff --git a/internal/repos/find.go b/internal/repos/find.go index 388c489..cb046d1 100644 --- a/internal/repos/find.go +++ b/internal/repos/find.go @@ -60,6 +60,13 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err) } + if len(result) == 0 { + result, err = rs.db.GetPkgs(ctx, "basepkg_name = ?", pkgName) + if err != nil { + return nil, nil, fmt.Errorf("FindPkgs: get by basepkg_name: %w", err) + } + } + if len(result) == 0 { result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName) } diff --git a/internal/stats/tracker.go b/internal/stats/tracker.go new file mode 100644 index 0000000..32aca6c --- /dev/null +++ b/internal/stats/tracker.go @@ -0,0 +1,106 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package stats + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" +) + +type InstallationData struct { + PackageName string `json:"packageName"` + Version string `json:"version,omitempty"` + InstallType string `json:"installType"` // "install" or "upgrade" + UserAgent string `json:"userAgent"` + Fingerprint string `json:"fingerprint,omitempty"` +} + +var ( + apiEndpoints = []string{ + "https://alr.plemya-x.ru/api/packages/track-install", + "http://localhost:3001/api/packages/track-install", + } + userAgent = "ALR-CLI/1.0" +) + +func generateFingerprint(packageName string) string { + hostname, _ := os.Hostname() + data := fmt.Sprintf("%s_%s_%s", hostname, packageName, time.Now().Format("2006-01-02")) + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:]) +} + +// TrackInstallation отправляет статистику установки пакета +func TrackInstallation(ctx context.Context, packageName string, installType string) { + // Запускаем в отдельной горутине, чтобы не блокировать основной процесс + go func() { + data := InstallationData{ + PackageName: packageName, + InstallType: installType, + UserAgent: userAgent, + Fingerprint: generateFingerprint(packageName), + } + + jsonData, err := json.Marshal(data) + if err != nil { + return // Тихо игнорируем ошибки - статистика не критична + } + + // Пробуем отправить запрос к разным endpoint-ам + for _, endpoint := range apiEndpoints { + if sendRequest(endpoint, jsonData) { + return // Если хотя бы один запрос прошёл успешно, выходим + } + } + }() +} + +func sendRequest(endpoint string, data []byte) bool { + client := &http.Client{ + Timeout: 5 * time.Second, + } + + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data)) + if err != nil { + return false + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", userAgent) + + resp, err := client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode >= 200 && resp.StatusCode < 300 +} + +// ShouldTrackPackage проверяет, нужно ли отслеживать установку этого пакета +func ShouldTrackPackage(packageName string) bool { + // Отслеживаем только alr-bin + return strings.Contains(packageName, "alr-bin") +} \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh index 84c6355..9560bab 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -56,6 +56,31 @@ installPkg() { esac } +trackInstallation() { + # Отправить статистику установки (не критично если не получится) + if command -v curl &>/dev/null; then + # Генерируем уникальный отпечаток на основе hostname и даты + fingerprint=$(echo "$(hostname)_$(date +%Y-%m-%d)" | sha256sum 2>/dev/null | cut -d' ' -f1 || echo "$(hostname)_$(date +%Y-%m-%d)") + + # Пробуем разные домены/порты для отправки статистики + for api_url in "https://alr.plemya-x.ru/api/packages/track-install" "http://localhost:3001/api/packages/track-install"; do + curl -s -m 5 -X POST "$api_url" \ + -H "Content-Type: application/json" \ + -H "User-Agent: ALR-InstallScript/1.0" \ + -d "{ + \"packageName\": \"alr-bin\", + \"installType\": \"script\", + \"userAgent\": \"ALR-InstallScript/1.0\", + \"fingerprint\": \"$fingerprint\" + }" >/dev/null 2>&1 + # Если один запрос удался, не пробуем остальные + if [ $? -eq 0 ]; then + break + fi + done + fi +} + if ! command -v curl &>/dev/null; then error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." fi @@ -186,6 +211,9 @@ if [ -z "$noPkgMgr" ]; then info "Установка пакета ALR" installPkg "$pkgMgr" "$fname" + # Отправляем статистику установки + trackInstallation + info "Очистка" rm -f "$fname" trap - EXIT