10 Commits

Author SHA1 Message Date
e8202060d8 chore: remove debug slog.Warn
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 26s
Create Release / changelog (push) Successful in 2m54s
2025-06-22 17:27:57 +03:00
c4a92c67d4 fix parsing overrides
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m34s
Update alr-git / changelog (push) Successful in 29s
2025-06-22 12:44:21 +03:00
85878f69d3 feat: add checksum for torrent downloader
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 28s
2025-06-20 20:12:43 +03:00
6bccce1db4 feat: add checksum for git downloader 2025-06-20 19:35:22 +03:00
b5474b1eb4 ci: disable building alr-bin
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 7m5s
Update alr-git / changelog (push) Successful in 30s
2025-06-20 09:21:03 +03:00
51fdea781b fix: correct pull for multiple repos 2025-06-20 09:08:34 +03:00
4c1f2ea90f feat: support mirrors
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m45s
Update alr-git / changelog (push) Successful in 25s
2025-06-19 19:00:08 +03:00
7fa7f8ba82 security: update vulnerable packages
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m11s
Update alr-git / changelog (push) Successful in 29s
Vulnerabilities detected by Trivy scan:
- github.com/cloudflare/circl (GHSA-2x5j-vhc8-9cwm)
2025-06-19 12:10:31 +03:00
25d001c1c9 fix: add find-files (#109)
All checks were successful
Update alr-git / changelog (push) Successful in 31s
closes #96

Reviewed-on: #109
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-19 09:03:37 +00:00
f86b3003b1 fix: add symlink handling in createFirejailedBinary (#108)
All checks were successful
Update alr-git / changelog (push) Successful in 30s
closes #107

Reviewed-on: #108
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-17 18:56:19 +00:00
39 changed files with 2232 additions and 581 deletions

View File

@ -84,31 +84,31 @@ jobs:
sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh
sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh
- name: Install alr # - name: Install alr
run: | # run: |
make install # make install
#
# # temporary fix
# groupadd wheel
# usermod -aG wheel root
# temporary fix # - name: Build packages
groupadd wheel # run: |
usermod -aG wheel root # SCRIPT_PATH=alr-default/alr-bin/alr.sh
# ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH"
- name: Build packages # - name: Upload assets
run: | # uses: akkuman/gitea-release-action@v1
SCRIPT_PATH=alr-default/alr-bin/alr.sh # with:
ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" # body: ${{ steps.changes.outputs.changes }}
ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" # files: |-
ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH" # alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \
ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH" # alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \
# alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \
- name: Upload assets # alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
uses: akkuman/gitea-release-action@v1
with:
body: ${{ steps.changes.outputs.changes }}
files: |-
alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \
alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \
alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \
alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
- name: Commit changes - name: Commit changes
run: | run: |

View File

@ -21,6 +21,7 @@ build: check-no-root $(BIN)
export CGO_ENABLED := 0 export CGO_ENABLED := 0
$(BIN): $(BIN):
go generate ./...
go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@ go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@
check-no-root: check-no-root:

View File

@ -11,7 +11,7 @@
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="33.5" y="14">coverage</text> <text x="33.5" y="14">coverage</text>
<text x="86" y="15" fill="#010101" fill-opacity=".3">19.0%</text> <text x="86" y="15" fill="#010101" fill-opacity=".3">19.7%</text>
<text x="86" y="14">19.0%</text> <text x="86" y="14">19.7%</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

@ -0,0 +1,53 @@
// 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/>.
//go:build e2e
package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
)
func TestE2EIssue78Mirrors(t *testing.T) {
dockerMultipleRun(
t,
"issue-78-mirrors",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "set-url", REPO_NAME_FOR_E2E_TESTS, "https://example.com")
execShouldNoError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "clear", REPO_NAME_FOR_E2E_TESTS)
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", "--partial", REPO_NAME_FOR_E2E_TESTS, "gitea.plemya-x.ru/Maks1mS")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
},
)
}

View File

@ -0,0 +1,251 @@
// 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 main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"reflect"
"strings"
"text/template"
)
func resolvedStructGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
type {{ .EntityNameLower }}Resolved struct {
{{ .StructFields }}
}
`))
var structFieldsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
// Поле с типом
fieldTypeStr := exprToString(field.Type)
// Структура поля
var buf bytes.Buffer
buf.WriteString("\t")
buf.WriteString(name.Name)
buf.WriteString(" ")
buf.WriteString(fieldTypeStr)
// Обработка json-тега
jsonTag := ""
if field.Tag != nil {
raw := strings.Trim(field.Tag.Value, "`")
tag := reflect.StructTag(raw)
if val := tag.Get("json"); val != "" {
jsonTag = val
}
}
if jsonTag == "" {
jsonTag = strings.ToLower(name.Name)
}
buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag))
buf.WriteString("\n")
structFieldsBuilder.Write(buf.Bytes())
}
}
params := struct {
EntityNameLower string
StructFields string
}{
EntityNameLower: "package",
StructFields: structFieldsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func toResolvedFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func {{ .EntityName }}ToResolved(src *{{ .EntityName }}) {{ .EntityNameLower }}Resolved {
return {{ .EntityNameLower }}Resolved{
{{ .Assignments }}
}
}
`))
var assignmentsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
var assignBuf bytes.Buffer
assignBuf.WriteString("\t\t")
assignBuf.WriteString(name.Name)
assignBuf.WriteString(": ")
if isOverridableField(field.Type) {
assignBuf.WriteString(fmt.Sprintf("src.%s.Resolved()", name.Name))
} else {
assignBuf.WriteString(fmt.Sprintf("src.%s", name.Name))
}
assignBuf.WriteString(",\n")
assignmentsBuilder.Write(assignBuf.Bytes())
}
}
params := struct {
EntityName string
EntityNameLower string
Assignments string
}{
EntityName: "Package",
EntityNameLower: "package",
Assignments: assignmentsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func resolveFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func Resolve{{ .EntityName }}(pkg *{{ .EntityName }}, overrides []string) {
{{.Code}}}
`))
var codeBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
if isOverridableField(field.Type) {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("\t\tpkg.%s.Resolve(overrides)\n", name.Name))
codeBuilder.Write(buf.Bytes())
}
}
}
params := struct {
EntityName string
Code string
}{
EntityName: "Package",
Code: codeBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func main() {
path := os.Getenv("GOFILE")
if path == "" {
log.Fatal("GOFILE must be set")
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Fatalf("parsing file: %v", err)
}
entityName := "Package" // имя структуры, которую анализируем
found := false
fields := make([]*ast.Field, 0)
// Ищем структуру с нужным именем
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec := spec.(*ast.TypeSpec)
if typeSpec.Name.Name != entityName {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
fields = structType.Fields.List
found = true
}
}
if !found {
log.Fatalf("struct %s not found", entityName)
}
var buf bytes.Buffer
buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n")
buf.WriteString("package alrsh")
resolvedStructGenerator(&buf, fields)
toResolvedFuncGenerator(&buf, fields)
resolveFuncGenerator(&buf, fields)
// Форматируем вывод
formatted, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("formatting: %v", err)
}
outPath := strings.TrimSuffix(path, ".go") + "_gen.go"
outFile, err := os.Create(outPath)
if err != nil {
log.Fatalf("create file: %v", err)
}
_, err = outFile.Write(formatted)
if err != nil {
log.Fatalf("writing output: %v", err)
}
outFile.Close()
}
func exprToString(expr ast.Expr) string {
if t, ok := expr.(*ast.IndexExpr); ok {
if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "OverridableField" {
return exprToString(t.Index) // T
}
}
var buf bytes.Buffer
if err := format.Node(&buf, token.NewFileSet(), expr); err != nil {
return "<invalid>"
}
return buf.String()
}
func isOverridableField(expr ast.Expr) bool {
indexExpr, ok := expr.(*ast.IndexExpr)
if !ok {
return false
}
ident, ok := indexExpr.X.(*ast.Ident)
return ok && ident.Name == "OverridableField"
}

