forked from Plemya-x/ALR
		
	Небольшие переводы и комментарии
This commit is contained in:
		| @@ -1,23 +1,23 @@ | |||||||
| /* | /* | ||||||
|  * ALR - Any Linux Repository | * ALR - Any Linux Repository | ||||||
|  * Copyright (C) 2024 Евгений Храмов | * Copyright (C) 2024 Евгений Храмов | ||||||
|  * | * | ||||||
|  * This program is free software: you can redistribute it and/or modify | * 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 | * it under the terms of the GNU General Public License as published by | ||||||
|  * the Free Software Foundation, either version 3 of the License, or | * the Free Software Foundation, either version 3 of the License, or | ||||||
|  * (at your option) any later version. | * (at your option) any later version. | ||||||
|  * | * | ||||||
|  * This program is distributed in the hope that it will be useful, | * This program is distributed in the hope that it will be useful, | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  * GNU General Public License for more details. | * GNU General Public License for more details. | ||||||
|  * | * | ||||||
|  * You should have received a copy of the GNU General Public License | * You should have received a copy of the GNU General Public License | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  */ | */ | ||||||
|  |  | ||||||
| // Package dl contains abstractions for downloadingfiles and directories | // Пакет dl содержит абстракции для загрузки файлов и каталогов | ||||||
| // from various sources. | // из различных источников. | ||||||
| package dl | package dl | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| @@ -43,31 +43,32 @@ import ( | |||||||
| 	"plemya-x.ru/alr/pkg/loggerctx" | 	"plemya-x.ru/alr/pkg/loggerctx" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // Константа для имени файла манифеста кэша | ||||||
| const manifestFileName = ".alr_cache_manifest" | const manifestFileName = ".alr_cache_manifest" | ||||||
|  |  | ||||||
| // ErrChecksumMismatch occurs when the checksum of a downloaded file | // Объявление ошибок для несоответствия контрольной суммы и отсутствия алгоритма хеширования | ||||||
| // does not match the expected checksum provided in the Options struct. |  | ||||||
| var ( | var ( | ||||||
| 	ErrChecksumMismatch = errors.New("dl: checksums did not match") | 	ErrChecksumMismatch = errors.New("dl: checksums did not match") | ||||||
| 	ErrNoSuchHashAlgo   = errors.New("dl: invalid hashing algorithm") | 	ErrNoSuchHashAlgo   = errors.New("dl: invalid hashing algorithm") | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Downloaders contains all the downloaders in the order in which | // Массив доступных загрузчиков в порядке их проверки | ||||||
| // they should be checked |  | ||||||
| var Downloaders = []Downloader{ | var Downloaders = []Downloader{ | ||||||
| 	GitDownloader{}, | 	GitDownloader{}, | ||||||
| 	TorrentDownloader{}, | 	TorrentDownloader{}, | ||||||
| 	FileDownloader{}, | 	FileDownloader{}, | ||||||
| } | } | ||||||
|  |  | ||||||
| // Type represents the type of download (file or directory) | // Тип данных, представляющий тип загрузки (файл или каталог) | ||||||
| type Type uint8 | type Type uint8 | ||||||
|  |  | ||||||
|  | // Объявление констант для типов загрузки | ||||||
| const ( | const ( | ||||||
| 	TypeFile Type = iota | 	TypeFile Type = iota | ||||||
| 	TypeDir | 	TypeDir | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // Метод для получения строки, представляющей тип загрузки | ||||||
| func (t Type) String() string { | func (t Type) String() string { | ||||||
| 	switch t { | 	switch t { | ||||||
| 	case TypeFile: | 	case TypeFile: | ||||||
| @@ -78,8 +79,7 @@ func (t Type) String() string { | |||||||
| 	return "<unknown>" | 	return "<unknown>" | ||||||
| } | } | ||||||
|  |  | ||||||
| // Options contains the options for downloading | // Структура Options содержит параметры для загрузки файлов и каталогов | ||||||
| // files and directories |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	Hash             []byte | 	Hash             []byte | ||||||
| 	HashAlgorithm    string | 	HashAlgorithm    string | ||||||
| @@ -92,6 +92,7 @@ type Options struct { | |||||||
| 	LocalDir         string | 	LocalDir         string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Метод для создания нового хеша на основе указанного алгоритма хеширования | ||||||
| func (opts Options) NewHash() (hash.Hash, error) { | func (opts Options) NewHash() (hash.Hash, error) { | ||||||
| 	switch opts.HashAlgorithm { | 	switch opts.HashAlgorithm { | ||||||
| 	case "", "sha256": | 	case "", "sha256": | ||||||
| @@ -119,49 +120,26 @@ func (opts Options) NewHash() (hash.Hash, error) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Manifest holds information about the type and name | // Структура Manifest хранит информацию о типе и имени загруженного файла или каталога | ||||||
| // of a downloaded file or directory. It is stored inside |  | ||||||
| // each cache directory for later use. |  | ||||||
| type Manifest struct { | type Manifest struct { | ||||||
| 	Type Type | 	Type Type | ||||||
| 	Name string | 	Name string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Интерфейс Downloader для реализации различных загрузчиков | ||||||
| type Downloader interface { | type Downloader interface { | ||||||
| 	// Name returns the name of the downloader |  | ||||||
| 	Name() string | 	Name() string | ||||||
| 	// MatchURL checks if the given URL matches |  | ||||||
| 	// the downloader. |  | ||||||
| 	MatchURL(string) bool | 	MatchURL(string) bool | ||||||
| 	// Download downloads the object at the URL |  | ||||||
| 	// provided in the options, to the destination |  | ||||||
| 	// given in the options. It returns a type, |  | ||||||
| 	// a name for the downloaded object (this may be empty), |  | ||||||
| 	// and an error. |  | ||||||
| 	Download(Options) (Type, string, error) | 	Download(Options) (Type, string, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // UpdatingDownloader extends the Downloader interface | // Интерфейс UpdatingDownloader расширяет Downloader методом Update | ||||||
| // with an Update method for protocols such as git, which |  | ||||||
| // allow for incremental updates without changing the URL. |  | ||||||
| type UpdatingDownloader interface { | type UpdatingDownloader interface { | ||||||
| 	Downloader | 	Downloader | ||||||
| 	// Update checks for and performs any |  | ||||||
| 	// available updates for the object |  | ||||||
| 	// described in the options. It returns |  | ||||||
| 	// true if an update was performed, or |  | ||||||
| 	// false if no update was required. |  | ||||||
| 	Update(Options) (bool, error) | 	Update(Options) (bool, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Download downloads a file or directory using the specified options. | // Функция Download загружает файл или каталог с использованием указанных параметров | ||||||
| // It first gets the appropriate downloader for the URL, then checks |  | ||||||
| // if caching is enabled. If caching is enabled, it attempts to get |  | ||||||
| // the cache directory for the URL and update it if necessary. |  | ||||||
| // If the source is found in the cache, it links it to the destination |  | ||||||
| // using hard links. If the source is not found in the cache, |  | ||||||
| // it downloads the source to a new cache directory and links it |  | ||||||
| // to the destination. |  | ||||||
| func Download(ctx context.Context, opts Options) (err error) { | func Download(ctx context.Context, opts Options) (err error) { | ||||||
| 	log := loggerctx.From(ctx) | 	log := loggerctx.From(ctx) | ||||||
| 	normalized, err := normalizeURL(opts.URL) | 	normalized, err := normalizeURL(opts.URL) | ||||||
| @@ -216,9 +194,6 @@ func Download(ctx context.Context, opts Options) (err error) { | |||||||
| 				return nil | 				return nil | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			// If we cannot read the manifest, |  | ||||||
| 			// this cache entry is invalid and |  | ||||||
| 			// the source must be re-downloaded. |  | ||||||
| 			err = os.RemoveAll(cacheDir) | 			err = os.RemoveAll(cacheDir) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| @@ -256,7 +231,7 @@ func Download(ctx context.Context, opts Options) (err error) { | |||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| // writeManifest writes the manifest to the specified cache directory. | // Функция writeManifest записывает манифест в указанный каталог кэша | ||||||
| func writeManifest(cacheDir string, m Manifest) error { | func writeManifest(cacheDir string, m Manifest) error { | ||||||
| 	fl, err := os.Create(filepath.Join(cacheDir, manifestFileName)) | 	fl, err := os.Create(filepath.Join(cacheDir, manifestFileName)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -266,7 +241,7 @@ func writeManifest(cacheDir string, m Manifest) error { | |||||||
| 	return msgpack.NewEncoder(fl).Encode(m) | 	return msgpack.NewEncoder(fl).Encode(m) | ||||||
| } | } | ||||||
|  |  | ||||||
| // getManifest reads the manifest from the specified cache directory. | // Функция getManifest считывает манифест из указанного каталога кэша | ||||||
| func getManifest(cacheDir string) (m Manifest, err error) { | func getManifest(cacheDir string) (m Manifest, err error) { | ||||||
| 	fl, err := os.Open(filepath.Join(cacheDir, manifestFileName)) | 	fl, err := os.Open(filepath.Join(cacheDir, manifestFileName)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -278,7 +253,7 @@ func getManifest(cacheDir string) (m Manifest, err error) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| // handleCache links the cache directory or a file within it to the destination | // Функция handleCache создает жесткие ссылки для файлов из каталога кэша в каталог назначения | ||||||
| func handleCache(cacheDir, dest, name string, t Type) (bool, error) { | func handleCache(cacheDir, dest, name string, t Type) (bool, error) { | ||||||
| 	switch t { | 	switch t { | ||||||
| 	case TypeFile: | 	case TypeFile: | ||||||
| @@ -313,12 +288,7 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) { | |||||||
| 	return false, nil | 	return false, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // linkDir recursively walks through a directory, creating | // Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest | ||||||
| // hard links for each file from the src directory to the |  | ||||||
| // dest directory. If it encounters a directory, it will |  | ||||||
| // create a directory with the same name and permissions |  | ||||||
| // in the dest directory, because hard links cannot be |  | ||||||
| // created for directories. |  | ||||||
| func linkDir(src, dest string) error { | func linkDir(src, dest string) error { | ||||||
| 	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { | 	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -329,6 +299,8 @@ func linkDir(src, dest string) error { | |||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 		rel, err := filepath.Rel(src, path) | 		rel, err := filepath.Rel(src, path) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -343,6 +315,7 @@ func linkDir(src, dest string) error { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Функция getDownloader возвращает загрузчик, соответствующий URL | ||||||
| func getDownloader(u string) Downloader { | func getDownloader(u string) Downloader { | ||||||
| 	for _, d := range Downloaders { | 	for _, d := range Downloaders { | ||||||
| 		if d.MatchURL(u) { | 		if d.MatchURL(u) { | ||||||
| @@ -352,8 +325,7 @@ func getDownloader(u string) Downloader { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // normalizeURL normalizes a URL string, so that insignificant | // Функция normalizeURL нормализует строку URL, чтобы незначительные различия не изменяли хеш | ||||||
| // differences don't change the hash. |  | ||||||
| func normalizeURL(u string) (string, error) { | func normalizeURL(u string) (string, error) { | ||||||
| 	const normalizationFlags = purell.FlagRemoveTrailingSlash | | 	const normalizationFlags = purell.FlagRemoveTrailingSlash | | ||||||
| 		purell.FlagRemoveDefaultPort | | 		purell.FlagRemoveDefaultPort | | ||||||
| @@ -373,7 +345,7 @@ func normalizeURL(u string) (string, error) { | |||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Fix magnet URLs after normalization | 	// Исправление URL-адресов magnet после нормализации | ||||||
| 	u = strings.Replace(u, "magnet://", "magnet:", 1) | 	u = strings.Replace(u, "magnet://", "magnet:", 1) | ||||||
| 	return u, nil | 	return u, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -36,40 +36,47 @@ import ( | |||||||
| 	"plemya-x.ru/alr/internal/shutils/handlers" | 	"plemya-x.ru/alr/internal/shutils/handlers" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // FileDownloader downloads files using HTTP | // FileDownloader загружает файлы с использованием HTTP | ||||||
| type FileDownloader struct{} | type FileDownloader struct{} | ||||||
|  |  | ||||||
| // Name always returns "file" | // Name всегда возвращает "file" | ||||||
| func (FileDownloader) Name() string { | func (FileDownloader) Name() string { | ||||||
| 	return "file" | 	return "file" | ||||||
| } | } | ||||||
|  |  | ||||||
| // MatchURL always returns true, as FileDownloader | // MatchURL всегда возвращает true, так как FileDownloader | ||||||
| // is used as a fallback if nothing else matches | // используется как резерв, если ничего другого не соответствует | ||||||
| func (FileDownloader) MatchURL(string) bool { | func (FileDownloader) MatchURL(string) bool { | ||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| // Download downloads a file using HTTP. If the file is | // Download загружает файл с использованием HTTP. Если файл | ||||||
| // compressed using a supported format, it will be extracted | // сжат в поддерживаемом формате, он будет распакован | ||||||
| func (FileDownloader) Download(opts Options) (Type, string, error) { | func (FileDownloader) Download(opts Options) (Type, string, error) { | ||||||
|  | 	// Разбор URL | ||||||
| 	u, err := url.Parse(opts.URL) | 	u, err := url.Parse(opts.URL) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, "", err | 		return 0, "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Получение параметров запроса | ||||||
| 	query := u.Query() | 	query := u.Query() | ||||||
|  |  | ||||||
|  | 	// Получение имени файла из параметров запроса | ||||||
| 	name := query.Get("~name") | 	name := query.Get("~name") | ||||||
| 	query.Del("~name") | 	query.Del("~name") | ||||||
|  |  | ||||||
|  | 	// Получение параметра архивации | ||||||
| 	archive := query.Get("~archive") | 	archive := query.Get("~archive") | ||||||
| 	query.Del("~archive") | 	query.Del("~archive") | ||||||
|  |  | ||||||
|  | 	// Кодирование измененных параметров запроса обратно в URL | ||||||
| 	u.RawQuery = query.Encode() | 	u.RawQuery = query.Encode() | ||||||
|  |  | ||||||
| 	var r io.ReadCloser | 	var r io.ReadCloser | ||||||
| 	var size int64 | 	var size int64 | ||||||
|  |  | ||||||
|  | 	// Проверка схемы URL на "local" | ||||||
| 	if u.Scheme == "local" { | 	if u.Scheme == "local" { | ||||||
| 		localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path)) | 		localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -85,6 +92,7 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 		} | 		} | ||||||
| 		r = localFl | 		r = localFl | ||||||
| 	} else { | 	} else { | ||||||
|  | 		// Выполнение HTTP GET запроса | ||||||
| 		res, err := http.Get(u.String()) | 		res, err := http.Get(u.String()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return 0, "", err | 			return 0, "", err | ||||||
| @@ -107,6 +115,7 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 	defer fl.Close() | 	defer fl.Close() | ||||||
|  |  | ||||||
| 	var bar io.WriteCloser | 	var bar io.WriteCloser | ||||||
|  | 	// Настройка индикатора прогресса | ||||||
| 	if opts.Progress != nil { | 	if opts.Progress != nil { | ||||||
| 		bar = progressbar.NewOptions64( | 		bar = progressbar.NewOptions64( | ||||||
| 			size, | 			size, | ||||||
| @@ -134,18 +143,21 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var w io.Writer | 	var w io.Writer | ||||||
|  | 	// Настройка MultiWriter для записи в файл, хеш и индикатор прогресса | ||||||
| 	if opts.Hash != nil { | 	if opts.Hash != nil { | ||||||
| 		w = io.MultiWriter(fl, h, bar) | 		w = io.MultiWriter(fl, h, bar) | ||||||
| 	} else { | 	} else { | ||||||
| 		w = io.MultiWriter(fl, bar) | 		w = io.MultiWriter(fl, bar) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Копирование содержимого из источника в файл назначения | ||||||
| 	_, err = io.Copy(w, r) | 	_, err = io.Copy(w, r) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, "", err | 		return 0, "", err | ||||||
| 	} | 	} | ||||||
| 	r.Close() | 	r.Close() | ||||||
|  |  | ||||||
|  | 	// Проверка контрольной суммы | ||||||
| 	if opts.Hash != nil { | 	if opts.Hash != nil { | ||||||
| 		sum := h.Sum(nil) | 		sum := h.Sum(nil) | ||||||
| 		if !bytes.Equal(sum, opts.Hash) { | 		if !bytes.Equal(sum, opts.Hash) { | ||||||
| @@ -153,6 +165,7 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Проверка необходимости постобработки | ||||||
| 	if opts.PostprocDisabled { | 	if opts.PostprocDisabled { | ||||||
| 		return TypeFile, name, nil | 		return TypeFile, name, nil | ||||||
| 	} | 	} | ||||||
| @@ -162,6 +175,7 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 		return 0, "", err | 		return 0, "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Идентификация формата архива | ||||||
| 	format, ar, err := archiver.Identify(name, fl) | 	format, ar, err := archiver.Identify(name, fl) | ||||||
| 	if err == archiver.ErrNoMatch { | 	if err == archiver.ErrNoMatch { | ||||||
| 		return TypeFile, name, nil | 		return TypeFile, name, nil | ||||||
| @@ -169,21 +183,25 @@ func (FileDownloader) Download(opts Options) (Type, string, error) { | |||||||
| 		return 0, "", err | 		return 0, "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Распаковка архива | ||||||
| 	err = extractFile(ar, format, name, opts) | 	err = extractFile(ar, format, name, opts) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, "", err | 		return 0, "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Удаление исходного архива | ||||||
| 	err = os.Remove(path) | 	err = os.Remove(path) | ||||||
| 	return TypeDir, "", err | 	return TypeDir, "", err | ||||||
| } | } | ||||||
|  |  | ||||||
| // extractFile extracts an archive or decompresses a file | // extractFile извлекает архив или распаковывает файл | ||||||
| func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) { | func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) { | ||||||
| 	fname := format.Name() | 	fname := format.Name() | ||||||
|  |  | ||||||
|  | 	// Проверка типа формата архива | ||||||
| 	switch format := format.(type) { | 	switch format := format.(type) { | ||||||
| 	case archiver.Extractor: | 	case archiver.Extractor: | ||||||
|  | 		// Извлечение файлов из архива | ||||||
| 		err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error { | 		err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error { | ||||||
| 			fr, err := f.Open() | 			fr, err := f.Open() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -224,6 +242,7 @@ func extractFile(r io.Reader, format archiver.Format, name string, opts Options) | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	case archiver.Decompressor: | 	case archiver.Decompressor: | ||||||
|  | 		// Распаковка сжатого файла | ||||||
| 		rc, err := format.OpenReader(r) | 		rc, err := format.OpenReader(r) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -247,10 +266,9 @@ func extractFile(r io.Reader, format archiver.Format, name string, opts Options) | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // getFilename attempts to parse the Content-Disposition | // getFilename пытается разобрать заголовок Content-Disposition | ||||||
| // HTTP response header and extract a filename. If the | // HTTP-ответа и извлечь имя файла. Если заголовок отсутствует, | ||||||
| // header does not exist, it will use the last element | // используется последний элемент пути. | ||||||
| // of the path. |  | ||||||
| func getFilename(res *http.Response) (name string) { | func getFilename(res *http.Response) (name string) { | ||||||
| 	_, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition")) | 	_, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -261,4 +279,4 @@ func getFilename(res *http.Response) (name string) { | |||||||
| 	} else { | 	} else { | ||||||
| 		return path.Base(res.Request.URL.Path) | 		return path.Base(res.Request.URL.Path) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user