mirror of
https://gitea.plemya-x.ru/Plemya-x/ALR.git
synced 2025-01-10 17:26:45 +00:00
351 lines
9.3 KiB
Go
351 lines
9.3 KiB
Go
/*
|
||
* 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 содержит абстракции для загрузки файлов и каталогов
|
||
// из различных источников.
|
||
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"
|
||
"plemya-x.ru/alr/internal/dlcache"
|
||
"plemya-x.ru/alr/pkg/loggerctx"
|
||
)
|
||
|
||
// Константа для имени файла манифеста кэша
|
||
const manifestFileName = ".alr_cache_manifest"
|
||
|
||
// Объявление ошибок для несоответствия контрольной суммы и отсутствия алгоритма хеширования
|
||
var (
|
||
ErrChecksumMismatch = errors.New("dl: checksums did not match")
|
||
ErrNoSuchHashAlgo = errors.New("dl: invalid hashing algorithm")
|
||
)
|
||
|
||
// Массив доступных загрузчиков в порядке их проверки
|
||
var Downloaders = []Downloader{
|
||
GitDownloader{},
|
||
TorrentDownloader{},
|
||
FileDownloader{},
|
||
}
|
||
|
||
// Тип данных, представляющий тип загрузки (файл или каталог)
|
||
type Type uint8
|
||
|
||
// Объявление констант для типов загрузки
|
||
const (
|
||
TypeFile Type = iota
|
||
TypeDir
|
||
)
|
||
|
||
// Метод для получения строки, представляющей тип загрузки
|
||
func (t Type) String() string {
|
||
switch t {
|
||
case TypeFile:
|
||
return "file"
|
||
case TypeDir:
|
||
return "dir"
|
||
}
|
||
return "<unknown>"
|
||
}
|
||
|
||
// Структура Options содержит параметры для загрузки файлов и каталогов
|
||
type Options struct {
|
||
Hash []byte
|
||
HashAlgorithm string
|
||
Name string
|
||
URL string
|
||
Destination string
|
||
CacheDisabled bool
|
||
PostprocDisabled bool
|
||
Progress io.Writer
|
||
LocalDir string
|
||
}
|
||
|
||
// Метод для создания нового хеша на основе указанного алгоритма хеширования
|
||
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)
|
||
}
|
||
}
|
||
|
||
// Структура Manifest хранит информацию о типе и имени загруженного файла или каталога
|
||
type Manifest struct {
|
||
Type Type
|
||
Name string
|
||
}
|
||
|
||
// Интерфейс Downloader для реализации различных загрузчиков
|
||
type Downloader interface {
|
||
Name() string
|
||
MatchURL(string) bool
|
||
Download(Options) (Type, string, error)
|
||
}
|
||
|
||
// Интерфейс UpdatingDownloader расширяет Downloader методом Update
|
||
type UpdatingDownloader interface {
|
||
Downloader
|
||
Update(Options) (bool, error)
|
||
}
|
||
|
||
// Функция Download загружает файл или каталог с использованием указанных параметров
|
||
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
|
||
}
|
||
|
||
// Функция writeManifest записывает манифест в указанный каталог кэша
|
||
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)
|
||
}
|
||
|
||
// Функция getManifest считывает манифест из указанного каталога кэша
|
||
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
|
||
}
|
||
|
||
// Функция handleCache создает жесткие ссылки для файлов из каталога кэша в каталог назначения
|
||
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
|
||
}
|
||
|
||
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest
|
||
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
|
||
}
|
||
|
||
|
||
|
||
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)
|
||
})
|
||
}
|
||
|
||
// Функция getDownloader возвращает загрузчик, соответствующий URL
|
||
func getDownloader(u string) Downloader {
|
||
for _, d := range Downloaders {
|
||
if d.MatchURL(u) {
|
||
return d
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Функция normalizeURL нормализует строку URL, чтобы незначительные различия не изменяли хеш
|
||
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
|
||
}
|
||
|
||
// Исправление URL-адресов magnet после нормализации
|
||
u = strings.Replace(u, "magnet://", "magnet:", 1)
|
||
return u, nil
|
||
}
|