7
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/PuerkitoBio/purell v1.2.0 github.com/PuerkitoBio/purell v1.2.0
github.com/alecthomas/chroma/v2 v2.9.1 github.com/alecthomas/chroma/v2 v2.9.1
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/caarlos0/env v3.5.0+incompatible github.com/caarlos0/env v3.5.0+incompatible
github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/bubbletea v1.2.4
@ -17,12 +18,12 @@ require (
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0 github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0
github.com/go-git/go-billy/v5 v5.6.0 github.com/go-git/go-billy/v5 v5.6.0
github.com/go-git/go-git/v5 v5.13.0 github.com/go-git/go-git/v5 v5.13.0
github.com/goccy/go-yaml v1.18.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/goreleaser/nfpm/v2 v2.41.0 github.com/goreleaser/nfpm/v2 v2.41.0
github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-plugin v1.6.3 github.com/hashicorp/go-plugin v1.6.3
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08
github.com/jmoiron/sqlx v1.3.5
github.com/leonelquinteros/gotext v1.7.0 github.com/leonelquinteros/gotext v1.7.0
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mholt/archiver/v4 v4.0.0-alpha.8
@ -38,7 +39,6 @@ require (
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0 golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0 modernc.org/sqlite v1.25.0
mvdan.cc/sh/v3 v3.10.0 mvdan.cc/sh/v3 v3.10.0
xorm.io/xorm v1.3.9 xorm.io/xorm v1.3.9
@ -62,7 +62,7 @@ require (
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/connesc/cipherio v0.2.1 // indirect github.com/connesc/cipherio v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/creack/pty v1.1.24 // indirect github.com/creack/pty v1.1.24 // indirect
@ -139,6 +139,7 @@ require (
google.golang.org/grpc v1.58.3 // indirect google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect

15
go.sum
View File

@ -67,6 +67,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8=
github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY=
@ -102,8 +104,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
@ -156,13 +158,14 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -251,8 +254,6 @@ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3m
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -281,9 +282,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8= github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8=
github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw= github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
@ -302,7 +300,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=

44
info.go
View File

@ -23,10 +23,10 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/goccy/go-yaml"
"github.com/jeandeaual/go-locale" "github.com/jeandeaual/go-locale"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
@ -121,35 +121,27 @@ func InfoCmd() *cli.Command {
systemLang = "en" systemLang = "en"
} }
if !all { info, err := distro.ParseOSRelease(ctx)
info, err := distro.ParseOSRelease(ctx) if err != nil {
if err != nil { return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err)
return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err) }
} names, err = overrides.Resolve(
names, err = overrides.Resolve( info,
info, overrides.DefaultOpts.
overrides.DefaultOpts. WithLanguages([]string{systemLang}),
WithLanguages([]string{systemLang}), )
) if err != nil {
if err != nil { return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
}
} }
for _, pkg := range pkgs { for _, pkg := range pkgs {
if !all { alrsh.ResolvePackage(&pkg, names)
alrsh.ResolvePackage(&pkg, names) view := alrsh.NewPackageView(pkg)
err = yaml.NewEncoder(os.Stdout).Encode(pkg) view.Resolved = !all
if err != nil { err = yaml.NewEncoder(os.Stdout, yaml.UseJSONMarshaler(), yaml.OmitEmpty()).Encode(view)
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err) if err != nil {
} return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
} else {
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
}
} }
fmt.Println("---") fmt.Println("---")
} }

View File

@ -367,7 +367,7 @@ func (b *Builder) BuildPackage(
} }
slog.Debug("ViewScript") slog.Debug("ViewScript")
slog.Debug("", "varsOfPackages", varsOfPackages) slog.Debug("", "varsOfPackages", varsOfPackages[0])
err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg) err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -28,7 +28,6 @@ import (
"github.com/goreleaser/nfpm/v2/files" "github.com/goreleaser/nfpm/v2/files"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/osutils"
"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/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
) )
@ -51,6 +50,92 @@ var binaryDirectories = []string{
"/usr/local/bin/", "/usr/local/bin/",
} }
func moveWithSymlinkHandling(src, dst string) error {
srcInfo, err := os.Lstat(src)
if err != nil {
return fmt.Errorf("failed to get source info: %w", err)
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
if srcInfo.Mode()&os.ModeSymlink != 0 {
return moveSymlink(src, dst)
}
if err := os.Rename(src, dst); err != nil {
return copyAndRemove(src, dst)
}
return nil
}
func moveSymlink(src, dst string) error {
target, err := os.Readlink(src)
if err != nil {
return fmt.Errorf("failed to read symlink: %w", err)
}
if err := os.Symlink(target, dst); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
if err := os.Remove(src); err != nil {
os.Remove(dst)
return fmt.Errorf("failed to remove original symlink: %w", err)
}
return nil
}
func copyAndRemove(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source: %w", err)
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination: %w", err)
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return fmt.Errorf("failed to copy content: %w", err)
}
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("failed to get source stats: %w", err)
}
if err := dstFile.Chmod(srcInfo.Mode()); err != nil {
return fmt.Errorf("failed to set permissions: %w", err)
}
if err := os.Remove(src); err != nil {
return fmt.Errorf("failed to remove source: %w", err)
}
return nil
}
func moveFileWithErrorHandling(src, dst string) error {
err := moveWithSymlinkHandling(src, dst)
if err != nil {
if os.IsPermission(err) {
return fmt.Errorf("permission denied: %w", err)
}
if os.IsNotExist(err) {
return fmt.Errorf("source file does not exist: %w", err)
}
return fmt.Errorf("failed to move file: %w", err)
}
return nil
}
func applyFirejailIntegration( func applyFirejailIntegration(
vars *alrsh.Package, vars *alrsh.Package,
dirs types.Directories, dirs types.Directories,
@ -143,12 +228,15 @@ func createFirejailedBinary(
return nil, err return nil, err
} }
if err := osutils.Move(content.Source, filepath.Join(dirs.PkgDir, origFilePath)); err != nil { if err := moveFileWithErrorHandling(filepath.Join(dirs.PkgDir, content.Destination), filepath.Join(dirs.PkgDir, origFilePath)); err != nil {
return nil, fmt.Errorf("failed to move original binary: %w", err) return nil, fmt.Errorf("failed to move original binary: %w", err)
} }
content.Type = "file"
content.Source = filepath.Join(dirs.PkgDir, content.Destination)
// Create wrapper script // Create wrapper script
if err := createWrapperScript(content.Source, origFilePath, dest); err != nil { if err := createWrapperScript(filepath.Join(dirs.PkgDir, content.Destination), origFilePath, dest); err != nil {
return nil, fmt.Errorf("failed to create wrapper script: %w", err) return nil, fmt.Errorf("failed to create wrapper script: %w", err)
} }

View File

@ -138,7 +138,10 @@ func TestCreateFirejailedBinary(t *testing.T) {
os.MkdirAll(pkgDir, 0o755) os.MkdirAll(pkgDir, 0o755)
os.MkdirAll(scriptDir, 0o755) os.MkdirAll(scriptDir, 0o755)
srcBinary := filepath.Join(tmpDir, "test-binary") binDir := filepath.Join(pkgDir, "usr", "bin")
os.MkdirAll(binDir, 0o755)
srcBinary := filepath.Join(binDir, "test-binary")
os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755) os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755)
defaultProfile := filepath.Join(scriptDir, "default.profile") defaultProfile := filepath.Join(scriptDir, "default.profile")
@ -154,7 +157,7 @@ func TestCreateFirejailedBinary(t *testing.T) {
content := &files.Content{ content := &files.Content{
Source: srcBinary, Source: srcBinary,
Destination: "./usr/bin/test-binary", Destination: "/usr/bin/test-binary",
Type: "file", Type: "file",
} }
@ -172,7 +175,10 @@ func TestCreateFirejailedBinary(t *testing.T) {
os.MkdirAll(pkgDir, 0o755) os.MkdirAll(pkgDir, 0o755)
os.MkdirAll(scriptDir, 0o755) os.MkdirAll(scriptDir, 0o755)
srcBinary := filepath.Join(tmpDir, "special-binary") binDir := filepath.Join(pkgDir, "usr", "bin")
os.MkdirAll(binDir, 0o755)
srcBinary := filepath.Join(binDir, "special-binary")
os.WriteFile(srcBinary, []byte("#!/bin/bash\necho special"), 0o755) os.WriteFile(srcBinary, []byte("#!/bin/bash\necho special"), 0o755)
defaultProfile := filepath.Join(scriptDir, "default.profile") defaultProfile := filepath.Join(scriptDir, "default.profile")
@ -191,7 +197,7 @@ func TestCreateFirejailedBinary(t *testing.T) {
content := &files.Content{ content := &files.Content{
Source: srcBinary, Source: srcBinary,
Destination: "./usr/bin/special-binary", Destination: "/usr/bin/special-binary",
Type: "file", Type: "file",
} }

View File

@ -23,8 +23,8 @@ import (
"os" "os"
"strings" "strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dl" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type SourceDownloader struct { type SourceDownloader struct {

View File

@ -65,6 +65,8 @@ func (a *HCLoggerAdapter) Log(level hclog.Level, msg string, args ...interface{}
var chLogLevel chLog.Level var chLogLevel chLog.Level
if msg == "plugin process exited" || if msg == "plugin process exited" ||
strings.HasPrefix(msg, "[ERR] plugin: stream copy 'stderr' error") || strings.HasPrefix(msg, "[ERR] plugin: stream copy 'stderr' error") ||
strings.HasPrefix(msg, "[WARN] error closing client during Kill") ||
strings.HasPrefix(msg, "[WARN] plugin failed to exit gracefully") ||
strings.HasPrefix(msg, "[DEBUG] plugin") { strings.HasPrefix(msg, "[DEBUG] plugin") {
chLogLevel = chLog.DebugLevel chLogLevel = chLog.DebugLevel
} else { } else {

View File

@ -31,7 +31,6 @@ import (
"strings" "strings"
"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config" gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@ -69,159 +68,219 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error {
} }
for _, repo := range repos { for _, repo := range repos {
repoURL, err := url.Parse(repo.URL) err := rs.pullRepo(ctx, repo)
if err != nil { if err != nil {
return err return err
} }
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name) return nil
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name) }
var repoFS billy.Filesystem func (rs *Repos) pullRepo(ctx context.Context, repo types.Repo) error {
gitDir := filepath.Join(repoDir, ".git") urls := []string{repo.URL}
// Only pull repos that contain valid git repos urls = append(urls, repo.Mirrors...)
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{ var lastErr error
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
w, err := r.Worktree() for i, repoURL := range urls {
if err != nil { if i > 0 {
return err slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL)
}
old, err := r.Head()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
if old.Hash() == *revHash {
slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() {
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
}
} else {
err = os.RemoveAll(repoDir)
if err != nil {
return err
}
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
return err
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return err
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoURL.String()},
})
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
repoFS = osfs.New(repoDir)
} }
fl, err := repoFS.Open("alr-repo.toml") err := rs.pullRepoFromURL(ctx, repoURL, repo)
if err != nil { if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name) lastErr = err
slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err)
continue continue
} }
var repoCfg types.RepoConfig // Success
err = toml.NewDecoder(fl).Decode(&repoCfg) return nil
}
return fmt.Errorf("failed to pull repository %s from any URL: %w", repo.Name, lastErr)
}
func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) {
gitDir := filepath.Join(repoDir, ".git")
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err == nil {
err = updateRemoteURL(r, repoUrl)
if err == nil {
_, err := r.Head()
if err == nil {
return r, false, nil
}
if errors.Is(err, plumbing.ErrReferenceNotFound) {
return r, true, nil
}
slog.Debug("error getting HEAD, reinitializing...", "err", err)
}
}
slog.Debug("error while reading repo, reinitializing...", "err", err)
}
if err := os.RemoveAll(repoDir); err != nil {
return nil, false, fmt.Errorf("failed to remove repo directory: %w", err)
}
if err := os.MkdirAll(repoDir, 0o755); err != nil {
return nil, false, fmt.Errorf("failed to create repo directory: %w", err)
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return nil, false, fmt.Errorf("failed to initialize git repo: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoUrl},
})
if err != nil {
return nil, false, err
}
return r, true, nil
}
func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo types.Repo) error {
repoURL, err := url.Parse(rawRepoUrl)
if err != nil {
return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err)
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name)
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name)
var repoFS billy.Filesystem
r, freshGit, err := readGitRepo(repoDir, repoURL.String())
if err != nil {
return fmt.Errorf("failed to open repo")
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
var old *plumbing.Reference
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
if !freshGit {
old, err = r.Head()
if err != nil { if err != nil {
return err return err
} }
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version. if old.Hash() == *revHash {
// It may be "unknown" or a git version, but either way, there's no way slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
} }
} }
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() || freshGit {
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
}
fl, err := repoFS.Open("alr-repo.toml")
if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name)
return nil
}
var repoCfg types.RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
if err != nil {
return err
}
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version.
// It may be "unknown" or a git version, but either way, there's no way
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
}
return nil
}
func updateRemoteURL(r *git.Repository, newURL string) error {
cfg, err := r.Config()
if err != nil {
return err
}
remote, ok := cfg.Remotes[git.DefaultRemoteName]
if !ok || len(remote.URLs) == 0 {
return fmt.Errorf("no remote '%s' found", git.DefaultRemoteName)
}
currentURL := remote.URLs[0]
if currentURL == newURL {
return nil
}
slog.Debug("Updating remote URL", "old", currentURL, "new", newURL)
err = r.DeleteRemote(git.DefaultRemoteName)
if err != nil {
return fmt.Errorf("failed to delete old remote: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{newURL},
})
if err != nil {
return fmt.Errorf("failed to create new remote: %w", err)
}
return nil return nil
} }

