Допустим, вам захотелось, на всякий случай, сохранить копию всех пакетов nuget.org. Как обнаружить и скачать все пакеты не привлекая внимания админов сервиса?
Протоколы NuGet
Как NuGet-клиент обнаруживает пакеты? У клиента есть список пакетов, которые пользователь хочет скачать, клиент должен выяснить откуда их скачать, рекурсивно разрешить зависимости и собственно произвести загрузку. Для получения всей необходимой информации он обращается к API сервиса.
Особо любознательные знают, что существует две версии протокола NuGet: v2 и v3 с соответствующими "source URLs":
V2 основан на OData (XML, странный синтаксис запросов - вот это вот все) и фактически является интерфейсом к БД. Официальной документации на него, насколько я понимаю, не существует, но есть неофициальная.
С помощью v2 можно, в принципе, перечислить все пакеты, но администрация сервиса не любит когда по v2 много стучатся и ограничивает частоту запросов v2 и урезает возможности. Не используйте v2.
Протокол v3 был разработан для улучшения масштабируемости. Практически весь v3 работает из статических файлов, которые раздаются через CDN, что масштабировать гораздо проще, чем веб-сервис с БД. Только поиск требует каких-то вычислительных мощностей для работы.
Подробнее про v3
Протокол более-менее адекватно документирован. Запрос к v3 source URL вернет нам JSON со списком "сервисов", предоставляемых реализацией NuGet-сервера.
Из реального ответа nuget.org, нас будут интересовать следующие сервисы:
{
"@id": "https://api.nuget.org/v3/catalog0/index.json",
"@type": "Catalog/3.0.0",
"comment": "Index of the NuGet package catalog."
},
{
"@id": "https://api.nuget.org/v3-flatcontainer/",
"@type": "PackageBaseAddress/3.0.0",
"comment": "Base URL of where NuGet packages are stored, in the format https://api.nuget.org/v3-flatcontainer/{id-lower}/{version-lower}/{id-lower}.{version-lower}.nupkg"
},
Все (или почти все) JSON объекты, возвращаемые v3, на самом деле являются JSON-LD если вдруг вы знаете что это такое и как этим пользоваться. Из-за этого они содержат занимательные свойства с @
в начале имени, которые можно спокойно игнорировть. Возможно из-за этого же есть... "особенности" парсинга некоторых объектов, об этом ниже.
Catalog
С помощью Catalog/3.0.0
мы будем перечислять все пакеты. Каталог представляет собой журнал всего, что происходило с NuGet-пакетами с "начала времен". Прочитав каталог от начала до конца мы воспроизведем все изменения на nuget.org, что даст нам список всех пакетов. Из каталога ничего не удаляется, только добавляются записи в конец.
Протокол v3 не существовал с самого начала жизни nuget.org, поэтому при его создании был произведен импорт всех пакетов, существовавщих на момент запуска, что привело к созданию большого количества записей в начале каталога с близкими временными метками (1 февраля 2015 года). После этого, все новые пакеты имеют отметку времени близкую ко времени публикации.
https://api.nuget.org/v3/catalog0/index.json
возвращает JSON примерно такой структуры:
{
"@id": "https://api.nuget.org/v3/catalog0/index.json",
"@type": [
"CatalogRoot",
"AppendOnlyCatalog",
"Permalink"
],
"commitId": "a304b4af-3a2c-4653-8ba8-2cdfd667951d",
"commitTimeStamp": "2024-10-10T02:42:09.3106213Z",
"count": 20671,
"nuget:lastCreated": "2024-10-10T02:41:49.91Z",
"nuget:lastDeleted": "2024-10-09T16:35:48.4746061Z",
"nuget:lastEdited": "2024-10-10T02:41:49.91Z",
"items": [
{
"@id": "https://api.nuget.org/v3/catalog0/page7713.json",
"@type": "CatalogPage",
"commitId": "9f4532df-09d2-473e-a5b5-acfe3fa3935a",
"commitTimeStamp": "2018-12-29T16:00:42.7935125Z",
"count": 533
}
...
]
}
Это "оглавление каталога" (catalog index) - информация о всех его страницах. Чтобы не иметь один огромный файл, каталог разбит на страницы.
Свойства, начинающиеся на nuget:
в этом ответе - недокументированные служебные поля, имеющие отношение к генерации каталога.
commitId
- GUID последней записи (точнее группы записей, подробности ниже). Если он изменился с последнего чтения, значит были добавлены новые записи.
commitTimeStamp
- время последней записи. Все временные метки используют UTC.
count
- число страниц каталога.
items
- массив объектов с информацией о каждой странице: GUID последней записи, время последней записи, число записей и ссылка на страницу.
Массив записей не отсортирован, если вам нужны страницы в порядке их создания, придется отсортировать его по полю commitTimeStamp
.
Страницы каталога
Если послать запрос с адресом страницы, мы получим, например:
{
"@id": "https://api.nuget.org/v3/catalog0/page0.json",
"@type": "CatalogPage",
"commitId": "19a4aedc-5139-4df5-81a3-b40aeabb3f3c",
"commitTimeStamp": "2015-02-01T06:30:11.7477681Z",
"count": 540,
"items": [
{
"@id": "https://api.nuget.org/v3/catalog0/data/2015.02.01.06.22.45/adam.jsgenerator.1.1.0.json",
"@type": "nuget:PackageDetails",
"commitTimeStamp": "2015-02-01T06:22:45.8488496Z",
"nuget:id": "Adam.JSGenerator",
"nuget:version": "1.1.0",
"commitId": "b3f4fc8a-7522-42a3-8fee-a91d5488c0b1"
},
{
"@id": "https://api.nuget.org/v3/catalog0/data/2015.02.01.06.22.45/agatha-rrsl.1.2.0.json",
"@type": "nuget:PackageDetails",
"commitTimeStamp": "2015-02-01T06:22:45.8488496Z",
"nuget:id": "Agatha-rrsl",
"nuget:version": "1.2.0",
"commitId": "b3f4fc8a-7522-42a3-8fee-a91d5488c0b1"
},
...,
{
"@id": "https://api.nuget.org/v3/catalog0/data/2015.02.01.06.30.11/superfarter.1.0.0.json",
"@type": "nuget:PackageDetails",
"commitTimeStamp": "2015-02-01T06:30:11.7477681Z",
"nuget:id": "SuperFarter",
"nuget:version": "1.0.0",
"commitId": "19a4aedc-5139-4df5-81a3-b40aeabb3f3c"
}
],
"parent": "https://api.nuget.org/v3/catalog0/index.json",
"@context": {
"@vocab": "http://schema.nuget.org/catalog#",
"nuget": "http://schema.nuget.org/schema#",
"items": {
"@id": "item",
"@container": "@set"
},
"parent": {
"@type": "@id"
},
"commitTimeStamp": {
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
},
"nuget:lastCreated": {
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
},
"nuget:lastEdited": {
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
}
}
}
Тут мы видим уже знакомые commitId
, commitTimeStamp
, count
и items
в корневом объекте с тем же смыслом, что и в предыдущем запросе, но только в контексте текущей страницы. items
, соответственно, содержит данные о записях каталога, вместо страниц.
parent
содержит ссылку на оглавление каталога.
@context
можно игнорировать.
Элементы массива items
содержат ссылку на собственно запись каталога, тип этой записи, время и GUID группы записей, идентификатор и версию пакета. Массив опять не отсортирован.
Группы записей
Как можно заметить в примере выше, первые два элемента items
имеют одинаковые значения свойств commitTimeStamp
и commitId
. Это особенности работы процесса, который формирует каталог.
Процесс-генератор просыпается каждые несколько минут и проверяет свежие изменения в базе данных. Все найденные изменения записываются в одну страницу с одним commitId
и commitTimeStamp
. Если добавление прочитанных изменений в текущую страницу превысит некий порог, процесс создает новую страницу и помещает записи туда. Из-за этого, также, размер страницы непостоянен.
Вероятно есть ограничения на размер одного коммита, иначе в начале каталога была бы одна (или несколько) огромная страница, но это явно не так.
Еще одно наблюдение: в начале каталога страницы были размером примерно 550 записей, а в конце - больше 2700. Размер был изменен в 2022 году, чтобы замедлить рост количества страниц.
Типы записей
Документация описывает два типа записей каталога:
nuget:PackageDetails
- создается для всех новых пакетов, а также если произошло изменение метаданных пакета.nuget:PackageDelete
- создается если пакет был удален. Удаление происходит в некоторых исключительных случаях, так что такие записи редки.
Несколько слов об изменениях метаданных пакета. До 2018 года загруженные пакеты можно было редактировать на сайте. Ввиду того, что метаданные находятся в пакете в .nuspec-файле, сайт переупаковывал пакет с новыми метаданными. В 2018 году была добавлена возможность авторам и репозиториям подписывать пакеты (подпись автора и репозитория могут присутствовать одновременно), что поставило крест на возможности редактировать пакеты после публикации и возможность была выпилена.
Соответственно, до 2018 года можно встретить несколько записей каталога для одной пары (идентификатор; версия)
с разными метаданными. В таком случае записи, следующие за первой содержат обновленные метаданные пакета и актуальная версия метаданных находится в последней записи.
Документация описывает в каких случаях на сегодняшний день могут быть добавлены новые записи для тех же пакетов.
Запись PackageDetails
{
"@id": "https://api.nuget.org/v3/catalog0/data/2015.02.01.08.38.05/bclcontrib-abstract.spring.0.1.6.json",
"@type": "nuget:PackageDetails",
"commitId": "0502702c-6a9e-4eb6-93a6-e0798a3a0dc7",
"commitTimeStamp": "2015-02-01T08:38:05.5456876Z",
"nuget:id": "BclContrib-Abstract.Spring",
"nuget:version": "0.1.6"
},
Содержит идентификатор и версию пакета, а так же ссылку на "лист" (catalog leaves, аналогично листовым узлам деревьев), который содержит больше информации: лист типа PackageDetails содержит данные из .nuspec-файла пакета: данные секции metadata
из .nuspec-файла, информацию о зависимостях, список файлов, дату и время создания и публикации, размер и хеш пакета, информацию о коммите: GUID и время создания, такие же как на странице каталога и признак видимости пакета.
Запись PackageDelete
Выглядит так:
{
"@id": "https://api.nuget.org/v3/catalog0/data/2015.10.28.10.44.16/imagesbuttoncontrol.1.0.0.json",
"@type": "nuget:PackageDelete",
"commitId": "15d2ae77-d9e4-413e-a5da-f3ea3d5abeb1",
"commitTimeStamp": "2015-10-28T10:44:16.9226556Z",
"nuget:id": "ImagesButtonControl",
"nuget:version": "1.0.0"
},
Скачивать лист, на который эта запись ссылается бессмысленно, т.к. там записано то же самое.
"Особенности" парсинга
Уж не знаю, исходит ли это от самого формата JSON-LD, библиотеки, которая была использована для генерации этого JSON-LD или это просто баги, но в JSON-файлах размещенных на api.nuget.org
некоторые свойства могут быть представлены либо строковым/числовым литералом, либо массивом.
Например, иногда в секции с зависимостями можно обнаружить такое:
{
"@id": "https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json#dependencygroup/.netplatform5.4/nuget.commandline",
"@type": "PackageDependency",
"id": "NuGet.CommandLine",
"range": "[3.3.0, )"
},
{
"@id": "https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json#dependencygroup/.netplatform5.4/system.runtime",
"@type": "PackageDependency",
"id": "System.Runtime",
"range": [
"[4.0.10, )",
"[4.0.21-beta-23516, )"
]
},
Свойство range
может быть строкой, а может быть и массивом. Не велика проблема, просто имейте в виду, что такое может встретиться.
Качаем пакеты
Если нас интересуют только сами пакеты, листы каталога можно не качать. Для скачивания пакета нам достаточно знать идентификатор и версию пакета: в комментарии к ресурсу PackageBaseAddress/3.0.0
написано все, что нам надо знать, чтобы сконструировать ссылку для скачивания. Любители документации обнаружат, что там записано то же самое: приводим идентификатор и версию пакета в нижний регистр и добавляем к адресу ресурса согласно схеме:
https://api.nuget.org/v3-flatcontainer/{id-lower}/{version-lower}/{id-lower}.{version-lower}.nupkg
Так что все, что надо сделать: пробежать по записям каталога, сгенерировать ссылки на пакеты и скачать их. Надо иметь в ввиду, что для пакета может быть несколько записей и качать пакет стоит только один раз; пакет может быть удален, так что запрос к сконструированной ссылке вернет 404.
Т.к. записи всегда добавляются только в конец каталога, если надо скачивать пакеты по мере их загрузки, то достаточно запоминать commitId
и проверять оглавление каталога раз в несколько минут на предмет изменения идентификатора последнего коммита, и когда он поменялся искать новые записи в последних страницах каталога. Ориентация по временным меткам позволит все быстро найти.
Если же целью стоит склонировать nuget.org с возможностью направить NuGet-клиент на ваш клон и восстанавливать с него пакеты, то нужно будет воссоздать ресурс RegistrationsBaseUrl/3.6.0
. Для вычисления полного дерева пакетов клиент пользуется только этим им, так что этого должно быть достаточно. Статья и так уже длинная, так что это я оставлю как упражнение для читателя.
Сколько потребуется места - я не знаю. Последний раз я это пробовал несколько лет назад, и тогда 4-х терабайтного диска мне не хватило. Количество пакетов с тех пор увеличилось в несколько раз. Наверняка, в наши дни общий объем перевалил далеко за 10 ТБ. Дерзайте.
Комментарии (9)
Mingun
17.10.2024 07:54Меня вот что удивляет: разве клиент nuget — не опенсорсное решение? А если да, то почему в его коде не посмотреть, как он пакеты выкачивает или хотя бы ищет?
andreishe Автор
17.10.2024 07:54У клиента нет задачи перечислить все пакеты, кода, который это делает там просто не существует. Он пользуется
RegistrationsBaseUrl/3.6.0
для получения метаданных пакетов, указанных как зависимости по идентификатору и версии пакета.Клиент работает используя документированный протокол, изучаеть его код стоит разве что для более полного понимания, что конкретно он делает.
Mingun
17.10.2024 07:54Неужели в нём нет параметра типа
--list
— перечислить все пакеты? Я почему-то думал, что он входит в джентельменский набор менеджера пакетов.andreishe Автор
17.10.2024 07:54Пакетов сотни тысяч (не считая индивидуальных версий, с ними - миллионы). Какой use case у этой команды был бы? У какого пакетного менеджера она есть?
rezdm
Что, если:
wget --mirror --convert-links --no-parent --no-clobber --execute robots=off --no-check-certificate --accept=nuget --reject=html --recursive --level=inf --domains=nuget.org --span-hosts --follow-tags=a https://www.nuget.org
ну или lftp ну или ещё с полдюжины похожего
Конечно, это все файлы с расширением nuget, как в заголовке заметки. Если есть желание только определённые версии -- это другой вопрос.
andreishe Автор
Существуют страницы для каждого пакета, но не на каждую такую страницу есть ссылка.
Плюс, цель была - не привлекать внимание санитаров. Если сайт долбить запросами, сайт вам скоро отвечать станет неохотно.
rezdm
https://www.nuget.org/packages
все тут.
Не привлекать внимания -- не поможет, хотя меня терзают смутные сомнения, что MS режет траффик там.
Самое правильное -- это настроить какой jfrog (ну или что там сейчас стильно-модно-молодёжно) проксей.
andreishe Автор
https://www.nuget.org/packages?page=1501&sortBy=relevance - попробуйте нажать next. Посчитайте, сколько вы перечислите таким образом.
Кроме того, пакеты можно скрывать. На скрытые пакеты ссылок нет в принципе и в коде соответствующих страниц указаны инструкции поисковикам игнорировать их.
andreishe Автор
А в рейт-лимиты можно просто в браузере вляпаться, если слишком активно бегать по сайту: