feat: add checksum for git downloader

This commit is contained in:
2025-06-20 19:35:22 +03:00
parent b5474b1eb4
commit 6bccce1db4
15 changed files with 343 additions and 65 deletions

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">18.9%</text> <text x="86" y="15" fill="#010101" fill-opacity=".3">20.2%</text>
<text x="86" y="14">18.9%</text> <text x="86" y="14">20.2%</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

@ -20,11 +20,12 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log/slog"
"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 {
@ -76,6 +77,7 @@ func (s *SourceDownloader) DownloadSources(
opts.DlCache = dlcache.New(s.cfg) opts.DlCache = dlcache.New(s.cfg)
slog.Warn("opts", "opts", opts)
err := dl.Download(ctx, opts) err := dl.Download(ctx, opts)
if err != nil { if err != nil {
return err return err

View File

@ -355,30 +355,6 @@ 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 ""
@ -465,6 +441,30 @@ 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 ""

View File

@ -369,30 +369,6 @@ 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 "ОШИБКА"
@ -481,6 +457,30 @@ 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 "Скачать все изменённые репозитории"

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

@ -20,8 +20,11 @@
package dl package dl
import ( import (
"bytes"
"context" "context"
"encoding/hex"
"errors" "errors"
"log/slog"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
@ -48,7 +51,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 +63,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 +127,11 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
} }
} }
err = d.verifyHash(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")
} }
@ -128,12 +139,36 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
return TypeDir, name, nil 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 // Update uses git to pull the repository and update it
// to the latest revision. It allows specifying the depth // to the latest revision. It allows specifying the depth
// 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 +218,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 = d.verifyHash(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)
})
}

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

@ -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 <http://www.gnu.org/licenses/>.
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
}

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 {