Добавление статистики
Исправление работы с мультипакетами
This commit is contained in:
		| @@ -32,6 +32,7 @@ import ( | |||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" | 	"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/config" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" | 	"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/alrsh" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | 	"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. | 	// We filter so as not to re-build what has already been built at the `installBuildDeps` stage. | ||||||
| 	var filteredDepends []string | 	var filteredDepends []string | ||||||
|  | 	 | ||||||
|  | 	// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей | ||||||
|  | 	currentPackageNames := make(map[string]struct{}) | ||||||
|  | 	for _, pkg := range input.packages { | ||||||
|  | 		currentPackageNames[pkg] = struct{}{} | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
| 	for _, d := range depends { | 	for _, d := range depends { | ||||||
| 		if _, found := depNames[d]; !found { | 		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 { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 		 | ||||||
|  | 		// Отслеживание установки ALR пакетов | ||||||
|  | 		for _, dep := range res { | ||||||
|  | 			if stats.ShouldTrackPackage(dep.Name) { | ||||||
|  | 				stats.TrackInstallation(ctx, dep.Name, "upgrade") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -552,11 +570,13 @@ func (b *Builder) BuildALRDeps( | |||||||
| 		repoDeps = notFound | 		repoDeps = notFound | ||||||
|  |  | ||||||
| 		// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез | 		// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез | ||||||
| 		pkgs := cliutils.FlattenPkgs( | 		// Для зависимостей указываем isDependency = true | ||||||
|  | 		pkgs := cliutils.FlattenPkgsWithContext( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			found, | 			found, | ||||||
| 			"install", | 			"install", | ||||||
| 			input.BuildOpts().Interactive, | 			input.BuildOpts().Interactive, | ||||||
|  | 			true, | ||||||
| 		) | 		) | ||||||
| 		type item struct { | 		type item struct { | ||||||
| 			pkg      *alrsh.Package | 			pkg      *alrsh.Package | ||||||
| @@ -691,6 +711,13 @@ func (i *Builder) InstallPkgs( | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | 		 | ||||||
|  | 		// Отслеживание установки локальных пакетов | ||||||
|  | 		for _, dep := range builtDeps { | ||||||
|  | 			if stats.ShouldTrackPackage(dep.Name) { | ||||||
|  | 				stats.TrackInstallation(ctx, dep.Name, "install") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(repoDeps) > 0 { | 	if len(repoDeps) > 0 { | ||||||
| @@ -700,6 +727,13 @@ func (i *Builder) InstallPkgs( | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | 		 | ||||||
|  | 		// Отслеживание установки пакетов из репозитория | ||||||
|  | 		for _, pkg := range repoDeps { | ||||||
|  | 			if stats.ShouldTrackPackage(pkg) { | ||||||
|  | 				stats.TrackInstallation(ctx, pkg, "install") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return builtDeps, nil | 	return builtDeps, nil | ||||||
|   | |||||||
| @@ -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 | // 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. | // 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 { | 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 | 	var outPkgs []alrsh.Package | ||||||
| 	for _, pkgs := range found { | 	for _, pkgs := range found { | ||||||
| 		if len(pkgs) > 1 && interactive { | 		if len(pkgs) > 1 { | ||||||
| 			choice, err := PkgPrompt(ctx, pkgs, verb, interactive) | 			// Проверяем, являются ли пакеты подпакетами одного мультипакета | ||||||
| 			if err != nil { | 			if isMultiPackage(pkgs) && verb == "install" { | ||||||
| 				slog.Error(gotext.Get("Error prompting for choice of package")) | 				// Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора | ||||||
| 				os.Exit(1) | 				// Это правильное поведение как для прямой установки, так и для зависимостей | ||||||
|  | 				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 { | ||||||
| 		} else if len(pkgs) == 1 || !interactive { | 			// Если только один пакет - берем его | ||||||
| 			outPkgs = append(outPkgs, pkgs[0]) | 			outPkgs = append(outPkgs, pkgs[0]) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return outPkgs | 	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. | // PkgPrompt asks the user to choose between multiple packages. | ||||||
| func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) { | func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) { | ||||||
| 	if !interactive { | 	if !interactive { | ||||||
|   | |||||||
| @@ -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) | 				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 { | 			if len(result) == 0 { | ||||||
| 				result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName) | 				result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName) | ||||||
| 			} | 			} | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								internal/stats/tracker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								internal/stats/tracker.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | 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") | ||||||
|  | } | ||||||
| @@ -56,6 +56,31 @@ installPkg() { | |||||||
|   esac |   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 | if ! command -v curl &>/dev/null; then | ||||||
|   error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." |   error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." | ||||||
| fi | fi | ||||||
| @@ -186,6 +211,9 @@ if [ -z "$noPkgMgr" ]; then | |||||||
|   info "Установка пакета ALR" |   info "Установка пакета ALR" | ||||||
|   installPkg "$pkgMgr" "$fname" |   installPkg "$pkgMgr" "$fname" | ||||||
|  |  | ||||||
|  |   # Отправляем статистику установки | ||||||
|  |   trackInstallation | ||||||
|  |  | ||||||
|   info "Очистка" |   info "Очистка" | ||||||
|   rm -f "$fname" |   rm -f "$fname" | ||||||
|   trap - EXIT |   trap - EXIT | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user