diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index ea0e0d5e73..58f16c9eca 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" @@ -53,8 +54,11 @@ func (l PackagePropertyList) GetByName(name string) string { // PackageDescriptor describes a package type PackageDescriptor struct { - Package *Package - Owner *user_model.User + // basic package info + Package *Package + Owner *user_model.User + + // package version info Repository *repo_model.Repository Version *PackageVersion SemVer *version.Version @@ -77,6 +81,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 +276,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 1600b27900..acb5a25087 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3506,6 +3506,7 @@ "packages.dependencies": "Dependencies", "packages.keywords": "Keywords", "packages.details": "Details", + "packages.name": "Package Name", "packages.details.author": "Author", "packages.details.project_site": "Project Site", "packages.details.repository_site": "Repository Site", @@ -3614,8 +3615,13 @@ "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.version.success": "The package version has been deleted.", "packages.settings.delete.error": "Failed to delete the package.", + "packages.settings.delete.version": "Delete version", + "packages.settings.delete.confirm": "Enter package name to confirm", + "packages.settings.delete.invalid_package_name": "The package name you entered is incorrect.", "packages.owner.settings.cargo.title": "Cargo Registry Index", "packages.owner.settings.cargo.initialize": "Initialize Index", "packages.owner.settings.cargo.initialize.description": "A special index Git repository is needed to use the Cargo registry. Using this option will (re-)create the repository and configure it automatically.", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ef38b75696..c1733095cf 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/admin/packages.go b/routers/web/admin/packages.go index a0f983914d..f71a55a219 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -93,7 +93,7 @@ func DeletePackageVersion(ctx *context.Context) { return } - ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) + ctx.Flash.Success(ctx.Tr("packages.settings.delete.version.success")) ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type"))) } diff --git a/routers/web/user/package.go b/routers/web/user/package.go index ffbfaa229b..b748ead543 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -491,18 +491,43 @@ 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 ctx.FormString("package_name") != pd.Package.Name { + ctx.Flash.Error(ctx.Tr("packages.settings.delete.invalid_package_name")) + ctx.Redirect(pd.PackageSettingsLink()) + return + } + + 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.version.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 +537,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 86397bb939..f85c2f7501 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("/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..6ed9de4367 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -4,6 +4,7 @@ package context import ( + "errors" "fmt" "net/http" @@ -58,23 +59,28 @@ func PackageAssignmentAPI() func(ctx *APIContext) { } func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package { - pkg := &Package{ - Owner: ctx.ContextUser, - } - var err error - pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer) + pkgOwner := ctx.ContextUser + accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer) if err != nil { errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err)) + return nil + } + + pkg := &Package{ + Owner: pkgOwner, + AccessMode: accessMode, + } + packageType := ctx.PathParam("type") + name := ctx.PathParam("name") + if packageType == "" || name == "" { return pkg } - packageType := ctx.PathParam("type") - name := ctx.PathParam("name") version := ctx.PathParam("version") - if packageType != "" && name != "" && version != "" { + if version != "" { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) } else { errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) @@ -87,12 +93,27 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package 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, + } } return pkg } -func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { +func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.AccessMode, error) { if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) { return perm.AccessModeNone, nil } @@ -103,8 +124,8 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A // TODO: ActionUser permission check accessMode := perm.AccessModeNone - if pkg.Owner.IsOrganization() { - org := organization.OrgFromUser(pkg.Owner) + if pkgOwner.IsOrganization() { + org := organization.OrgFromUser(pkgOwner) if doer != nil && !doer.IsGhost() { // 1. If user is logged in, check all team packages permissions @@ -128,19 +149,19 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A } } } - if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) { + if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkgOwner, doer) { // 2. If user is unauthorized or no org member, check if org is visible accessMode = perm.AccessModeRead } } else { if doer != nil && !doer.IsGhost() { // 1. Check if user is package owner - if doer.ID == pkg.Owner.ID { + if doer.ID == pkgOwner.ID { accessMode = perm.AccessModeOwner - } else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited + } else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited accessMode = perm.AccessModeRead } - } else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public + } else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public accessMode = perm.AccessModeRead } } diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index f2b5bce4f6..5cff16fe40 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -184,6 +184,7 @@ func CleanupExpiredData(ctx context.Context, olderThan time.Duration) error { } } + // HINT: PACKAGE-DEFER-STORAGE-DELETE: Handle blob deletion for package storage pbs, err = packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan) if err != nil { return err diff --git a/services/packages/packages.go b/services/packages/packages.go index 3b4e11e041..47714add82 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -473,7 +473,8 @@ func RemovePackageVersion(ctx context.Context, doer *user_model.User, pv *packag if err != nil { return err } - + // HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by the cleanup_packages cron task. + // If there are no more versions for the package, the same task removes that as well. if err := db.WithTx(ctx, func(ctx context.Context) error { log.Trace("Deleting package: %v", pv.ID) return DeletePackageVersionAndReferences(ctx, pv) @@ -532,16 +533,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 +625,46 @@ 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 + } + // HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by cleanup_packages cron task. + 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/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 395c63053b..8701cb0d57 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -89,7 +89,7 @@