diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index fb8fa414db..e1eb4adc27 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -657,7 +657,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro genericLocalRegistry := generic2.LocalRegistryProvider(localBase, fileManager, upstreamProxyConfigRepository, transactor, registryRepository, imageRepository, artifactRepository, provider) localRegistryHelper := generic2.LocalRegistryHelperProvider(genericLocalRegistry, localBase) proxy := generic2.ProxyProvider(upstreamProxyConfigRepository, registryRepository, imageRepository, artifactRepository, fileManager, transactor, provider, spaceFinder, secretService, localRegistryHelper) - genericController := generic.ControllerProvider(spaceStore, authorizer, fileManager, genericDBStore, transactor, spaceFinder, genericLocalRegistry, proxy, finder, dependencyFirewallChecker, auditService) + genericController := generic.ControllerProvider(spaceStore, authorizer, fileManager, genericDBStore, transactor, spaceFinder, genericLocalRegistry, proxy, finder, dependencyFirewallChecker, auditService, packageWrapper) packagesHandler := api2.NewPackageHandlerProvider(registryRepository, downloadStatRepository, bandwidthStatRepository, spaceStore, tokenStore, controller, authenticator, provider, authorizer, spaceFinder, registryFinder, fileManager, finder, packageWrapper, auditService, artifactRepository) genericHandler := api2.NewGenericHandlerProvider(spaceStore, genericController, tokenStore, controller, authenticator, provider, authorizer, packagesHandler, spaceFinder, registryFinder, auditService) handler3 := router.GenericHandlerProvider(genericHandler) diff --git a/registry/app/api/controller/mocks/package_wrapper.go b/registry/app/api/controller/mocks/package_wrapper.go index 159cebe72a..40a31ef29a 100644 --- a/registry/app/api/controller/mocks/package_wrapper.go +++ b/registry/app/api/controller/mocks/package_wrapper.go @@ -1070,6 +1070,57 @@ func (_c *MockPackageWrapper_IsValidUpstreamSources_Call) RunAndReturn(run func( return _c } +// IsFileOperationSupported provides a mock function for the type MockPackageWrapper +func (_mock *MockPackageWrapper) IsFileOperationSupported(packageType string) bool { + ret := _mock.Called(packageType) + + if len(ret) == 0 { + panic("no return value specified for IsFileOperationSupported") + } + + var r0 bool + if returnFunc, ok := ret.Get(0).(func(string) bool); ok { + r0 = returnFunc(packageType) + } else { + r0 = ret.Get(0).(bool) + } + return r0 +} + +// MockPackageWrapper_IsFileOperationSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFileOperationSupported' +type MockPackageWrapper_IsFileOperationSupported_Call struct { + *mock.Call +} + +// IsFileOperationSupported is a helper method to define mock.On call +// - packageType string +func (_e *MockPackageWrapper_Expecter) IsFileOperationSupported(packageType interface{}) *MockPackageWrapper_IsFileOperationSupported_Call { + return &MockPackageWrapper_IsFileOperationSupported_Call{Call: _e.mock.On("IsFileOperationSupported", packageType)} +} + +func (_c *MockPackageWrapper_IsFileOperationSupported_Call) Run(run func(packageType string)) *MockPackageWrapper_IsFileOperationSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockPackageWrapper_IsFileOperationSupported_Call) Return(b bool) *MockPackageWrapper_IsFileOperationSupported_Call { + _c.Call.Return(b) + return _c +} + +func (_c *MockPackageWrapper_IsFileOperationSupported_Call) RunAndReturn(run func(packageType string) bool) *MockPackageWrapper_IsFileOperationSupported_Call { + _c.Call.Return(run) + return _c +} + // ValidateRepoType provides a mock function for the type MockPackageWrapper func (_mock *MockPackageWrapper) ValidateRepoType(packageType string, repoType string) bool { ret := _mock.Called(packageType, repoType) diff --git a/registry/app/api/controller/pkg/generic/controller.go b/registry/app/api/controller/pkg/generic/controller.go index 57724fbbe0..855f4f48cc 100644 --- a/registry/app/api/controller/pkg/generic/controller.go +++ b/registry/app/api/controller/pkg/generic/controller.go @@ -59,6 +59,7 @@ type Controller struct { quarantineFinder quarantine.Finder dependencyFirewallChecker interfaces.DependencyFirewallChecker auditService audit.Service + packageWrapper interfaces.PackageWrapper } type DBStore struct { @@ -82,6 +83,7 @@ func NewController( quarantineFinder quarantine.Finder, dependencyFirewallChecker interfaces.DependencyFirewallChecker, auditService audit.Service, + packageWrapper interfaces.PackageWrapper, ) *Controller { return &Controller{ SpaceStore: spaceStore, @@ -95,6 +97,7 @@ func NewController( quarantineFinder: quarantineFinder, dependencyFirewallChecker: dependencyFirewallChecker, auditService: auditService, + packageWrapper: packageWrapper, } } diff --git a/registry/app/api/controller/pkg/generic/v2.go b/registry/app/api/controller/pkg/generic/v2.go index d832b6ebac..2be432b6a9 100644 --- a/registry/app/api/controller/pkg/generic/v2.go +++ b/registry/app/api/controller/pkg/generic/v2.go @@ -19,9 +19,10 @@ import ( "fmt" "io" + artifact2 "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact" "github.com/harness/gitness/registry/app/pkg" "github.com/harness/gitness/registry/app/pkg/base" - generic2 "github.com/harness/gitness/registry/app/pkg/generic" + genericpkg "github.com/harness/gitness/registry/app/pkg/generic" "github.com/harness/gitness/registry/app/pkg/response" "github.com/harness/gitness/registry/app/pkg/types/generic" registrytypes "github.com/harness/gitness/registry/types" @@ -34,14 +35,18 @@ func (c Controller) DownloadFile( ) *GetArtifactResponse { f := func(registry registrytypes.Registry, a pkg.Artifact) response.Response { info.UpdateRegistryInfo(registry) - genericRegistry, ok := a.(generic2.Registry) - if !ok { + + // Check if file operations are supported for this package type + if !c.packageWrapper.IsFileOperationSupported(string(registry.PackageType)) { return &GetArtifactResponse{ BaseResponse: BaseResponse{ - Error: fmt.Errorf("invalid registry type: expected generic.Registry, got %T", a), + Error: fmt.Errorf("file operations not supported for package type: %s", registry.PackageType), }, } } + + //nolint:errcheck + genericRegistry := base.GetRegistry(artifact2.PackageTypeGENERIC, registry.Type).(genericpkg.Registry) headers, fileReader, readCloser, redirectURL, err := genericRegistry.DownloadFile(ctx, info, filePath) return &GetArtifactResponse{ BaseResponse: BaseResponse{ @@ -89,14 +94,18 @@ func (c Controller) HeadFile( ) *HeadArtifactResponse { f := func(registry registrytypes.Registry, a pkg.Artifact) response.Response { info.UpdateRegistryInfo(registry) - genericRegistry, ok := a.(generic2.Registry) - if !ok { + + // Check if file operations are supported for this package type + if !c.packageWrapper.IsFileOperationSupported(string(registry.PackageType)) { return &HeadArtifactResponse{ BaseResponse: BaseResponse{ - Error: fmt.Errorf("invalid registry type: expected generic.Registry, got %T", a), + Error: fmt.Errorf("file operations not supported for package type: %s", registry.PackageType), }, } } + + //nolint:errcheck + genericRegistry := base.GetRegistry(artifact2.PackageTypeGENERIC, registry.Type).(genericpkg.Registry) headers, err := genericRegistry.HeadFile(ctx, info, filePath) return &HeadArtifactResponse{ BaseResponse: BaseResponse{ @@ -128,14 +137,18 @@ func (c Controller) HeadFile( func (c Controller) DeleteFile(ctx context.Context, info generic.ArtifactInfo) *DeleteArtifactResponse { f := func(registry registrytypes.Registry, a pkg.Artifact) response.Response { info.UpdateRegistryInfo(registry) - genericRegistry, ok := a.(generic2.Registry) - if !ok { + + // Check if file operations are supported for this package type + if !c.packageWrapper.IsFileOperationSupported(string(registry.PackageType)) { return &DeleteArtifactResponse{ BaseResponse: BaseResponse{ - Error: fmt.Errorf("invalid registry type: expected generic.Registry, got %T", a), + Error: fmt.Errorf("file operations not supported for package type: %s", registry.PackageType), }, } } + + //nolint:errcheck + genericRegistry := base.GetRegistry(artifact2.PackageTypeGENERIC, registry.Type).(genericpkg.Registry) headers, err := genericRegistry.DeleteFile(ctx, info) return &DeleteArtifactResponse{ BaseResponse: BaseResponse{ @@ -172,14 +185,18 @@ func (c Controller) PutFile( ) *PutArtifactResponse { f := func(registry registrytypes.Registry, a pkg.Artifact) response.Response { info.UpdateRegistryInfo(registry) - genericRegistry, ok := a.(generic2.Registry) - if !ok { + + // Check if file operations are supported for this package type + if !c.packageWrapper.IsFileOperationSupported(string(registry.PackageType)) { return &PutArtifactResponse{ BaseResponse: BaseResponse{ - Error: fmt.Errorf("invalid registry type: expected generic.Registry, got %T", a), + Error: fmt.Errorf("file operations not supported for package type: %s", registry.PackageType), }, } } + + //nolint:errcheck + genericRegistry := base.GetRegistry(artifact2.PackageTypeGENERIC, registry.Type).(genericpkg.Registry) headers, sha256, err := genericRegistry.PutFile(ctx, info, reader, contentType) return &PutArtifactResponse{ BaseResponse: BaseResponse{ diff --git a/registry/app/api/controller/pkg/generic/wire.go b/registry/app/api/controller/pkg/generic/wire.go index cf5748ed46..1ed36e3ddf 100644 --- a/registry/app/api/controller/pkg/generic/wire.go +++ b/registry/app/api/controller/pkg/generic/wire.go @@ -51,6 +51,7 @@ func ControllerProvider( quarantineFinder quarantine.Finder, dependencyFirewallChecker interfaces.DependencyFirewallChecker, auditService audit.Service, + packageWrapper interfaces.PackageWrapper, ) *Controller { return NewController( spaceStore, @@ -64,6 +65,7 @@ func ControllerProvider( quarantineFinder, dependencyFirewallChecker, auditService, + packageWrapper, ) } diff --git a/registry/app/api/handler/generic/base.go b/registry/app/api/handler/generic/base.go index 82bc6def0d..2b08ab94f0 100644 --- a/registry/app/api/handler/generic/base.go +++ b/registry/app/api/handler/generic/base.go @@ -42,6 +42,8 @@ import ( generic2 "github.com/harness/gitness/registry/app/pkg/types/generic" refcache2 "github.com/harness/gitness/registry/app/services/refcache" "github.com/harness/gitness/registry/request" + regtypes "github.com/harness/gitness/registry/types" + coretypes "github.com/harness/gitness/types" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" @@ -193,23 +195,9 @@ func (h *Handler) GetGenericArtifactInfoV2(r *http.Request) (generic2.ArtifactIn path := r.URL.Path path = strings.TrimPrefix(path, "/") splits := strings.Split(path, "/") - filePath := strings.Join(splits[6:], "/") - fileName := splits[len(splits)-1] - rootIdentifier, registryIdentifier, packageName, version := - chi.URLParam(r, "rootIdentifier"), - chi.URLParam(r, "registryIdentifier"), - chi.URLParam(r, "package"), - chi.URLParam(r, "version") - - if err := validatePackageVersionV2(packageName, version); err != nil { - return generic2.ArtifactInfo{}, fmt.Errorf("invalid image name/version/fileName: %q/%q %w", packageName, - version, err) - } - - if err := validateFilePath(filePath); err != nil { - return generic2.ArtifactInfo{}, usererror.BadRequestf("%v", err) - } + rootIdentifier := chi.URLParam(r, "rootIdentifier") + registryIdentifier := chi.URLParam(r, "registryIdentifier") rootSpace, err := h.SpaceFinder.FindByRef(ctx, rootIdentifier) if err != nil { @@ -226,6 +214,46 @@ func (h *Handler) GetGenericArtifactInfoV2(r *http.Request) (generic2.ArtifactIn rootSpace.Identifier) } + if registry.PackageType != artifact2.PackageTypeGENERIC { + return h.buildFileArtifactInfo(ctx, registry, rootSpace, rootIdentifier, registryIdentifier, path, splits) + } + + // Handle GENERIC package type + // Path format: /pkg/{rootIdentifier}/{registryIdentifier}/files/* + // splits[0] = "pkg", splits[1] = rootIdentifier, splits[2] = registryIdentifier, splits[3] = "files" + // Remaining path starts at splits[4] + remainingSegments := splits[4:] + + if len(remainingSegments) == 0 { + return generic2.ArtifactInfo{}, usererror.BadRequestf("Invalid request: no file path provided") + } + + // For GENERIC package type: + // remainingSegments = [a, b, c, d, e] + // packageName = a (first segment) + // version = b (second segment) + // fileName = e (last segment) + // filePath = c/d/e (segments from index 2 onwards) + if len(remainingSegments) < 3 { + return generic2.ArtifactInfo{}, usererror.BadRequestf( + "Invalid request: expected at least package/version/file , got: %s", + strings.Join(remainingSegments, "/")) + } + + packageName := remainingSegments[0] + version := remainingSegments[1] + fileName := remainingSegments[len(remainingSegments)-1] + filePath := strings.Join(remainingSegments[2:], "/") + + if err = validatePackageVersionV2(packageName, version); err != nil { + return generic2.ArtifactInfo{}, fmt.Errorf("invalid package name/version/fileName: %q/%q %w", packageName, + version, err) + } + + if err = validateFilePath(filePath); err != nil { + return generic2.ArtifactInfo{}, usererror.BadRequestf("%v", err) + } + info := generic2.ArtifactInfo{ ArtifactInfo: pkg.ArtifactInfo{ BaseInfo: &pkg.BaseInfo{ @@ -268,6 +296,58 @@ func (h *Handler) GetGenericArtifactInfoV2(r *http.Request) (generic2.ArtifactIn return info, nil } +func (h *Handler) buildFileArtifactInfo( + ctx context.Context, + registry *regtypes.Registry, + rootSpace *coretypes.SpaceCore, + rootIdentifier string, + registryIdentifier string, + path string, + splits []string, +) (generic2.ArtifactInfo, error) { + // Path format: /pkg/{rootIdentifier}/{registryIdentifier}/files/* + // splits[0] = "pkg", splits[1] = rootIdentifier, splits[2] = registryIdentifier, splits[3] = "files" + // Remaining path starts at splits[4] + remainingSegments := splits[4:] + + if len(remainingSegments) == 0 { + log.Ctx(ctx).Error(). + Str("path", path). + Str("registryIdentifier", registryIdentifier). + Msg("No file path provided in request") + return generic2.ArtifactInfo{}, usererror.BadRequestf("Invalid request: no file path provided") + } + + fileName := remainingSegments[len(remainingSegments)-1] + filePath := strings.Join(remainingSegments, "/") + + if err := validateFilePath(filePath); err != nil { + log.Ctx(ctx).Error().Err(err). + Str("filePath", filePath). + Str("registryIdentifier", registryIdentifier). + Msg("Invalid file path") + return generic2.ArtifactInfo{}, usererror.BadRequestf("%v", err) + } + + info := generic2.ArtifactInfo{ + ArtifactInfo: pkg.ArtifactInfo{ + BaseInfo: &pkg.BaseInfo{ + PathPackageType: registry.PackageType, + ParentID: registry.ParentID, + RootIdentifier: rootIdentifier, + RootParentID: rootSpace.ID, + }, + RegIdentifier: registryIdentifier, + RegistryID: registry.ID, + Registry: *registry, + }, + FileName: fileName, + FilePath: filePath, + } + log.Ctx(ctx).Info().Msgf("Dispatch: URI: %s", path) + return info, nil +} + func (h *Handler) GetPackageArtifactInfo(r *http.Request) (pkg.PackageArtifactInfo, error) { path := r.URL.Path path = strings.TrimPrefix(path, "/") diff --git a/registry/app/api/handler/generic/base_test.go b/registry/app/api/handler/generic/base_test.go index 2984058f3b..7b16df69f7 100644 --- a/registry/app/api/handler/generic/base_test.go +++ b/registry/app/api/handler/generic/base_test.go @@ -399,3 +399,107 @@ func TestValidateFilePathRegexPattern(t *testing.T) { }) } } + +// Test helpers for URL path parsing + +type urlPathTest struct { + name string + urlPath string + wantSegments []string + wantPackage string + wantVersion string + wantFileName string + wantFilePath string + isGeneric bool +} + +func validateGenericPackageSegments(t *testing.T, segments []string, tt urlPathTest) { + t.Helper() + if len(segments) < 3 { + t.Errorf("GENERIC package needs at least 3 segments") + return + } + + packageName := segments[0] + version := segments[1] + fileName := segments[len(segments)-1] + filePath := strings.Join(segments[2:], "/") + + if packageName != tt.wantPackage { + t.Errorf("Expected package=%s, got %s", tt.wantPackage, packageName) + } + if version != tt.wantVersion { + t.Errorf("Expected version=%s, got %s", tt.wantVersion, version) + } + if fileName != tt.wantFileName { + t.Errorf("Expected fileName=%s, got %s", tt.wantFileName, fileName) + } + if filePath != tt.wantFilePath { + t.Errorf("Expected filePath=%s, got %s", tt.wantFilePath, filePath) + } +} + +func validateNonGenericPackageSegments(t *testing.T, segments []string, tt urlPathTest) { + t.Helper() + fileName := segments[len(segments)-1] + filePath := strings.Join(segments, "/") + + if fileName != tt.wantFileName { + t.Errorf("Expected fileName=%s, got %s", tt.wantFileName, fileName) + } + if filePath != tt.wantFilePath { + t.Errorf("Expected filePath=%s, got %s", tt.wantFilePath, filePath) + } +} + +func TestURLPathParsing(t *testing.T) { + tests := []urlPathTest{ + { + name: "GENERIC package - simple", + urlPath: "/pkg/root/registry/files/mypackage/1.0.0/file.jar", + wantSegments: []string{"mypackage", "1.0.0", "file.jar"}, + wantPackage: "mypackage", + wantVersion: "1.0.0", + wantFileName: "file.jar", + wantFilePath: "file.jar", + isGeneric: true, + }, + { + name: "GENERIC package - nested path", + urlPath: "/pkg/root/registry/files/org.example/2.0.0/sub/dir/file.jar", + wantSegments: []string{"org.example", "2.0.0", "sub", "dir", "file.jar"}, + wantPackage: "org.example", + wantVersion: "2.0.0", + wantFileName: "file.jar", + wantFilePath: "sub/dir/file.jar", + isGeneric: true, + }, + { + name: "non-GENERIC package - MAVEN", + urlPath: "/pkg/root/registry/files/org/example/lib/1.0/file.jar", + wantSegments: []string{"org", "example", "lib", "1.0", "file.jar"}, + wantFileName: "file.jar", + wantFilePath: "org/example/lib/1.0/file.jar", + isGeneric: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := strings.TrimPrefix(tt.urlPath, "/") + splits := strings.Split(path, "/") + // splits[0] = "pkg", splits[1] = root, splits[2] = registry, splits[3] = "files" + remainingSegments := splits[4:] + + if len(remainingSegments) != len(tt.wantSegments) { + t.Errorf("Expected %d segments, got %d", len(tt.wantSegments), len(remainingSegments)) + } + + if tt.isGeneric { + validateGenericPackageSegments(t, remainingSegments, tt) + } else { + validateNonGenericPackageSegments(t, remainingSegments, tt) + } + }) + } +} diff --git a/registry/app/api/interfaces/package_type_factory.go b/registry/app/api/interfaces/package_type_factory.go index 019641d29f..86f456021c 100644 --- a/registry/app/api/interfaces/package_type_factory.go +++ b/registry/app/api/interfaces/package_type_factory.go @@ -24,6 +24,7 @@ import ( type PackageHelper interface { GetPackageType() string GetPathPackageType() string + IsFileOperationSupported() bool GetPackageURL(ctx context.Context, rootIdentifier string, registryIdentifier string, diff --git a/registry/app/api/interfaces/package_wrapper.go b/registry/app/api/interfaces/package_wrapper.go index c59315a6c1..690bcc95a7 100644 --- a/registry/app/api/interfaces/package_wrapper.go +++ b/registry/app/api/interfaces/package_wrapper.go @@ -22,6 +22,7 @@ import ( ) type PackageWrapper interface { + IsFileOperationSupported(packageType string) bool IsValidPackageType(packageType string) bool IsValidPackageTypes(packageTypes []string) bool IsValidRepoType(repoType string) bool diff --git a/registry/app/api/router/packages/route.go b/registry/app/api/router/packages/route.go index a5350dff34..6b171b938d 100644 --- a/registry/app/api/router/packages/route.go +++ b/registry/app/api/router/packages/route.go @@ -96,27 +96,18 @@ func NewRouter( // Files uses Generic Engine to serve and manage files r.Route("/files", func(r chi.Router) { r.Use(middlewareauthn.Attempt(packageHandler.GetAuthenticator())) - // We currently support managing files for a given package and a version. If requirements change in the future, - // this line will need to be removed - r.Route("/{package}/{version}", func(r chi.Router) { - r.Use(middleware.StoreArtifactInfo(genericHandler)) - - r.Route("/", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.With(middleware.CheckQuarantineStatus(packageHandler)). - With(middleware.TrackDownloadStats(packageHandler)). - With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). - Get("/*", genericHandler.GetFile) - r.With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDelete)). - Delete("/*", genericHandler.DeleteFile) - r.With(middleware.CheckQuarantineStatus(packageHandler)). - With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). - Head("/*", genericHandler.HeadFile) - r.With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsUpload)). - Put("/*", genericHandler.PutFile) - }) - }) - }) + r.Use(middleware.StoreArtifactInfo(genericHandler)) + r.With(middleware.CheckQuarantineStatus(packageHandler)). + With(middleware.TrackDownloadStats(packageHandler)). + With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). + Get("/*", genericHandler.GetFile) + r.With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDelete)). + Delete("/*", genericHandler.DeleteFile) + r.With(middleware.CheckQuarantineStatus(packageHandler)). + With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDownload)). + Head("/*", genericHandler.HeadFile) + r.With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsUpload)). + Put("/*", genericHandler.PutFile) }) r.Route("/python", func(r chi.Router) { diff --git a/registry/app/helpers/package_wrapper.go b/registry/app/helpers/package_wrapper.go index 1a69d62fcf..f014ab1df1 100644 --- a/registry/app/helpers/package_wrapper.go +++ b/registry/app/helpers/package_wrapper.go @@ -45,6 +45,17 @@ func (p *packageWrapper) GetPackage(packageType string) interfaces.PackageHelper return p.packageFactory.Get(packageType) } +func (p *packageWrapper) IsFileOperationSupported(packageType string) bool { + if packageType == "" { + return false + } + pkg := p.GetPackage(packageType) + if pkg == nil { + return false + } + return pkg.IsFileOperationSupported() +} + func (p *packageWrapper) IsValidPackageType(packageType string) bool { if packageType == "" { return true diff --git a/registry/app/helpers/pkg/cargo.go b/registry/app/helpers/pkg/cargo.go index 71c3be5302..b07142a733 100644 --- a/registry/app/helpers/pkg/cargo.go +++ b/registry/app/helpers/pkg/cargo.go @@ -81,6 +81,10 @@ func (c *cargoPackageType) GetPackageType() string { return c.packageType } +func (c *cargoPackageType) IsFileOperationSupported() bool { + return false +} + func (c *cargoPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/docker.go b/registry/app/helpers/pkg/docker.go index dd5ea657da..0986d65936 100644 --- a/registry/app/helpers/pkg/docker.go +++ b/registry/app/helpers/pkg/docker.go @@ -72,6 +72,10 @@ func (c *dockerPackageType) GetPackageType() string { return c.packageType } +func (c *dockerPackageType) IsFileOperationSupported() bool { + return false +} + func (c *dockerPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/generic.go b/registry/app/helpers/pkg/generic.go index 786bc21480..095416b495 100644 --- a/registry/app/helpers/pkg/generic.go +++ b/registry/app/helpers/pkg/generic.go @@ -63,6 +63,10 @@ func (c *genericPackageType) GetPackageType() string { return c.packageType } +func (c *genericPackageType) IsFileOperationSupported() bool { + return true +} + func (c *genericPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/gopkg.go b/registry/app/helpers/pkg/gopkg.go index a0a52f0a5c..f1af3a3a7f 100644 --- a/registry/app/helpers/pkg/gopkg.go +++ b/registry/app/helpers/pkg/gopkg.go @@ -67,6 +67,10 @@ func (c *goPackageType) GetPackageType() string { return c.packageType } +func (c *goPackageType) IsFileOperationSupported() bool { + return false +} + func (c *goPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/helm.go b/registry/app/helpers/pkg/helm.go index f0647822a2..6851fd6a72 100644 --- a/registry/app/helpers/pkg/helm.go +++ b/registry/app/helpers/pkg/helm.go @@ -64,6 +64,10 @@ func (c *helmPackageType) GetPackageType() string { return c.packageType } +func (c *helmPackageType) IsFileOperationSupported() bool { + return false +} + func (c *helmPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/huggingface.go b/registry/app/helpers/pkg/huggingface.go index 30b804a7d1..c09816dc30 100644 --- a/registry/app/helpers/pkg/huggingface.go +++ b/registry/app/helpers/pkg/huggingface.go @@ -62,6 +62,10 @@ func (c *huggingFacePackageType) GetPackageType() string { return c.packageType } +func (c *huggingFacePackageType) IsFileOperationSupported() bool { + return false +} + func (c *huggingFacePackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/maven.go b/registry/app/helpers/pkg/maven.go index 9e1fe11888..6b724a2b1e 100644 --- a/registry/app/helpers/pkg/maven.go +++ b/registry/app/helpers/pkg/maven.go @@ -68,6 +68,10 @@ func (c *mavenPackageType) GetPackageType() string { return c.packageType } +func (c *mavenPackageType) IsFileOperationSupported() bool { + return false +} + func (c *mavenPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/npm.go b/registry/app/helpers/pkg/npm.go index 4c40edb749..08d554b006 100644 --- a/registry/app/helpers/pkg/npm.go +++ b/registry/app/helpers/pkg/npm.go @@ -68,6 +68,10 @@ func (c *npmPackageType) GetPackageType() string { return c.packageType } +func (c *npmPackageType) IsFileOperationSupported() bool { + return false +} + func (c *npmPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/nuget.go b/registry/app/helpers/pkg/nuget.go index fb2b83bf6b..beeece4877 100644 --- a/registry/app/helpers/pkg/nuget.go +++ b/registry/app/helpers/pkg/nuget.go @@ -67,6 +67,10 @@ func (c *nugetPackageType) GetPackageType() string { return c.packageType } +func (c *nugetPackageType) IsFileOperationSupported() bool { + return false +} + func (c *nugetPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/python.go b/registry/app/helpers/pkg/python.go index 9e117254db..f7dc8c038f 100644 --- a/registry/app/helpers/pkg/python.go +++ b/registry/app/helpers/pkg/python.go @@ -67,6 +67,10 @@ func (c *pythonPackageType) GetPackageType() string { return c.packageType } +func (c *pythonPackageType) IsFileOperationSupported() bool { + return false +} + func (c *pythonPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/helpers/pkg/rpm.go b/registry/app/helpers/pkg/rpm.go index 789b27cba4..b77b8dfdee 100644 --- a/registry/app/helpers/pkg/rpm.go +++ b/registry/app/helpers/pkg/rpm.go @@ -64,6 +64,10 @@ func (c *rpmPackageType) GetPackageType() string { return c.packageType } +func (c *rpmPackageType) IsFileOperationSupported() bool { + return false +} + func (c *rpmPackageType) GetPathPackageType() string { return c.pathPackageType } diff --git a/registry/app/pkg/base/base.go b/registry/app/pkg/base/base.go index 800d8a74b3..855601678d 100644 --- a/registry/app/pkg/base/base.go +++ b/registry/app/pkg/base/base.go @@ -72,6 +72,20 @@ type LocalBase interface { file io.ReadCloser, metadata metadata.Metadata, ) (*commons.ResponseHeaders, string, error) + // UploadRawFile uploads a raw file to storage without creating Image/Artifact records. + UploadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, + file io.ReadCloser, + failOnConflict bool, + ) (*commons.ResponseHeaders, string, error) + // DownloadRawFile downloads a raw file from storage using the file path directly. + DownloadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, + ) (*commons.ResponseHeaders, *storage.FileReader, string, error) UpdateFileManagerAndCreateArtifact( ctx context.Context, info pkg.ArtifactInfo, @@ -187,6 +201,94 @@ func (l *localBase) Upload( return l.uploadInternal(ctx, info, fileName, version, path, nil, file, metadata) } +func (l *localBase) UploadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, + file io.ReadCloser, + failOnConflict bool, +) (*commons.ResponseHeaders, string, error) { + responseHeaders := &commons.ResponseHeaders{ + Headers: make(map[string]string), + Code: 0, + } + exists, err := l.ExistsByFilePath(ctx, info.RegistryID, filePath) + if err != nil { + log.Ctx(ctx).Error().Err(err). + Int64("registryID", info.RegistryID). + Str("filePath", filePath). + Msg("Failed to check if file exists") + return responseHeaders, "", errcode.ErrCodeUnknown.WithDetail(err) + } + if exists && failOnConflict { + log.Ctx(ctx).Warn(). + Str("filePath", filePath). + Int64("registryID", info.RegistryID). + Msg("File already exists and failOnConflict is true") + return responseHeaders, "", usererror.Conflict( + fmt.Sprintf("File already exists at path: %s", filePath)) + } + session, ok := request.AuthSessionFrom(ctx) + if !ok { + log.Ctx(ctx).Error(). + Str("filePath", filePath). + Msg("Authentication required for raw file upload") + return responseHeaders, "", usererror.ErrUnauthorized + } + fileInfo, err := l.fileManager.UploadFile( + ctx, + filePath, + info.RegistryID, + info.RootParentID, + info.RootIdentifier, + nil, // multipart.File not available + file, + session.Principal.ID, + ) + if err != nil { + log.Ctx(ctx).Error().Err(err). + Str("filePath", filePath). + Int64("registryID", info.RegistryID). + Int64("principalID", session.Principal.ID). + Msg("Failed to upload raw file") + return responseHeaders, "", errcode.ErrCodeUnknown.WithDetail(err) + } + + log.Ctx(ctx).Info(). + Str("filePath", filePath). + Str("sha256", fileInfo.Sha256). + Int64("registryID", info.RegistryID). + Msg("Successfully uploaded raw file") + responseHeaders.Code = http.StatusCreated + return responseHeaders, fileInfo.Sha256, nil +} + +func (l *localBase) DownloadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, +) (*commons.ResponseHeaders, *storage.FileReader, string, error) { + responseHeaders := &commons.ResponseHeaders{ + Headers: make(map[string]string), + Code: 0, + } + fileReader, _, redirectURL, err := l.fileManager.DownloadFileByPath(ctx, filePath, info.RegistryID, + info.RegIdentifier, info.RootIdentifier, true) + if err != nil { + log.Ctx(ctx).Error().Err(err). + Str("filePath", filePath). + Int64("registryID", info.RegistryID). + Msg("Failed to download raw file") + return responseHeaders, nil, "", err + } + log.Ctx(ctx).Info(). + Str("filePath", filePath). + Int64("registryID", info.RegistryID). + Msg("Successfully downloaded raw file") + responseHeaders.Code = http.StatusOK + return responseHeaders, fileReader, redirectURL, nil +} + func (l *localBase) UpdateFileManagerAndCreateArtifact( ctx context.Context, info pkg.ArtifactInfo, diff --git a/registry/app/pkg/base/utils.go b/registry/app/pkg/base/utils.go index 8af2b61e78..8d81c9d2dd 100644 --- a/registry/app/pkg/base/utils.go +++ b/registry/app/pkg/base/utils.go @@ -17,5 +17,8 @@ package base import "github.com/harness/gitness/registry/app/pkg" func GetCompletePath(info pkg.PackageArtifactInfo, filePath string) string { + if info.GetImage() == "" && info.GetVersion() == "" { + return filePath + } return info.GetImage() + "/" + info.GetVersion() + "/" + filePath } diff --git a/registry/app/pkg/base/wrapper.go b/registry/app/pkg/base/wrapper.go index 388f058309..d0f4e4a435 100644 --- a/registry/app/pkg/base/wrapper.go +++ b/registry/app/pkg/base/wrapper.go @@ -210,6 +210,11 @@ func GetArtifactRegistry(registry registrytypes.Registry) pkg.Artifact { return factory(key) } +func GetRegistry(packageType artifact.PackageType, registryType artifact.RegistryType) pkg.Artifact { + key := getFactoryKey(packageType, registryType) + return factory(key) +} + func filterRegs( ctx context.Context, registryDao store.RegistryRepository, diff --git a/registry/app/pkg/generic/local.go b/registry/app/pkg/generic/local.go index 7f5d324415..f2eada3445 100644 --- a/registry/app/pkg/generic/local.go +++ b/registry/app/pkg/generic/local.go @@ -88,6 +88,12 @@ func (c *localRegistry) PutFile( reader io.ReadCloser, contentType string, ) (*commons.ResponseHeaders, string, error) { + // For non-GENERIC package types, use raw file upload + if info.Registry.PackageType != artifact.PackageTypeGENERIC { + return c.uploadRawFile(ctx, info, reader) + } + + // For GENERIC package type, use the existing upload flow completePath := pkg.JoinWithSeparator("/", info.Image, info.Version, info.FilePath) headers, sha256, err := c.localBase.Upload(ctx, info.ArtifactInfo, info.FileName, info.Version, completePath, reader, &generic2.GenericMetadata{}) @@ -105,6 +111,12 @@ func (c *localRegistry) DownloadFile( info generic.ArtifactInfo, filePath string, ) (*commons.ResponseHeaders, *storage.FileReader, io.ReadCloser, string, error) { + // For non-GENERIC package types, use raw file download + if info.Registry.PackageType != artifact.PackageTypeGENERIC { + return c.downloadRawFile(ctx, info, filePath) + } + + // For GENERIC package type, use the existing download flow download, reader, url, err := c.localBase.Download(ctx, info.ArtifactInfo, info.Version, filePath) if err != nil { log.Ctx(ctx).Error().Msgf("Failed to download file: %q, %q, %q", info.FileName, info.Version, filePath) @@ -124,3 +136,28 @@ func (c *localRegistry) HeadFile( ) (headers *commons.ResponseHeaders, err error) { return c.localBase.ExistsE(ctx, info, filePath) } + +func (c *localRegistry) uploadRawFile( + ctx context.Context, + info generic.ArtifactInfo, + reader io.ReadCloser, +) (*commons.ResponseHeaders, string, error) { + headers, sha256, err := c.localBase.UploadRawFile(ctx, info.ArtifactInfo, info.FilePath, reader, true) + if err != nil { + return nil, "", fmt.Errorf("failed to upload raw file: %w", err) + } + return headers, sha256, nil +} + +func (c *localRegistry) downloadRawFile( + ctx context.Context, + info generic.ArtifactInfo, + filePath string, +) (*commons.ResponseHeaders, *storage.FileReader, io.ReadCloser, string, error) { + filePath = "/" + filePath + headers, reader, url, err := c.localBase.DownloadRawFile(ctx, info.ArtifactInfo, filePath) + if err != nil { + return nil, nil, nil, "", fmt.Errorf("failed to download raw file: %w", err) + } + return headers, reader, nil, url, nil +} diff --git a/registry/app/pkg/npm/local_test.go b/registry/app/pkg/npm/local_test.go index 9b282a40ec..5dd70f74b6 100644 --- a/registry/app/pkg/npm/local_test.go +++ b/registry/app/pkg/npm/local_test.go @@ -199,6 +199,24 @@ func (m *mockLocalBase) AuditPush( } } +func (m *mockLocalBase) UploadRawFile( + context.Context, + pkg.ArtifactInfo, + string, + io.ReadCloser, + bool, +) (*commons.ResponseHeaders, string, error) { + panic("not implemented in tests") +} + +func (m *mockLocalBase) DownloadRawFile( + context.Context, + pkg.ArtifactInfo, + string, +) (*commons.ResponseHeaders, *storage.FileReader, string, error) { + panic("not implemented in tests") +} + type mockTagsDAO struct { findByImageNameAndRegID func( ctx context.Context, diff --git a/registry/app/pkg/python/local_helper_test.go b/registry/app/pkg/python/local_helper_test.go index e0bfa47c8d..60e37ad7e9 100644 --- a/registry/app/pkg/python/local_helper_test.go +++ b/registry/app/pkg/python/local_helper_test.go @@ -98,6 +98,11 @@ func (m *MockLocalBase) ExistsE( return args.Get(0).(*commons.ResponseHeaders), args.Error(1) } +func (m *MockLocalBase) ExistsByFilePath(ctx context.Context, registryID int64, filePath string) (bool, error) { + args := m.Called(ctx, registryID, filePath) + return args.Bool(0), args.Error(1) +} + func (m *MockLocalBase) DeleteFile( ctx context.Context, info pkg.PackageArtifactInfo, @@ -140,11 +145,6 @@ func (m *MockLocalBase) Exists(ctx context.Context, info pkg.ArtifactInfo, path return args.Bool(0) } -func (m *MockLocalBase) ExistsByFilePath(ctx context.Context, registryID int64, filePath string) (bool, error) { - args := m.Called(ctx, registryID, filePath) - return args.Bool(0), args.Error(1) -} - func (m *MockLocalBase) Download(ctx context.Context, info pkg.ArtifactInfo, version, filename string) ( *commons.ResponseHeaders, *storage.FileReader, string, error, ) { @@ -171,6 +171,29 @@ func (m *MockLocalBase) UploadFile( return args.Get(0).(*commons.ResponseHeaders), args.String(1), args.Error(2) //nolint:errcheck } +func (m *MockLocalBase) UploadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, + file io.ReadCloser, + failOnConflict bool, +) (*commons.ResponseHeaders, string, error) { + args := m.Called(ctx, info, filePath, file, failOnConflict) + //nolint:errcheck + return args.Get(0).(*commons.ResponseHeaders), args.String(1), args.Error(2) +} + +func (m *MockLocalBase) DownloadRawFile( + ctx context.Context, + info pkg.ArtifactInfo, + filePath string, +) (*commons.ResponseHeaders, *storage.FileReader, string, error) { + args := m.Called(ctx, info, filePath) + //nolint:errcheck + return args.Get(0).(*commons.ResponseHeaders), args.Get(1).(*storage.FileReader), + args.String(2), args.Error(3) +} + func (m *MockLocalBase) MoveMultipleTempFilesAndCreateArtifact( ctx context.Context, info *pkg.ArtifactInfo, diff --git a/registry/app/pkg/types/generic/types.go b/registry/app/pkg/types/generic/types.go index 986fd84ab3..00142c4515 100644 --- a/registry/app/pkg/types/generic/types.go +++ b/registry/app/pkg/types/generic/types.go @@ -35,6 +35,8 @@ func (a ArtifactInfo) BaseArtifactInfo() pkg.ArtifactInfo { func (a ArtifactInfo) GetImageVersion() (exists bool, imageVersion string) { if a.Image != "" && a.Version != "" { return true, pkg.JoinWithSeparator(":", a.Image, a.Version) + } else if a.Image == "" && a.Version == "" { + return true, a.FilePath } return false, "" }