Как скопировать все пакеты с nuget.org

от автора

Допустим, вам захотелось, на всякий случай, сохранить копию всех пакетов 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 ТБ. Дерзайте.


ссылка на оригинал статьи https://habr.com/ru/articles/850238/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *