Compare commits
8 Commits
3d3b4f29b9
...
master
Author | SHA1 | Date | |
---|---|---|---|
bb91f35e58 | |||
cbca05ef41 | |||
72131fc7ac | |||
b826188019 | |||
d6fe11c8e3 | |||
4c4e645d21 | |||
5b2b370a39 | |||
990e091e8b |
83
Makefile
Normal file
83
Makefile
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
.PHONY: build install clean test generate-plugins analyze-repo
|
||||||
|
|
||||||
|
# Версия и инфо
|
||||||
|
VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo "dev")
|
||||||
|
BUILD_TIME ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
GO_VERSION ?= $(shell go version | cut -d " " -f 3)
|
||||||
|
|
||||||
|
# Флаги линковки
|
||||||
|
LDFLAGS = -ldflags "\
|
||||||
|
-X main.Version=$(VERSION) \
|
||||||
|
-X main.BuildTime=$(BUILD_TIME) \
|
||||||
|
-X main.GoVersion=$(GO_VERSION) \
|
||||||
|
-s -w"
|
||||||
|
|
||||||
|
# Основные цели
|
||||||
|
build: alr-updater analyze-repo
|
||||||
|
|
||||||
|
alr-updater:
|
||||||
|
@echo "🔨 Building ALR-updater..."
|
||||||
|
CGO_ENABLED=1 go build $(LDFLAGS) -o alr-updater main.go
|
||||||
|
|
||||||
|
analyze-repo:
|
||||||
|
@echo "🔨 Building repository analyzer..."
|
||||||
|
cd cmd/analyze-repo && CGO_ENABLED=1 go build $(LDFLAGS) -o ../../analyze-repo .
|
||||||
|
|
||||||
|
install: build
|
||||||
|
@echo "📦 Installing ALR-updater..."
|
||||||
|
sudo install -m 755 alr-updater /usr/local/bin/
|
||||||
|
sudo install -m 755 analyze-repo /usr/local/bin/
|
||||||
|
@echo "✅ Installation complete!"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "🧹 Cleaning..."
|
||||||
|
rm -f alr-updater analyze-repo
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo "🧪 Running tests..."
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Генерация плагинов
|
||||||
|
generate-plugins:
|
||||||
|
@echo "🤖 Generating missing plugins..."
|
||||||
|
./alr-updater --generate-plugins
|
||||||
|
|
||||||
|
# Анализ репозитория
|
||||||
|
analyze:
|
||||||
|
@echo "📊 Analyzing repository..."
|
||||||
|
./analyze-repo --format=table
|
||||||
|
|
||||||
|
analyze-json:
|
||||||
|
@echo "📊 Analyzing repository (JSON)..."
|
||||||
|
./analyze-repo --format=json
|
||||||
|
|
||||||
|
# Генерация через анализатор
|
||||||
|
generate-missing:
|
||||||
|
@echo "🤖 Generating missing plugins via analyzer..."
|
||||||
|
./analyze-repo --generate
|
||||||
|
|
||||||
|
# Помощь
|
||||||
|
help:
|
||||||
|
@echo "ALR-updater Build System"
|
||||||
|
@echo "========================"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " build - Build all binaries"
|
||||||
|
@echo " alr-updater - Build main updater"
|
||||||
|
@echo " analyze-repo - Build repository analyzer"
|
||||||
|
@echo " install - Install binaries to /usr/local/bin"
|
||||||
|
@echo " clean - Remove built binaries"
|
||||||
|
@echo " test - Run tests"
|
||||||
|
@echo ""
|
||||||
|
@echo "Plugin Generation:"
|
||||||
|
@echo " generate-plugins - Generate missing plugins"
|
||||||
|
@echo " analyze - Analyze repository (table format)"
|
||||||
|
@echo " analyze-json - Analyze repository (JSON format)"
|
||||||
|
@echo " generate-missing - Analyze and generate missing plugins"
|
||||||
|
@echo ""
|
||||||
|
@echo "Variables:"
|
||||||
|
@echo " VERSION - Version string (default: git describe)"
|
||||||
|
@echo " BUILD_TIME - Build timestamp"
|
||||||
|
|
||||||
|
# Цель по умолчанию
|
||||||
|
all: build
|
38
README.md
38
README.md
@@ -249,6 +249,44 @@ run_every.week(check_python_packages)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
ALR-Updater поддерживает логирование как в консоль (stderr), так и в файл с автоматической ротацией:
|
||||||
|
|
||||||
|
### Настройка логирования в файл
|
||||||
|
|
||||||
|
В файле `/etc/alr-updater/config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[logging]
|
||||||
|
# Включить логирование в файл
|
||||||
|
enable_file = true
|
||||||
|
# Путь к файлу логов
|
||||||
|
log_file = "/var/log/alr-updater.log"
|
||||||
|
# Максимальный размер файла в байтах (100MB)
|
||||||
|
max_size = 104857600
|
||||||
|
```
|
||||||
|
|
||||||
|
### Особенности:
|
||||||
|
- При достижении максимального размера файл автоматически ротируется
|
||||||
|
- Старый файл сохраняется с временной меткой (например, `alr-updater.log.20250125-143022`)
|
||||||
|
- Хранится до 5 резервных копий логов
|
||||||
|
- Логи пишутся одновременно в stderr и файл
|
||||||
|
|
||||||
|
### Просмотр логов:
|
||||||
|
```bash
|
||||||
|
# Через systemd
|
||||||
|
journalctl -u alr-updater -f
|
||||||
|
|
||||||
|
# Из файла
|
||||||
|
tail -f /var/log/alr-updater.log
|
||||||
|
|
||||||
|
# Поиск ошибок
|
||||||
|
grep ERROR /var/log/alr-updater.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
### Ручной запуск
|
### Ручной запуск
|
||||||
|
@@ -13,16 +13,42 @@ reposBaseDir = "/var/cache/alr-updater"
|
|||||||
username = "CHANGE ME"
|
username = "CHANGE ME"
|
||||||
password = "CHANGE ME"
|
password = "CHANGE ME"
|
||||||
|
|
||||||
# Можно добавить дополнительные репозитории
|
# Дополнительные репозитории для мониторинга
|
||||||
# [repositories.another-repo]
|
[repositories.alr-LG]
|
||||||
# repoURL = "https://github.com/example/another-repo.git"
|
repoURL = "https://git.linux-gaming.ru/Linux-Gaming/alr-LG.git"
|
||||||
# [repositories.another-repo.commit]
|
[repositories.alr-LG.commit]
|
||||||
# name = "Bot User"
|
name = "CHANGE ME"
|
||||||
# email = "bot@example.com"
|
email = "CHANGE ME"
|
||||||
# [repositories.another-repo.credentials]
|
[repositories.alr-LG.credentials]
|
||||||
# username = "bot-user"
|
username = "CHANGE ME"
|
||||||
# password = "github-token"
|
password = "CHANGE ME"
|
||||||
|
|
||||||
|
[repositories.alr-default]
|
||||||
|
repoURL = "https://gitea.plemya-x.ru/Plemya-x/alr-default.git"
|
||||||
|
[repositories.alr-default.commit]
|
||||||
|
name = "CHANGE ME"
|
||||||
|
email = "CHANGE ME"
|
||||||
|
[repositories.alr-default.credentials]
|
||||||
|
username = "CHANGE ME"
|
||||||
|
password = "CHANGE ME"
|
||||||
|
|
||||||
[webhook]
|
[webhook]
|
||||||
# Хэш пароля для webhook. Сгенерируйте его, используя `alr-updater -g`.
|
# Хэш пароля для webhook. Сгенерируйте его, используя `alr-updater -g`.
|
||||||
pwd_hash = "CHANGE ME"
|
pwd_hash = "CHANGE ME"
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
# Включить логирование в файл
|
||||||
|
enable_file = false
|
||||||
|
# Путь к файлу логов (по умолчанию /var/log/alr-updater.log)
|
||||||
|
log_file = "/var/log/alr-updater.log"
|
||||||
|
# Максимальный размер файла логов в байтах (по умолчанию 100MB)
|
||||||
|
# При достижении этого размера файл будет ротирован
|
||||||
|
max_size = 104857600
|
||||||
|
|
||||||
|
[github]
|
||||||
|
# GitHub Personal Access Token для увеличения лимита API запросов
|
||||||
|
# Без токена: 60 запросов/час
|
||||||
|
# С токеном: 5000 запросов/час
|
||||||
|
# Создать токен: https://github.com/settings/tokens
|
||||||
|
# Требуемые права: public_repo (или repo для приватных репозиториев)
|
||||||
|
token = "CHANGE ME"
|
BIN
analyze-repo
Executable file
BIN
analyze-repo
Executable file
Binary file not shown.
BIN
cmd/analyze-repo/analyze-repo
Executable file
BIN
cmd/analyze-repo/analyze-repo
Executable file
Binary file not shown.
232
cmd/analyze-repo/main.go
Normal file
232
cmd/analyze-repo/main.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
||||||
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnalysisResult struct {
|
||||||
|
PackageName string `json:"package_name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
GitHubRepo string `json:"github_repo,omitempty"`
|
||||||
|
Sources []string `json:"sources,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Homepage string `json:"homepage,omitempty"`
|
||||||
|
PackageType string `json:"package_type"`
|
||||||
|
HasPlugin bool `json:"has_plugin"`
|
||||||
|
PluginGenerated bool `json:"can_generate_plugin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := pflag.StringP("config", "c", "/etc/alr-updater/config.toml", "Path to config file")
|
||||||
|
pluginDir := pflag.StringP("plugin-dir", "p", "/etc/alr-updater/plugins", "Path to plugin directory")
|
||||||
|
repoName := pflag.StringP("repo", "r", "alr-repo", "Repository name to analyze")
|
||||||
|
outputFormat := pflag.StringP("format", "f", "table", "Output format: table, json")
|
||||||
|
generateMissing := pflag.BoolP("generate", "g", false, "Generate missing plugins")
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
// Загружаем конфигурацию
|
||||||
|
cfg := &config.Config{}
|
||||||
|
fl, err := os.Open(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error opening config file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer fl.Close()
|
||||||
|
|
||||||
|
err = toml.NewDecoder(fl).Decode(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error decoding config file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем генератор плагинов
|
||||||
|
gen, err := generator.NewPluginGenerator(cfg, *pluginDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating plugin generator: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сканируем репозиторий
|
||||||
|
packages, err := gen.ScanRepository(*repoName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error scanning repository: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем список существующих плагинов
|
||||||
|
existingPlugins := make(map[string]bool)
|
||||||
|
pluginFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star"))
|
||||||
|
if err == nil {
|
||||||
|
for _, pluginFile := range pluginFiles {
|
||||||
|
pluginName := strings.TrimSuffix(filepath.Base(pluginFile), ".star")
|
||||||
|
existingPlugins[pluginName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подготавливаем результаты анализа
|
||||||
|
var results []AnalysisResult
|
||||||
|
var canGenerate []generator.DetectedPackage
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
result := AnalysisResult{
|
||||||
|
PackageName: pkg.Name,
|
||||||
|
Version: pkg.Version,
|
||||||
|
GitHubRepo: pkg.GitHubRepo,
|
||||||
|
Sources: pkg.Sources,
|
||||||
|
Description: pkg.Description,
|
||||||
|
Homepage: pkg.Homepage,
|
||||||
|
PackageType: pkg.PackageType,
|
||||||
|
HasPlugin: existingPlugins[pkg.Name],
|
||||||
|
PluginGenerated: pkg.GitHubRepo != "" && pkg.PackageType == "github_release",
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
|
||||||
|
if !result.HasPlugin && result.PluginGenerated {
|
||||||
|
canGenerate = append(canGenerate, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем пакеты с существующими плагинами
|
||||||
|
repoPath := filepath.Join(cfg.ReposBaseDir, *repoName)
|
||||||
|
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.Name() != "alr.sh" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packageName := filepath.Base(filepath.Dir(path))
|
||||||
|
if existingPlugins[packageName] {
|
||||||
|
// Проверяем, есть ли уже в результатах
|
||||||
|
found := false
|
||||||
|
for _, r := range results {
|
||||||
|
if r.PackageName == packageName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
// Добавляем пакет с существующим плагином
|
||||||
|
pkgInfo := analyzePackageFile(path)
|
||||||
|
result := AnalysisResult{
|
||||||
|
PackageName: packageName,
|
||||||
|
Version: pkgInfo.Version,
|
||||||
|
Sources: pkgInfo.Sources,
|
||||||
|
Description: pkgInfo.Description,
|
||||||
|
Homepage: pkgInfo.Homepage,
|
||||||
|
PackageType: "has_plugin",
|
||||||
|
HasPlugin: true,
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Выводим результаты
|
||||||
|
if *outputFormat == "json" {
|
||||||
|
output, _ := json.MarshalIndent(results, "", " ")
|
||||||
|
fmt.Println(string(output))
|
||||||
|
} else {
|
||||||
|
printTable(results, canGenerate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем недостающие плагины
|
||||||
|
if *generateMissing {
|
||||||
|
fmt.Printf("\n🚀 Generating %d missing plugins...\n", len(canGenerate))
|
||||||
|
for _, pkg := range canGenerate {
|
||||||
|
err := gen.GeneratePlugin(pkg, *repoName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ Error generating plugin for %s: %v\n", pkg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Plugin generation completed!\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzePackageFile(alrFile string) generator.PackageInfo {
|
||||||
|
content, err := os.ReadFile(alrFile)
|
||||||
|
if err != nil {
|
||||||
|
return generator.PackageInfo{Name: filepath.Base(filepath.Dir(alrFile))}
|
||||||
|
}
|
||||||
|
|
||||||
|
info := generator.PackageInfo{
|
||||||
|
Name: filepath.Base(filepath.Dir(alrFile)),
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "version=") {
|
||||||
|
versionMatch := regexp.MustCompile(`version=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
|
||||||
|
if len(versionMatch) > 1 {
|
||||||
|
info.Version = versionMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(line, "github.com") || strings.Contains(line, "https://") {
|
||||||
|
urlMatch := regexp.MustCompile(`https://[^\s"']+`).FindAllString(line, -1)
|
||||||
|
info.Sources = append(info.Sources, urlMatch...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "desc=") {
|
||||||
|
descMatch := regexp.MustCompile(`desc=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
|
||||||
|
if len(descMatch) > 1 {
|
||||||
|
info.Description = descMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTable(results []AnalysisResult, canGenerate []generator.DetectedPackage) {
|
||||||
|
fmt.Printf("📊 Анализ репозитория\n")
|
||||||
|
fmt.Printf("═══════════════════════════════════════════════════════════════════════════════════\n")
|
||||||
|
|
||||||
|
withPlugins := 0
|
||||||
|
withoutPlugins := 0
|
||||||
|
canGenerateCount := len(canGenerate)
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
if r.HasPlugin {
|
||||||
|
withPlugins++
|
||||||
|
} else {
|
||||||
|
withoutPlugins++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("📦 Всего пакетов: %d\n", len(results))
|
||||||
|
fmt.Printf("✅ С плагинами: %d\n", withPlugins)
|
||||||
|
fmt.Printf("❌ Без плагинов: %d\n", withoutPlugins)
|
||||||
|
fmt.Printf("🤖 Можно сгенерировать: %d\n", canGenerateCount)
|
||||||
|
|
||||||
|
if canGenerateCount > 0 {
|
||||||
|
fmt.Printf("\n🎯 Пакеты для автогенерации:\n")
|
||||||
|
fmt.Printf("─────────────────────────────────────────────────────────────────────────────────\n")
|
||||||
|
for _, pkg := range canGenerate {
|
||||||
|
fmt.Printf("📦 %-25s │ %s\n", pkg.Name, pkg.GitHubRepo)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n💡 Запустите с флагом --generate для создания плагинов\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if withoutPlugins > canGenerateCount {
|
||||||
|
fmt.Printf("\n⚠️ Пакеты требующие ручной настройки:\n")
|
||||||
|
fmt.Printf("─────────────────────────────────────────────────────────────────────────────────\n")
|
||||||
|
for _, r := range results {
|
||||||
|
if !r.HasPlugin && !r.PluginGenerated {
|
||||||
|
fmt.Printf("📦 %-25s │ %s\n", r.PackageName, r.PackageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
docs/plugin-generation.md
Normal file
197
docs/plugin-generation.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Автоматическая генерация плагинов
|
||||||
|
|
||||||
|
ALR-updater поддерживает автоматическую генерацию плагинов для пакетов, размещённых на GitHub. Система может автоматически обнаруживать пакеты без плагинов и создавать для них соответствующие .star файлы.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
### Автоматическое обнаружение
|
||||||
|
- ✅ Сканирование репозиториев для поиска пакетов без плагинов
|
||||||
|
- ✅ Определение GitHub репозиториев из файлов alr.sh
|
||||||
|
- ✅ Классификация пакетов по типам (binary, source, git)
|
||||||
|
- ✅ Генерация подходящих шаблонов плагинов
|
||||||
|
|
||||||
|
### Поддерживаемые типы пакетов
|
||||||
|
- **Binary packages (-bin)**: AppImage, tar.gz, zip архивы
|
||||||
|
- **Source packages**: tar.gz архивы с исходным кодом
|
||||||
|
- **Library packages**: C++ библиотеки, фреймворки
|
||||||
|
- **Tool packages**: Утилиты командной строки
|
||||||
|
|
||||||
|
### Автоматическое определение
|
||||||
|
- 📦 **Версии**: Извлечение из тегов GitHub (v1.2.3, release-1.2.3)
|
||||||
|
- 🔗 **URL скачивания**: Поиск подходящих assets в релизах
|
||||||
|
- 📅 **Расписание**: Автоматический выбор частоты проверок
|
||||||
|
- 🔍 **Asset паттерны**: Распознавание типов файлов
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### 1. Встроенная генерация
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Генерация всех недостающих плагинов
|
||||||
|
./alr-updater --generate-plugins
|
||||||
|
|
||||||
|
# С пользовательскими путями
|
||||||
|
./alr-updater --generate-plugins \
|
||||||
|
--config=/path/to/config.toml \
|
||||||
|
--plugin-dir=/path/to/plugins/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Анализ репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка анализатора
|
||||||
|
make analyze-repo
|
||||||
|
|
||||||
|
# Анализ репозитория (табличный формат)
|
||||||
|
./analyze-repo --repo=alr-repo
|
||||||
|
|
||||||
|
# JSON формат для программной обработки
|
||||||
|
./analyze-repo --repo=alr-repo --format=json
|
||||||
|
|
||||||
|
# Анализ + генерация недостающих плагинов
|
||||||
|
./analyze-repo --repo=alr-repo --generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Через Makefile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Анализ репозитория
|
||||||
|
make analyze
|
||||||
|
|
||||||
|
# Генерация недостающих плагинов
|
||||||
|
make generate-missing
|
||||||
|
|
||||||
|
# Анализ в JSON формате
|
||||||
|
make analyze-json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры вывода
|
||||||
|
|
||||||
|
### Табличный анализ
|
||||||
|
```
|
||||||
|
📊 Анализ репозитория
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
📦 Всего пакетов: 67
|
||||||
|
✅ С плагинами: 48
|
||||||
|
❌ Без плагинов: 19
|
||||||
|
🤖 Можно сгенерировать: 15
|
||||||
|
|
||||||
|
🎯 Пакеты для автогенерации:
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
📦 telegram-desktop-bin │ telegramdesktop/tdesktop
|
||||||
|
📦 obsidian-bin │ obsidianmd/obsidian-releases
|
||||||
|
📦 yarn │ yarnpkg/yarn
|
||||||
|
📦 electron-bin │ electron/electron
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON анализ
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"package_name": "telegram-desktop-bin",
|
||||||
|
"version": "5.0.1",
|
||||||
|
"github_repo": "telegramdesktop/tdesktop",
|
||||||
|
"sources": ["https://github.com/telegramdesktop/tdesktop/..."],
|
||||||
|
"description": "Official Telegram Desktop client",
|
||||||
|
"package_type": "github_release",
|
||||||
|
"has_plugin": false,
|
||||||
|
"can_generate_plugin": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура сгенерированных плагинов
|
||||||
|
|
||||||
|
Сгенерированные плагины включают:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Repository: alr-repo
|
||||||
|
# Auto-generated plugin for package-name
|
||||||
|
|
||||||
|
REPO = "alr-repo"
|
||||||
|
|
||||||
|
def check_package_name():
|
||||||
|
"""Проверка обновлений для package-name с автоматическим обновлением checksums"""
|
||||||
|
# Автоматическое определение версий
|
||||||
|
# GitHub API интеграция
|
||||||
|
# Поиск подходящих assets
|
||||||
|
# Обновление хеш-сумм
|
||||||
|
# Автоматические коммиты
|
||||||
|
|
||||||
|
# Умное расписание на основе типа пакета
|
||||||
|
run_every.day(check_package_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
### Специальные случаи
|
||||||
|
|
||||||
|
Можно настроить специальные правила для пакетов:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/generator/plugin_generator.go
|
||||||
|
|
||||||
|
func (pg *PluginGenerator) detectBinaryPackage(packageName string, detected DetectedPackage) DetectedPackage {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(packageName, "telegram"):
|
||||||
|
detected.AssetPattern = `tsetup\..*\.tar\.xz`
|
||||||
|
detected.URLTemplate = "https://github.com/telegramdesktop/tdesktop/releases/download/{tag}/tsetup.{version}.tar.xz"
|
||||||
|
case strings.Contains(packageName, "obsidian"):
|
||||||
|
detected.AssetPattern = `.*\.AppImage$`
|
||||||
|
// ... другие специальные случаи
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Расписание проверок
|
||||||
|
|
||||||
|
Автоматически определяется на основе типа пакета:
|
||||||
|
|
||||||
|
- **Binary packages**: Каждые 6 часов
|
||||||
|
- **Library packages**: Каждую неделю
|
||||||
|
- **Tool packages**: Каждый день
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
### Текущие ограничения
|
||||||
|
- ❌ Только GitHub репозитории
|
||||||
|
- ❌ Требует стандартную структуру релизов
|
||||||
|
- ❌ Не поддерживает сложные схемы версионирования
|
||||||
|
|
||||||
|
### Планируемые улучшения
|
||||||
|
- 🔄 Поддержка GitLab, Gitea
|
||||||
|
- 🔄 Поддержка PyPI, npm, crates.io
|
||||||
|
- 🔄 Веб-интерфейс для настройки
|
||||||
|
- 🔄 Валидация сгенерированных плагинов
|
||||||
|
|
||||||
|
## Отладка
|
||||||
|
|
||||||
|
### Проверка сгенерированного плагина
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка синтаксиса
|
||||||
|
./alr-updater --plugin-dir=./plugins --config=test-config.toml --debug
|
||||||
|
|
||||||
|
# Тестовый запуск
|
||||||
|
./alr-updater --now --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
```bash
|
||||||
|
# Включить отладочные сообщения
|
||||||
|
./alr-updater --debug --generate-plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
## Вклад в развитие
|
||||||
|
|
||||||
|
Для улучшения системы генерации:
|
||||||
|
|
||||||
|
1. **Добавление новых типов пакетов** в `detectPackageType()`
|
||||||
|
2. **Улучшение паттернов** в `detectBinaryPackage()`
|
||||||
|
3. **Новые источники** кроме GitHub
|
||||||
|
4. **Тестирование** на реальных пакетах
|
||||||
|
|
||||||
|
См. также:
|
||||||
|
- [Создание плагинов вручную](manual-plugins.md)
|
||||||
|
- [API документация](api.md)
|
||||||
|
- [Конфигурация](configuration.md)
|
@@ -25,6 +25,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
||||||
@@ -44,30 +45,89 @@ var (
|
|||||||
ErrIncorrectPassword = errors.New("incorrect password")
|
ErrIncorrectPassword = errors.New("incorrect password")
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpModule = &starlarkstruct.Module{
|
func newHTTPModule(cfg *config.Config) *starlarkstruct.Module {
|
||||||
Name: "http",
|
return &starlarkstruct.Module{
|
||||||
Members: starlark.StringDict{
|
Name: "http",
|
||||||
"get": starlark.NewBuiltin("http.get", httpGet),
|
Members: starlark.StringDict{
|
||||||
"post": starlark.NewBuiltin("http.post", httpPost),
|
"get": httpGetWithConfig(cfg),
|
||||||
"put": starlark.NewBuiltin("http.put", httpPut),
|
"post": httpPostWithConfig(cfg),
|
||||||
"head": starlark.NewBuiltin("http.head", httpHead),
|
"put": httpPutWithConfig(cfg),
|
||||||
},
|
"head": httpHeadWithConfig(cfg),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func httpGetWithConfig(cfg *config.Config) *starlark.Builtin {
|
||||||
return makeRequest("http.get", http.MethodGet, args, kwargs, thread)
|
return starlark.NewBuiltin("http.get", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
|
return makeRequestWithConfig("http.get", http.MethodGet, args, kwargs, thread, cfg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpPost(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func httpPostWithConfig(cfg *config.Config) *starlark.Builtin {
|
||||||
return makeRequest("http.post", http.MethodPost, args, kwargs, thread)
|
return starlark.NewBuiltin("http.post", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
|
return makeRequestWithConfig("http.post", http.MethodPost, args, kwargs, thread, cfg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpPut(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func httpPutWithConfig(cfg *config.Config) *starlark.Builtin {
|
||||||
return makeRequest("http.put", http.MethodPut, args, kwargs, thread)
|
return starlark.NewBuiltin("http.put", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
|
return makeRequestWithConfig("http.put", http.MethodPut, args, kwargs, thread, cfg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpHead(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func httpHeadWithConfig(cfg *config.Config) *starlark.Builtin {
|
||||||
return makeRequest("http.head", http.MethodHead, args, kwargs, thread)
|
return starlark.NewBuiltin("http.head", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
|
return makeRequestWithConfig("http.head", http.MethodHead, args, kwargs, thread, cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRequestWithConfig(name, method string, args starlark.Tuple, kwargs []starlark.Tuple, thread *starlark.Thread, cfg *config.Config) (starlark.Value, error) {
|
||||||
|
var (
|
||||||
|
url string
|
||||||
|
redirect = true
|
||||||
|
headers = &starlarkHeaders{}
|
||||||
|
body = newBodyReader()
|
||||||
|
timeout int64 = 30 // Таймаут по умолчанию 30 секунд
|
||||||
|
)
|
||||||
|
err := starlark.UnpackArgs(name, args, kwargs, "url", &url, "redirect??", &redirect, "headers??", headers, "body??", body, "timeout??", &timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки из аргументов
|
||||||
|
if headers.Header != nil {
|
||||||
|
req.Header = headers.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем GitHub токен для запросов к api.github.com
|
||||||
|
if cfg.GitHub.Token != "" && strings.Contains(url, "api.github.com") {
|
||||||
|
req.Header.Set("Authorization", "token "+cfg.GitHub.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Duration(timeout) * time.Second,
|
||||||
|
}
|
||||||
|
if !redirect {
|
||||||
|
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Making HTTP request").Str("url", url).Str("method", req.Method).Bool("redirect", redirect).Stringer("pos", thread.CallFrame(1).Pos).Send()
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Got HTTP response").Str("host", res.Request.URL.Host).Int("code", res.StatusCode).Stringer("pos", thread.CallFrame(1).Pos).Send()
|
||||||
|
|
||||||
|
return starlarkResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type starlarkBodyReader struct {
|
type starlarkBodyReader struct {
|
||||||
|
@@ -38,7 +38,7 @@ type Options struct {
|
|||||||
func Register(sd starlark.StringDict, opts *Options) {
|
func Register(sd starlark.StringDict, opts *Options) {
|
||||||
sd["run_every"] = runEveryModule
|
sd["run_every"] = runEveryModule
|
||||||
sd["sleep"] = starlark.NewBuiltin("sleep", sleep)
|
sd["sleep"] = starlark.NewBuiltin("sleep", sleep)
|
||||||
sd["http"] = httpModule
|
sd["http"] = newHTTPModule(opts.Config)
|
||||||
sd["regex"] = regexModule
|
sd["regex"] = regexModule
|
||||||
sd["store"] = storeModule(opts.DB, opts.Name)
|
sd["store"] = storeModule(opts.DB, opts.Name)
|
||||||
sd["updater"] = updaterModule(opts.Config)
|
sd["updater"] = updaterModule(opts.Config)
|
||||||
|
@@ -116,38 +116,46 @@ var runEveryModule = &starlarkstruct.Module{
|
|||||||
|
|
||||||
func runEveryMinute(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func runEveryMinute(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
var fn *starlark.Function
|
var fn *starlark.Function
|
||||||
err := starlark.UnpackArgs("run_every.minute", args, kwargs, "function", &fn)
|
var count int64 = 1
|
||||||
|
err := starlark.UnpackArgs("run_every.minute", args, kwargs, "function", &fn, "count?", &count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return runScheduled(thread, fn, "1m")
|
duration := time.Duration(count) * time.Minute
|
||||||
|
return runScheduled(thread, fn, duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func runEveryHour(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func runEveryHour(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
var fn *starlark.Function
|
var fn *starlark.Function
|
||||||
err := starlark.UnpackArgs("run_every.hour", args, kwargs, "function", &fn)
|
var count int64 = 1
|
||||||
|
err := starlark.UnpackArgs("run_every.hour", args, kwargs, "function", &fn, "count?", &count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return runScheduled(thread, fn, "1h")
|
duration := time.Duration(count) * time.Hour
|
||||||
|
return runScheduled(thread, fn, duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func runEveryDay(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func runEveryDay(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
var fn *starlark.Function
|
var fn *starlark.Function
|
||||||
err := starlark.UnpackArgs("run_every.day", args, kwargs, "function", &fn)
|
var count int64 = 1
|
||||||
|
err := starlark.UnpackArgs("run_every.day", args, kwargs, "function", &fn, "count?", &count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return runScheduled(thread, fn, "24h")
|
duration := time.Duration(count) * 24 * time.Hour
|
||||||
|
return runScheduled(thread, fn, duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func runEveryWeek(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
func runEveryWeek(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
var fn *starlark.Function
|
var fn *starlark.Function
|
||||||
err := starlark.UnpackArgs("run_every.week", args, kwargs, "function", &fn)
|
var count int64 = 1
|
||||||
|
err := starlark.UnpackArgs("run_every.week", args, kwargs, "function", &fn, "count?", &count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return runScheduled(thread, fn, "168h")
|
duration := time.Duration(count) * 7 * 24 * time.Hour
|
||||||
|
return runScheduled(thread, fn, duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func runScheduled(thread *starlark.Thread, fn *starlark.Function, duration string) (starlark.Value, error) {
|
func runScheduled(thread *starlark.Thread, fn *starlark.Function, duration string) (starlark.Value, error) {
|
||||||
@@ -170,12 +178,23 @@ func runScheduled(thread *starlark.Thread, fn *starlark.Function, duration strin
|
|||||||
tickerMtx.Unlock()
|
tickerMtx.Unlock()
|
||||||
log.Debug("Created new scheduled ticker").Int("handle", handle).Str("duration", duration).Stringer("pos", thread.CallFrame(1).Pos).Send()
|
log.Debug("Created new scheduled ticker").Int("handle", handle).Str("duration", duration).Stringer("pos", thread.CallFrame(1).Pos).Send()
|
||||||
|
|
||||||
|
// Запускаем функцию немедленно при первой регистрации
|
||||||
|
go func() {
|
||||||
|
newThread := &starlark.Thread{Name: thread.Name}
|
||||||
|
log.Info("Running plugin function immediately on startup").Str("plugin", thread.Name).Str("function", fn.Name()).Send()
|
||||||
|
_, err := starlark.Call(newThread, fn, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while executing initial plugin function").Str("plugin", thread.Name).Str("function", fn.Name()).Err(err).Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range t.C {
|
for range t.C {
|
||||||
log.Debug("Calling scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Send()
|
newThread := &starlark.Thread{Name: thread.Name}
|
||||||
_, err := starlark.Call(thread, fn, nil, nil)
|
log.Info("Calling scheduled function").Str("plugin", thread.Name).Str("function", fn.Name()).Send()
|
||||||
|
_, err := starlark.Call(newThread, fn, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while executing scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Err(err).Send()
|
log.Warn("Error while executing scheduled function").Str("plugin", thread.Name).Str("function", fn.Name()).Err(err).Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@@ -87,11 +87,34 @@ func updaterPull(cfg *config.Config) *starlark.Builtin {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Исправляем права доступа после git pull
|
||||||
|
err = fixRepoPermissions(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to fix repository permissions after pull").Str("repo", repoName).Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
_ = repoConfig // Избегаем неиспользованной переменной
|
_ = repoConfig // Избегаем неиспользованной переменной
|
||||||
return starlark.None, nil
|
return starlark.None, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixRepoPermissions рекурсивно устанавливает права 775 для директорий и 664 для файлов
|
||||||
|
func fixRepoPermissions(path string) error {
|
||||||
|
return filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// Устанавливаем права 2775 для директорий (setgid)
|
||||||
|
return os.Chmod(filePath, 0o2775)
|
||||||
|
} else {
|
||||||
|
// Устанавливаем права 664 для файлов
|
||||||
|
return os.Chmod(filePath, 0o664)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func updaterPushChanges(cfg *config.Config) *starlark.Builtin {
|
func updaterPushChanges(cfg *config.Config) *starlark.Builtin {
|
||||||
return starlark.NewBuiltin("updater.push_changes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
return starlark.NewBuiltin("updater.push_changes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
var repoName, msg string
|
var repoName, msg string
|
||||||
|
@@ -22,6 +22,8 @@ type Config struct {
|
|||||||
ReposBaseDir string `toml:"reposBaseDir" env:"REPOS_BASE_DIR"`
|
ReposBaseDir string `toml:"reposBaseDir" env:"REPOS_BASE_DIR"`
|
||||||
Repositories map[string]GitRepo `toml:"repositories"`
|
Repositories map[string]GitRepo `toml:"repositories"`
|
||||||
Webhook Webhook `toml:"webhook" envPrefix:"WEBHOOK_"`
|
Webhook Webhook `toml:"webhook" envPrefix:"WEBHOOK_"`
|
||||||
|
Logging Logging `toml:"logging" envPrefix:"LOGGING_"`
|
||||||
|
GitHub GitHub `toml:"github" envPrefix:"GITHUB_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitRepo struct {
|
type GitRepo struct {
|
||||||
@@ -43,3 +45,13 @@ type Commit struct {
|
|||||||
type Webhook struct {
|
type Webhook struct {
|
||||||
PasswordHash string `toml:"pwd_hash" env:"PASSWORD_HASH"`
|
PasswordHash string `toml:"pwd_hash" env:"PASSWORD_HASH"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Logging struct {
|
||||||
|
LogFile string `toml:"log_file" env:"LOG_FILE"`
|
||||||
|
MaxSize int64 `toml:"max_size" env:"MAX_SIZE"`
|
||||||
|
EnableFile bool `toml:"enable_file" env:"ENABLE_FILE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHub struct {
|
||||||
|
Token string `toml:"token" env:"TOKEN"`
|
||||||
|
}
|
||||||
|
465
internal/generator/plugin_generator.go
Normal file
465
internal/generator/plugin_generator.go
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginTemplate представляет шаблон плагина
|
||||||
|
type PluginTemplate struct {
|
||||||
|
PackageName string
|
||||||
|
FunctionName string
|
||||||
|
Repository string
|
||||||
|
GitHubRepo string
|
||||||
|
AssetPattern string
|
||||||
|
URLTemplate string
|
||||||
|
URLPattern string
|
||||||
|
VersionPrefix string
|
||||||
|
Schedule string
|
||||||
|
ScheduleCalls string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageInfo содержит информацию о пакете из alr.sh
|
||||||
|
type PackageInfo struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Sources []string
|
||||||
|
Description string
|
||||||
|
Homepage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectedPackage содержит автоматически определенную информацию о пакете
|
||||||
|
type DetectedPackage struct {
|
||||||
|
PackageInfo
|
||||||
|
GitHubRepo string
|
||||||
|
PackageType string
|
||||||
|
AssetPattern string
|
||||||
|
URLTemplate string
|
||||||
|
VersionPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginTemplateStr = `# Repository: {{.Repository}}
|
||||||
|
# Auto-generated plugin for {{.PackageName}}
|
||||||
|
|
||||||
|
REPO = "{{.Repository}}"
|
||||||
|
|
||||||
|
def check_{{.FunctionName}}():
|
||||||
|
"""Проверка обновлений для {{.PackageName}} с автоматическим обновлением checksums"""
|
||||||
|
package_name = "{{.PackageName}}"
|
||||||
|
|
||||||
|
# Обновляем репозиторий перед проверкой
|
||||||
|
updater.pull(REPO)
|
||||||
|
|
||||||
|
# Получение текущей версии из пакета
|
||||||
|
current_content = updater.get_package_file(REPO, package_name, "alr.sh")
|
||||||
|
# Универсальное распознавание: без кавычек, одинарные кавычки, двойные кавычки
|
||||||
|
current_version_matches = regex.find(r"version=['\"]?([0-9.]+)['\"]?", current_content)
|
||||||
|
|
||||||
|
if not current_version_matches or len(current_version_matches) == 0:
|
||||||
|
log.error("Не удалось найти текущую версию для " + package_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
current_version = current_version_matches[0]
|
||||||
|
|
||||||
|
# Проверка новой версии через GitHub API
|
||||||
|
api_url = "https://api.github.com/repos/{{.GitHubRepo}}/releases/latest"
|
||||||
|
response = http.get(api_url)
|
||||||
|
if not response:
|
||||||
|
log.error("Не удалось получить ответ от GitHub API для " + package_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
release_data = response.json()
|
||||||
|
if not release_data or "tag_name" not in release_data:
|
||||||
|
log.error("Нет данных о релизе для " + package_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Извлекаем версию из тега{{if .VersionPrefix}} (убираем '{{.VersionPrefix}}' если есть){{end}}
|
||||||
|
tag_name = release_data["tag_name"]
|
||||||
|
{{if .VersionPrefix}}new_version = tag_name[{{len .VersionPrefix}}:] if tag_name.startswith("{{.VersionPrefix}}") else tag_name{{else}}new_version = tag_name[1:] if tag_name.startswith("v") else tag_name{{end}}
|
||||||
|
|
||||||
|
# Сравнение версий
|
||||||
|
if new_version != current_version:
|
||||||
|
log.info("Обнаружено обновление " + package_name + ": " + current_version + " -> " + new_version)
|
||||||
|
|
||||||
|
{{if .URLTemplate}}# URL для скачивания по шаблону
|
||||||
|
download_url = "{{.URLTemplate}}".replace("{tag}", tag_name).replace("{version}", new_version){{else if .AssetPattern}}# Находим URL для файла по паттерну
|
||||||
|
download_url = ""
|
||||||
|
if "assets" in release_data:
|
||||||
|
for asset in release_data["assets"]:
|
||||||
|
asset_matches = regex.find(r"{{.AssetPattern}}", asset["name"])
|
||||||
|
if asset_matches and len(asset_matches) > 0:
|
||||||
|
download_url = asset["browser_download_url"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not download_url:
|
||||||
|
log.error("Не найден подходящий asset для " + package_name)
|
||||||
|
return{{else}}# Ищем первый подходящий asset
|
||||||
|
download_url = ""
|
||||||
|
if "assets" in release_data and len(release_data["assets"]) > 0:
|
||||||
|
download_url = release_data["assets"][0]["browser_download_url"]{{end}}
|
||||||
|
|
||||||
|
# Обновление версии в файле (сохраняем формат кавычек)
|
||||||
|
# Определяем какой формат используется
|
||||||
|
if regex.find(r"version='[0-9.]+'", current_content):
|
||||||
|
# Одинарные кавычки
|
||||||
|
new_content = regex.replace(
|
||||||
|
current_content,
|
||||||
|
r"version='[0-9.]+'",
|
||||||
|
"version='" + new_version + "'"
|
||||||
|
)
|
||||||
|
elif regex.find(r'version="[0-9.]+"', current_content):
|
||||||
|
# Двойные кавычки
|
||||||
|
new_content = regex.replace(
|
||||||
|
current_content,
|
||||||
|
r'version="[0-9.]+"',
|
||||||
|
'version="' + new_version + '"'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Без кавычек
|
||||||
|
new_content = regex.replace(
|
||||||
|
current_content,
|
||||||
|
r"version=[0-9.]+",
|
||||||
|
"version=" + new_version
|
||||||
|
)
|
||||||
|
|
||||||
|
{{if .URLPattern}}# Обновление URL загрузки в файле
|
||||||
|
new_content = regex.replace(
|
||||||
|
new_content,
|
||||||
|
r"{{.URLPattern}}",
|
||||||
|
download_url
|
||||||
|
){{end}}
|
||||||
|
|
||||||
|
# Вычисление новой хеш-суммы
|
||||||
|
log.info("Вычисление SHA256 для " + download_url)
|
||||||
|
new_checksum = checksum.calculate_sha256(download_url)
|
||||||
|
log.info("Новая хеш-сумма: " + new_checksum)
|
||||||
|
|
||||||
|
# Обновление файла с новыми checksums
|
||||||
|
updater.update_checksums(
|
||||||
|
REPO,
|
||||||
|
package_name,
|
||||||
|
"alr.sh",
|
||||||
|
new_content,
|
||||||
|
[new_checksum]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Коммит и push изменений
|
||||||
|
updater.push_changes(REPO, package_name + ": обновление до версии " + new_version)
|
||||||
|
|
||||||
|
log.info("Пакет " + package_name + " успешно обновлён до версии " + new_version + " с новой хеш-суммой")
|
||||||
|
else:
|
||||||
|
log.info("Пакет " + package_name + " уже имеет последнюю версию: " + current_version)
|
||||||
|
|
||||||
|
# {{.Schedule}}
|
||||||
|
{{.ScheduleCalls}}`
|
||||||
|
|
||||||
|
// PluginGenerator представляет генератор плагинов
|
||||||
|
type PluginGenerator struct {
|
||||||
|
config *config.Config
|
||||||
|
template *template.Template
|
||||||
|
reposDir string
|
||||||
|
pluginsDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginGenerator создает новый генератор плагинов
|
||||||
|
func NewPluginGenerator(cfg *config.Config, pluginsDir string) (*PluginGenerator, error) {
|
||||||
|
tmpl, err := template.New("plugin").Parse(pluginTemplateStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PluginGenerator{
|
||||||
|
config: cfg,
|
||||||
|
template: tmpl,
|
||||||
|
reposDir: cfg.ReposBaseDir,
|
||||||
|
pluginsDir: pluginsDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanRepository сканирует репозиторий и находит пакеты без плагинов
|
||||||
|
func (pg *PluginGenerator) ScanRepository(repoName string) ([]DetectedPackage, error) {
|
||||||
|
repoPath := filepath.Join(pg.reposDir, repoName)
|
||||||
|
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("repository directory not found: %s", repoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packages []DetectedPackage
|
||||||
|
|
||||||
|
err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем файлы alr.sh
|
||||||
|
if info.Name() == "alr.sh" {
|
||||||
|
packageDir := filepath.Dir(path)
|
||||||
|
packageName := filepath.Base(packageDir)
|
||||||
|
|
||||||
|
// Пропускаем если плагин уже существует
|
||||||
|
pluginFile := filepath.Join(pg.pluginsDir, packageName+".star")
|
||||||
|
if _, err := os.Stat(pluginFile); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Анализируем пакет
|
||||||
|
pkgInfo, err := pg.analyzePackage(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to analyze package %s: %v\n", packageName, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
detected := pg.detectPackageType(packageName, *pkgInfo)
|
||||||
|
if detected.GitHubRepo != "" {
|
||||||
|
packages = append(packages, detected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return packages, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzePackage анализирует файл alr.sh пакета
|
||||||
|
func (pg *PluginGenerator) analyzePackage(alrFile string) (*PackageInfo, error) {
|
||||||
|
content, err := os.ReadFile(alrFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &PackageInfo{
|
||||||
|
Name: filepath.Base(filepath.Dir(alrFile)),
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// Извлекаем версию
|
||||||
|
if strings.HasPrefix(line, "version=") {
|
||||||
|
versionMatch := regexp.MustCompile(`version=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
|
||||||
|
if len(versionMatch) > 1 {
|
||||||
|
info.Version = versionMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем источники
|
||||||
|
if strings.Contains(line, "github.com") || strings.Contains(line, "https://") {
|
||||||
|
urlMatch := regexp.MustCompile(`https://[^\s"']+`).FindAllString(line, -1)
|
||||||
|
info.Sources = append(info.Sources, urlMatch...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем описание
|
||||||
|
if strings.HasPrefix(line, "desc=") {
|
||||||
|
descMatch := regexp.MustCompile(`desc=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
|
||||||
|
if len(descMatch) > 1 {
|
||||||
|
info.Description = descMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем homepage
|
||||||
|
if strings.HasPrefix(line, "homepage=") {
|
||||||
|
homepageMatch := regexp.MustCompile(`homepage=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
|
||||||
|
if len(homepageMatch) > 1 {
|
||||||
|
info.Homepage = homepageMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectPackageType определяет тип пакета и GitHub репозиторий
|
||||||
|
func (pg *PluginGenerator) detectPackageType(packageName string, info PackageInfo) DetectedPackage {
|
||||||
|
detected := DetectedPackage{
|
||||||
|
PackageInfo: info,
|
||||||
|
PackageType: "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Анализируем источники для GitHub репозиториев
|
||||||
|
githubRegex := regexp.MustCompile(`github\.com/([^/]+/[^/]+)`)
|
||||||
|
|
||||||
|
for _, source := range info.Sources {
|
||||||
|
if match := githubRegex.FindStringSubmatch(source); len(match) > 1 {
|
||||||
|
detected.GitHubRepo = match[1]
|
||||||
|
detected.PackageType = "github_release"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не найдено в источниках, проверяем homepage
|
||||||
|
if detected.GitHubRepo == "" && info.Homepage != "" {
|
||||||
|
if match := githubRegex.FindStringSubmatch(info.Homepage); len(match) > 1 {
|
||||||
|
detected.GitHubRepo = match[1]
|
||||||
|
detected.PackageType = "github_release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем паттерны для разных типов пакетов
|
||||||
|
if strings.HasSuffix(packageName, "-bin") {
|
||||||
|
detected = pg.detectBinaryPackage(packageName, detected)
|
||||||
|
} else if strings.HasSuffix(packageName, "-git") {
|
||||||
|
detected.PackageType = "git_commits"
|
||||||
|
} else {
|
||||||
|
detected = pg.detectSourcePackage(packageName, detected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectBinaryPackage определяет паттерны для бинарных пакетов
|
||||||
|
func (pg *PluginGenerator) detectBinaryPackage(packageName string, detected DetectedPackage) DetectedPackage {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(packageName, "telegram"):
|
||||||
|
detected.AssetPattern = `tsetup\..*\.tar\.xz`
|
||||||
|
detected.URLTemplate = "https://github.com/telegramdesktop/tdesktop/releases/download/{tag}/tsetup.{version}.tar.xz"
|
||||||
|
case strings.Contains(packageName, "discord"):
|
||||||
|
detected.AssetPattern = `.*\.tar\.gz$`
|
||||||
|
case strings.Contains(packageName, "obsidian"):
|
||||||
|
detected.AssetPattern = `.*\.AppImage$`
|
||||||
|
case strings.Contains(packageName, "electron"):
|
||||||
|
detected.AssetPattern = `.*-linux-x64\.zip$`
|
||||||
|
case strings.Contains(packageName, "vscode") || strings.Contains(packageName, "code"):
|
||||||
|
detected.AssetPattern = `.*\.tar\.gz$`
|
||||||
|
default:
|
||||||
|
// Общие паттерны для бинарных пакетов
|
||||||
|
detected.AssetPattern = `.*linux.*\.(tar\.gz|tar\.xz|zip|AppImage)$`
|
||||||
|
}
|
||||||
|
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectSourcePackage определяет паттерны для исходных пакетов
|
||||||
|
func (pg *PluginGenerator) detectSourcePackage(packageName string, detected DetectedPackage) DetectedPackage {
|
||||||
|
// Для большинства исходных пакетов используем архив тега
|
||||||
|
detected.URLTemplate = "https://github.com/{repo}/archive/{tag}.tar.gz"
|
||||||
|
|
||||||
|
// Специальные случаи
|
||||||
|
switch packageName {
|
||||||
|
case "md4c":
|
||||||
|
detected.VersionPrefix = "release-"
|
||||||
|
case "steamcmd":
|
||||||
|
detected.PackageType = "http_lastmodified"
|
||||||
|
}
|
||||||
|
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePlugin генерирует плагин для пакета
|
||||||
|
func (pg *PluginGenerator) GeneratePlugin(detected DetectedPackage, repoName string) error {
|
||||||
|
functionName := strings.ReplaceAll(strings.ReplaceAll(detected.Name, "-", "_"), ".", "_")
|
||||||
|
|
||||||
|
schedule := pg.determineSchedule(detected)
|
||||||
|
scheduleCalls := pg.generateScheduleCalls(functionName, schedule)
|
||||||
|
|
||||||
|
data := PluginTemplate{
|
||||||
|
PackageName: detected.Name,
|
||||||
|
FunctionName: functionName,
|
||||||
|
Repository: repoName,
|
||||||
|
GitHubRepo: detected.GitHubRepo,
|
||||||
|
AssetPattern: detected.AssetPattern,
|
||||||
|
URLTemplate: detected.URLTemplate,
|
||||||
|
URLPattern: pg.generateURLPattern(detected),
|
||||||
|
VersionPrefix: detected.VersionPrefix,
|
||||||
|
Schedule: schedule,
|
||||||
|
ScheduleCalls: scheduleCalls,
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginFile := filepath.Join(pg.pluginsDir, detected.Name+".star")
|
||||||
|
file, err := os.Create(pluginFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create plugin file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = pg.template.Execute(file, data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Generated plugin: %s\n", pluginFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineSchedule определяет расписание проверки обновлений
|
||||||
|
func (pg *PluginGenerator) determineSchedule(detected DetectedPackage) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(detected.Name, "-bin"):
|
||||||
|
return "Запуск проверки каждые 6 часов"
|
||||||
|
case strings.Contains(detected.Name, "lib") || strings.Contains(detected.Name, "cmake"):
|
||||||
|
return "Запуск проверки каждую неделю"
|
||||||
|
default:
|
||||||
|
return "Запуск проверки каждый день"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateScheduleCalls генерирует вызовы функций расписания
|
||||||
|
func (pg *PluginGenerator) generateScheduleCalls(functionName, schedule string) string {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(schedule, "6 часов"):
|
||||||
|
return fmt.Sprintf("run_every.hour(check_%s, 6)", functionName)
|
||||||
|
case strings.Contains(schedule, "день"):
|
||||||
|
return fmt.Sprintf("run_every.day(check_%s)", functionName)
|
||||||
|
case strings.Contains(schedule, "неделю"):
|
||||||
|
return fmt.Sprintf("run_every.week(check_%s)", functionName)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("run_every.day(check_%s)", functionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateURLPattern генерирует regex паттерн для обновления URL в файле
|
||||||
|
func (pg *PluginGenerator) generateURLPattern(detected DetectedPackage) string {
|
||||||
|
if detected.URLTemplate != "" {
|
||||||
|
return "" // URL pattern будет сгенерирован автоматически
|
||||||
|
}
|
||||||
|
|
||||||
|
// Базовые паттерны для разных типов
|
||||||
|
switch detected.GitHubRepo {
|
||||||
|
case "telegramdesktop/tdesktop":
|
||||||
|
return `https://github\.com/telegramdesktop/tdesktop/releases/download/[^/]+/tsetup\.[0-9.]+\.tar\.xz`
|
||||||
|
case "obsidianmd/obsidian-releases":
|
||||||
|
return `https://github\.com/obsidianmd/obsidian-releases/releases/download/[^/]+/Obsidian-[0-9.]+\.AppImage`
|
||||||
|
default:
|
||||||
|
return `https://github\.com/` + strings.ReplaceAll(detected.GitHubRepo, "/", `\/`) + `/releases/download/[^/]+/.*`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAllPlugins сканирует все репозитории и генерирует недостающие плагины
|
||||||
|
func (pg *PluginGenerator) GenerateAllPlugins() error {
|
||||||
|
var totalGenerated int
|
||||||
|
|
||||||
|
for repoName := range pg.config.Repositories {
|
||||||
|
fmt.Printf("🔍 Сканирование репозитория %s...\n", repoName)
|
||||||
|
|
||||||
|
packages, err := pg.ScanRepository(repoName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to scan repository %s: %v\n", repoName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Найдено %d пакетов без плагинов\n", len(packages))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg.GitHubRepo == "" {
|
||||||
|
fmt.Printf("⚠️ Пропущен %s: GitHub репозиторий не определен\n", pkg.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pg.GeneratePlugin(pkg, repoName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ Ошибка генерации плагина для %s: %v\n", pkg.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
totalGenerated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n🎉 Всего сгенерировано %d плагинов\n", totalGenerated)
|
||||||
|
return nil
|
||||||
|
}
|
149
internal/logger/file_logger.go
Normal file
149
internal/logger/file_logger.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* ALR Updater - Automated updater bot for ALR packages
|
||||||
|
* 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 logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RotatingFileWriter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
file *os.File
|
||||||
|
filename string
|
||||||
|
maxSize int64
|
||||||
|
currentSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRotatingFileWriter(filename string, maxSize int64) (*RotatingFileWriter, error) {
|
||||||
|
dir := filepath.Dir(filename)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rfw := &RotatingFileWriter{
|
||||||
|
filename: filename,
|
||||||
|
maxSize: maxSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rfw.openFile(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rfw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rfw *RotatingFileWriter) openFile() error {
|
||||||
|
file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return fmt.Errorf("failed to stat log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rfw.file = file
|
||||||
|
rfw.currentSize = info.Size()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rfw *RotatingFileWriter) rotate() error {
|
||||||
|
if rfw.file != nil {
|
||||||
|
rfw.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переименовываем текущий файл с временной меткой
|
||||||
|
backupName := fmt.Sprintf("%s.%s", rfw.filename, time.Now().Format("20060102-150405"))
|
||||||
|
if err := os.Rename(rfw.filename, backupName); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to rotate log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новый файл
|
||||||
|
if err := rfw.openFile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем старые резервные копии (оставляем только последние 5)
|
||||||
|
pattern := rfw.filename + ".*"
|
||||||
|
matches, _ := filepath.Glob(pattern)
|
||||||
|
if len(matches) > 5 {
|
||||||
|
// Сортировка не требуется, так как имена файлов содержат временную метку
|
||||||
|
for i := 0; i < len(matches)-5; i++ {
|
||||||
|
os.Remove(matches[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rfw *RotatingFileWriter) Write(p []byte) (n int, err error) {
|
||||||
|
rfw.mu.Lock()
|
||||||
|
defer rfw.mu.Unlock()
|
||||||
|
|
||||||
|
// Проверяем, нужна ли ротация
|
||||||
|
if rfw.currentSize+int64(len(p)) > rfw.maxSize {
|
||||||
|
if err := rfw.rotate(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = rfw.file.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rfw.currentSize += int64(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rfw *RotatingFileWriter) Close() error {
|
||||||
|
rfw.mu.Lock()
|
||||||
|
defer rfw.mu.Unlock()
|
||||||
|
|
||||||
|
if rfw.file != nil {
|
||||||
|
return rfw.file.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiWriter объединяет вывод в несколько writers
|
||||||
|
type MultiWriter struct {
|
||||||
|
writers []io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiWriter(writers ...io.Writer) *MultiWriter {
|
||||||
|
return &MultiWriter{writers: writers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MultiWriter) Write(p []byte) (n int, err error) {
|
||||||
|
for _, w := range mw.writers {
|
||||||
|
n, err = w.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
153
main.go
153
main.go
@@ -36,12 +36,16 @@ import (
|
|||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/builtins"
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/builtins"
|
||||||
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
|
||||||
|
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/generator"
|
||||||
|
filelogger "gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/logger"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
"go.starlark.net/starlark"
|
"go.starlark.net/starlark"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var fileWriter *filelogger.RotatingFileWriter
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.Logger = logger.NewPretty(os.Stderr)
|
log.Logger = logger.NewPretty(os.Stderr)
|
||||||
}
|
}
|
||||||
@@ -76,6 +80,23 @@ func parseRepositoryFromPlugin(filePath string) (string, error) {
|
|||||||
return "", scanner.Err()
|
return "", scanner.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixRepoPermissions рекурсивно устанавливает права 775 для директорий и 664 для файлов
|
||||||
|
func fixRepoPermissions(path string) error {
|
||||||
|
return filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// Устанавливаем права 2775 для директорий (setgid)
|
||||||
|
return os.Chmod(filePath, 0o2775)
|
||||||
|
} else {
|
||||||
|
// Устанавливаем права 664 для файлов
|
||||||
|
return os.Chmod(filePath, 0o664)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configPath := pflag.StringP("config", "c", "/etc/alr-updater/config.toml", "Path to config file")
|
configPath := pflag.StringP("config", "c", "/etc/alr-updater/config.toml", "Path to config file")
|
||||||
dbPath := pflag.StringP("database", "d", "/var/lib/alr-updater/db", "Path to database file")
|
dbPath := pflag.StringP("database", "d", "/var/lib/alr-updater/db", "Path to database file")
|
||||||
@@ -85,6 +106,7 @@ func main() {
|
|||||||
useEnv := pflag.BoolP("use-env", "E", false, "Use environment variables for configuration")
|
useEnv := pflag.BoolP("use-env", "E", false, "Use environment variables for configuration")
|
||||||
debug := pflag.BoolP("debug", "D", false, "Enable debug logging")
|
debug := pflag.BoolP("debug", "D", false, "Enable debug logging")
|
||||||
runNow := pflag.BoolP("now", "n", false, "Run all plugin checks immediately on startup")
|
runNow := pflag.BoolP("now", "n", false, "Run all plugin checks immediately on startup")
|
||||||
|
generatePlugins := pflag.Bool("generate-plugins", false, "Generate missing plugins automatically")
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
if *debug {
|
if *debug {
|
||||||
@@ -105,14 +127,10 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bbolt.Open(*dbPath, 0o644, nil)
|
// Загружаем конфигурацию сначала для генератора плагинов
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error opening database").Err(err).Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
if *useEnv {
|
if *useEnv {
|
||||||
err = env.Parse(cfg)
|
err := env.Parse(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error parsing environment variables").Err(err).Send()
|
log.Fatal("Error parsing environment variables").Err(err).Send()
|
||||||
}
|
}
|
||||||
@@ -125,10 +143,73 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error decoding config file").Err(err).Send()
|
log.Fatal("Error decoding config file").Err(err).Send()
|
||||||
}
|
}
|
||||||
err = fl.Close()
|
fl.Close()
|
||||||
if err != nil {
|
}
|
||||||
log.Fatal("Error closing config file").Err(err).Send()
|
|
||||||
|
// Настройка логирования в файл
|
||||||
|
if cfg.Logging.EnableFile {
|
||||||
|
logFile := cfg.Logging.LogFile
|
||||||
|
if logFile == "" {
|
||||||
|
logFile = "/var/log/alr-updater.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxSize := cfg.Logging.MaxSize
|
||||||
|
if maxSize == 0 {
|
||||||
|
maxSize = 100 * 1024 * 1024 // 100 MB по умолчанию
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
fileWriter, err = filelogger.NewRotatingFileWriter(logFile, maxSize)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create file logger, continuing with stderr only").Err(err).Send()
|
||||||
|
} else {
|
||||||
|
// Создаем MultiWriter для вывода в stderr и файл одновременно
|
||||||
|
multiWriter := filelogger.NewMultiWriter(os.Stderr, fileWriter)
|
||||||
|
log.Logger = logger.NewPretty(multiWriter)
|
||||||
|
log.Info("File logging enabled").Str("file", logFile).Int64("maxSize", maxSize).Send()
|
||||||
|
|
||||||
|
// Закрываем файл при завершении
|
||||||
|
defer func() {
|
||||||
|
if fileWriter != nil {
|
||||||
|
fileWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка генерации плагинов
|
||||||
|
if *generatePlugins {
|
||||||
|
log.Info("Starting automatic plugin generation...")
|
||||||
|
gen, err := generator.NewPluginGenerator(cfg, *pluginDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error creating plugin generator").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gen.GenerateAllPlugins()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error generating plugins").Err(err).Send()
|
||||||
|
}
|
||||||
|
log.Info("Plugin generation completed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для базы данных, если её нет
|
||||||
|
dbDir := filepath.Dir(*dbPath)
|
||||||
|
if _, err := os.Stat(dbDir); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(dbDir, 0o2775)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error creating database directory").
|
||||||
|
Err(err).
|
||||||
|
Str("path", dbDir).
|
||||||
|
Str("hint", "Run as root or create directory manually: sudo mkdir -p "+dbDir+" && sudo chown root:wheel "+dbDir+" && sudo chmod 2775 "+dbDir).
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
log.Info("Created database directory").Str("path", dbDir).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := bbolt.Open(*dbPath, 0o644, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error opening database").Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем базовый каталог для репозиториев
|
// Создаем базовый каталог для репозиториев
|
||||||
@@ -137,10 +218,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(cfg.ReposBaseDir); os.IsNotExist(err) {
|
if _, err := os.Stat(cfg.ReposBaseDir); os.IsNotExist(err) {
|
||||||
err = os.MkdirAll(cfg.ReposBaseDir, 0o755)
|
err = os.MkdirAll(cfg.ReposBaseDir, 0o2775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error creating repositories base directory").Err(err).Send()
|
log.Fatal("Error creating repositories base directory").
|
||||||
|
Err(err).
|
||||||
|
Str("path", cfg.ReposBaseDir).
|
||||||
|
Str("hint", "Run as root or create directory manually: sudo mkdir -p "+cfg.ReposBaseDir+" && sudo chown root:wheel "+cfg.ReposBaseDir+" && sudo chmod 2775 "+cfg.ReposBaseDir).
|
||||||
|
Send()
|
||||||
}
|
}
|
||||||
|
log.Info("Created repositories base directory").Str("path", cfg.ReposBaseDir).Send()
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatal("Cannot stat configured repos base directory").Err(err).Send()
|
log.Fatal("Cannot stat configured repos base directory").Err(err).Send()
|
||||||
}
|
}
|
||||||
@@ -152,12 +238,12 @@ func main() {
|
|||||||
if _, err := os.Stat(repoDir); os.IsNotExist(err) {
|
if _, err := os.Stat(repoDir); os.IsNotExist(err) {
|
||||||
log.Info("Cloning repository").Str("name", repoName).Str("url", repoConfig.RepoURL).Send()
|
log.Info("Cloning repository").Str("name", repoName).Str("url", repoConfig.RepoURL).Send()
|
||||||
|
|
||||||
err = os.MkdirAll(repoDir, 0o755)
|
err = os.MkdirAll(repoDir, 0o2775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error creating repository directory").Str("repo", repoName).Err(err).Send()
|
log.Fatal("Error creating repository directory").Str("repo", repoName).Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := git.PlainClone(repoDir, false, &git.CloneOptions{
|
repo, err := git.PlainClone(repoDir, false, &git.CloneOptions{
|
||||||
URL: repoConfig.RepoURL,
|
URL: repoConfig.RepoURL,
|
||||||
Progress: os.Stderr,
|
Progress: os.Stderr,
|
||||||
})
|
})
|
||||||
@@ -165,11 +251,39 @@ func main() {
|
|||||||
log.Fatal("Error cloning repository").Str("repo", repoName).Err(err).Send()
|
log.Fatal("Error cloning repository").Str("repo", repoName).Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Настраиваем Git для корректной работы с правами доступа
|
||||||
|
gitConfig, err := repo.Config()
|
||||||
|
if err == nil {
|
||||||
|
gitConfig.Raw.Section("core").SetOption("sharedRepository", "group")
|
||||||
|
err = repo.SetConfig(gitConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to set Git sharedRepository config").Str("repo", repoName).Err(err).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исправляем права доступа после клонирования
|
||||||
|
if err := fixRepoPermissions(repoDir); err != nil {
|
||||||
|
log.Error("Error fixing repository permissions").Str("repo", repoName).Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("Repository cloned successfully").Str("name", repoName).Send()
|
log.Info("Repository cloned successfully").Str("name", repoName).Send()
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatal("Cannot stat repository directory").Str("repo", repoName).Err(err).Send()
|
log.Fatal("Cannot stat repository directory").Str("repo", repoName).Err(err).Send()
|
||||||
} else {
|
} else {
|
||||||
log.Info("Repository already exists").Str("name", repoName).Send()
|
log.Info("Repository already exists").Str("name", repoName).Send()
|
||||||
|
|
||||||
|
// Настраиваем Git конфигурацию для существующих репозиториев
|
||||||
|
repo, err := git.PlainOpen(repoDir)
|
||||||
|
if err == nil {
|
||||||
|
gitConfig, err := repo.Config()
|
||||||
|
if err == nil {
|
||||||
|
gitConfig.Raw.Section("core").SetOption("sharedRepository", "group")
|
||||||
|
err = repo.SetConfig(gitConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to set Git sharedRepository config").Str("repo", repoName).Err(err).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +292,19 @@ func main() {
|
|||||||
log.Fatal("No repositories configured. At least one repository is required.").Send()
|
log.Fatal("No repositories configured. At least one repository is required.").Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для плагинов, если её нет
|
||||||
|
if _, err := os.Stat(*pluginDir); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(*pluginDir, 0o2775)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error creating plugin directory").
|
||||||
|
Err(err).
|
||||||
|
Str("path", *pluginDir).
|
||||||
|
Str("hint", "Run as root or create directory manually: sudo mkdir -p "+*pluginDir+" && sudo chown root:wheel "+*pluginDir+" && sudo chmod 2775 "+*pluginDir).
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
log.Info("Created plugin directory").Str("path", *pluginDir).Send()
|
||||||
|
}
|
||||||
|
|
||||||
starFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star"))
|
starFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error finding plugin files").Err(err).Send()
|
log.Fatal("Error finding plugin files").Err(err).Send()
|
||||||
|
132
scripts/install.sh
Normal file
132
scripts/install.sh
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}ALR Updater Installation Script${NC}"
|
||||||
|
echo "==============================="
|
||||||
|
|
||||||
|
# Проверка прав root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo -e "${RED}This script must be run as root${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Переменные
|
||||||
|
BINARY_PATH="/usr/local/bin/alr-updater"
|
||||||
|
SERVICE_NAME="alr-updater"
|
||||||
|
SERVICE_USER="alr-updater"
|
||||||
|
SERVICE_GROUP="wheel"
|
||||||
|
CONFIG_DIR="/etc/alr-updater"
|
||||||
|
DATA_DIR="/var/lib/alr-updater"
|
||||||
|
CACHE_DIR="/var/cache/alr-updater"
|
||||||
|
PLUGIN_DIR="${CONFIG_DIR}/plugins"
|
||||||
|
LOG_FILE="/var/log/alr-updater.log"
|
||||||
|
|
||||||
|
# Создание пользователя и добавление в группу wheel
|
||||||
|
echo -e "${YELLOW}Creating user and adding to wheel group...${NC}"
|
||||||
|
if ! id -u ${SERVICE_USER} >/dev/null 2>&1; then
|
||||||
|
useradd -r -s /bin/false -d /var/lib/${SERVICE_USER} -G wheel ${SERVICE_USER}
|
||||||
|
echo -e "${GREEN}User ${SERVICE_USER} created and added to wheel group${NC}"
|
||||||
|
else
|
||||||
|
# Добавляем существующего пользователя в группу wheel
|
||||||
|
usermod -a -G wheel ${SERVICE_USER}
|
||||||
|
echo -e "${GREEN}User ${SERVICE_USER} already exists, added to wheel group${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создание директорий
|
||||||
|
echo -e "${YELLOW}Creating directories...${NC}"
|
||||||
|
mkdir -p ${CONFIG_DIR}
|
||||||
|
mkdir -p ${DATA_DIR}
|
||||||
|
mkdir -p ${CACHE_DIR}
|
||||||
|
mkdir -p ${PLUGIN_DIR}
|
||||||
|
|
||||||
|
# Создание файла лога
|
||||||
|
echo -e "${YELLOW}Creating log file...${NC}"
|
||||||
|
touch ${LOG_FILE}
|
||||||
|
chown ${SERVICE_USER}:${SERVICE_GROUP} ${LOG_FILE}
|
||||||
|
chmod 664 ${LOG_FILE}
|
||||||
|
|
||||||
|
# Установка прав доступа с setgid битом
|
||||||
|
echo -e "${YELLOW}Setting permissions with setgid...${NC}"
|
||||||
|
chown -R root:${SERVICE_GROUP} ${DATA_DIR}
|
||||||
|
chown -R root:${SERVICE_GROUP} ${CACHE_DIR}
|
||||||
|
chown -R root:${SERVICE_GROUP} ${CONFIG_DIR}
|
||||||
|
chmod 2775 ${CONFIG_DIR}
|
||||||
|
chmod 2775 ${PLUGIN_DIR}
|
||||||
|
chmod 2775 ${DATA_DIR}
|
||||||
|
chmod 2775 ${CACHE_DIR}
|
||||||
|
|
||||||
|
# Копирование бинарника
|
||||||
|
if [ -f "./alr-updater" ]; then
|
||||||
|
echo -e "${YELLOW}Installing binary...${NC}"
|
||||||
|
cp ./alr-updater ${BINARY_PATH}
|
||||||
|
chmod 755 ${BINARY_PATH}
|
||||||
|
echo -e "${GREEN}Binary installed to ${BINARY_PATH}${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Binary not found in current directory, skipping binary installation${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создание примера конфигурации, если не существует
|
||||||
|
if [ ! -f "${CONFIG_DIR}/config.toml" ]; then
|
||||||
|
if [ -f "./alr-updater.example.toml" ]; then
|
||||||
|
echo -e "${YELLOW}Creating example configuration...${NC}"
|
||||||
|
cp ./alr-updater.example.toml ${CONFIG_DIR}/config.toml
|
||||||
|
chown root:${SERVICE_GROUP} ${CONFIG_DIR}/config.toml
|
||||||
|
chmod 640 ${CONFIG_DIR}/config.toml
|
||||||
|
echo -e "${GREEN}Configuration created at ${CONFIG_DIR}/config.toml${NC}"
|
||||||
|
echo -e "${YELLOW}Please edit the configuration file before starting the service${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создание systemd service файла
|
||||||
|
echo -e "${YELLOW}Creating systemd service...${NC}"
|
||||||
|
cat > /etc/systemd/system/${SERVICE_NAME}.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=ALR Updater Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${SERVICE_USER}
|
||||||
|
Group=${SERVICE_GROUP}
|
||||||
|
ExecStart=${BINARY_PATH}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=${SERVICE_NAME}
|
||||||
|
|
||||||
|
# Безопасность
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=${DATA_DIR} ${CACHE_DIR} ${LOG_FILE}
|
||||||
|
ReadOnlyPaths=${CONFIG_DIR}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Перезагрузка systemd
|
||||||
|
echo -e "${YELLOW}Reloading systemd...${NC}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Включение сервиса
|
||||||
|
echo -e "${YELLOW}Enabling service...${NC}"
|
||||||
|
systemctl enable ${SERVICE_NAME}.service
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Installation completed!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Edit configuration: nano ${CONFIG_DIR}/config.toml"
|
||||||
|
echo "2. Add plugins to: ${PLUGIN_DIR}/"
|
||||||
|
echo "3. Start service: systemctl start ${SERVICE_NAME}"
|
||||||
|
echo "4. Check status: systemctl status ${SERVICE_NAME}"
|
||||||
|
echo "5. View logs: journalctl -u ${SERVICE_NAME} -f"
|
Reference in New Issue
Block a user