View File

@ -26,7 +26,6 @@ import (
"testing" "testing"
"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/db"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/repos" "gitea.plemya-x.ru/Plemya-x/ALR/internal/repos"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@ -35,7 +34,7 @@ import (
type TestEnv struct { type TestEnv struct {
Ctx context.Context Ctx context.Context
Cfg *TestALRConfig Cfg *TestALRConfig
Db *db.Database Db *database.Database
} }
type TestALRConfig struct { type TestALRConfig struct {

View File

@ -23,7 +23,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"reflect" "reflect"
"strings" "strings"
@ -75,75 +74,99 @@ func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
// DecodeVar decodes a variable to val using reflection. // DecodeVar decodes a variable to val using reflection.
// Structs should use the "sh" struct tag. // Structs should use the "sh" struct tag.
func (d *Decoder) DecodeVar(name string, val any) error { func (d *Decoder) DecodeVar(name string, val any) error {
variable := d.getVar(name) origType := reflect.TypeOf(val).Elem()
if variable == nil { isOverridableField := strings.Contains(origType.String(), "OverridableField[")
return VarNotFoundError{name}
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ if !isOverridableField {
WeaklyTypedInput: true, variable := d.getVarNoOverrides(name)
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { if variable == nil {
if strings.Contains(to.Type().String(), "alrsh.OverridableField") { return VarNotFoundError{name}
if to.Kind() != reflect.Ptr && to.CanAddr() { }
to = to.Addr()
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
if err != nil { WeaklyTypedInput: true,
return nil, err Result: val, // передаем указатель на новое значение
} TagName: "sh",
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
isNotSet := true if from.Kind() == reflect.Slice && to.Kind() == reflect.String {
s, ok := from.Interface().([]string)
setMethod := to.MethodByName("Set") if ok && len(s) == 1 {
setResolvedMethod := to.MethodByName("SetResolved") return s[0], nil
for _, varName := range names {
val := d.getVarNoOverrides(varName)
if val == nil {
continue
} }
t := setMethod.Type().In(1)
newVal := from
if !newVal.Type().AssignableTo(t) {
newVal = reflect.New(t)
err = d.DecodeVar(name, newVal.Interface())
if err != nil {
return nil, err
}
newVal = newVal.Elem()
}
if isNotSet {
setResolvedMethod.Call([]reflect.Value{newVal})
}
override := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
setMethod.Call([]reflect.Value{reflect.ValueOf(override), newVal})
} }
return from.Interface(), nil
}),
})
if err != nil {
return err
}
return to, nil switch variable.Kind {
case expand.Indexed:
return dec.Decode(variable.List)
case expand.Associative:
return dec.Decode(variable.Map)
default:
return dec.Decode(variable.Str)
}
} else {
vars := d.getVarsByPrefix(name)
if len(vars) == 0 {
return VarNotFoundError{name}
}
reflectVal := reflect.ValueOf(val)
overridableVal := reflect.ValueOf(val).Elem()
dataField := overridableVal.FieldByName("data")
if !dataField.IsValid() {
return fmt.Errorf("data field not found in OverridableField")
}
mapType := dataField.Type() // map[string]T
elemType := mapType.Elem() // T
var overridablePtr reflect.Value
if reflectVal.Kind() == reflect.Ptr {
overridablePtr = reflectVal
} else {
if !reflectVal.CanAddr() {
return fmt.Errorf("OverridableField value is not addressable")
} }
return from.Interface(), nil overridablePtr = reflectVal.Addr()
}), }
Result: val,
TagName: "sh",
})
if err != nil {
slog.Warn("err", "err", err)
return err
}
switch variable.Kind { setValue := overridablePtr.MethodByName("Set")
case expand.Indexed: if !setValue.IsValid() {
return dec.Decode(variable.List) return fmt.Errorf("method Set not found on OverridableField")
case expand.Associative: }
return dec.Decode(variable.Map)
default: for _, v := range vars {
return dec.Decode(variable.Str) varName := v.Name
key := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
newVal := reflect.New(elemType)
if err := d.DecodeVar(varName, newVal.Interface()); err != nil {
return err
}
keyValue := reflect.ValueOf(key)
setValue.Call([]reflect.Value{keyValue, newVal.Elem()})
}
resolveValue := overridablePtr.MethodByName("Resolve")
if !resolveValue.IsValid() {
return fmt.Errorf("method Resolve not found on OverridableField")
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts)
if err != nil {
return err
}
resolveValue.Call([]reflect.Value{reflect.ValueOf(names)})
return nil
} }
} }
@ -284,23 +307,6 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
return nil return nil
} }
// getVar gets a variable based on its name, taking into account
// override variables and nameref variables.
func (d *Decoder) getVar(name string) *expand.Variable {
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil
}
for _, varName := range names {
res := d.getVarNoOverrides(varName)
if res != nil {
return res
}
}
return nil
}
func (d *Decoder) getVarNoOverrides(name string) *expand.Variable { func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
val, ok := d.Runner.Vars[name] val, ok := d.Runner.Vars[name]
if ok { if ok {
@ -318,6 +324,32 @@ func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
return nil return nil
} }
type vars struct {
Name string
Value *expand.Variable
}
func (d *Decoder) getVarsByPrefix(prefix string) []*vars {
result := make([]*vars, 0)
for name, val := range d.Runner.Vars {
if !strings.HasPrefix(name, prefix) {
continue
}
switch prefix {
case "auto_req":
if strings.HasPrefix(name, "auto_req_skiplist") {
continue
}
case "auto_prov":
if strings.HasPrefix(name, "auto_prov_skiplist") {
continue
}
}
result = append(result, &vars{name, &val})
}
return result
}
func IsTruthy(value string) bool { func IsTruthy(value string) bool {
value = strings.ToLower(strings.TrimSpace(value)) value = strings.ToLower(strings.TrimSpace(value))
return value == "true" || value == "yes" || value == "1" return value == "true" || value == "yes" || value == "1"

View File

@ -32,24 +32,25 @@ import (
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"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"
) )
type BuildVars struct { type BuildVars struct {
Name string `sh:"name,required"` Name string `sh:"name,required"`
Version string `sh:"version,required"` Version string `sh:"version,required"`
Release int `sh:"release,required"` Release int `sh:"release,required"`
Epoch uint `sh:"epoch"` Epoch uint `sh:"epoch"`
Description string `sh:"desc"` Description alrsh.OverridableField[string] `sh:"desc"`
Homepage string `sh:"homepage"` Homepage string `sh:"homepage"`
Maintainer string `sh:"maintainer"` Maintainer string `sh:"maintainer"`
Architectures []string `sh:"architectures"` Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"` Licenses []string `sh:"license"`
Provides []string `sh:"provides"` Provides []string `sh:"provides"`
Conflicts []string `sh:"conflicts"` Conflicts []string `sh:"conflicts"`
Depends []string `sh:"deps"` Depends []string `sh:"deps"`
BuildDepends []string `sh:"build_deps"` BuildDepends alrsh.OverridableField[[]string] `sh:"build_deps"`
Replaces []string `sh:"replaces"` Replaces alrsh.OverridableField[[]string] `sh:"replaces"`
} }
const testScript = ` const testScript = `
@ -113,22 +114,34 @@ func TestDecodeVars(t *testing.T) {
} }
expected := BuildVars{ expected := BuildVars{
Name: "test", Name: "test",
Version: "0.0.1", Version: "0.0.1",
Release: 1, Release: 1,
Epoch: 2, Epoch: 2,
Description: "Test package", Description: alrsh.OverridableFromMap(map[string]string{
"": "Test package",
}),
Homepage: "https://gitea.plemya-x.ru/xpamych/ALR", Homepage: "https://gitea.plemya-x.ru/xpamych/ALR",
Maintainer: "Евгений Храмов <xpamych@yandex.ru>", Maintainer: "Евгений Храмов <xpamych@yandex.ru>",
Architectures: []string{"arm64", "amd64"}, Architectures: []string{"arm64", "amd64"},
Licenses: []string{"GPL-3.0-or-later"}, Licenses: []string{"GPL-3.0-or-later"},
Provides: []string{"test"}, Provides: []string{"test"},
Conflicts: []string{"test"}, Conflicts: []string{"test"},
Replaces: []string{"test-legacy"}, Replaces: alrsh.OverridableFromMap(map[string][]string{
Depends: []string{"sudo"}, "": {"test-old"},
BuildDepends: []string{"go"}, "test_os": {"test-legacy"},
}),
Depends: []string{"sudo"},
BuildDepends: alrsh.OverridableFromMap(map[string][]string{
"": {"golang"},
"arch": {"go"},
}),
} }
expected.Description.SetResolved("Test package")
expected.Replaces.SetResolved([]string{"test-legacy"})
expected.BuildDepends.SetResolved([]string{"go"})
if !reflect.DeepEqual(bv, expected) { if !reflect.DeepEqual(bv, expected) {
t.Errorf("Expected %v, got %v", expected, bv) t.Errorf("Expected %v, got %v", expected, bv)
} }

View File

@ -0,0 +1,178 @@
// 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 helpers
import (
"fmt"
"log/slog"
"os"
"path"
"path/filepath"
"github.com/bmatcuk/doublestar/v4"
"mvdan.cc/sh/v3/interp"
)
func matchNamePattern(name, pattern string) bool {
matched, err := filepath.Match(pattern, name)
if err != nil {
return false
}
return matched
}
func validateDir(dirPath, commandName string) error {
info, err := os.Stat(dirPath)
if err != nil {
return fmt.Errorf("%s: %w", commandName, err)
}
if !info.IsDir() {
return fmt.Errorf("%s: %s is not a directory", commandName, dirPath)
}
return nil
}
func outputFiles(hc interp.HandlerContext, files []string) {
for _, file := range files {
fmt.Fprintln(hc.Stdout, file)
}
}
func makeRelativePath(basePath, fullPath string) (string, error) {
relPath, err := filepath.Rel(basePath, fullPath)
if err != nil {
return "", err
}
return "./" + relPath, nil
}
func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*.mo"
if len(args) > 0 {
namePattern = args[0] + ".mo"
}
localePath := "./usr/share/locale/"
realPath := path.Join(hc.Dir, localePath)
if err := validateDir(realPath, "files-find-lang"); err != nil {
return err
}
var langFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
langFiles = append(langFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
outputFiles(hc, langFiles)
return nil
}
func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
docPath := "./usr/share/doc/"
docRealPath := path.Join(hc.Dir, docPath)
if err := validateDir(docRealPath, "files-find-doc"); err != nil {
return err
}
var docFiles []string
entries, err := os.ReadDir(docRealPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
for _, entry := range entries {
if matchNamePattern(entry.Name(), namePattern) {
targetPath := filepath.Join(docRealPath, entry.Name())
targetInfo, err := os.Stat(targetPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
if targetInfo.IsDir() {
err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
if subErr != nil {
return subErr
}
relPath, err := makeRelativePath(hc.Dir, subPath)
if err != nil {
return err
}
docFiles = append(docFiles, relPath)
return nil
})
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
}
}
}
outputFiles(hc, docFiles)
return nil
}
func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
if len(args) == 0 {
return fmt.Errorf("find-files: at least one glob pattern is required")
}
var foundFiles []string
for _, globPattern := range args {
searchPath := path.Join(hc.Dir, globPattern)
basepath, pattern := doublestar.SplitPattern(searchPath)
fsys := os.DirFS(basepath)
matches, err := doublestar.Glob(fsys, pattern, doublestar.WithNoFollow())
if err != nil {
slog.Warn("find-files: invalid glob pattern", "pattern", globPattern, "error", err)
continue
}
for _, match := range matches {
relPath, err := makeRelativePath(hc.Dir, path.Join(basepath, match))
if err != nil {
continue
}
foundFiles = append(foundFiles, relPath)
}
}
outputFiles(hc, foundFiles)
return nil
}

