Microsoft MVP성태의 닷넷 이야기
.NET Framework: 793. C# - REST API를 이용해 NuGet 저장소 제어 [링크 복사], [링크+제목 복사],
조회: 12327
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
개발 환경 구성: 296. .NET Core 프로젝트를 NuGet Gallery에 배포하는 방법
; https://www.sysnet.pe.kr/2/0/11034

.NET Framework: 793. C# - REST API를 이용해 NuGet 저장소 제어
; https://www.sysnet.pe.kr/2/0/11696

개발 환경 구성: 423. NuGet 패키지 제작 - Native와 Managed DLL을 분리하는 방법
; https://www.sysnet.pe.kr/2/0/11793

개발 환경 구성: 494. NuGet - nuspec의 패키지 스키마 버전(네임스페이스) 업데이트 방법
; https://www.sysnet.pe.kr/2/0/12234




C# - REST API를 이용해 NuGet 저장소 제어

NuGet 저장소의 REST API는 다음의 공식 문서에서 자세하게 설명하고 있습니다.

NuGet API
; https://learn.microsoft.com/en-us/nuget/api/overview

물론 다음과 같은 잘 정리된 라이브러리를 이용해 제어하는 것도 가능하지만,

NuGet.Protocol
; https://www.nuget.org/packages/NuGet.Protocol/4.8.0

이 글에서는 그냥 HTTP 통신으로 만들어 보겠습니다. ^^




우선, 시작점은 다음의 JSON 결과물로부터 출발합니다.

https://api.nuget.org/v3/index.json

[index.json 파일 내용]

{
  "version": "3.0.0",
  "resources": [
    {
      "@id": "https://api-v2v3search-0.nuget.org/query",
      "@type": "SearchQueryService",
      "comment": "Query endpoint of NuGet Search service (primary)"
    },
    {
      "@id": "https://api-v2v3search-1.nuget.org/query",
      "@type": "SearchQueryService",
      "comment": "Query endpoint of NuGet Search service (secondary)"
    },

    ...[생략]...

    {
      "@id": "https://api.nuget.org/v3/registration3-gz-semver2/",
      "@type": "RegistrationsBaseUrl/Versioned",
      "clientVersion": "4.3.0-alpha",
      "comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL includes SemVer 2.0.0 packages."
    },
    {
      "@id": "https://api.nuget.org/v3/catalog0/index.json",
      "@type": "Catalog/3.0.0",
      "comment": "Index of the NuGet package catalog."
    }
  ],
  "@context": {
    "@vocab": "http://schema.nuget.org/services#",
    "comment": "http://www.w3.org/2000/01/rdf-schema#comment"
  }
}

보는 바와 같이 "@type"에 해당하는 기능을 "@id"에서 제공하는 링크를 통해 서비스를 제공합니다. 따라서, 다음과 같은 코드로 시작할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace NugetRestClient
{
    class NugetClient
    {
        const string _sourceUrl = "https://api.nuget.org/v3/index.json";
        CookieContainer _cookies;
        HttpClient _httpClient;
        ServiceIndex _serviceIndex;

        private string GetServiceEndPoint(string serviceTypeName)
        {
            foreach (var item in _serviceIndex.resources)
            {
                if (item.type == serviceTypeName)
                {
                    return item.id;
                }
            }

            throw new ApplicationException("ServiceNotFound: " + serviceTypeName);
        }

        public NugetClient()
        {
            HttpClientHandler handler = new HttpClientHandler();
            _cookies = new CookieContainer();
            handler.CookieContainer = _cookies;

            HttpClient hc = new HttpClient(handler);
            _httpClient = hc;
        }

        public async Task GetFeedAsync()
        {
            string text = await _httpClient.GetStringAsync(_sourceUrl);
            _serviceIndex = Newtonsoft.Json.JsonConvert.DeserializeObject<ServiceIndex>(text);
        }
    }
}

위의 코드를 이용해 다음과 같이 service index 정보를 가져옵니다.

public static async Task<int> Main(string[] args)
{
    NugetClient client = new NugetClient();
    await client.GetFeedAsync();

    return 0;
}

이제 NuGet으로부터 등록된 패키지의 정보를 다음과 같은 식으로 처리할 수 있습니다.

public async Task<SearchQueryServiceResult> GetPackageInfoAsync(string packageId, bool includePrerelease)
{
    string url = GetServiceEndPoint("SearchQueryService");
    string query = string.Format("{0}?q={1}", url, packageId);

    if (includePrerelease == true)
    {
        query += "&prerelease=true";
    }

    string text = await _httpClient.GetStringAsync(query);
    return Newtonsoft.Json.JsonConvert.DeserializeObject<SearchQueryServiceResult>(text);
}

위의 query 변수는 index.json에 정의된 SearchQueryService 서비스의 url에 대해 다음과 같은 쿼리를,

{servicequery}?q={packageId}

{servicequery}?q={packageId}&prerelease=true

NuGet에 전송합니다. Search와 관련해 어떤 유형의 서비스들이 있는지는 문서에 잘 나와 있습니다.

Docs / NuGet / API - Search
; https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource

위의 문서에 보면, @type으로 다음의 값들이 가능하다고 나옵니다.

SearchQueryService : The initial release
SearchQueryService/3.0.0-beta : Alias of SearchQueryService
SearchQueryService/3.0.0-rc : Alias of SearchQueryService

