diff --git a/models/packages/package.go b/models/packages/package.go index 380a076f9dfe1..159967ac29175 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -175,6 +175,21 @@ func (pt Type) SVGName() string { panic(fmt.Sprintf("unknown package type: %s", string(pt))) } +// String converts the package type to a string +func (pt Type) String() string { + return string(pt) +} + +// GetPackageTypeByString returns the type for the given string +func GetPackageTypeByString(typeName string) *Type { + for _, currentType := range TypeList { + if currentType.String() == typeName { + return ¤tType + } + } + return nil +} + // Package represents a package type Package struct { ID int64 `xorm:"pk autoincr"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index de25392d884cc..81cfbf9611460 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -140,6 +140,8 @@ confirm_delete_selected = Confirm to delete all selected items? name = Name value = Value +upload = Upload + [aria] navbar = Navigation Bar footer = Footer @@ -3445,6 +3447,22 @@ owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted. owner.settings.chef.title = Chef Registry owner.settings.chef.keypair = Generate key pair owner.settings.chef.keypair.description = A key pair is necessary to authenticate to the Chef registry. If you have generated a key pair before, generating a new key pair will discard the old key pair. +upload.title = Upload %s Package +upload.branch.label = Branch: +upload.branch.placeholder = The branch to use +upload.repo.label = Repository: +upload.repo.placeholder = The repository to use +upload.file.label = File: +upload.distribution.label = Distribution: +upload.distribution.placeholder = The distribution may match the release name of the OS +upload.component.label = Component: +upload.component.placeholder = The component can be used to group packages or just main or similar +upload.name.label = Name: +upload.name.placeholder = Package name +upload.version.label = Version: +upload.version.placeholder = Package version +upload.filename.label = Filename: +upload.filename.placeholder = Filename (leave empty to use name from uploaded file) [secrets] secrets = Secrets diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index c261eee2d22a7..29390605f527a 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -9,15 +9,11 @@ import ( "encoding/pem" "errors" "fmt" - "io" "net/http" "strings" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/json" - packages_module "code.gitea.io/gitea/modules/packages" - alpine_module "code.gitea.io/gitea/modules/packages/alpine" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" @@ -105,79 +101,20 @@ func UploadPackageFile(ctx *context.Context) { defer upload.Close() } - buf, err := packages_module.CreateHashedBufferFromReader(upload) + userError, _, err := alpine_service.UploadAlpinePackage(ctx, upload, branch, repository) if err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - defer buf.Close() - - pck, err := alpine_module.ParsePackage(buf) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { - apiError(ctx, http.StatusBadRequest, err) + if userError { + if err == packages_service.ErrQuotaTotalCount || err == packages_service.ErrQuotaTypeSize || err == packages_service.ErrQuotaTotalSize { + apiError(ctx, http.StatusForbidden, err) + } else { + apiError(ctx, http.StatusBadRequest, err) + } } else { apiError(ctx, http.StatusInternalServerError, err) } return } - if _, err := buf.Seek(0, io.SeekStart); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - - fileMetadataRaw, err := json.Marshal(pck.FileMetadata) - if err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - - _, _, err = packages_service.CreatePackageOrAddFileToExisting( - ctx, - &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeAlpine, - Name: pck.Name, - Version: pck.Version, - }, - Creator: ctx.Doer, - Metadata: pck.VersionMetadata, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version), - CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, pck.FileMetadata.Architecture), - }, - Creator: ctx.Doer, - Data: buf, - IsLead: true, - Properties: map[string]string{ - alpine_module.PropertyBranch: branch, - alpine_module.PropertyRepository: repository, - alpine_module.PropertyArchitecture: pck.FileMetadata.Architecture, - alpine_module.PropertyMetadata: string(fileMetadataRaw), - }, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: - apiError(ctx, http.StatusBadRequest, err) - case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - apiError(ctx, http.StatusForbidden, err) - default: - apiError(ctx, http.StatusInternalServerError, err) - } - return - } - - if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - ctx.Status(http.StatusCreated) } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index dc7ebab09687b..7590e92e72791 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -7,15 +7,12 @@ import ( stdctx "context" "errors" "fmt" - "io" "net/http" "strings" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" - packages_module "code.gitea.io/gitea/modules/packages" - debian_module "code.gitea.io/gitea/modules/packages/debian" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" notify_service "code.gitea.io/gitea/services/notify" @@ -136,73 +133,20 @@ func UploadPackageFile(ctx *context.Context) { defer upload.Close() } - buf, err := packages_module.CreateHashedBufferFromReader(upload) + userError, _, err := debian_service.UploadDebianPackage(ctx, upload, distribution, component) if err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - defer buf.Close() - - pck, err := debian_module.ParsePackage(buf) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - apiError(ctx, http.StatusBadRequest, err) + if userError { + if err == packages_service.ErrQuotaTotalCount || err == packages_service.ErrQuotaTypeSize || err == packages_service.ErrQuotaTotalSize { + apiError(ctx, http.StatusForbidden, err) + } else { + apiError(ctx, http.StatusBadRequest, err) + } } else { apiError(ctx, http.StatusInternalServerError, err) } return } - if _, err := buf.Seek(0, io.SeekStart); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - - _, _, err = packages_service.CreatePackageOrAddFileToExisting( - ctx, - &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeDebian, - Name: pck.Name, - Version: pck.Version, - }, - Creator: ctx.Doer, - Metadata: pck.Metadata, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s_%s_%s.deb", pck.Name, pck.Version, pck.Architecture), - CompositeKey: fmt.Sprintf("%s|%s", distribution, component), - }, - Creator: ctx.Doer, - Data: buf, - IsLead: true, - Properties: map[string]string{ - debian_module.PropertyDistribution: distribution, - debian_module.PropertyComponent: component, - debian_module.PropertyArchitecture: pck.Architecture, - debian_module.PropertyControl: pck.Control, - }, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: - apiError(ctx, http.StatusBadRequest, err) - case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - apiError(ctx, http.StatusForbidden, err) - default: - apiError(ctx, http.StatusInternalServerError, err) - } - return - } - - if err := debian_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, distribution, component, pck.Architecture); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - ctx.Status(http.StatusCreated) } diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 30854335c0936..67845cd3abb84 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -11,10 +11,9 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" - packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" + generic_service "code.gitea.io/gitea/services/packages/generic" ) var ( @@ -80,41 +79,15 @@ func UploadPackage(ctx *context.Context) { defer upload.Close() } - buf, err := packages_module.CreateHashedBufferFromReader(upload) + userError, _, err := generic_service.UploadGenericPackage(ctx, upload, packageName, packageVersion, filename) if err != nil { - log.Error("Error creating hashed buffer: %v", err) - apiError(ctx, http.StatusInternalServerError, err) - return - } - defer buf.Close() - - _, _, err = packages_service.CreatePackageOrAddFileToExisting( - ctx, - &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeGeneric, - Name: packageName, - Version: packageVersion, - }, - Creator: ctx.Doer, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: filename, - }, - Creator: ctx.Doer, - Data: buf, - IsLead: true, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageFile: - apiError(ctx, http.StatusConflict, err) - case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - apiError(ctx, http.StatusForbidden, err) - default: + if userError { + if err == packages_service.ErrQuotaTotalCount || err == packages_service.ErrQuotaTypeSize || err == packages_service.ErrQuotaTotalSize { + apiError(ctx, http.StatusForbidden, err) + } else { + apiError(ctx, http.StatusBadRequest, err) + } + } else { apiError(ctx, http.StatusInternalServerError, err) } return diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index f5d8b67e16d93..86d6c09553a7a 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -7,16 +7,12 @@ import ( stdctx "context" "errors" "fmt" - "io" "net/http" "strings" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/json" - packages_module "code.gitea.io/gitea/modules/packages" - rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" @@ -94,75 +90,20 @@ func UploadPackageFile(ctx *context.Context) { defer upload.Close() } - buf, err := packages_module.CreateHashedBufferFromReader(upload) + userError, _, err := rpm_service.UploadRpmPackage(ctx, upload) if err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - defer buf.Close() - - pck, err := rpm_module.ParsePackage(buf) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - apiError(ctx, http.StatusBadRequest, err) + if userError { + if err == packages_service.ErrQuotaTotalCount || err == packages_service.ErrQuotaTypeSize || err == packages_service.ErrQuotaTotalSize { + apiError(ctx, http.StatusForbidden, err) + } else { + apiError(ctx, http.StatusBadRequest, err) + } } else { apiError(ctx, http.StatusInternalServerError, err) } return } - if _, err := buf.Seek(0, io.SeekStart); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - - fileMetadataRaw, err := json.Marshal(pck.FileMetadata) - if err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - - _, _, err = packages_service.CreatePackageOrAddFileToExisting( - ctx, - &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeRpm, - Name: pck.Name, - Version: pck.Version, - }, - Creator: ctx.Doer, - Metadata: pck.VersionMetadata, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture), - }, - Creator: ctx.Doer, - Data: buf, - IsLead: true, - Properties: map[string]string{ - rpm_module.PropertyMetadata: string(fileMetadataRaw), - }, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: - apiError(ctx, http.StatusConflict, err) - case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - apiError(ctx, http.StatusForbidden, err) - default: - apiError(ctx, http.StatusInternalServerError, err) - } - return - } - - if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - ctx.Status(http.StatusCreated) } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index ac9e64d774e3f..169a52a0de63c 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -4,15 +4,17 @@ package repo import ( + "fmt" "net/http" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/packages" + packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/shared/packages" ) const ( @@ -28,15 +30,15 @@ func Packages(ctx *context.Context) { query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") - pvs, total, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{ + pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ Paginator: &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, }, OwnerID: ctx.ContextUser.ID, RepoID: ctx.Repo.Repository.ID, - Type: packages.Type(packageType), - Name: packages.SearchValue{Value: query}, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, IsInternal: util.OptionalBoolFalse, }) if err != nil { @@ -44,13 +46,13 @@ func Packages(ctx *context.Context) { return } - pds, err := packages.GetPackageDescriptors(ctx, pvs) + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { ctx.ServerError("GetPackageDescriptors", err) return } - hasPackages, err := packages.HasRepositoryPackages(ctx, ctx.Repo.Repository.ID) + hasPackages, err := packages_model.HasRepositoryPackages(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("HasRepositoryPackages", err) return @@ -60,10 +62,14 @@ func Packages(ctx *context.Context) { ctx.Data["IsPackagesPage"] = true ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType - ctx.Data["AvailableTypes"] = packages.TypeList + ctx.Data["AvailableTypes"] = packages_model.TypeList ctx.Data["HasPackages"] = hasPackages if ctx.Repo != nil { ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin() + ctx.Data["ShowPackageUploadButton"] = ctx.Data["CanWritePackages"] + ctx.Data["PackageUploadUrl"] = fmt.Sprintf("%s/-/packages/upload/", ctx.Repo.Owner.HTMLURL()) + ctx.Data["PackageUploadRepo"] = ctx.Repo.Repository.Name + ctx.Data["AllowedUploadTypes"] = packages.UploadTypeList } ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total diff --git a/routers/web/shared/packages/upload.go b/routers/web/shared/packages/upload.go new file mode 100644 index 0000000000000..ac112cdc194a6 --- /dev/null +++ b/routers/web/shared/packages/upload.go @@ -0,0 +1,171 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "fmt" + + packages_model "code.gitea.io/gitea/models/packages" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" + alpine_service "code.gitea.io/gitea/services/packages/alpine" + debian_service "code.gitea.io/gitea/services/packages/debian" + generic_service "code.gitea.io/gitea/services/packages/generic" + rpm_service "code.gitea.io/gitea/services/packages/rpm" +) + +var UploadTypeList = []packages_model.Type{ + packages_model.TypeAlpine, + packages_model.TypeDebian, + packages_model.TypeGeneric, + packages_model.TypeRpm, +} + +func servePackageUploadError(ctx *context.Context, err error, packageType, repo string) { + ctx.Flash.Error(err.Error()) + + if repo == "" { + ctx.Redirect(fmt.Sprintf("%s/-/packages/upload/%s", ctx.ContextUser.HTMLURL(), packageType)) + } else { + ctx.Redirect(fmt.Sprintf("%s/-/packages/upload/%s?repo=%s", ctx.ContextUser.HTMLURL(), packageType, repo)) + } +} + +func addRepoToUploadedPackage(ctx *context.Context, packageType, repoName string, packageID int64) bool { + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.ContextUser.Name, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + servePackageUploadError(ctx, fmt.Errorf("repo not found"), packageType, repoName) + return false + } + + ctx.ServerError("GetRepositoryByOwnerAndName", err) + return false + } + + err = packages_model.SetRepositoryLink(ctx, packageID, repo.ID) + if err != nil { + ctx.ServerError("SetRepositoryLink", err) + return false + } + + return true +} + +func uploadPackageFinish(ctx *context.Context, packageType, packageRepo string, pv *packages_model.PackageVersion) { + if packageRepo != "" { + if !addRepoToUploadedPackage(ctx, packageType, packageRepo, pv.PackageID) { + return + } + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + ctx.ServerError("GetPackageDescriptor", err) + return + } + + ctx.Redirect(pd.FullWebLink()) +} + +func UploadAlpinePackagePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.PackageUploadAlpineForm) + upload, err := form.File.Open() + if err != nil { + ctx.ServerError("GetPackageFile", err) + return + } + defer upload.Close() + + userError, pv, err := alpine_service.UploadAlpinePackage(ctx, upload, form.Repo, form.Branch) + if err != nil { + if !userError { + ctx.ServerError("UploadAlpinePackage", err) + return + } + + servePackageUploadError(ctx, err, "alpine", form.Repo) + return + } + + uploadPackageFinish(ctx, "alpine", form.Repo, pv) +} + +func UploadDebianPackagePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.PackageUploadDebianForm) + upload, err := form.File.Open() + if err != nil { + ctx.ServerError("GetPackageFile", err) + return + } + defer upload.Close() + + userError, pv, err := debian_service.UploadDebianPackage(ctx, upload, form.Distribution, form.Component) + if err != nil { + if !userError { + ctx.ServerError("UploadDebianPackage", err) + return + } + + servePackageUploadError(ctx, err, "debian", form.Repo) + return + } + + uploadPackageFinish(ctx, "debian", form.Repo, pv) +} + +func UploadGenericPackagePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.PackageUploadGenericForm) + upload, err := form.File.Open() + if err != nil { + ctx.ServerError("GetPackageFile", err) + return + } + defer upload.Close() + + var filename string + if form.Filename == "" { + filename = form.File.Filename + } else { + filename = form.Filename + } + + userError, pv, err := generic_service.UploadGenericPackage(ctx, upload, form.Name, form.Version, filename) + if err != nil { + if !userError { + ctx.ServerError("UploadGenericPackage", err) + return + } + + servePackageUploadError(ctx, err, "generic", form.Repo) + return + } + + uploadPackageFinish(ctx, "debian", form.Repo, pv) +} + +func UploadRpmPackagePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.PackageUploadRpmForm) + upload, err := form.File.Open() + if err != nil { + ctx.ServerError("GetPackageFile", err) + return + } + defer upload.Close() + + userError, pv, err := rpm_service.UploadRpmPackage(ctx, upload) + if err != nil { + if !userError { + ctx.ServerError("UploadRpmPackage", err) + return + } + + servePackageUploadError(ctx, err, "rpm", form.Repo) + return + } + + uploadPackageFinish(ctx, "rpm", form.Repo, pv) +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 146b94dfdf3cf..5891db13fb35b 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -4,7 +4,9 @@ package user import ( + "fmt" "net/http" + "slices" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" @@ -23,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" packages_helper "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/routers/web/shared/packages" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/forms" packages_service "code.gitea.io/gitea/services/packages" @@ -33,6 +36,7 @@ const ( tplPackagesView base.TplName = "package/view" tplPackageVersionList base.TplName = "user/overview/package_versions" tplPackagesSettings base.TplName = "package/settings" + tplPackageUpload base.TplName = "user/overview/package_upload" ) // ListPackages displays a list of all packages of the context user @@ -100,6 +104,8 @@ func ListPackages(ctx *context.Context) { ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = repositoryAccessMap + ctx.Data["PackageUploadUrl"] = fmt.Sprintf("%s/-/packages/upload/", ctx.ContextUser.HTMLURL()) + ctx.Data["AllowedUploadTypes"] = packages.UploadTypeList err = shared_user.LoadHeaderCount(ctx) if err != nil { @@ -116,10 +122,16 @@ func ListPackages(ctx *context.Context) { if ctx.Doer != nil { ctx.Data["IsOrganizationMember"], _ = org_model.IsOrganizationMember(ctx, org.ID, ctx.Doer.ID) ctx.Data["IsOrganizationOwner"], _ = org_model.IsOrganizationOwner(ctx, org.ID, ctx.Doer.ID) + ctx.Data["ShowPackageUploadButton"] = ctx.Data["IsOrganizationMember"] } else { ctx.Data["IsOrganizationMember"] = false ctx.Data["IsOrganizationOwner"] = false + ctx.Data["ShowPackageUploadButton"] = false } + } else if ctx.Doer != nil { + ctx.Data["ShowPackageUploadButton"] = ctx.Doer.ID == ctx.ContextUser.ID + } else { + ctx.Data["ShowPackageUploadButton"] = false } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) @@ -478,3 +490,27 @@ func DownloadPackageFile(ctx *context.Context) { packages_helper.ServePackageFile(ctx, s, u, pf) } + +func UploadPackage(ctx *context.Context) { + shared_user.PrepareContextForProfileBigAvatar(ctx) + + packageType := packages_model.GetPackageTypeByString(ctx.Params("upload_type")) + + if packageType == nil || !slices.Contains(packages.UploadTypeList, *packageType) { + ctx.NotFound("", nil) + return + } + + ctx.Data["IsPackagesPage"] = true + ctx.Data["PackageUploadType"] = packageType + ctx.Data["PackageUploadRepo"] = ctx.FormString("repo") + ctx.Data["Title"] = ctx.Tr("packages.upload.title", packageType.Name()) + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + ctx.HTML(http.StatusOK, tplPackageUpload) +} diff --git a/routers/web/web.go b/routers/web/web.go index 99862505b48a1..9bf54c5cfabad 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -37,6 +37,7 @@ import ( "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" + "code.gitea.io/gitea/routers/web/shared/packages" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/routers/web/user/setting/security" @@ -937,6 +938,13 @@ func registerRoutes(m *web.Route) { }, reqPackageAccess(perm.AccessModeWrite)) }) }) + m.Group("/upload", func() { + m.Get("/{upload_type}", user.UploadPackage) + m.Post("/alpine/upload", web.Bind(forms.PackageUploadAlpineForm{}), packages.UploadAlpinePackagePost) + m.Post("/debian/upload", web.Bind(forms.PackageUploadDebianForm{}), packages.UploadDebianPackagePost) + m.Post("/generic/upload", web.Bind(forms.PackageUploadGenericForm{}), packages.UploadGenericPackagePost) + m.Post("/rpm/upload", web.Bind(forms.PackageUploadRpmForm{}), packages.UploadRpmPackagePost) + }, reqPackageAccess(perm.AccessModeWrite)) }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) } diff --git a/services/forms/package_form.go b/services/forms/package_form.go index 2f08dfe9f4864..e8abb96c8b57f 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -4,6 +4,7 @@ package forms import ( + "mime/multipart" "net/http" "code.gitea.io/gitea/modules/context" @@ -28,3 +29,30 @@ func (f *PackageCleanupRuleForm) Validate(req *http.Request, errs binding.Errors ctx := context.GetValidateContext(req) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } + +type PackageUploadAlpineForm struct { + Repo string + Branch string + Repository string + File *multipart.FileHeader +} + +type PackageUploadDebianForm struct { + Repo string + Distribution string + Component string + File *multipart.FileHeader +} + +type PackageUploadGenericForm struct { + Repo string + Name string + Version string + Filename string + File *multipart.FileHeader +} + +type PackageUploadRpmForm struct { + Repo string + File *multipart.FileHeader +} diff --git a/services/packages/alpine/upload.go b/services/packages/alpine/upload.go new file mode 100644 index 0000000000000..187b86936a001 --- /dev/null +++ b/services/packages/alpine/upload.go @@ -0,0 +1,88 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "errors" + "fmt" + "io" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" +) + +// UploadAlpinePackage adds a Alpine Package to the registry. The first return value indictaes if the error is a user error. +func UploadAlpinePackage(ctx *context.Context, upload io.Reader, branch, repository string) (bool, *packages_model.PackageVersion, error) { + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + return false, nil, err + } + defer buf.Close() + + pck, err := alpine_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + return true, nil, err + } + + return false, nil, err + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + return false, nil, err + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + return false, nil, err + } + + pv, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeAlpine, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + alpine_module.PropertyBranch: branch, + alpine_module.PropertyRepository: repository, + alpine_module.PropertyArchitecture: pck.FileMetadata.Architecture, + alpine_module.PropertyMetadata: string(fileMetadataRaw), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + return true, nil, err + default: + return false, nil, err + } + } + + if err := BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil { + return false, nil, err + } + + return false, pv, nil +} diff --git a/services/packages/debian/upload.go b/services/packages/debian/upload.go new file mode 100644 index 0000000000000..a483785977dbc --- /dev/null +++ b/services/packages/debian/upload.go @@ -0,0 +1,82 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package debian + +import ( + "errors" + "fmt" + "io" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + debian_module "code.gitea.io/gitea/modules/packages/debian" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" +) + +// UploadDebianPackage adds a Debian Package to the registry. The first return value indictaes if the error is a user error. +func UploadDebianPackage(ctx *context.Context, upload io.Reader, distribution, component string) (bool, *packages_model.PackageVersion, error) { + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + return false, nil, err + } + defer buf.Close() + + pck, err := debian_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + return true, nil, err + } + + return false, nil, err + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + return false, nil, err + } + + pv, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeDebian, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.Metadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s_%s_%s.deb", pck.Name, pck.Version, pck.Architecture), + CompositeKey: fmt.Sprintf("%s|%s", distribution, component), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + debian_module.PropertyDistribution: distribution, + debian_module.PropertyComponent: component, + debian_module.PropertyArchitecture: pck.Architecture, + debian_module.PropertyControl: pck.Control, + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + return true, nil, err + default: + return false, nil, err + } + } + + if err := BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, distribution, component, pck.Architecture); err != nil { + return false, nil, err + } + + return false, pv, nil +} diff --git a/services/packages/generic/upload.go b/services/packages/generic/upload.go new file mode 100644 index 0000000000000..6fae3a2e16e27 --- /dev/null +++ b/services/packages/generic/upload.go @@ -0,0 +1,54 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package generic + +import ( + "io" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + packages_service "code.gitea.io/gitea/services/packages" +) + +// UploadGenericPackage adds a Generic Package to the registry. The first return value indictaes if the error is a user error. +func UploadGenericPackage(ctx *context.Context, upload io.Reader, name, version, filename string) (bool, *packages_model.PackageVersion, error) { + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + ctx.ServerError("CreateHashedBufferFromReader", err) + return false, nil, err + } + defer buf.Close() + + pv, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeGeneric, + Name: name, + Version: version, + }, + Creator: ctx.Doer, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: filename, + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + return true, nil, err + default: + return false, nil, err + } + } + + return false, pv, nil +} diff --git a/services/packages/rpm/upload.go b/services/packages/rpm/upload.go new file mode 100644 index 0000000000000..a1d07e4426684 --- /dev/null +++ b/services/packages/rpm/upload.go @@ -0,0 +1,84 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rpm + +import ( + "errors" + "fmt" + "io" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + rpm_module "code.gitea.io/gitea/modules/packages/rpm" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" +) + +// UploadRpmPackage adds a RPM Package to the registry. The first return value indictaes if the error is a user error. +func UploadRpmPackage(ctx *context.Context, upload io.Reader) (bool, *packages_model.PackageVersion, error) { + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + return false, nil, err + } + defer buf.Close() + + pck, err := rpm_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + return true, nil, err + } + + return false, nil, err + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + return false, nil, err + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + return false, nil, err + } + + pv, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeRpm, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + rpm_module.PropertyMetadata: string(fileMetadataRaw), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + return true, nil, err + default: + return false, nil, err + } + } + + if err := BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil { + return false, nil, err + } + + return false, pv, nil +} diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index af79490f4dd3a..f9dea0a070130 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -10,6 +10,17 @@ {{end}} + {{if .ShowPackageUploadButton}} +
+ {{end}}