View File

@ -24,7 +24,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -57,6 +56,7 @@ var Helpers = handlers.ExecFuncs{
"install-library": installLibraryCmd, "install-library": installLibraryCmd,
"git-version": gitVersionCmd, "git-version": gitVersionCmd,
"files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd, "files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd, "files-find-doc": filesFindDocCmd,
} }
@ -65,6 +65,7 @@ var Helpers = handlers.ExecFuncs{
// that don't modify any state // that don't modify any state
var Restricted = handlers.ExecFuncs{ var Restricted = handlers.ExecFuncs{
"git-version": gitVersionCmd, "git-version": gitVersionCmd,
"files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd, "files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd, "files-find-doc": filesFindDocCmd,
} }
@ -265,114 +266,6 @@ func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
return nil return nil
} }
func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*.mo"
if len(args) > 0 {
namePattern = args[0] + ".mo"
}
localePath := "./usr/share/locale/"
realPath := path.Join(hc.Dir, localePath)
info, err := os.Stat(realPath)
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("files-find-lang: %s is not a directory", localePath)
}
var langFiles []string
err = filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := filepath.Rel(hc.Dir, p)
if relErr != nil {
return relErr
}
langFiles = append(langFiles, "./"+relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
for _, file := range langFiles {
fmt.Fprintln(hc.Stdout, file)
}
return nil
}
func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
docPath := "./usr/share/doc/"
docRealPath := path.Join(hc.Dir, docPath)
info, err := os.Stat(docRealPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("files-find-doc: %s is not a directory", docPath)
}
var docFiles []string
entries, err := os.ReadDir(docRealPath)
if err != nil {
return err
}
for _, entry := range entries {
if matchNamePattern(entry.Name(), namePattern) {
targetPath := filepath.Join(docRealPath, entry.Name())
targetInfo, err := os.Stat(targetPath)
if err != nil {
return err
}
if targetInfo.IsDir() {
err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
relPath, err := filepath.Rel(hc.Dir, subPath)
if err != nil {
return err
}
docFiles = append(docFiles, "./"+relPath)
return nil
})
if err != nil {
return err
}
}
}
}
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
for _, file := range docFiles {
fmt.Fprintln(hc.Stdout, file)
}
return nil
}
func matchNamePattern(name, pattern string) bool {
matched, err := filepath.Match(pattern, name)
if err != nil {
return false
}
return matched
}
func helperInstall(from, to string, perms os.FileMode) error { func helperInstall(from, to string, perms os.FileMode) error {
err := os.MkdirAll(filepath.Dir(to), 0o755) err := os.MkdirAll(filepath.Dir(to), 0o755)
if err != nil { if err != nil {

View File

@ -31,12 +31,18 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
) )
type symlink struct {
linkPath string
targetPath string
}
type testCase struct { type testCase struct {
name string name string
dirsToCreate []string dirsToCreate []string
filesToCreate []string filesToCreate []string
expectedOutput []string expectedOutput []string
args string symlinksToCreate []symlink
args string
} }
func TestFindFilesDoc(t *testing.T) { func TestFindFilesDoc(t *testing.T) {
@ -214,3 +220,94 @@ files-find-lang ` + tc.args
}) })
} }
} }
func TestFindFiles(t *testing.T) {
tests := []testCase{
{
name: "With file and dir symlinks",
dirsToCreate: []string{
"usr/share/locale/ru/LC_MESSAGES",
"usr/share/locale/tr/LC_MESSAGES",
"opt/app",
"opt/app/internal",
},
filesToCreate: []string{
"usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
"usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo",
"usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo",
"opt/app/internal/test",
},
symlinksToCreate: []symlink{
{
linkPath: "/opt/app/etc",
targetPath: "/etc",
},
},
expectedOutput: []string{
"./usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
"./usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo",
"./usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo",
"./opt/app/etc",
"./opt/app/internal",
"./opt/app/internal/test",
},
args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\" \"/opt/app/**/*\"",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test-files-find")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
for _, dir := range tc.dirsToCreate {
dirPath := filepath.Join(tempDir, dir)
err := os.MkdirAll(dirPath, 0o755)
assert.NoError(t, err)
}
for _, file := range tc.filesToCreate {
filePath := filepath.Join(tempDir, file)
err := os.WriteFile(filePath, []byte("test content"), 0o644)
assert.NoError(t, err)
}
for _, sl := range tc.symlinksToCreate {
linkFullPath := filepath.Join(tempDir, sl.linkPath)
targetFullPath := sl.targetPath
// make sure parent dir exists
err := os.MkdirAll(filepath.Dir(linkFullPath), 0o755)
assert.NoError(t, err)
err = os.Symlink(targetFullPath, linkFullPath)
assert.NoError(t, err)
}
helpers := handlers.ExecFuncs{
"files-find": filesFindCmd,
}
buf := &bytes.Buffer{}
runner, err := interp.New(
interp.Dir(tempDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
interp.ExecHandler(helpers.ExecHandler(interp.DefaultExecHandler(1000))),
)
assert.NoError(t, err)
scriptContent := `
shopt -s globstar
files-find ` + tc.args
script, err := syntax.NewParser().Parse(strings.NewReader(scriptContent), "")
assert.NoError(t, err)
err = runner.Run(context.Background(), script)
assert.NoError(t, err)
contents := strings.Fields(strings.TrimSpace(buf.String()))
assert.ElementsMatch(t, tc.expectedOutput, contents)
})
}
}

View File

@ -138,11 +138,11 @@ msgstr ""
msgid "Can't detect system language" msgid "Can't detect system language"
msgstr "" msgstr ""
#: info.go:135 #: info.go:134
msgid "Error resolving overrides" msgid "Error resolving overrides"
msgstr "" msgstr ""
#: info.go:144 info.go:149 #: info.go:143
msgid "Error encoding script variables" msgid "Error encoding script variables"
msgstr "" msgstr ""
@ -220,7 +220,7 @@ msgstr ""
msgid "AutoReq is not implemented for this package format, so it's skipped" msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr "" msgstr ""
#: internal/build/firejail.go:59 #: internal/build/firejail.go:144
msgid "Applying FireJail integration" msgid "Applying FireJail integration"
msgstr "" msgstr ""
@ -355,47 +355,31 @@ msgid ""
"Database version does not exist. Run alr fix if something isn't working." "Database version does not exist. Run alr fix if something isn't working."
msgstr "" msgstr ""
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr ""
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr ""
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr ""
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: internal/logger/log.go:41 #: internal/logger/log.go:41
msgid "ERROR" msgid "ERROR"
msgstr "" msgstr ""
#: internal/repos/pull.go:77 #: internal/repos/pull.go:88
msgid "Trying mirror"
msgstr ""
#: internal/repos/pull.go:94
msgid "Failed to pull from URL"
msgstr ""
#: internal/repos/pull.go:158
msgid "Pulling repository" msgid "Pulling repository"
msgstr "" msgstr ""
#: internal/repos/pull.go:113 #: internal/repos/pull.go:195
msgid "Repository up to date" msgid "Repository up to date"
msgstr "" msgstr ""
#: internal/repos/pull.go:204 #: internal/repos/pull.go:230
msgid "Git repository does not appear to be a valid ALR repo" msgid "Git repository does not appear to be a valid ALR repo"
msgstr "" msgstr ""
#: internal/repos/pull.go:220 #: internal/repos/pull.go:246
msgid "" msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try " "ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work." "updating ALR if something doesn't work."
@ -457,67 +441,132 @@ msgstr ""
msgid "Error while running app" msgid "Error while running app"
msgstr "" msgstr ""
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: pkg/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr ""
#: pkg/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr ""
#: pkg/dl/dl.go:222
msgid "Downloading source"
msgstr ""
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: refresh.go:30 #: refresh.go:30
msgid "Pull all repositories that have changed" msgid "Pull all repositories that have changed"
msgstr "" msgstr ""
#: repo.go:39 #: repo.go:41
msgid "Manage repos" msgid "Manage repos"
msgstr "" msgstr ""
#: repo.go:51 repo.go:269 #: repo.go:55 repo.go:625
msgid "Remove an existing repository" msgid "Remove an existing repository"
msgstr "" msgstr ""
#: repo.go:53 #: repo.go:57 repo.go:521
msgid "<name>" msgid "<name>"
msgstr "" msgstr ""
#: repo.go:83 #: repo.go:102 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist" msgid "Repo \"%s\" does not exist"
msgstr "" msgstr ""
#: repo.go:90 #: repo.go:109
msgid "Error removing repo directory" msgid "Error removing repo directory"
msgstr "" msgstr ""
#: repo.go:94 repo.go:161 repo.go:219 #: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config" msgid "Error saving config"
msgstr "" msgstr ""
#: repo.go:113 #: repo.go:132
msgid "Error removing packages from database" msgid "Error removing packages from database"
msgstr "" msgstr ""
#: repo.go:124 repo.go:239 #: repo.go:143 repo.go:595
msgid "Add a new repository" msgid "Add a new repository"
msgstr "" msgstr ""
#: repo.go:125 #: repo.go:144 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>" msgid "<name> <url>"
msgstr "" msgstr ""
#: repo.go:150 #: repo.go:169
msgid "Repo \"%s\" already exists" msgid "Repo \"%s\" already exists"
msgstr "" msgstr ""
#: repo.go:187 #: repo.go:206
msgid "Set the reference of the repository" msgid "Set the reference of the repository"
msgstr "" msgstr ""
#: repo.go:188 #: repo.go:207
msgid "<name> <ref>" msgid "<name> <ref>"
msgstr "" msgstr ""
#: repo.go:246 #: repo.go:269
msgid "Set the main url of the repository"
msgstr ""
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr ""
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr ""
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr ""
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr ""
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr ""
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr ""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr ""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr ""
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr ""
#: repo.go:602
msgid "Name of the new repo" msgid "Name of the new repo"
msgstr "" msgstr ""
#: repo.go:252 #: repo.go:608
msgid "URL of the new repo" msgid "URL of the new repo"
msgstr "" msgstr ""
#: repo.go:276 #: repo.go:632
msgid "Name of the repo to be deleted" msgid "Name of the repo to be deleted"
msgstr "" msgstr ""

View File

@ -5,15 +5,15 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: unnamed project\n" "Project-Id-Version: unnamed project\n"
"PO-Revision-Date: 2025-06-15 16:05+0300\n" "PO-Revision-Date: 2025-06-19 18:54+0300\n"
"Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru\n" "Language: ru\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 48.0\n" "X-Generator: Gtranslator 48.0\n"
#: build.go:42 #: build.go:42
@ -145,11 +145,11 @@ msgstr "Ошибка при поиске пакетов"
msgid "Can't detect system language" msgid "Can't detect system language"
msgstr "Ошибка при определении языка системы" msgstr "Ошибка при определении языка системы"
#: info.go:135 #: info.go:134
msgid "Error resolving overrides" msgid "Error resolving overrides"
msgstr "Ошибка устранения переорпеделений" msgstr "Ошибка устранения переорпеделений"
#: info.go:144 info.go:149 #: info.go:143
msgid "Error encoding script variables" msgid "Error encoding script variables"
msgstr "Ошибка кодирования переменных скрита" msgstr "Ошибка кодирования переменных скрита"
@ -231,7 +231,7 @@ msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr "" msgstr ""
"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено"
#: internal/build/firejail.go:59 #: internal/build/firejail.go:144
msgid "Applying FireJail integration" msgid "Applying FireJail integration"
msgstr "Применение интеграции FireJail" msgstr "Применение интеграции FireJail"
@ -356,8 +356,8 @@ msgid ""
"This command is deprecated and would be removed in the future, use \"%s\" " "This command is deprecated and would be removed in the future, use \"%s\" "
"instead!" "instead!"
msgstr "" msgstr ""
"Эта команда устарела и будет удалена в будущем, используйте вместо нее " "Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s"
"\"%s\"!" "\"!"
#: internal/db/db.go:76 #: internal/db/db.go:76
msgid "Database version mismatch; resetting" msgid "Database version mismatch; resetting"
@ -369,47 +369,31 @@ msgid ""
msgstr "" msgstr ""
"Версия базы данных не существует. Запустите alr fix, если что-то не работает." "Версия базы данных не существует. Запустите alr fix, если что-то не работает."
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr "Скачивание источника"
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: internal/logger/log.go:41 #: internal/logger/log.go:41
msgid "ERROR" msgid "ERROR"
msgstr "ОШИБКА" msgstr "ОШИБКА"
#: internal/repos/pull.go:77 #: internal/repos/pull.go:88
msgid "Trying mirror"
msgstr "Пробую зеркало"
#: internal/repos/pull.go:94
msgid "Failed to pull from URL"
msgstr "Не удалось извлечь из URL"
#: internal/repos/pull.go:158
msgid "Pulling repository" msgid "Pulling repository"
msgstr "Скачивание репозитория" msgstr "Скачивание репозитория"
#: internal/repos/pull.go:113 #: internal/repos/pull.go:195
msgid "Repository up to date" msgid "Repository up to date"
msgstr "Репозиторий уже обновлён" msgstr "Репозиторий уже обновлён"
#: internal/repos/pull.go:204 #: internal/repos/pull.go:230
msgid "Git repository does not appear to be a valid ALR repo" msgid "Git repository does not appear to be a valid ALR repo"
msgstr "Репозиторий Git не поддерживается репозиторием ALR" msgstr "Репозиторий Git не поддерживается репозиторием ALR"
#: internal/repos/pull.go:220 #: internal/repos/pull.go:246
msgid "" msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try " "ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work." "updating ALR if something doesn't work."
@ -473,67 +457,132 @@ msgstr "Показать справку"
msgid "Error while running app" msgid "Error while running app"
msgstr "Ошибка при запуске приложения" msgstr "Ошибка при запуске приложения"
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: pkg/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: pkg/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: pkg/dl/dl.go:222
msgid "Downloading source"
msgstr "Скачивание источника"
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: refresh.go:30 #: refresh.go:30
msgid "Pull all repositories that have changed" msgid "Pull all repositories that have changed"
msgstr "Скачать все изменённые репозитории" msgstr "Скачать все изменённые репозитории"
#: repo.go:39 #: repo.go:41
msgid "Manage repos" msgid "Manage repos"
msgstr "Управление репозиториями" msgstr "Управление репозиториями"
#: repo.go:51 repo.go:269 #: repo.go:55 repo.go:625
msgid "Remove an existing repository" msgid "Remove an existing repository"
msgstr "Удалить существующий репозиторий" msgstr "Удалить существующий репозиторий"
#: repo.go:53 #: repo.go:57 repo.go:521
msgid "<name>" msgid "<name>"
msgstr "<имя>" msgstr "<имя>"
#: repo.go:83 #: repo.go:102 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist" msgid "Repo \"%s\" does not exist"
msgstr "Репозитория \"%s\" не существует" msgstr "Репозитория \"%s\" не существует"
#: repo.go:90 #: repo.go:109
msgid "Error removing repo directory" msgid "Error removing repo directory"
msgstr "Ошибка при удалении каталога репозитория" msgstr "Ошибка при удалении каталога репозитория"
#: repo.go:94 repo.go:161 repo.go:219 #: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config" msgid "Error saving config"
msgstr "Ошибка при сохранении конфигурации" msgstr "Ошибка при сохранении конфигурации"
#: repo.go:113 #: repo.go:132
msgid "Error removing packages from database" msgid "Error removing packages from database"
msgstr "Ошибка при удалении пакетов из базы данных" msgstr "Ошибка при удалении пакетов из базы данных"
#: repo.go:124 repo.go:239 #: repo.go:143 repo.go:595
msgid "Add a new repository" msgid "Add a new repository"
msgstr "Добавить новый репозиторий" msgstr "Добавить новый репозиторий"
#: repo.go:125 #: repo.go:144 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>" msgid "<name> <url>"
msgstr "<имя> <url>" msgstr "<имя> <url>"
#: repo.go:150 #: repo.go:169
msgid "Repo \"%s\" already exists" msgid "Repo \"%s\" already exists"
msgstr "Репозиторий \"%s\" уже существует" msgstr "Репозиторий \"%s\" уже существует"
#: repo.go:187 #: repo.go:206
msgid "Set the reference of the repository" msgid "Set the reference of the repository"
msgstr "Установить ссылку на версию репозитория" msgstr "Установить ссылку на версию репозитория"
#: repo.go:188 #: repo.go:207
msgid "<name> <ref>" msgid "<name> <ref>"
msgstr "<имя> <ссылкааерсию>" msgstr "<имя> <ссылкааерсию>"
#: repo.go:246 #: repo.go:269
msgid "Set the main url of the repository"
msgstr "Установить главный URL репозитория"
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr "Управление зеркалами репозитория"
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr "Добавить зеркало репозитория"
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr "Удалить зеркало из репозитория"
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr "Игнорировать, если зеркала не существует"
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr "Соответствует частичному URL (например, github.com вместо полного URL)"
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr "В репозитории \"%s\" не найдено зеркал, содержащих \"%s\""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr "URL \"%s\" не существует в репозитории \"%s\""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr "Удалены зеркала %d из репозитория \"%s\"\n"
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr "Удалить все зеркала из репозитория"
#: repo.go:602
msgid "Name of the new repo" msgid "Name of the new repo"
msgstr "Название нового репозитория" msgstr "Название нового репозитория"
#: repo.go:252 #: repo.go:608
msgid "URL of the new repo" msgid "URL of the new repo"
msgstr "URL-адрес нового репозитория" msgstr "URL-адрес нового репозитория"
#: repo.go:276 #: repo.go:632
msgid "Name of the repo to be deleted" msgid "Name of the repo to be deleted"
msgstr "Название репозитория удалён" msgstr "Название репозитория удалён"

View File

@ -68,10 +68,12 @@ func (o *OverridableField[T]) Resolve(overrides []string) {
for _, override := range overrides { for _, override := range overrides {
if v, ok := o.Has(override); ok { if v, ok := o.Has(override); ok {
o.SetResolved(v) o.SetResolved(v)
return
} }
} }
} }
// Database serialization (JSON)
func (f *OverridableField[T]) ToDB() ([]byte, error) { func (f *OverridableField[T]) ToDB() ([]byte, error) {
var data map[string]T var data map[string]T
@ -103,6 +105,7 @@ func (f *OverridableField[T]) FromDB(data []byte) error {
return nil return nil
} }
// Gob serialization
type overridableFieldGobPayload[T any] struct { type overridableFieldGobPayload[T any] struct {
Data map[string]T Data map[string]T
Resolved T Resolved T
@ -136,6 +139,48 @@ func (f *OverridableField[T]) GobDecode(data []byte) error {
return nil return nil
} }
type overridableFieldJSONPayload[T any] struct {
Resolved *T `json:"resolved,omitempty,omitzero"`
Data map[string]T `json:"overrides,omitempty,omitzero"`
}
func (f OverridableField[T]) MarshalJSON() ([]byte, error) {
data := make(map[string]T)
for k, v := range f.data {
if k == "" {
data["default"] = v
} else {
data[k] = v
}
}
payload := overridableFieldJSONPayload[T]{
Data: data,
Resolved: &f.resolved,
}
return json.Marshal(payload)
}
func (f *OverridableField[T]) UnmarshalJSON(data []byte) error {
var payload overridableFieldJSONPayload[T]
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
if payload.Data == nil {
payload.Data = make(map[string]T)
}
f.data = payload.Data
if payload.Resolved != nil {
f.resolved = *payload.Resolved
}
return nil
}
func OverridableFromMap[T any](data map[string]T) OverridableField[T] { func OverridableFromMap[T any](data map[string]T) OverridableField[T] {
if data == nil { if data == nil {
data = make(map[string]T) data = make(map[string]T)

View File

@ -14,9 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate go run ../../generators/alrsh-package
package alrsh package alrsh
import ( import (
"encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
@ -39,38 +42,38 @@ func ParseNames(dec *decoder.Decoder) (*PackageNames, error) {
} }
type Package struct { type Package struct {
Repository string `xorm:"pk 'repository'"` Repository string `xorm:"pk 'repository'" json:"repository"`
Name string `xorm:"pk 'name'"` Name string `xorm:"pk 'name'" json:"name"`
BasePkgName string `xorm:"notnull 'basepkg_name'"` BasePkgName string `xorm:"notnull 'basepkg_name'" json:"basepkg_name"`
Version string `sh:"version" xorm:"notnull 'version'"` Version string `sh:"version" xorm:"notnull 'version'" json:"version"`
Release int `sh:"release" xorm:"notnull 'release'"` Release int `sh:"release" xorm:"notnull 'release'" json:"release"`
Epoch uint `sh:"epoch" xorm:"'epoch'"` Epoch uint `sh:"epoch" xorm:"'epoch'" json:"epoch"`
Architectures []string `sh:"architectures" xorm:"json 'architectures'"` Architectures []string `sh:"architectures" xorm:"json 'architectures'" json:"architectures"`
Licenses []string `sh:"license" xorm:"json 'licenses'"` Licenses []string `sh:"license" xorm:"json 'licenses'" json:"license"`
Provides []string `sh:"provides" xorm:"json 'provides'"` Provides []string `sh:"provides" xorm:"json 'provides'" json:"provides"`
Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'"` Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'" json:"conflicts"`
Replaces []string `sh:"replaces" xorm:"json 'replaces'"` Replaces []string `sh:"replaces" xorm:"json 'replaces'" json:"replaces"`
Summary OverridableField[string] `sh:"summary" xorm:"'summary'"` Summary OverridableField[string] `sh:"summary" xorm:"'summary'" json:"summary"`
Description OverridableField[string] `sh:"desc" xorm:"'description'"` Description OverridableField[string] `sh:"desc" xorm:"'description'" json:"description"`
Group OverridableField[string] `sh:"group" xorm:"'group_name'"` Group OverridableField[string] `sh:"group" xorm:"'group_name'" json:"group"`
Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'"` Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'" json:"homepage"`
Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'"` Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'" json:"maintainer"`
Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'"` Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'" json:"deps"`
BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'"` BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'" json:"build_deps"`
OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'"` OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'" json:"opt_deps,omitempty"`
Sources OverridableField[[]string] `sh:"sources" xorm:"-"` Sources OverridableField[[]string] `sh:"sources" xorm:"-" json:"sources"`
Checksums OverridableField[[]string] `sh:"checksums" xorm:"-"` Checksums OverridableField[[]string] `sh:"checksums" xorm:"-" json:"checksums,omitempty"`
Backup OverridableField[[]string] `sh:"backup" xorm:"-"` Backup OverridableField[[]string] `sh:"backup" xorm:"-" json:"backup"`
Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-"` Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-" json:"scripts,omitempty"`
AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-"` AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-" json:"auto_req"`
AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-"` AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-" json:"auto_prov"`
AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"` AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-" json:"auto_req_skiplist,omitempty"`
AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"` AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-" json:"auto_prov_skiplist,omitempty"`
FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-"` FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-" json:"firejailed"`
FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-"` FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-" json:"firejail_profiles,omitempty"`
} }
type Scripts struct { type Scripts struct {
@ -84,25 +87,70 @@ type Scripts struct {
PostTrans string `sh:"posttrans"` PostTrans string `sh:"posttrans"`
} }
func ResolvePackage(p *Package, overrides []string) { func (p Package) MarshalJSONWithOptions(includeOverrides bool) ([]byte, error) {
val := reflect.ValueOf(p).Elem() // Сначала сериализуем обычным способом для получения базовой структуры
typ := val.Type() type PackageAlias Package
baseData, err := json.Marshal(PackageAlias(p))
if err != nil {
return nil, err
}
for i := range val.NumField() { // Десериализуем в map для модификации
field := val.Field(i) var result map[string]json.RawMessage
fieldType := typ.Field(i) if err := json.Unmarshal(baseData, &result); err != nil {
return nil, err
}
if !field.CanInterface() { // Теперь заменяем OverridableField поля
v := reflect.ValueOf(p)
t := reflect.TypeOf(p)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue continue
} }
if field.Kind() == reflect.Struct && strings.HasPrefix(fieldType.Type.String(), "alrsh.OverridableField") { fieldName := jsonTag
of := field.Addr().Interface() if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
if res, ok := of.(interface { fieldName = jsonTag[:commaIdx]
Resolve([]string) }
}); ok {
res.Resolve(overrides) if field.Type().Name() == "OverridableField" ||
(field.Type().Kind() == reflect.Struct &&
strings.Contains(field.Type().String(), "OverridableField")) {
fieldPtr := field.Addr()
resolvedMethod := fieldPtr.MethodByName("Resolved")
if resolvedMethod.IsValid() {
resolved := resolvedMethod.Call(nil)[0]
fieldData := map[string]interface{}{
"resolved": resolved.Interface(),
}
if includeOverrides {
allMethod := field.MethodByName("All")
if allMethod.IsValid() {
overrides := allMethod.Call(nil)[0]
if !overrides.IsNil() && overrides.Len() > 0 {
fieldData["overrides"] = overrides.Interface()
}
}
}
fieldJSON, err := json.Marshal(fieldData)
if err != nil {
return nil, err
}
result[fieldName] = json.RawMessage(fieldJSON)
} }
} }
} }
return json.Marshal(result)
} }

105
pkg/alrsh/package_gen.go Normal file
View File

@ -0,0 +1,105 @@
// 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/>.
// DO NOT EDIT MANUALLY. This file is generated.
package alrsh
type packageResolved struct {
Repository string `json:"repository"`
Name string `json:"name"`
BasePkgName string `json:"basepkg_name"`
Version string `json:"version"`
Release int `json:"release"`
Epoch uint `json:"epoch"`
Architectures []string `json:"architectures"`
Licenses []string `json:"license"`
Provides []string `json:"provides"`
Conflicts []string `json:"conflicts"`
Replaces []string `json:"replaces"`
Summary string `json:"summary"`
Description string `json:"description"`
Group string `json:"group"`
Homepage string `json:"homepage"`
Maintainer string `json:"maintainer"`
Depends []string `json:"deps"`
BuildDepends []string `json:"build_deps"`
OptDepends []string `json:"opt_deps,omitempty"`
Sources []string `json:"sources"`
Checksums []string `json:"checksums,omitempty"`
Backup []string `json:"backup"`
Scripts Scripts `json:"scripts,omitempty"`
AutoReq []string `json:"auto_req"`
AutoProv []string `json:"auto_prov"`
AutoReqSkipList []string `json:"auto_req_skiplist,omitempty"`
AutoProvSkipList []string `json:"auto_prov_skiplist,omitempty"`
FireJailed bool `json:"firejailed"`
FireJailProfiles map[string]string `json:"firejail_profiles,omitempty"`
}
func PackageToResolved(src *Package) packageResolved {
return packageResolved{
Repository: src.Repository,
Name: src.Name,
BasePkgName: src.BasePkgName,
Version: src.Version,
Release: src.Release,
Epoch: src.Epoch,
Architectures: src.Architectures,
Licenses: src.Licenses,
Provides: src.Provides,
Conflicts: src.Conflicts,
Replaces: src.Replaces,
Summary: src.Summary.Resolved(),
Description: src.Description.Resolved(),
Group: src.Group.Resolved(),
Homepage: src.Homepage.Resolved(),
Maintainer: src.Maintainer.Resolved(),
Depends: src.Depends.Resolved(),
BuildDepends: src.BuildDepends.Resolved(),
OptDepends: src.OptDepends.Resolved(),
Sources: src.Sources.Resolved(),
Checksums: src.Checksums.Resolved(),
Backup: src.Backup.Resolved(),
Scripts: src.Scripts.Resolved(),
AutoReq: src.AutoReq.Resolved(),
AutoProv: src.AutoProv.Resolved(),
AutoReqSkipList: src.AutoReqSkipList.Resolved(),
AutoProvSkipList: src.AutoProvSkipList.Resolved(),
FireJailed: src.FireJailed.Resolved(),
FireJailProfiles: src.FireJailProfiles.Resolved(),
}
}
func ResolvePackage(pkg *Package, overrides []string) {
pkg.Summary.Resolve(overrides)
pkg.Description.Resolve(overrides)
pkg.Group.Resolve(overrides)
pkg.Homepage.Resolve(overrides)
pkg.Maintainer.Resolve(overrides)
pkg.Depends.Resolve(overrides)
pkg.BuildDepends.Resolve(overrides)
pkg.OptDepends.Resolve(overrides)
pkg.Sources.Resolve(overrides)
pkg.Checksums.Resolve(overrides)
pkg.Backup.Resolve(overrides)
pkg.Scripts.Resolve(overrides)
pkg.AutoReq.Resolve(overrides)
pkg.AutoProv.Resolve(overrides)
pkg.AutoReqSkipList.Resolve(overrides)
pkg.AutoProvSkipList.Resolve(overrides)
pkg.FireJailed.Resolve(overrides)
pkg.FireJailProfiles.Resolve(overrides)
}

37
pkg/alrsh/view.go Normal file
View File

@ -0,0 +1,37 @@
// 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 alrsh
import "encoding/json"
type PackageView struct {
pkg Package
Resolved bool
}
func NewPackageView(v Package) PackageView {
return PackageView{pkg: v}
}
func (p PackageView) MarshalJSON() ([]byte, error) {
if p.Resolved {
return json.Marshal(PackageToResolved(&p.pkg))
} else {
return json.Marshal(p.pkg)
}
}

View File

@ -55,7 +55,7 @@ var (
// Массив доступных загрузчиков в порядке их проверки // Массив доступных загрузчиков в порядке их проверки
var Downloaders = []Downloader{ var Downloaders = []Downloader{
GitDownloader{}, &GitDownloader{},
TorrentDownloader{}, TorrentDownloader{},
FileDownloader{}, FileDownloader{},
} }

View File

@ -32,8 +32,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"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/dl" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type TestALRConfig struct{} type TestALRConfig struct{}

View File

@ -48,7 +48,7 @@ func (GitDownloader) MatchURL(u string) bool {
// Download uses git to clone the repository from the specified URL. // Download uses git to clone the repository from the specified URL.
// It allows specifying the revision, depth and recursion options // It allows specifying the revision, depth and recursion options
// via query string // via query string
func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) { func (d *GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) {
u, err := url.Parse(opts.URL) u, err := url.Parse(opts.URL)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
@ -60,6 +60,9 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
rev := query.Get("~rev") rev := query.Get("~rev")
query.Del("~rev") query.Del("~rev")
// Right now, this only affects the return value of name,
// which will be used by dl_cache.
// It seems wrong, but for now it's better to leave it as it is.
name := query.Get("~name") name := query.Get("~name")
query.Del("~name") query.Del("~name")
@ -121,6 +124,11 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
} }
} }
err = VerifyHashFromLocal("", opts)
if err != nil {
return 0, "", err
}
if name == "" { if name == "" {
name = strings.TrimSuffix(path.Base(u.Path), ".git") name = strings.TrimSuffix(path.Base(u.Path), ".git")
} }
@ -133,7 +141,7 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
// and recursion options via query string. It returns // and recursion options via query string. It returns
// true if update was successful and false if the // true if update was successful and false if the
// repository is already up-to-date // repository is already up-to-date
func (GitDownloader) Update(opts Options) (bool, error) { func (d *GitDownloader) Update(opts Options) (bool, error) {
u, err := url.Parse(opts.URL) u, err := url.Parse(opts.URL)
if err != nil { if err != nil {
return false, err return false, err
@ -183,18 +191,21 @@ func (GitDownloader) Update(opts Options) (bool, error) {
manifestOK := err == nil manifestOK := err == nil
err = w.Pull(po) err = w.Pull(po)
if errors.Is(err, git.NoErrAlreadyUpToDate) { if err != nil {
return false, nil if errors.Is(err, git.NoErrAlreadyUpToDate) {
} else if err != nil { return false, nil
}
return false, err
}
err = VerifyHashFromLocal("", opts)
if err != nil {
return false, err return false, err
} }
if manifestOK { if manifestOK {
err = writeManifest(opts.Destination, m) err = writeManifest(opts.Destination, m)
if err != nil {
return true, err
}
} }
return true, nil return true, err
} }

183
pkg/dl/git_test.go Normal file
View File

@ -0,0 +1,183 @@
// 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 dl_test
import (
"context"
"encoding/hex"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
)
func TestGitDownloaderMatchUrl(t *testing.T) {
d := dl.GitDownloader{}
assert.True(t, d.MatchURL("git+https://example.com/org/project.git"))
assert.False(t, d.MatchURL("https://example.com/org/project.git"))
}
func TestGitDownloaderDownload(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "simple")
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "repo-for-tests", name)
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "with-hash")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=init&~name=test",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "test", name)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "with-hash-checksum-mismatch")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, _, err = d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}
func TestGitDownloaderUpdate(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
setupOldRepo := func(t *testing.T, dest string) {
t.Helper()
cmd := exec.Command("git", "clone", "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git", dest)
err := cmd.Run()
assert.NoError(t, err)
cmd = exec.Command("git", "-C", dest, "reset", "--hard", "init")
err = cmd.Run()
assert.NoError(t, err)
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
cmd := exec.Command("git", "-C", dest, "rev-parse", "HEAD")
oldHash, err := cmd.Output()
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.True(t, updated)
cmd = exec.Command("git", "-C", dest, "rev-parse", "HEAD")
newHash, err := cmd.Output()
assert.NoError(t, err)
assert.NotEqual(t, string(oldHash), string(newHash), "Repository should be updated")
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("0dc4f3c68c435d0cd7a5ee960f965815fa9c4ee0571839cdb8f9de56e06f91eb")
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.True(t, updated)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, err = d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}

View File

@ -71,7 +71,17 @@ func (TorrentDownloader) Download(ctx context.Context, opts Options) (Type, stri
return 0, "", err return 0, "", err
} }
return determineType(opts.Destination) dlType, name, err := determineType(opts.Destination)
if err != nil {
return 0, "", err
}
err = VerifyHashFromLocal(name, opts)
if err != nil {
return 0, "", err
}
return dlType, name, nil
} }
func removeTorrentFiles(path string) error { func removeTorrentFiles(path string) error {

95
pkg/dl/utils.go Normal file
View File

@ -0,0 +1,95 @@
// 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 dl
import (
"bytes"
"encoding/hex"
"fmt"
"hash"
"io"
"log/slog"
"os"
"path/filepath"
)
// If the checksum does not match, returns ErrChecksumMismatch
func VerifyHashFromLocal(path string, opts Options) error {
if opts.Hash != nil {
h, err := opts.NewHash()
if err != nil {
return err
}
err = HashLocal(filepath.Join(opts.Destination, path), h)
if err != nil {
return err
}
sum := h.Sum(nil)
slog.Debug("validate checksum", "real", hex.EncodeToString(sum), "expected", hex.EncodeToString(opts.Hash))
if !bytes.Equal(sum, opts.Hash) {
return ErrChecksumMismatch
}
}
return nil
}
func HashLocal(path string, h hash.Hash) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Mode().IsRegular() {
// Single file
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
}
if info.IsDir() {
// Walk directory
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
})
}
return fmt.Errorf("unsupported file type: %s", path)
}

View File

@ -29,7 +29,7 @@ import (
"testing" "testing"
"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/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type TestALRConfig struct { type TestALRConfig struct {

View File

@ -32,7 +32,8 @@ type Config struct {
// Repo represents a ALR repo within a configuration file // Repo represents a ALR repo within a configuration file
type Repo struct { type Repo struct {
Name string `toml:"name"` Name string `toml:"name"`
URL string `toml:"url"` URL string `toml:"url"`
Ref string `toml:"ref"` Ref string `toml:"ref"`
Mirrors []string `toml:"mirrors"`
} }

356
repo.go
View File

@ -20,8 +20,10 @@
package main package main
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -41,6 +43,8 @@ func RepoCmd() *cli.Command {
RemoveRepoCmd(), RemoveRepoCmd(),
AddRepoCmd(), AddRepoCmd(),
SetRepoRefCmd(), SetRepoRefCmd(),
RepoMirrorCmd(),
SetUrlCmd(),
}, },
} }
} }
@ -51,6 +55,21 @@ func RemoveRepoCmd() *cli.Command {
Usage: gotext.Get("Remove an existing repository"), Usage: gotext.Get("Remove an existing repository"),
Aliases: []string{"rm"}, Aliases: []string{"rm"},
ArgsUsage: gotext.Get("<name>"), ArgsUsage: gotext.Get("<name>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error { Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 1 { if c.Args().Len() < 1 {
return cliutils.FormatCliExit("missing args", nil) return cliutils.FormatCliExit("missing args", nil)
@ -186,6 +205,21 @@ func SetRepoRefCmd() *cli.Command {
Name: "set-ref", Name: "set-ref",
Usage: gotext.Get("Set the reference of the repository"), Usage: gotext.Get("Set the reference of the repository"),
ArgsUsage: gotext.Get("<name> <ref>"), ArgsUsage: gotext.Get("<name> <ref>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error { Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 { if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil) return cliutils.FormatCliExit("missing args", nil)
@ -229,6 +263,328 @@ func SetRepoRefCmd() *cli.Command {
} }
} }
func SetUrlCmd() *cli.Command {
return &cli.Command{
Name: "set-url",
Usage: gotext.Get("Set the main url of the repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
repoUrl := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
repos := deps.Cfg.Repos()
newRepos := []types.Repo{}
for _, repo := range repos {
if repo.Name == name {
repo.URL = repoUrl
}
newRepos = append(newRepos, repo)
}
deps.Cfg.SetRepos(newRepos)
err = deps.Cfg.SaveUserConfig()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
err = deps.Repos.Pull(c.Context, newRepos)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error pulling repositories"), err)
}
return nil
}),
}
}
func RepoMirrorCmd() *cli.Command {
return &cli.Command{
Name: "mirror",
Usage: gotext.Get("Manage mirrors of repos"),
Subcommands: []*cli.Command{
AddMirror(),
RemoveMirror(),
ClearMirrors(),
},
}
}
func AddMirror() *cli.Command {
return &cli.Command{
Name: "add",
Usage: gotext.Get("Add a mirror URL to repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
url := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
repos := deps.Cfg.Repos()
for i, repo := range repos {
if repo.Name == name {
repos[i].Mirrors = append(repos[i].Mirrors, url)
break
}
}
deps.Cfg.SetRepos(repos)
err = deps.Cfg.SaveUserConfig()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
return nil
}),
}
}
func RemoveMirror() *cli.Command {
return &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: gotext.Get("Remove mirror from the repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
if c.NArg() == 0 {
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "ignore-missing",
Usage: gotext.Get("Ignore if mirror does not exist"),
},
&cli.BoolFlag{
Name: "partial",
Aliases: []string{"p"},
Usage: gotext.Get("Match partial URL (e.g., github.com instead of full URL)"),
},
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
urlToRemove := c.Args().Get(1)
ignoreMissing := c.Bool("ignore-missing")
partialMatch := c.Bool("partial")
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
reposSlice := deps.Cfg.Repos()
repoIndex := -1
urlIndicesToRemove := []int{}
// Находим репозиторий
for i, repo := range reposSlice {
if repo.Name == name {
repoIndex = i
break
}
}
if repoIndex == -1 {
if ignoreMissing {
return nil // Тихо завершаем, если репозиторий не найден
}
return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil)
}
// Ищем зеркала для удаления
repo := reposSlice[repoIndex]
for j, mirror := range repo.Mirrors {
var match bool
if partialMatch {
// Частичное совпадение - проверяем, содержит ли зеркало указанную строку
match = strings.Contains(mirror, urlToRemove)
} else {
// Точное совпадение
match = mirror == urlToRemove
}
if match {
urlIndicesToRemove = append(urlIndicesToRemove, j)
}
}
if len(urlIndicesToRemove) == 0 {
if ignoreMissing {
return nil
}
if partialMatch {
return cliutils.FormatCliExit(gotext.Get("No mirrors containing \"%s\" found in repo \"%s\"", urlToRemove, name), nil)
} else {
return cliutils.FormatCliExit(gotext.Get("URL \"%s\" does not exist in repo \"%s\"", urlToRemove, name), nil)
}
}
for i := len(urlIndicesToRemove) - 1; i >= 0; i-- {
urlIndex := urlIndicesToRemove[i]
reposSlice[repoIndex].Mirrors = slices.Delete(reposSlice[repoIndex].Mirrors, urlIndex, urlIndex+1)
}
deps.Cfg.SetRepos(reposSlice)
err = deps.Cfg.SaveUserConfig()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
if len(urlIndicesToRemove) > 1 {
fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name))
}
return nil
}),
}
}
func ClearMirrors() *cli.Command {
return &cli.Command{
Name: "clear",
Aliases: []string{"rm-all"},
Usage: gotext.Get("Remove all mirrors from the repository"),
ArgsUsage: gotext.Get("<name>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 1 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
reposSlice := deps.Cfg.Repos()
repoIndex := -1
urlIndicesToRemove := []int{}
// Находим репозиторий
for i, repo := range reposSlice {
if repo.Name == name {
repoIndex = i
break
}
}
if repoIndex == -1 {
return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil)
}
reposSlice[repoIndex].Mirrors = []string{}
deps.Cfg.SetRepos(reposSlice)
err = deps.Cfg.SaveUserConfig()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
if len(urlIndicesToRemove) > 1 {
fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name))
}
return nil
}),
}
}
// TODO: remove // TODO: remove
// //
// Deprecated: use "alr repo add" // Deprecated: use "alr repo add"