2024-01-22 10:36:06 +00:00
|
|
|
|
/*
|
2024-07-10 10:10:14 +00:00
|
|
|
|
* ALR - Any Linux Repository
|
|
|
|
|
* Copyright (C) 2024 Евгений Храмов
|
|
|
|
|
*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Пакет dl содержит абстракции для загрузки файлов и каталогов
|
|
|
|
|
// из различных источников.
|
2024-01-22 10:36:06 +00:00
|
|
|
|
package dl
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/md5"
|
|
|
|
|
"crypto/sha1"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"crypto/sha512"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"hash"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/PuerkitoBio/purell"
|
|
|
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
|
|
|
"golang.org/x/crypto/blake2b"
|
|
|
|
|
"golang.org/x/crypto/blake2s"
|
|
|
|
|
"golang.org/x/exp/slices"
|
2024-05-05 10:32:08 +00:00
|
|
|
|
"plemya-x.ru/alr/internal/dlcache"
|
|
|
|
|
"plemya-x.ru/alr/pkg/loggerctx"
|
2024-01-22 10:36:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Константа для имени файла манифеста кэша
|
2024-05-05 10:32:08 +00:00
|
|
|
|
const manifestFileName = ".alr_cache_manifest"
|
2024-01-22 10:36:06 +00:00
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Объявление ошибок для несоответствия контрольной суммы и отсутствия алгоритма хеширования
|
2024-01-22 10:36:06 +00:00
|
|
|
|
var (
|
|
|
|
|
ErrChecksumMismatch = errors.New("dl: checksums did not match")
|
|
|
|
|
ErrNoSuchHashAlgo = errors.New("dl: invalid hashing algorithm")
|
|
|
|
|
)
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Массив доступных загрузчиков в порядке их проверки
|
2024-01-22 10:36:06 +00:00
|
|
|
|
var Downloaders = []Downloader{
|
|
|
|
|
GitDownloader{},
|
|
|
|
|
TorrentDownloader{},
|
|
|
|
|
FileDownloader{},
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Тип данных, представляющий тип загрузки (файл или каталог)
|
2024-01-22 10:36:06 +00:00
|
|
|
|
type Type uint8
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Объявление констант для типов загрузки
|
2024-01-22 10:36:06 +00:00
|
|
|
|
const (
|
|
|
|
|
TypeFile Type = iota
|
|
|
|
|
TypeDir
|
|
|
|
|
)
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Метод для получения строки, представляющей тип загрузки
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func (t Type) String() string {
|
|
|
|
|
switch t {
|
|
|
|
|
case TypeFile:
|
|
|
|
|
return "file"
|
|
|
|
|
case TypeDir:
|
|
|
|
|
return "dir"
|
|
|
|
|
}
|
|
|
|
|
return "<unknown>"
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Структура Options содержит параметры для загрузки файлов и каталогов
|
2024-01-22 10:36:06 +00:00
|
|
|
|
type Options struct {
|
|
|
|
|
Hash []byte
|
|
|
|
|
HashAlgorithm string
|
|
|
|
|
Name string
|
|
|
|
|
URL string
|
|
|
|
|
Destination string
|
|
|
|
|
CacheDisabled bool
|
|
|
|
|
PostprocDisabled bool
|
|
|
|
|
Progress io.Writer
|
|
|
|
|
LocalDir string
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Метод для создания нового хеша на основе указанного алгоритма хеширования
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func (opts Options) NewHash() (hash.Hash, error) {
|
|
|
|
|
switch opts.HashAlgorithm {
|
|
|
|
|
case "", "sha256":
|
|
|
|
|
return sha256.New(), nil
|
|
|
|
|
case "sha224":
|
|
|
|
|
return sha256.New224(), nil
|
|
|
|
|
case "sha512":
|
|
|
|
|
return sha512.New(), nil
|
|
|
|
|
case "sha384":
|
|
|
|
|
return sha512.New384(), nil
|
|
|
|
|
case "sha1":
|
|
|
|
|
return sha1.New(), nil
|
|
|
|
|
case "md5":
|
|
|
|
|
return md5.New(), nil
|
|
|
|
|
case "blake2s-128":
|
|
|
|
|
return blake2s.New256(nil)
|
|
|
|
|
case "blake2s-256":
|
|
|
|
|
return blake2s.New256(nil)
|
|
|
|
|
case "blake2b-256":
|
|
|
|
|
return blake2b.New(32, nil)
|
|
|
|
|
case "blake2b-512":
|
|
|
|
|
return blake2b.New(64, nil)
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchHashAlgo, opts.HashAlgorithm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Структура Manifest хранит информацию о типе и имени загруженного файла или каталога
|
2024-01-22 10:36:06 +00:00
|
|
|
|
type Manifest struct {
|
|
|
|
|
Type Type
|
|
|
|
|
Name string
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Интерфейс Downloader для реализации различных загрузчиков
|
2024-01-22 10:36:06 +00:00
|
|
|
|
type Downloader interface {
|
|
|
|
|
Name() string
|
|
|
|
|
MatchURL(string) bool
|
|
|
|
|
Download(Options) (Type, string, error)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Интерфейс UpdatingDownloader расширяет Downloader методом Update
|
2024-01-22 10:36:06 +00:00
|
|
|
|
type UpdatingDownloader interface {
|
|
|
|
|
Downloader
|
|
|
|
|
Update(Options) (bool, error)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция Download загружает файл или каталог с использованием указанных параметров
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func Download(ctx context.Context, opts Options) (err error) {
|
|
|
|
|
log := loggerctx.From(ctx)
|
|
|
|
|
normalized, err := normalizeURL(opts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
opts.URL = normalized
|
|
|
|
|
|
|
|
|
|
d := getDownloader(opts.URL)
|
|
|
|
|
|
|
|
|
|
if opts.CacheDisabled {
|
|
|
|
|
_, _, err = d.Download(opts)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var t Type
|
|
|
|
|
cacheDir, ok := dlcache.Get(ctx, opts.URL)
|
|
|
|
|
if ok {
|
|
|
|
|
var updated bool
|
|
|
|
|
if d, ok := d.(UpdatingDownloader); ok {
|
|
|
|
|
log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send()
|
|
|
|
|
|
|
|
|
|
updated, err = d.Update(Options{
|
|
|
|
|
Hash: opts.Hash,
|
|
|
|
|
HashAlgorithm: opts.HashAlgorithm,
|
|
|
|
|
Name: opts.Name,
|
|
|
|
|
URL: opts.URL,
|
|
|
|
|
Destination: cacheDir,
|
|
|
|
|
Progress: opts.Progress,
|
|
|
|
|
LocalDir: opts.LocalDir,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m, err := getManifest(cacheDir)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t = m.Type
|
|
|
|
|
|
|
|
|
|
dest := filepath.Join(opts.Destination, m.Name)
|
|
|
|
|
ok, err := handleCache(cacheDir, dest, m.Name, t)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ok && !updated {
|
|
|
|
|
log.Info("Source found in cache and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
|
|
|
|
|
return nil
|
|
|
|
|
} else if ok {
|
|
|
|
|
log.Info("Source updated and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
err = os.RemoveAll(cacheDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
|
|
|
|
|
|
|
|
|
|
cacheDir, err = dlcache.New(ctx, opts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t, name, err := d.Download(Options{
|
|
|
|
|
Hash: opts.Hash,
|
|
|
|
|
HashAlgorithm: opts.HashAlgorithm,
|
|
|
|
|
Name: opts.Name,
|
|
|
|
|
URL: opts.URL,
|
|
|
|
|
Destination: cacheDir,
|
|
|
|
|
Progress: opts.Progress,
|
|
|
|
|
LocalDir: opts.LocalDir,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = writeManifest(cacheDir, Manifest{t, name})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dest := filepath.Join(opts.Destination, name)
|
|
|
|
|
_, err = handleCache(cacheDir, dest, name, t)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция writeManifest записывает манифест в указанный каталог кэша
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func writeManifest(cacheDir string, m Manifest) error {
|
|
|
|
|
fl, err := os.Create(filepath.Join(cacheDir, manifestFileName))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer fl.Close()
|
|
|
|
|
return msgpack.NewEncoder(fl).Encode(m)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция getManifest считывает манифест из указанного каталога кэша
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func getManifest(cacheDir string) (m Manifest, err error) {
|
|
|
|
|
fl, err := os.Open(filepath.Join(cacheDir, manifestFileName))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Manifest{}, err
|
|
|
|
|
}
|
|
|
|
|
defer fl.Close()
|
|
|
|
|
|
|
|
|
|
err = msgpack.NewDecoder(fl).Decode(&m)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция handleCache создает жесткие ссылки для файлов из каталога кэша в каталог назначения
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
|
|
|
|
|
switch t {
|
|
|
|
|
case TypeFile:
|
|
|
|
|
cd, err := os.Open(cacheDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
names, err := cd.Readdirnames(0)
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cd.Close()
|
|
|
|
|
|
|
|
|
|
if slices.Contains(names, name) {
|
|
|
|
|
err = os.Link(filepath.Join(cacheDir, name), dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
case TypeDir:
|
|
|
|
|
err := linkDir(cacheDir, dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func linkDir(src, dest string) error {
|
|
|
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if info.Name() == manifestFileName {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
|
|
|
|
|
|
2024-01-22 10:36:06 +00:00
|
|
|
|
rel, err := filepath.Rel(src, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newPath := filepath.Join(dest, rel)
|
|
|
|
|
if info.IsDir() {
|
|
|
|
|
return os.MkdirAll(newPath, info.Mode())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return os.Link(path, newPath)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция getDownloader возвращает загрузчик, соответствующий URL
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func getDownloader(u string) Downloader {
|
|
|
|
|
for _, d := range Downloaders {
|
|
|
|
|
if d.MatchURL(u) {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Функция normalizeURL нормализует строку URL, чтобы незначительные различия не изменяли хеш
|
2024-01-22 10:36:06 +00:00
|
|
|
|
func normalizeURL(u string) (string, error) {
|
|
|
|
|
const normalizationFlags = purell.FlagRemoveTrailingSlash |
|
|
|
|
|
purell.FlagRemoveDefaultPort |
|
|
|
|
|
purell.FlagLowercaseHost |
|
|
|
|
|
purell.FlagLowercaseScheme |
|
|
|
|
|
purell.FlagRemoveDuplicateSlashes |
|
|
|
|
|
purell.FlagRemoveFragment |
|
|
|
|
|
purell.FlagRemoveUnnecessaryHostDots |
|
|
|
|
|
purell.FlagSortQuery |
|
|
|
|
|
purell.FlagDecodeHexHost |
|
|
|
|
|
purell.FlagDecodeOctalHost |
|
|
|
|
|
purell.FlagDecodeUnnecessaryEscapes |
|
|
|
|
|
purell.FlagRemoveEmptyPortSeparator
|
|
|
|
|
|
|
|
|
|
u, err := purell.NormalizeURLString(u, normalizationFlags)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 10:10:14 +00:00
|
|
|
|
// Исправление URL-адресов magnet после нормализации
|
2024-01-22 10:36:06 +00:00
|
|
|
|
u = strings.Replace(u, "magnet://", "magnet:", 1)
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|