현재(2018-09-18) 기준으로 정식 서비스는 "SearchQueryService"이므로 이 글에서는 그 옵션을 사용한 것입니다. 이와 함께 query에 전달할 수 있는 인자들을 다음과 같이 소개하고 있습니다.

GET {@id}?q={QUERY}&skip={SKIP}&take={TAKE}&prerelease={PRERELEASE}&semVerLevel={SEMVERLEVEL}

보면 페이징 기능도 있으므로 적절하게 사용하시면 됩니다.




예제 시나리오를 하나 작성해서 구현해 보겠습니다. ^^

빌드 시스템을 통해 생성된 바이너리를 NuGet 패키지의 새 버전에 등록할 때마다 alpha, alpha2, alpha3, alpha4, ...와 같은 식으로 버전을 늘려 가며 등록하는 것입니다. 정식 릴리스는 아니므로 기존 alpha(N) 버전이 있다면 unlist로 만들고 다음 릴리스 번호 값을 구하는 정도까지만 구현해 보겠습니다.

이를 위해서는 다음의 2개 메서드를 추가 구현하면 됩니다.

public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId, string vesrionPostfix)
{
    string url = GetServiceEndPoint("RegistrationsBaseUrl");
    string query = string.Format("{0}{1}/{2}.json", url, packageId.ToLower(), vesrionPostfix);

    HttpResponseMessage hrm = await _httpClient.GetAsync(query);

    if (hrm.StatusCode == HttpStatusCode.NotFound)
    {
        return null;
    }

    string text = await hrm.Content.ReadAsStringAsync();
    PackageMetadata searchResult = Newtonsoft.Json.JsonConvert.DeserializeObject<PackageMetadata>(text);
    return searchResult;
}

public async Task<bool> UnlistVersionPackageAsync(string packageId, string vesrionPostfix)
{
    string url = GetServiceEndPoint("PackagePublish/2.0.0");
    string query = string.Format("{0}/{1}/{2}", url, packageId.ToLower(), vesrionPostfix);

    HttpResponseMessage hrm = await _httpClient.DeleteAsync(query);
    return hrm.StatusCode == HttpStatusCode.OK;
}

주의할 것은, 등록된 패키지의 특정 버전 상태(Status)를 "Unlisted"로 바꾸기 위해서는 API Key를 HTTP 요청의 "X-NuGet-ApiKey" 헤더에 전송해야 합니다. 이 작업은 HttpClient를 생성할 때 미리 해주는 것으로 처리하면 편리합니다.

public NugetClient(string apiKey = "")
{
    HttpClientHandler handler = new HttpClientHandler();
    _cookies = new CookieContainer();
    handler.CookieContainer = _cookies;

    HttpClient hc = new HttpClient(handler);
    _httpClient = hc;

    if (string.IsNullOrEmpty(apiKey) == false)
    {
        _httpClient.DefaultRequestHeaders.Add("X-NuGet-ApiKey", apiKey);
    }
}

래퍼 API가 만들어졌으니 이제 다음과 같이 사용할 수 있습니다.

string packageId = "MyTestPackage";
string versionPrefix = "1.0.0.0";
string checkPostfix = versionPrefix + "-alpha";
int alphaNumber = 1;

while (true)
{
    PackageMetadata item = await client.GetPackageMetadataAsync(packageId, checkPostfix);
    if (item == null)
    {
        break;
    }

    if (item.listed == true)
    {
        await client.UnlistVersionPackageAsync(packageId, checkPostfix);
    }

    alphaNumber++;
    checkPostfix = $"{versionPrefix}-alpha{alphaNumber}";
}

Console.WriteLine("Next candidate version: " + checkPostfix);

이 정도면 제법 감각을 익히셨을 테니 여러분이 필요한 나머지 기능들도 쉽게 구현할 수 있을 것입니다. ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, NuGet.exe 프로그램에 대한 배포 목록도 다음의 json 파일로 구할 수 있습니다.

https://dist.nuget.org/tools.json
https://dist.nuget.org/tools.schema.json




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/22/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13383정성태6/26/20233481개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233526.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20233925오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233348스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233397스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233288오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20237136오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233506.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20233133스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233253오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234567오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233246개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233292개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233459개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233256개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233418개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233542오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/20233302.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/20233050오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
13364정성태6/8/20233863.NET Framework: 2127. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 [1]
13363정성태6/7/20233452스크립트: 49. 파이썬 - "Transformers (신경망 언어모델 라이브러리) 강좌" - 1장 2절 코드 실행 결과
13362정성태6/1/20233365.NET Framework: 2126. C# - 서버 측의 요청 제어 (Microsoft.AspNetCore.RateLimiting)파일 다운로드1
13361정성태5/31/20233790오류 유형: 862. Facebook - ASP.NET/WebClient 사용 시 graph.facebook.com/me 호출에 대해 403 Forbidden 오류
13360정성태5/31/20233176오류 유형: 861. WSL/docker - failed to start shim: start failed: io.containerd.runc.v2: create new shim socket
13359정성태5/19/20233496오류 유형: 860. Docker Desktop - k8s 초기화 무한 반복한다면?
13358정성태5/17/20233824.NET Framework: 2125. C# - Semantic Kernel의 Semantic Memory 사용 예제 [1]파일 다운로드1
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...