/* * 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 . */ // Пакет 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 "" } // Структура 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 }