From 1407064240feef98267bc211df11fe365e803f18 Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Tue, 24 Feb 2026 16:25:28 +0530 Subject: [PATCH 1/3] added readme api for nuget --- .../api/controller/pkg/nuget/controller.go | 2 + .../api/controller/pkg/nuget/get_readme.go | 107 ++++++++++++++++++ .../app/api/controller/pkg/nuget/response.go | 5 + registry/app/api/handler/nuget/handler.go | 35 ++++++ registry/app/api/router/packages/route.go | 4 + registry/app/pkg/nuget/helper.go | 10 ++ registry/app/pkg/nuget/local.go | 2 +- registry/app/pkg/types/nuget/types.go | 2 + 8 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 registry/app/api/controller/pkg/nuget/get_readme.go diff --git a/registry/app/api/controller/pkg/nuget/controller.go b/registry/app/api/controller/pkg/nuget/controller.go index 8196e5aa58..d21652275f 100644 --- a/registry/app/api/controller/pkg/nuget/controller.go +++ b/registry/app/api/controller/pkg/nuget/controller.go @@ -73,6 +73,8 @@ type Controller interface { CountPackageV2(ctx context.Context, info nugettype.ArtifactInfo, searchTerm string) *EntityCountResponse GetServiceMetadataV2(ctx context.Context, info nugettype.ArtifactInfo) *GetServiceMetadataV2Response + + GetReadme(ctx context.Context, info nugettype.ArtifactInfo) *GetReadmeResponse } // Controller handles Python package operations. diff --git a/registry/app/api/controller/pkg/nuget/get_readme.go b/registry/app/api/controller/pkg/nuget/get_readme.go new file mode 100644 index 0000000000..671d79dc4a --- /dev/null +++ b/registry/app/api/controller/pkg/nuget/get_readme.go @@ -0,0 +1,107 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nuget + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/rs/zerolog/log" + + "github.com/harness/gitness/registry/app/pkg/commons" + nugetmetadata "github.com/harness/gitness/registry/app/metadata/nuget" + nugettype "github.com/harness/gitness/registry/app/pkg/types/nuget" +) + +func (c *controller) GetReadme( + ctx context.Context, + info nugettype.ArtifactInfo, +) *GetReadmeResponse { + // Get image first + image, err := c.imageDao.GetByName(ctx, info.RegistryID, info.Image) + if err != nil { + log.Ctx(ctx).Error().Err(err). + Str("registry", info.RegIdentifier). + Str("package", info.Image). + Msg("failed to get image") + return &GetReadmeResponse{ + BaseResponse: BaseResponse{ + Error: fmt.Errorf("failed to get image: %w", err), + }, + ReadmeContent: "", + } + } + + // Get artifact by version + artifact, err := c.artifactDao.GetByName(ctx, image.ID, info.Version) + if err != nil { + log.Ctx(ctx).Error().Err(err). + Str("registry", info.RegIdentifier). + Str("package", info.Image). + Str("version", info.Version). + Msg("failed to get artifact") + return &GetReadmeResponse{ + BaseResponse: BaseResponse{ + Error: fmt.Errorf("failed to get artifact: %w", err), + }, + ReadmeContent: "", + } + } + + // Unmarshal the metadata to get the readme field + var metadata nugetmetadata.NugetMetadata + if err := json.Unmarshal(artifact.Metadata, &metadata); err != nil { + log.Ctx(ctx).Error().Err(err). + Str("registry", info.RegIdentifier). + Str("package", info.Image). + Str("version", info.Version). + Str("metadata_raw", string(artifact.Metadata)). + Msg("failed to unmarshal nuget metadata") + return &GetReadmeResponse{ + BaseResponse: BaseResponse{ + Error: fmt.Errorf("failed to unmarshal metadata: %w", err), + }, + ReadmeContent: "", + } + } + + // Get the readme content from metadata + readmeContent := metadata.Metadata.PackageMetadata.Readme + + if readmeContent == "" { + return &GetReadmeResponse{ + BaseResponse: BaseResponse{ + Error: fmt.Errorf("readme not found for package %s version %s", info.Image, info.Version), + }, + ReadmeContent: "", + } + } + + // Set response headers for markdown content + headers := &commons.ResponseHeaders{ + Headers: map[string]string{ + "Content-Type": "text/markdown; charset=utf-8", + }, + } + + return &GetReadmeResponse{ + BaseResponse: BaseResponse{ + Error: nil, + ResponseHeaders: headers, + }, + ReadmeContent: readmeContent, + } +} diff --git a/registry/app/api/controller/pkg/nuget/response.go b/registry/app/api/controller/pkg/nuget/response.go index f0b25740a4..62daf0f635 100644 --- a/registry/app/api/controller/pkg/nuget/response.go +++ b/registry/app/api/controller/pkg/nuget/response.go @@ -107,3 +107,8 @@ type GetPackageVersionMetadataResponse struct { BaseResponse RegistrationLeafResponse *nuget.RegistrationLeafResponse } + +type GetReadmeResponse struct { + BaseResponse + ReadmeContent string +} diff --git a/registry/app/api/handler/nuget/handler.go b/registry/app/api/handler/nuget/handler.go index 94020930cf..a7e28838a6 100644 --- a/registry/app/api/handler/nuget/handler.go +++ b/registry/app/api/handler/nuget/handler.go @@ -15,6 +15,7 @@ package nuget import ( + "fmt" "net/http" "strings" @@ -33,6 +34,7 @@ type Handler interface { UploadSymbolPackage(writer http.ResponseWriter, request *http.Request) DownloadPackage(http.ResponseWriter, *http.Request) DeletePackage(writer http.ResponseWriter, request *http.Request) + GetReadme(writer http.ResponseWriter, request *http.Request) GetServiceEndpoint(http.ResponseWriter, *http.Request) GetServiceEndpointV2(http.ResponseWriter, *http.Request) ListPackageVersion(http.ResponseWriter, *http.Request) @@ -92,3 +94,36 @@ func (h *handler) GetPackageArtifactInfo(r *http.Request) (pkg.PackageArtifactIn NestedPath: strings.TrimSuffix(r.PathValue("*"), "/"), }, nil } + +func (h *handler) GetReadme(writer http.ResponseWriter, request *http.Request) { + ctx := request.Context() + + // Get artifact info from request + info, err := h.GetPackageArtifactInfo(request) + if err != nil { + http.Error(writer, fmt.Sprintf("Failed to get artifact info: %v", err), http.StatusBadRequest) + return + } + + nugetInfo, ok := info.(*nugettype.ArtifactInfo) + if !ok { + http.Error(writer, "Invalid artifact info type", http.StatusInternalServerError) + return + } + + // Call controller to get readme + response := h.controller.GetReadme(ctx, *nugetInfo) + if response.Error != nil { + http.Error(writer, response.Error.Error(), http.StatusNotFound) + return + } + + // Write response headers + if response.ResponseHeaders != nil { + response.ResponseHeaders.WriteHeadersToResponse(writer) + } + + // Write readme content + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write([]byte(response.ReadmeContent)) +} diff --git a/registry/app/api/router/packages/route.go b/registry/app/api/router/packages/route.go index 29a789706f..ba3669d669 100644 --- a/registry/app/api/router/packages/route.go +++ b/registry/app/api/router/packages/route.go @@ -212,6 +212,10 @@ func NewRouter( r.With(middleware.StoreArtifactInfo(nugetHandler)). With(middleware.RequestNugetPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nugetHandler.GetPackageVersionMetadataV2) + r.With(middleware.StoreArtifactInfo(nugetHandler)). + With(middleware.TrackDownloadStats(packageHandler)). + With(middleware.RequestNugetPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). + Get("/{id}/{version}/readme", nugetHandler.GetReadme) r.With(middleware.StoreArtifactInfo(nugetHandler)). With(middleware.RequestNugetPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). Get("/registration/{id}/{version}", nugetHandler.GetPackageVersionMetadata) diff --git a/registry/app/pkg/nuget/helper.go b/registry/app/pkg/nuget/helper.go index e10fb49b3a..f2c47cb7f9 100644 --- a/registry/app/pkg/nuget/helper.go +++ b/registry/app/pkg/nuget/helper.go @@ -64,6 +64,10 @@ func buildServiceEndpoint(baseURL string) *nuget.ServiceEndpoint { ID: baseURL + "/package", Type: "PackageBaseAddress/3.0.0", }, + { + ID: baseURL + "/{lower_id}/{lower_version}/readme", + Type: "ReadmeUriTemplate/6.13.0", + }, { ID: baseURL, Type: "PackagePublish/2.0.0", @@ -128,6 +132,11 @@ func getPackageDownloadURL(baseURL, id, version string) string { return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", baseURL, id, version, id, version) } +// getReadmeURL builds the readme url. +func getReadmeURL(baseURL, id, version string) string { + return fmt.Sprintf("%s/%s/%s/readme", baseURL, id, version) +} + // GetPackageMetadataURL builds the package metadata url. func getPackageMetadataURL(baseURL, id, version string) string { return fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", baseURL, id, version) @@ -672,6 +681,7 @@ func createRegistrationIndexPageItem(baseURL string, info nuget.ArtifactInfo, ar Tags: metadata.PackageMetadata.Tags, Title: metadata.PackageMetadata.Title, Published: artifact.CreatedAt.Format(time.RFC3339), + ReadmeURL: getReadmeURL(baseURL, info.Image, artifact.Version), }, } return res, nil diff --git a/registry/app/pkg/nuget/local.go b/registry/app/pkg/nuget/local.go index 9258c46fda..fb85bc9b11 100644 --- a/registry/app/pkg/nuget/local.go +++ b/registry/app/pkg/nuget/local.go @@ -331,7 +331,7 @@ func (c *localRegistry) buildMetadata(fileReader io.Reader) (metadata nugetmetad if err != nil { return metadata, fmt.Errorf("failed to parse metadata from .nuspec file: %w", err2) } - } else if strings.HasSuffix(header.Name, "README.md") { + } else if strings.HasSuffix(strings.ToLower(header.Name), "readme.md") { readme, err2 = c.parseReadme(zr) if err2 != nil { return metadata, fmt.Errorf("failed to parse metadata from README.md file: %w", err2) diff --git a/registry/app/pkg/types/nuget/types.go b/registry/app/pkg/types/nuget/types.go index e4c1ebae6b..5929d8ccfc 100644 --- a/registry/app/pkg/types/nuget/types.go +++ b/registry/app/pkg/types/nuget/types.go @@ -373,6 +373,8 @@ type CatalogEntry struct { Tags string `json:"tags,omitempty"` Title string `json:"title,omitempty"` Published string `json:"published,omitempty"` + //nolint: tagliatelle + ReadmeURL string `json:"readmeUrl,omitempty"` } // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group From 4d8e16526f865da88f7bc8d24fec0b0871445792 Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Tue, 24 Feb 2026 16:52:38 +0530 Subject: [PATCH 2/3] changes --- .../api/controller/pkg/nuget/get_readme.go | 113 ++++++++---------- registry/app/api/handler/nuget/handler.go | 10 +- registry/app/api/router/packages/route.go | 1 - registry/app/pkg/nuget/local.go | 28 +++++ registry/app/pkg/nuget/proxy.go | 8 ++ registry/app/pkg/nuget/registry.go | 2 + 6 files changed, 91 insertions(+), 71 deletions(-) diff --git a/registry/app/api/controller/pkg/nuget/get_readme.go b/registry/app/api/controller/pkg/nuget/get_readme.go index 671d79dc4a..7566ed552d 100644 --- a/registry/app/api/controller/pkg/nuget/get_readme.go +++ b/registry/app/api/controller/pkg/nuget/get_readme.go @@ -16,92 +16,75 @@ package nuget import ( "context" - "encoding/json" "fmt" - "github.com/rs/zerolog/log" - + "github.com/harness/gitness/registry/app/pkg" + "github.com/harness/gitness/registry/app/pkg/base" "github.com/harness/gitness/registry/app/pkg/commons" - nugetmetadata "github.com/harness/gitness/registry/app/metadata/nuget" + "github.com/harness/gitness/registry/app/pkg/nuget" + "github.com/harness/gitness/registry/app/pkg/response" nugettype "github.com/harness/gitness/registry/app/pkg/types/nuget" + registrytypes "github.com/harness/gitness/registry/types" ) func (c *controller) GetReadme( ctx context.Context, info nugettype.ArtifactInfo, ) *GetReadmeResponse { - // Get image first - image, err := c.imageDao.GetByName(ctx, info.RegistryID, info.Image) - if err != nil { - log.Ctx(ctx).Error().Err(err). - Str("registry", info.RegIdentifier). - Str("package", info.Image). - Msg("failed to get image") - return &GetReadmeResponse{ - BaseResponse: BaseResponse{ - Error: fmt.Errorf("failed to get image: %w", err), - }, - ReadmeContent: "", + f := func(registry registrytypes.Registry, a pkg.Artifact) response.Response { + info.RegIdentifier = registry.Name + info.RegistryID = registry.ID + nugetRegistry, ok := a.(nuget.Registry) + if !ok { + return &GetReadmeResponse{ + BaseResponse{ + fmt.Errorf("invalid registry type: expected nuget.Registry"), + nil, + }, "", + } + } + readmeContent, err := nugetRegistry.GetReadme(ctx, info) + if err != nil { + return &GetReadmeResponse{ + BaseResponse{ + err, + nil, + }, "", + } } - } - // Get artifact by version - artifact, err := c.artifactDao.GetByName(ctx, image.ID, info.Version) - if err != nil { - log.Ctx(ctx).Error().Err(err). - Str("registry", info.RegIdentifier). - Str("package", info.Image). - Str("version", info.Version). - Msg("failed to get artifact") - return &GetReadmeResponse{ - BaseResponse: BaseResponse{ - Error: fmt.Errorf("failed to get artifact: %w", err), + headers := &commons.ResponseHeaders{ + Headers: map[string]string{ + "Content-Type": "text/markdown; charset=utf-8", }, - ReadmeContent: "", } - } - // Unmarshal the metadata to get the readme field - var metadata nugetmetadata.NugetMetadata - if err := json.Unmarshal(artifact.Metadata, &metadata); err != nil { - log.Ctx(ctx).Error().Err(err). - Str("registry", info.RegIdentifier). - Str("package", info.Image). - Str("version", info.Version). - Str("metadata_raw", string(artifact.Metadata)). - Msg("failed to unmarshal nuget metadata") return &GetReadmeResponse{ - BaseResponse: BaseResponse{ - Error: fmt.Errorf("failed to unmarshal metadata: %w", err), - }, - ReadmeContent: "", + BaseResponse{ + nil, + headers, + }, readmeContent, } } - // Get the readme content from metadata - readmeContent := metadata.Metadata.PackageMetadata.Readme - - if readmeContent == "" { + result, err := base.NoProxyWrapper(ctx, c.registryDao, f, info) + + if err != nil { return &GetReadmeResponse{ - BaseResponse: BaseResponse{ - Error: fmt.Errorf("readme not found for package %s version %s", info.Image, info.Version), - }, - ReadmeContent: "", + BaseResponse{ + err, + nil, + }, "", } } - - // Set response headers for markdown content - headers := &commons.ResponseHeaders{ - Headers: map[string]string{ - "Content-Type": "text/markdown; charset=utf-8", - }, - } - - return &GetReadmeResponse{ - BaseResponse: BaseResponse{ - Error: nil, - ResponseHeaders: headers, - }, - ReadmeContent: readmeContent, + readmeResponse, ok := result.(*GetReadmeResponse) + if !ok { + return &GetReadmeResponse{ + BaseResponse{ + fmt.Errorf("invalid response type: expected GetReadmeResponse"), + nil, + }, "", + } } + return readmeResponse } diff --git a/registry/app/api/handler/nuget/handler.go b/registry/app/api/handler/nuget/handler.go index a7e28838a6..4435a07b04 100644 --- a/registry/app/api/handler/nuget/handler.go +++ b/registry/app/api/handler/nuget/handler.go @@ -97,32 +97,32 @@ func (h *handler) GetPackageArtifactInfo(r *http.Request) (pkg.PackageArtifactIn func (h *handler) GetReadme(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() - + // Get artifact info from request info, err := h.GetPackageArtifactInfo(request) if err != nil { http.Error(writer, fmt.Sprintf("Failed to get artifact info: %v", err), http.StatusBadRequest) return } - + nugetInfo, ok := info.(*nugettype.ArtifactInfo) if !ok { http.Error(writer, "Invalid artifact info type", http.StatusInternalServerError) return } - + // Call controller to get readme response := h.controller.GetReadme(ctx, *nugetInfo) if response.Error != nil { http.Error(writer, response.Error.Error(), http.StatusNotFound) return } - + // Write response headers if response.ResponseHeaders != nil { response.ResponseHeaders.WriteHeadersToResponse(writer) } - + // Write readme content writer.WriteHeader(http.StatusOK) _, _ = writer.Write([]byte(response.ReadmeContent)) diff --git a/registry/app/api/router/packages/route.go b/registry/app/api/router/packages/route.go index ba3669d669..a5350dff34 100644 --- a/registry/app/api/router/packages/route.go +++ b/registry/app/api/router/packages/route.go @@ -213,7 +213,6 @@ func NewRouter( With(middleware.RequestNugetPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nugetHandler.GetPackageVersionMetadataV2) r.With(middleware.StoreArtifactInfo(nugetHandler)). - With(middleware.TrackDownloadStats(packageHandler)). With(middleware.RequestNugetPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). Get("/{id}/{version}/readme", nugetHandler.GetReadme) r.With(middleware.StoreArtifactInfo(nugetHandler)). diff --git a/registry/app/pkg/nuget/local.go b/registry/app/pkg/nuget/local.go index fb85bc9b11..a1d9353c26 100644 --- a/registry/app/pkg/nuget/local.go +++ b/registry/app/pkg/nuget/local.go @@ -16,6 +16,7 @@ package nuget import ( "context" + "encoding/json" "encoding/xml" "errors" "fmt" @@ -447,3 +448,30 @@ func (c *localRegistry) GetArtifactType() apicontract.RegistryType { func (c *localRegistry) GetPackageTypes() []apicontract.PackageType { return []apicontract.PackageType{apicontract.PackageTypeNUGET} } + +func (c *localRegistry) GetReadme( + ctx context.Context, + info nugettype.ArtifactInfo, +) (string, error) { + image, err := c.imageDao.GetByName(ctx, info.RegistryID, info.Image) + if err != nil { + return "", fmt.Errorf("failed to get image: %w", err) + } + + artifact, err := c.artifactDao.GetByName(ctx, image.ID, info.Version) + if err != nil { + return "", fmt.Errorf("failed to get artifact: %w", err) + } + + var metadata nugetmetadata.NugetMetadata + if err := json.Unmarshal(artifact.Metadata, &metadata); err != nil { + return "", fmt.Errorf("failed to unmarshal metadata: %w", err) + } + + readmeContent := metadata.Metadata.PackageMetadata.Readme + if readmeContent == "" { + return "", fmt.Errorf("readme not found for package %s version %s", info.Image, info.Version) + } + + return readmeContent, nil +} diff --git a/registry/app/pkg/nuget/proxy.go b/registry/app/pkg/nuget/proxy.go index 219b8009e7..fb84faa1a9 100644 --- a/registry/app/pkg/nuget/proxy.go +++ b/registry/app/pkg/nuget/proxy.go @@ -517,6 +517,14 @@ func (r *proxy) GetPackageTypes() []artifact.PackageType { return []artifact.PackageType{artifact.PackageTypeNUGET} } +func (r *proxy) GetReadme( + ctx context.Context, + _ nugettype.ArtifactInfo, +) (string, error) { + log.Error().Ctx(ctx).Msg("Not implemented") + return "", errcode.ErrCodeInvalidRequest.WithDetail(fmt.Errorf("not implemented")) +} + func (r *proxy) putFileToLocal( ctx context.Context, info *nugettype.ArtifactInfo, remote RemoteRegistryHelper, diff --git a/registry/app/pkg/nuget/registry.go b/registry/app/pkg/nuget/registry.go index 132665f7c0..3751e3d8ff 100644 --- a/registry/app/pkg/nuget/registry.go +++ b/registry/app/pkg/nuget/registry.go @@ -60,4 +60,6 @@ type Registry interface { GetServiceEndpointV2(ctx context.Context, info nuget.ArtifactInfo) *nuget.ServiceEndpointV2 GetServiceMetadataV2(ctx context.Context, info nuget.ArtifactInfo) *nuget.ServiceMetadataV2 + + GetReadme(ctx context.Context, info nuget.ArtifactInfo) (string, error) } From 2803ad1ede3b86dd5c2cbdf8e05b086774bf3956 Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Tue, 24 Feb 2026 17:07:58 +0530 Subject: [PATCH 3/3] corrected error --- registry/app/api/handler/nuget/handler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/app/api/handler/nuget/handler.go b/registry/app/api/handler/nuget/handler.go index 4435a07b04..ea35d764f8 100644 --- a/registry/app/api/handler/nuget/handler.go +++ b/registry/app/api/handler/nuget/handler.go @@ -101,20 +101,20 @@ func (h *handler) GetReadme(writer http.ResponseWriter, request *http.Request) { // Get artifact info from request info, err := h.GetPackageArtifactInfo(request) if err != nil { - http.Error(writer, fmt.Sprintf("Failed to get artifact info: %v", err), http.StatusBadRequest) + h.HandleErrors(ctx, []error{err}, writer) return } nugetInfo, ok := info.(*nugettype.ArtifactInfo) if !ok { - http.Error(writer, "Invalid artifact info type", http.StatusInternalServerError) + h.HandleErrors(ctx, []error{fmt.Errorf("failed to fetch info from context")}, writer) return } // Call controller to get readme response := h.controller.GetReadme(ctx, *nugetInfo) if response.Error != nil { - http.Error(writer, response.Error.Error(), http.StatusNotFound) + h.HandleError(ctx, writer, response.Error) return }