diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg
index 259d4b3..665bcb6 100644
--- a/assets/coverage-badge.svg
+++ b/assets/coverage-badge.svg
@@ -11,7 +11,7 @@
coverage
coverage
- 18.9%
- 18.9%
+ 20.2%
+ 20.2%
diff --git a/internal/build/source_downloader.go b/internal/build/source_downloader.go
index ef287fd..f5ac2d6 100644
--- a/internal/build/source_downloader.go
+++ b/internal/build/source_downloader.go
@@ -20,11 +20,12 @@ import (
"context"
"encoding/hex"
"fmt"
+ "log/slog"
"os"
"strings"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/dl"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
)
type SourceDownloader struct {
@@ -76,6 +77,7 @@ func (s *SourceDownloader) DownloadSources(
opts.DlCache = dlcache.New(s.cfg)
+ slog.Warn("opts", "opts", opts)
err := dl.Download(ctx, opts)
if err != nil {
return err
diff --git a/internal/translations/default.pot b/internal/translations/default.pot
index ca81861..cce76d6 100644
--- a/internal/translations/default.pot
+++ b/internal/translations/default.pot
@@ -355,30 +355,6 @@ msgid ""
"Database version does not exist. Run alr fix if something isn't working."
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
msgid "ERROR"
msgstr ""
@@ -465,6 +441,30 @@ msgstr ""
msgid "Error while running app"
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
msgid "Pull all repositories that have changed"
msgstr ""
diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po
index b109c78..9866102 100644
--- a/internal/translations/po/ru/default.po
+++ b/internal/translations/po/ru/default.po
@@ -369,30 +369,6 @@ msgid ""
msgstr ""
"Версия базы данных не существует. Запустите 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
msgid "ERROR"
msgstr "ОШИБКА"
@@ -481,6 +457,30 @@ msgstr "Показать справку"
msgid "Error while running app"
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
msgid "Pull all repositories that have changed"
msgstr "Скачать все изменённые репозитории"
diff --git a/internal/dl/dl.go b/pkg/dl/dl.go
similarity index 99%
rename from internal/dl/dl.go
rename to pkg/dl/dl.go
index a1c0fff..6c349c1 100644
--- a/internal/dl/dl.go
+++ b/pkg/dl/dl.go
@@ -55,7 +55,7 @@ var (
// Массив доступных загрузчиков в порядке их проверки
var Downloaders = []Downloader{
- GitDownloader{},
+ &GitDownloader{},
TorrentDownloader{},
FileDownloader{},
}
diff --git a/internal/dl/dl_test.go b/pkg/dl/dl_test.go
similarity index 97%
rename from internal/dl/dl_test.go
rename to pkg/dl/dl_test.go
index 07dc817..c7326b8 100644
--- a/internal/dl/dl_test.go
+++ b/pkg/dl/dl_test.go
@@ -32,8 +32,8 @@ import (
"github.com/stretchr/testify/assert"
"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/internal/dlcache"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
)
type TestALRConfig struct{}
diff --git a/internal/dl/file.go b/pkg/dl/file.go
similarity index 100%
rename from internal/dl/file.go
rename to pkg/dl/file.go
diff --git a/internal/dl/git.go b/pkg/dl/git.go
similarity index 80%
rename from internal/dl/git.go
rename to pkg/dl/git.go
index c1ea822..0de751f 100644
--- a/internal/dl/git.go
+++ b/pkg/dl/git.go
@@ -20,8 +20,11 @@
package dl
import (
+ "bytes"
"context"
+ "encoding/hex"
"errors"
+ "log/slog"
"net/url"
"path"
"strconv"
@@ -48,7 +51,7 @@ func (GitDownloader) MatchURL(u string) bool {
// Download uses git to clone the repository from the specified URL.
// It allows specifying the revision, depth and recursion options
// 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)
if err != nil {
return 0, "", err
@@ -60,6 +63,9 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
rev := query.Get("~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")
query.Del("~name")
@@ -121,6 +127,11 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
}
}
+ err = d.verifyHash(opts)
+ if err != nil {
+ return 0, "", err
+ }
+
if name == "" {
name = strings.TrimSuffix(path.Base(u.Path), ".git")
}
@@ -128,12 +139,36 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
return TypeDir, name, nil
}
+func (GitDownloader) verifyHash(opts Options) error {
+ if opts.Hash != nil {
+ h, err := opts.NewHash()
+ if err != nil {
+ return err
+ }
+
+ err = HashDir(opts.Destination, h)
+ if err != nil {
+ return err
+ }
+
+ sum := h.Sum(nil)
+
+ slog.Warn("validate checksum", "real", hex.EncodeToString(sum), "expected", hex.EncodeToString(opts.Hash))
+
+ if !bytes.Equal(sum, opts.Hash) {
+ return ErrChecksumMismatch
+ }
+ }
+
+ return nil
+}
+
// Update uses git to pull the repository and update it
// to the latest revision. It allows specifying the depth
// and recursion options via query string. It returns
// true if update was successful and false if the
// 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)
if err != nil {
return false, err
@@ -183,18 +218,21 @@ func (GitDownloader) Update(opts Options) (bool, error) {
manifestOK := err == nil
err = w.Pull(po)
- if errors.Is(err, git.NoErrAlreadyUpToDate) {
- return false, nil
- } else if err != nil {
+ if err != nil {
+ if errors.Is(err, git.NoErrAlreadyUpToDate) {
+ return false, nil
+ }
+ return false, err
+ }
+
+ err = d.verifyHash(opts)
+ if err != nil {
return false, err
}
if manifestOK {
err = writeManifest(opts.Destination, m)
- if err != nil {
- return true, err
- }
}
- return true, nil
+ return true, err
}
diff --git a/pkg/dl/git_test.go b/pkg/dl/git_test.go
new file mode 100644
index 0000000..1c65db4
--- /dev/null
+++ b/pkg/dl/git_test.go
@@ -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 .
+
+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)
+ })
+}
diff --git a/internal/dl/progress_tui.go b/pkg/dl/progress_tui.go
similarity index 100%
rename from internal/dl/progress_tui.go
rename to pkg/dl/progress_tui.go
diff --git a/internal/dl/torrent.go b/pkg/dl/torrent.go
similarity index 100%
rename from internal/dl/torrent.go
rename to pkg/dl/torrent.go
diff --git a/pkg/dl/utils.go b/pkg/dl/utils.go
new file mode 100644
index 0000000..a4e4a62
--- /dev/null
+++ b/pkg/dl/utils.go
@@ -0,0 +1,55 @@
+// ALR - Any Linux Repository
+// Copyright (C) 2025 The ALR Authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package dl
+
+import (
+ "hash"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+func HashDir(dirPath string, h hash.Hash) error {
+ err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ // Skip .git directory
+ if info.IsDir() && info.Name() == ".git" {
+ return filepath.SkipDir
+ }
+ // Skip directories (only process files)
+ if !info.Mode().IsRegular() {
+ return nil
+ }
+ // Open file
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ // Write file content to hasher
+ if _, err := io.Copy(h, f); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/dlcache/dlcache.go b/pkg/dlcache/dlcache.go
similarity index 100%
rename from internal/dlcache/dlcache.go
rename to pkg/dlcache/dlcache.go
diff --git a/internal/dlcache/dlcache_test.go b/pkg/dlcache/dlcache_test.go
similarity index 97%
rename from internal/dlcache/dlcache_test.go
rename to pkg/dlcache/dlcache_test.go
index d189a0e..555cdba 100644
--- a/internal/dlcache/dlcache_test.go
+++ b/pkg/dlcache/dlcache_test.go
@@ -29,7 +29,7 @@ import (
"testing"
"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 {
diff --git a/internal/dlcache/utils.go b/pkg/dlcache/utils.go
similarity index 100%
rename from internal/dlcache/utils.go
rename to pkg/dlcache/utils.go