- {{ctx.Locale.Tr "packages.settings.delete.notice" .PackageDescriptor.Package.Name .PackageDescriptor.Version.Version}}
+ {{ctx.Locale.Tr "packages.settings.delete.notice.package" .PackageDescriptor.Package.Name}}
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index ea0e0d5e73..a5e647981e 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -9,6 +9,7 @@ import ( "fmt" "net/url" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" @@ -77,6 +78,11 @@ func (pd *PackageDescriptor) PackageWebLink() string { return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } +// PackageSettingsLink returns the relative package settings link +func (pd *PackageDescriptor) PackageSettingsLink() string { + return fmt.Sprintf("%s/-/packages-settings/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + // VersionWebLink returns the relative package version web link func (pd *PackageDescriptor) VersionWebLink() string { return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) @@ -267,6 +273,15 @@ func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*Packa return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) } +// GetAllPackageDescriptors gets all package descriptors for a package +func GetAllPackageDescriptors(ctx context.Context, p *Package) ([]*PackageDescriptor, error) { + pvs := make([]*PackageVersion, 0, 10) + if err := db.GetEngine(ctx).Where("package_id = ?", p.ID).Find(&pvs); err != nil { + return nil, err + } + return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) +} + func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { diff --git a/models/packages/package_file.go b/models/packages/package_file.go index bf877485d6..69401eee3e 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -115,6 +115,20 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +// DeleteFilesByPackageID deletes all files of a specific package +// Versions must not be deleted prior to this call +func DeleteFilesByPackageID(ctx context.Context, packageID int64) error { + deleteStmt := builder.Delete(builder.In("version_id", builder.Select("package_version.id").From("package_version").Where(builder.Eq{"package_id": packageID}))).From("package_file") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilesByVersionID deletes all files of a specific version +func DeleteFilesByVersionID(ctx context.Context, versionID int64) error { + _, err := db.GetEngine(ctx).Where("version_id = ?", versionID).Delete(&PackageFile{}) + return err +} + func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) return err diff --git a/models/packages/package_property.go b/models/packages/package_property.go index acc05d8d5a..c297fd8901 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -5,6 +5,7 @@ package packages import ( "context" + "errors" "code.gitea.io/gitea/models/db" @@ -86,6 +87,46 @@ func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) return err } +// DeletePropertiesByPackageID deletes properties of a typed linked to the package +// Use to avoid for loops in mass deletion of properties +func DeletePropertiesByPackageID(ctx context.Context, refType PropertyType, packageID int64) error { + var deleteStmt *builder.Builder + + switch refType { + case PropertyTypeFile: + deleteStmt = builder.Delete( + // Delete all properties that are attached to a file and are in ids from a subquery + // which returns ids from the package_file table joined on package_version to link it with package id + builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", + builder.Select("package_file.id").From("package_file"). + LeftJoin("package_version", "package_file.version_id = package_version.id"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypeVersion: + // Delete all properties that are attached to a version and are in ids from subquery to the package_version filtered by package id + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypeVersion}, builder.In("ref_id", + builder.Select("package_version.id").From("package_version"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypePackage: + // Delete all properties that are attached to a package and their reference links to the given package ID + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypePackage}, builder.Eq{"ref_id": packageID}). + From("package_property") + default: + return errors.New("invalid ref type") + } + + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilePropertiesByVersionID deletes all file properties linked to specific version +func DeleteFilePropertiesByVersionID(ctx context.Context, versionID int64) error { + deleteStmt := builder.Delete(builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", builder.Select("id").From("package_file").Where(builder.Eq{"version_id": versionID}))).From("package_property") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + // DeletePropertyByID deletes a property func DeletePropertyByID(ctx context.Context, propertyID int64) error { _, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{}) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 0a478c0323..3e0e1899ea 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -157,6 +157,12 @@ func DeleteVersionByID(ctx context.Context, versionID int64) error { return err } +// DeleteVersionsByPackageID deletes all versions of a specific package +func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"package_id": packageID}).Delete(&PackageVersion{}) + return err +} + // HasVersionFileReferences checks if there are associated files func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) { return db.GetEngine(ctx).Get(&PackageFile{ diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index e796064ce3..71554a5bfe 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3615,6 +3615,7 @@ "packages.settings.delete": "Delete package", "packages.settings.delete.description": "Deleting a package is permanent and cannot be undone.", "packages.settings.delete.notice": "You are about to delete %s (%s). This operation is irreversible, are you sure?", + "packages.settings.delete.notice.package": "You are about to delete %s and all its versions. This operation is irreversible, are you sure?", "packages.settings.delete.success": "The package has been deleted.", "packages.settings.delete.error": "Failed to delete the package.", "packages.owner.settings.cargo.title": "Cargo Registry Index", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e1d836b5c8..042971c614 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1578,10 +1578,11 @@ func Routes() *web.Router { m.Group("/packages/{username}", func() { m.Group("/{type}/{name}", func() { m.Get("/", packages.ListPackageVersions) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Group("/{version}", func() { m.Get("", packages.GetPackage) - m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackageVersion) m.Get("/files", packages.ListPackageFiles) }) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index cee0daccae..376867ab82 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -118,7 +118,7 @@ func GetPackage(ctx *context.APIContext) { // DeletePackage deletes a package func DeletePackage(ctx *context.APIContext) { - // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage + // swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage // --- // summary: Delete a package // parameters: @@ -137,6 +137,41 @@ func DeletePackage(ctx *context.APIContext) { // description: name of the package // type: string // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} + +// DeletePackageVersion deletes a package version +func DeletePackageVersion(ctx *context.APIContext) { + // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackageVersion + // --- + // summary: Delete a package version + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true // - name: version // in: path // description: version of the package diff --git a/routers/web/user/package.go b/routers/web/user/package.go index ffbfaa229b..da206a4105 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -491,18 +491,37 @@ func packageSettingsPostActionLink(ctx *context.Context, form *forms.PackageSett } func packageSettingsPostActionDelete(ctx *context.Context) { - err := packages_service.RemovePackageVersion(ctx, ctx.Doer, ctx.Package.Descriptor.Version) - if err != nil { + pd := ctx.Package.Descriptor + + if err := packages_service.RemovePackage(ctx, ctx.Doer, pd.Package); err != nil { log.Error("Error deleting package: %v", err) ctx.Flash.Error(ctx.Tr("packages.settings.delete.error")) } else { ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) } + ctx.Redirect(ctx.Package.Owner.HomeLink() + "/-/packages") +} + +// PackageVersionDelete deletes a package version +func PackageVersionDelete(ctx *context.Context) { + pd := ctx.Package.Descriptor + if pd.Version == nil { + ctx.NotFound(nil) + return + } + + if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pd.Version); err != nil { + log.Error("Error deleting package version: %v", err) + ctx.Flash.Error(ctx.Tr("packages.settings.delete.error")) + } else { + ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) + } + redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" // redirect to the package if there are still versions available - if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has { - redirectURL = ctx.Package.Descriptor.PackageWebLink() + if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: pd.Package.ID, IsInternal: optional.Some(false)}); has { + redirectURL = pd.PackageWebLink() } ctx.Redirect(redirectURL) @@ -512,7 +531,7 @@ func packageSettingsPostActionDelete(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) { pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64("fileid")) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { ctx.NotFound(err) } else { ctx.ServerError("GetFileForVersionByID", err) diff --git a/routers/web/web.go b/routers/web/web.go index e3dcf27cc4..9735273eda 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1071,14 +1071,15 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/versions", user.ListPackageVersions) m.Group("/{version}", func() { m.Get("", user.ViewPackageVersion) + m.Post("", reqPackageAccess(perm.AccessModeWrite), user.PackageVersionDelete) m.Get("/{version_sub}", user.ViewPackageVersion) m.Get("/files/{fileid}", user.DownloadPackageFile) - m.Group("/settings", func() { - m.Get("", user.PackageSettings) - m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost) - }, reqPackageAccess(perm.AccessModeWrite)) }) }) + m.Group("/packages-settings/{type}/{name}", func() { + m.Get("", user.PackageSettings) + m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost) + }, reqPackageAccess(perm.AccessModeWrite)) }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) } diff --git a/services/context/package.go b/services/context/package.go index 0e9210515b..fede5cef94 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -4,6 +4,7 @@ package context import ( + "errors" "fmt" "net/http" @@ -70,22 +71,39 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package packageType := ctx.PathParam("type") name := ctx.PathParam("name") - version := ctx.PathParam("version") - if packageType != "" && name != "" && version != "" { - pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) - if err != nil { - if err == packages_model.ErrPackageNotExist { - errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) - } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + if packageType != "" && name != "" { + version := ctx.PathParam("version") + if version != "" { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) { + errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + } else { + errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + } + return pkg } - return pkg - } - pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) - if err != nil { - errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err)) - return pkg + pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err)) + return pkg + } + } else { + p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) { + errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err)) + } else { + errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err)) + } + return pkg + } + + pkg.Descriptor = &packages_model.PackageDescriptor{ + Package: p, + Owner: pkg.Owner, + } } } diff --git a/services/packages/packages.go b/services/packages/packages.go index 3b4e11e041..7f244d26d3 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -532,16 +532,11 @@ func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.P if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil { return err } - - pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) - if err != nil { + if err := packages_model.DeleteFilePropertiesByVersionID(ctx, pv.ID); err != nil { return err } - - for _, pf := range pfs { - if err := DeletePackageFile(ctx, pf); err != nil { - return err - } + if err := packages_model.DeleteFilesByVersionID(ctx, pv.ID); err != nil { + return err } return packages_model.DeleteVersionByID(ctx, pv.ID) @@ -629,6 +624,45 @@ func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb return s, u, pf, nil } +// RemovePackage deletes the package and all its versions +func RemovePackage(ctx context.Context, doer *user_model.User, p *packages_model.Package) error { + pds, err := packages_model.GetAllPackageDescriptors(ctx, p) + if err != nil { + return err + } + err = db.WithTx(ctx, func(ctx context.Context) error { + err := packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypePackage, p.ID) + if err != nil { + return err + } + err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeFile, p.ID) + if err != nil { + return err + } + err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeVersion, p.ID) + if err != nil { + return err + } + err = packages_model.DeleteFilesByPackageID(ctx, p.ID) + if err != nil { + return err + } + err = packages_model.DeleteVersionsByPackageID(ctx, p.ID) + if err != nil { + return err + } + + return packages_model.DeletePackageByID(ctx, p.ID) + }) + if err != nil { + return err + } + for _, pd := range pds { + notify_service.PackageDelete(ctx, doer, pd) + } + return nil +} + // RemoveAllPackages for User func RemoveAllPackages(ctx context.Context, userID int64) (int, error) { count := 0 diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index 0124f26820..4545575618 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -10,7 +10,7 @@ {{template "user/overview/header" .}} {{end}} {{template "base/alert" .}} -
{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}}) / {{ctx.Locale.Tr "repo.settings"}}
+{{.PackageDescriptor.Package.Name}} / {{ctx.Locale.Tr "repo.settings"}}