forked from gitea/gitea
		
	Add Alpine package registry (#23714)
This PR adds an Alpine package registry. You can follow [this tutorial](https://wiki.alpinelinux.org/wiki/Creating_an_Alpine_package) to build a *.apk package for testing. This functionality is similar to the Debian registry (#22854) and therefore shares some methods. I marked this PR as blocked because it should be merged after #22854.  --------- Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		
							parent
							
								
									80bde0141b
								
							
						
					
					
						commit
						9173e079ae
					
				| @ -2442,6 +2442,8 @@ ROUTER = console | ||||
| ;LIMIT_TOTAL_OWNER_COUNT = -1 | ||||
| ;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| ;LIMIT_TOTAL_OWNER_SIZE = -1 | ||||
| ;; Maximum size of an Alpine upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| ;LIMIT_SIZE_ALPINE = -1 | ||||
| ;; Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| ;LIMIT_SIZE_CARGO = -1 | ||||
| ;; Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
|  | ||||
| @ -1213,6 +1213,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf | ||||
| - `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload` | ||||
| - `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maximum count of package versions a single owner can have (`-1` means no limits) | ||||
| - `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| - `LIMIT_SIZE_ALPINE`: **-1**: Maximum size of an Alpine upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| - `LIMIT_SIZE_CARGO`: **-1**: Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| - `LIMIT_SIZE_CHEF`: **-1**: Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
| - `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) | ||||
|  | ||||
							
								
								
									
										133
									
								
								docs/content/doc/usage/packages/alpine.en-us.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								docs/content/doc/usage/packages/alpine.en-us.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | ||||
| --- | ||||
| date: "2023-03-25T00:00:00+00:00" | ||||
| title: "Alpine Packages Repository" | ||||
| slug: "packages/alpine" | ||||
| draft: false | ||||
| toc: false | ||||
| menu: | ||||
|   sidebar: | ||||
|     parent: "packages" | ||||
|     name: "Alpine" | ||||
|     weight: 4 | ||||
|     identifier: "alpine" | ||||
| --- | ||||
| 
 | ||||
| # Alpine Packages Repository | ||||
| 
 | ||||
| Publish [Alpine](https://pkgs.alpinelinux.org/) packages for your user or organization. | ||||
| 
 | ||||
| **Table of Contents** | ||||
| 
 | ||||
| {{< toc >}} | ||||
| 
 | ||||
| ## Requirements | ||||
| 
 | ||||
| To work with the Alpine registry, you need to use a HTTP client like `curl` to upload and a package manager like `apk` to consume packages. | ||||
| 
 | ||||
| The following examples use `apk`. | ||||
| 
 | ||||
| ## Configuring the package registry | ||||
| 
 | ||||
| To register the Alpine registry add the url to the list of known apk sources (`/etc/apk/repositories`): | ||||
| 
 | ||||
| ``` | ||||
| https://gitea.example.com/api/packages/{owner}/alpine/<branch>/<repository> | ||||
| ``` | ||||
| 
 | ||||
| | Placeholder  | Description | | ||||
| | ------------ | ----------- | | ||||
| | `owner`      | The owner of the packages. | | ||||
| | `branch`     | The branch to use. | | ||||
| | `repository` | The repository to use. | | ||||
| 
 | ||||
| If the registry is private, provide credentials in the url. You can use a password or a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}): | ||||
| 
 | ||||
| ``` | ||||
| https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/alpine/<branch>/<repository> | ||||
| ``` | ||||
| 
 | ||||
| The Alpine registry files are signed with a RSA key which must be known to apk. Download the public key and store it in `/etc/apk/keys/`: | ||||
| 
 | ||||
| ```shell | ||||
| curl -JO https://gitea.example.com/api/packages/{owner}/alpine/key | ||||
| ``` | ||||
| 
 | ||||
| Afterwards update the local package index: | ||||
| 
 | ||||
| ```shell | ||||
| apk update | ||||
| ``` | ||||
| 
 | ||||
| ## Publish a package | ||||
| 
 | ||||
| To publish an Alpine package (`*.apk`), perform a HTTP `PUT` operation with the package content in the request body. | ||||
| 
 | ||||
| ``` | ||||
| PUT https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository} | ||||
| ``` | ||||
| 
 | ||||
| | Parameter    | Description | | ||||
| | ------------ | ----------- | | ||||
| | `owner`      | The owner of the package. | | ||||
| | `branch`     | The branch may match the release version of the OS, ex: `v3.17`. | | ||||
| | `repository` | The repository can be used [to group packages](https://wiki.alpinelinux.org/wiki/Repositories) or just `main` or similar. | | ||||
| 
 | ||||
| Example request using HTTP Basic authentication: | ||||
| 
 | ||||
| ```shell | ||||
| curl --user your_username:your_password_or_token \ | ||||
|      --upload-file path/to/file.apk \ | ||||
|      https://gitea.example.com/api/packages/testuser/alpine/v3.17/main | ||||
| ``` | ||||
| 
 | ||||
| If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. | ||||
| You cannot publish a file with the same name twice to a package. You must delete the existing package file first. | ||||
| 
 | ||||
| The server responds with the following HTTP Status codes. | ||||
| 
 | ||||
| | HTTP Status Code  | Meaning | | ||||
| | ----------------- | ------- | | ||||
| | `201 Created`     | The package has been published. | | ||||
| | `400 Bad Request` | The package name, version, branch, repository or architecture are invalid. | | ||||
| | `409 Conflict`    | A package file with the same combination of parameters exist already in the package. | | ||||
| 
 | ||||
| ## Delete a package | ||||
| 
 | ||||
| To delete an Alpine package perform a HTTP `DELETE` operation. This will delete the package version too if there is no file left. | ||||
| 
 | ||||
| ``` | ||||
| DELETE https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}/{architecture}/{filename} | ||||
| ``` | ||||
| 
 | ||||
| | Parameter      | Description | | ||||
| | -------------- | ----------- | | ||||
| | `owner`        | The owner of the package. | | ||||
| | `branch`       | The branch to use. | | ||||
| | `repository`   | The repository to use. | | ||||
| | `architecture` | The package architecture. | | ||||
| | `filename`     | The file to delete. | ||||
| 
 | ||||
| Example request using HTTP Basic authentication: | ||||
| 
 | ||||
| ```shell | ||||
| curl --user your_username:your_token_or_password -X DELETE \ | ||||
|      https://gitea.example.com/api/packages/testuser/alpine/v3.17/main/test-package-1.0.0.apk | ||||
| ``` | ||||
| 
 | ||||
| The server responds with the following HTTP Status codes. | ||||
| 
 | ||||
| | HTTP Status Code  | Meaning | | ||||
| | ----------------- | ------- | | ||||
| | `204 No Content`  | Success | | ||||
| | `404 Not Found`   | The package or file was not found. | | ||||
| 
 | ||||
| ## Install a package | ||||
| 
 | ||||
| To install a package from the Alpine registry, execute the following commands: | ||||
| 
 | ||||
| ```shell | ||||
| # use latest version | ||||
| apk add {package_name} | ||||
| # use specific version | ||||
| apk add {package_name}={package_version} | ||||
| ``` | ||||
| @ -27,6 +27,7 @@ The following package managers are currently supported: | ||||
| 
 | ||||
| | Name | Language | Package client | | ||||
| | ---- | -------- | -------------- | | ||||
| | [Alpine]({{< relref "doc/usage/packages/alpine.en-us.md" >}}) | - | `apk` | | ||||
| | [Cargo]({{< relref "doc/usage/packages/cargo.en-us.md" >}}) | Rust | `cargo` | | ||||
| | [Chef]({{< relref "doc/usage/packages/chef.en-us.md" >}}) | - | `knife` | | ||||
| | [Composer]({{< relref "doc/usage/packages/composer.en-us.md" >}}) | PHP | `composer` | | ||||
|  | ||||
| @ -9,7 +9,7 @@ menu: | ||||
|   sidebar: | ||||
|     parent: "packages" | ||||
|     name: "Storage" | ||||
|     weight: 5 | ||||
|     weight: 2 | ||||
|     identifier: "storage" | ||||
| --- | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										53
									
								
								models/packages/alpine/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								models/packages/alpine/search.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package alpine | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	packages_model "code.gitea.io/gitea/models/packages" | ||||
| 	alpine_module "code.gitea.io/gitea/modules/packages/alpine" | ||||
| ) | ||||
| 
 | ||||
| // GetBranches gets all available branches | ||||
| func GetBranches(ctx context.Context, ownerID int64) ([]string, error) { | ||||
| 	return packages_model.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages_model.TypeAlpine, | ||||
| 		ownerID, | ||||
| 		packages_model.PropertyTypeFile, | ||||
| 		alpine_module.PropertyBranch, | ||||
| 		nil, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetRepositories gets all available repositories for the given branch | ||||
| func GetRepositories(ctx context.Context, ownerID int64, branch string) ([]string, error) { | ||||
| 	return packages_model.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages_model.TypeAlpine, | ||||
| 		ownerID, | ||||
| 		packages_model.PropertyTypeFile, | ||||
| 		alpine_module.PropertyRepository, | ||||
| 		&packages_model.DistinctPropertyDependency{ | ||||
| 			Name:  alpine_module.PropertyBranch, | ||||
| 			Value: branch, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetArchitectures gets all available architectures for the given repository | ||||
| func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { | ||||
| 	return packages_model.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages_model.TypeAlpine, | ||||
| 		ownerID, | ||||
| 		packages_model.PropertyTypeFile, | ||||
| 		alpine_module.PropertyArchitecture, | ||||
| 		&packages_model.DistinctPropertyDependency{ | ||||
| 			Name:  alpine_module.PropertyRepository, | ||||
| 			Value: repository, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
| @ -88,44 +88,42 @@ func SearchLatestPackages(ctx context.Context, opts *PackageSearchOptions) ([]*p | ||||
| 
 | ||||
| // GetDistributions gets all available distributions | ||||
| func GetDistributions(ctx context.Context, ownerID int64) ([]string, error) { | ||||
| 	return getDistinctPropertyValues(ctx, ownerID, "", debian_module.PropertyDistribution) | ||||
| 	return packages.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages.TypeDebian, | ||||
| 		ownerID, | ||||
| 		packages.PropertyTypeFile, | ||||
| 		debian_module.PropertyDistribution, | ||||
| 		nil, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetComponents gets all available components for the given distribution | ||||
| func GetComponents(ctx context.Context, ownerID int64, distribution string) ([]string, error) { | ||||
| 	return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyComponent) | ||||
| 	return packages.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages.TypeDebian, | ||||
| 		ownerID, | ||||
| 		packages.PropertyTypeFile, | ||||
| 		debian_module.PropertyComponent, | ||||
| 		&packages.DistinctPropertyDependency{ | ||||
| 			Name:  debian_module.PropertyDistribution, | ||||
| 			Value: distribution, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetArchitectures gets all available architectures for the given distribution | ||||
| func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) { | ||||
| 	return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyArchitecture) | ||||
| } | ||||
| 
 | ||||
| func getDistinctPropertyValues(ctx context.Context, ownerID int64, distribution, propName string) ([]string, error) { | ||||
| 	var cond builder.Cond = builder.Eq{ | ||||
| 		"package_property.ref_type": packages.PropertyTypeFile, | ||||
| 		"package_property.name":     propName, | ||||
| 		"package.type":              packages.TypeDebian, | ||||
| 		"package.owner_id":          ownerID, | ||||
| 	} | ||||
| 	if distribution != "" { | ||||
| 		innerCond := builder. | ||||
| 			Expr("pp.ref_id = package_property.ref_id"). | ||||
| 			And(builder.Eq{ | ||||
| 				"pp.ref_type": packages.PropertyTypeFile, | ||||
| 				"pp.name":     debian_module.PropertyDistribution, | ||||
| 				"pp.value":    distribution, | ||||
| 			}) | ||||
| 		cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) | ||||
| 	} | ||||
| 
 | ||||
| 	values := make([]string, 0, 5) | ||||
| 	return values, db.GetEngine(ctx). | ||||
| 		Table("package_property"). | ||||
| 		Distinct("package_property.value"). | ||||
| 		Join("INNER", "package_file", "package_file.id = package_property.ref_id"). | ||||
| 		Join("INNER", "package_version", "package_version.id = package_file.version_id"). | ||||
| 		Join("INNER", "package", "package.id = package_version.package_id"). | ||||
| 		Where(cond). | ||||
| 		Find(&values) | ||||
| 	return packages.GetDistinctPropertyValues( | ||||
| 		ctx, | ||||
| 		packages.TypeDebian, | ||||
| 		ownerID, | ||||
| 		packages.PropertyTypeFile, | ||||
| 		debian_module.PropertyArchitecture, | ||||
| 		&packages.DistinctPropertyDependency{ | ||||
| 			Name:  debian_module.PropertyDistribution, | ||||
| 			Value: distribution, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,7 @@ import ( | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/packages/alpine" | ||||
| 	"code.gitea.io/gitea/modules/packages/cargo" | ||||
| 	"code.gitea.io/gitea/modules/packages/chef" | ||||
| 	"code.gitea.io/gitea/modules/packages/composer" | ||||
| @ -136,6 +137,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc | ||||
| 
 | ||||
| 	var metadata interface{} | ||||
| 	switch p.Type { | ||||
| 	case TypeAlpine: | ||||
| 		metadata = &alpine.VersionMetadata{} | ||||
| 	case TypeCargo: | ||||
| 		metadata = &cargo.Metadata{} | ||||
| 	case TypeChef: | ||||
|  | ||||
| @ -30,6 +30,7 @@ type Type string | ||||
| 
 | ||||
| // List of supported packages | ||||
| const ( | ||||
| 	TypeAlpine    Type = "alpine" | ||||
| 	TypeCargo     Type = "cargo" | ||||
| 	TypeChef      Type = "chef" | ||||
| 	TypeComposer  Type = "composer" | ||||
| @ -51,6 +52,7 @@ const ( | ||||
| ) | ||||
| 
 | ||||
| var TypeList = []Type{ | ||||
| 	TypeAlpine, | ||||
| 	TypeCargo, | ||||
| 	TypeChef, | ||||
| 	TypeComposer, | ||||
| @ -74,6 +76,8 @@ var TypeList = []Type{ | ||||
| // Name gets the name of the package type | ||||
| func (pt Type) Name() string { | ||||
| 	switch pt { | ||||
| 	case TypeAlpine: | ||||
| 		return "Alpine" | ||||
| 	case TypeCargo: | ||||
| 		return "Cargo" | ||||
| 	case TypeChef: | ||||
| @ -117,6 +121,8 @@ func (pt Type) Name() string { | ||||
| // SVGName gets the name of the package type svg image | ||||
| func (pt Type) SVGName() string { | ||||
| 	switch pt { | ||||
| 	case TypeAlpine: | ||||
| 		return "gitea-alpine" | ||||
| 	case TypeCargo: | ||||
| 		return "gitea-cargo" | ||||
| 	case TypeChef: | ||||
|  | ||||
| @ -7,6 +7,8 @@ import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| @ -81,3 +83,39 @@ func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64 | ||||
| 	_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| type DistinctPropertyDependency struct { | ||||
| 	Name  string | ||||
| 	Value string | ||||
| } | ||||
| 
 | ||||
| // GetDistinctPropertyValues returns all distinct property values for a given type. | ||||
| // Optional: Search only in dependence of another property. | ||||
| func GetDistinctPropertyValues(ctx context.Context, packageType Type, ownerID int64, refType PropertyType, propertyName string, dep *DistinctPropertyDependency) ([]string, error) { | ||||
| 	var cond builder.Cond = builder.Eq{ | ||||
| 		"package_property.ref_type": refType, | ||||
| 		"package_property.name":     propertyName, | ||||
| 		"package.type":              packageType, | ||||
| 		"package.owner_id":          ownerID, | ||||
| 	} | ||||
| 	if dep != nil { | ||||
| 		innerCond := builder. | ||||
| 			Expr("pp.ref_id = package_property.ref_id"). | ||||
| 			And(builder.Eq{ | ||||
| 				"pp.ref_type": refType, | ||||
| 				"pp.name":     dep.Name, | ||||
| 				"pp.value":    dep.Value, | ||||
| 			}) | ||||
| 		cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) | ||||
| 	} | ||||
| 
 | ||||
| 	values := make([]string, 0, 5) | ||||
| 	return values, db.GetEngine(ctx). | ||||
| 		Table("package_property"). | ||||
| 		Distinct("package_property.value"). | ||||
| 		Join("INNER", "package_file", "package_file.id = package_property.ref_id"). | ||||
| 		Join("INNER", "package_version", "package_version.id = package_file.version_id"). | ||||
| 		Join("INNER", "package", "package.id = package_version.package_id"). | ||||
| 		Where(cond). | ||||
| 		Find(&values) | ||||
| } | ||||
|  | ||||
							
								
								
									
										236
									
								
								modules/packages/alpine/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								modules/packages/alpine/metadata.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,236 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package alpine | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bufio" | ||||
| 	"compress/gzip" | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/base64" | ||||
| 	"io" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/validation" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing") | ||||
| 	ErrInvalidName        = util.NewInvalidArgumentErrorf("package name is invalid") | ||||
| 	ErrInvalidVersion     = util.NewInvalidArgumentErrorf("package version is invalid") | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	PropertyMetadata     = "alpine.metadata" | ||||
| 	PropertyBranch       = "alpine.branch" | ||||
| 	PropertyRepository   = "alpine.repository" | ||||
| 	PropertyArchitecture = "alpine.architecture" | ||||
| 
 | ||||
| 	SettingKeyPrivate = "alpine.key.private" | ||||
| 	SettingKeyPublic  = "alpine.key.public" | ||||
| 
 | ||||
| 	RepositoryPackage = "_alpine" | ||||
| 	RepositoryVersion = "_repository" | ||||
| ) | ||||
| 
 | ||||
| // https://wiki.alpinelinux.org/wiki/Apk_spec | ||||
| 
 | ||||
| // Package represents an Alpine package | ||||
| type Package struct { | ||||
| 	Name            string | ||||
| 	Version         string | ||||
| 	VersionMetadata VersionMetadata | ||||
| 	FileMetadata    FileMetadata | ||||
| } | ||||
| 
 | ||||
| // Metadata of an Alpine package | ||||
| type VersionMetadata struct { | ||||
| 	Description string `json:"description,omitempty"` | ||||
| 	License     string `json:"license,omitempty"` | ||||
| 	ProjectURL  string `json:"project_url,omitempty"` | ||||
| 	Maintainer  string `json:"maintainer,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type FileMetadata struct { | ||||
| 	Checksum     string   `json:"checksum"` | ||||
| 	Packager     string   `json:"packager,omitempty"` | ||||
| 	BuildDate    int64    `json:"build_date,omitempty"` | ||||
| 	Size         int64    `json:"size,omitempty"` | ||||
| 	Architecture string   `json:"architecture,omitempty"` | ||||
| 	Origin       string   `json:"origin,omitempty"` | ||||
| 	CommitHash   string   `json:"commit_hash,omitempty"` | ||||
| 	InstallIf    string   `json:"install_if,omitempty"` | ||||
| 	Provides     []string `json:"provides,omitempty"` | ||||
| 	Dependencies []string `json:"dependencies,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // ParsePackage parses the Alpine package file | ||||
| func ParsePackage(r io.Reader) (*Package, error) { | ||||
| 	// Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata. | ||||
| 
 | ||||
| 	br := bufio.NewReader(r) // needed for gzip Multistream | ||||
| 
 | ||||
| 	h := sha1.New() | ||||
| 
 | ||||
| 	gzr, err := gzip.NewReader(&teeByteReader{br, h}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer gzr.Close() | ||||
| 
 | ||||
| 	for { | ||||
| 		gzr.Multistream(false) | ||||
| 
 | ||||
| 		tr := tar.NewReader(gzr) | ||||
| 		for { | ||||
| 			hd, err := tr.Next() | ||||
| 			if err == io.EOF { | ||||
| 				break | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 
 | ||||
| 			if hd.Name == ".PKGINFO" { | ||||
| 				p, err := ParsePackageInfo(tr) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				// drain the reader | ||||
| 				for { | ||||
| 					if _, err := tr.Next(); err != nil { | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil)) | ||||
| 
 | ||||
| 				return p, nil | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		h = sha1.New() | ||||
| 
 | ||||
| 		err = gzr.Reset(&teeByteReader{br, h}) | ||||
| 		if err == io.EOF { | ||||
| 			break | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, ErrMissingPKGINFOFile | ||||
| } | ||||
| 
 | ||||
| // ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package | ||||
| func ParsePackageInfo(r io.Reader) (*Package, error) { | ||||
| 	p := &Package{} | ||||
| 
 | ||||
| 	scanner := bufio.NewScanner(r) | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
| 
 | ||||
| 		if strings.HasPrefix(line, "#") { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		i := strings.IndexRune(line, '=') | ||||
| 		if i == -1 { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		key := strings.TrimSpace(line[:i]) | ||||
| 		value := strings.TrimSpace(line[i+1:]) | ||||
| 
 | ||||
| 		switch key { | ||||
| 		case "pkgname": | ||||
| 			p.Name = value | ||||
| 		case "pkgver": | ||||
| 			p.Version = value | ||||
| 		case "pkgdesc": | ||||
| 			p.VersionMetadata.Description = value | ||||
| 		case "url": | ||||
| 			p.VersionMetadata.ProjectURL = value | ||||
| 		case "builddate": | ||||
| 			n, err := strconv.ParseInt(value, 10, 64) | ||||
| 			if err == nil { | ||||
| 				p.FileMetadata.BuildDate = n | ||||
| 			} | ||||
| 		case "size": | ||||
| 			n, err := strconv.ParseInt(value, 10, 64) | ||||
| 			if err == nil { | ||||
| 				p.FileMetadata.Size = n | ||||
| 			} | ||||
| 		case "arch": | ||||
| 			p.FileMetadata.Architecture = value | ||||
| 		case "origin": | ||||
| 			p.FileMetadata.Origin = value | ||||
| 		case "commit": | ||||
| 			p.FileMetadata.CommitHash = value | ||||
| 		case "maintainer": | ||||
| 			p.VersionMetadata.Maintainer = value | ||||
| 		case "packager": | ||||
| 			p.FileMetadata.Packager = value | ||||
| 		case "license": | ||||
| 			p.VersionMetadata.License = value | ||||
| 		case "install_if": | ||||
| 			p.FileMetadata.InstallIf = value | ||||
| 		case "provides": | ||||
| 			if value != "" { | ||||
| 				p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) | ||||
| 			} | ||||
| 		case "depend": | ||||
| 			if value != "" { | ||||
| 				p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if err := scanner.Err(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if p.Name == "" { | ||||
| 		return nil, ErrInvalidName | ||||
| 	} | ||||
| 
 | ||||
| 	if p.Version == "" { | ||||
| 		return nil, ErrInvalidVersion | ||||
| 	} | ||||
| 
 | ||||
| 	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { | ||||
| 		p.VersionMetadata.ProjectURL = "" | ||||
| 	} | ||||
| 
 | ||||
| 	return p, nil | ||||
| } | ||||
| 
 | ||||
| // Same as io.TeeReader but implements io.ByteReader | ||||
| type teeByteReader struct { | ||||
| 	r *bufio.Reader | ||||
| 	w io.Writer | ||||
| } | ||||
| 
 | ||||
| func (t *teeByteReader) Read(p []byte) (int, error) { | ||||
| 	n, err := t.r.Read(p) | ||||
| 	if n > 0 { | ||||
| 		if n, err := t.w.Write(p[:n]); err != nil { | ||||
| 			return n, err | ||||
| 		} | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
| 
 | ||||
| func (t *teeByteReader) ReadByte() (byte, error) { | ||||
| 	b, err := t.r.ReadByte() | ||||
| 	if err == nil { | ||||
| 		if _, err := t.w.Write([]byte{b}); err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 	} | ||||
| 	return b, err | ||||
| } | ||||
							
								
								
									
										143
									
								
								modules/packages/alpine/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								modules/packages/alpine/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,143 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package alpine | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"io" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	packageName        = "gitea" | ||||
| 	packageVersion     = "1.0.1" | ||||
| 	packageDescription = "Package Description" | ||||
| 	packageProjectURL  = "https://gitea.io" | ||||
| 	packageMaintainer  = "KN4CK3R <dummy@gitea.io>" | ||||
| ) | ||||
| 
 | ||||
| func createPKGINFOContent(name, version string) []byte { | ||||
| 	return []byte(`pkgname = ` + name + ` | ||||
| pkgver = ` + version + ` | ||||
| pkgdesc = ` + packageDescription + ` | ||||
| url = ` + packageProjectURL + ` | ||||
| # comment | ||||
| builddate = 1678834800 | ||||
| packager = Gitea <pack@ag.er> | ||||
| size = 123456 | ||||
| arch = aarch64 | ||||
| origin = origin | ||||
| commit = 1111e709613fbc979651b09ac2bc27c6591a9999 | ||||
| maintainer = ` + packageMaintainer + ` | ||||
| license = MIT | ||||
| depend = common | ||||
| install_if = value | ||||
| depend = gitea | ||||
| provides = common | ||||
| provides = gitea`) | ||||
| } | ||||
| 
 | ||||
| func TestParsePackage(t *testing.T) { | ||||
| 	createPackage := func(name string, content []byte) io.Reader { | ||||
| 		names := []string{"first.stream", name} | ||||
| 		contents := [][]byte{{0}, content} | ||||
| 
 | ||||
| 		var buf bytes.Buffer | ||||
| 		zw := gzip.NewWriter(&buf) | ||||
| 
 | ||||
| 		for i := range names { | ||||
| 			if i != 0 { | ||||
| 				zw.Close() | ||||
| 				zw.Reset(&buf) | ||||
| 			} | ||||
| 
 | ||||
| 			tw := tar.NewWriter(zw) | ||||
| 			hdr := &tar.Header{ | ||||
| 				Name: names[i], | ||||
| 				Mode: 0o600, | ||||
| 				Size: int64(len(contents[i])), | ||||
| 			} | ||||
| 			tw.WriteHeader(hdr) | ||||
| 			tw.Write(contents[i]) | ||||
| 			tw.Close() | ||||
| 		} | ||||
| 
 | ||||
| 		zw.Close() | ||||
| 
 | ||||
| 		return &buf | ||||
| 	} | ||||
| 
 | ||||
| 	t.Run("MissingPKGINFOFile", func(t *testing.T) { | ||||
| 		data := createPackage("dummy.txt", []byte{}) | ||||
| 
 | ||||
| 		pp, err := ParsePackage(data) | ||||
| 		assert.Nil(t, pp) | ||||
| 		assert.ErrorIs(t, err, ErrMissingPKGINFOFile) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("InvalidPKGINFOFile", func(t *testing.T) { | ||||
| 		data := createPackage(".PKGINFO", []byte{}) | ||||
| 
 | ||||
| 		pp, err := ParsePackage(data) | ||||
| 		assert.Nil(t, pp) | ||||
| 		assert.ErrorIs(t, err, ErrInvalidName) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Valid", func(t *testing.T) { | ||||
| 		data := createPackage(".PKGINFO", createPKGINFOContent(packageName, packageVersion)) | ||||
| 
 | ||||
| 		p, err := ParsePackage(data) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, p) | ||||
| 
 | ||||
| 		assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestParsePackageInfo(t *testing.T) { | ||||
| 	t.Run("InvalidName", func(t *testing.T) { | ||||
| 		data := createPKGINFOContent("", packageVersion) | ||||
| 
 | ||||
| 		p, err := ParsePackageInfo(bytes.NewReader(data)) | ||||
| 		assert.Nil(t, p) | ||||
| 		assert.ErrorIs(t, err, ErrInvalidName) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("InvalidVersion", func(t *testing.T) { | ||||
| 		data := createPKGINFOContent(packageName, "") | ||||
| 
 | ||||
| 		p, err := ParsePackageInfo(bytes.NewReader(data)) | ||||
| 		assert.Nil(t, p) | ||||
| 		assert.ErrorIs(t, err, ErrInvalidVersion) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Valid", func(t *testing.T) { | ||||
| 		data := createPKGINFOContent(packageName, packageVersion) | ||||
| 
 | ||||
| 		p, err := ParsePackageInfo(bytes.NewReader(data)) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, p) | ||||
| 
 | ||||
| 		assert.Equal(t, packageName, p.Name) | ||||
| 		assert.Equal(t, packageVersion, p.Version) | ||||
| 		assert.Equal(t, packageDescription, p.VersionMetadata.Description) | ||||
| 		assert.Equal(t, packageMaintainer, p.VersionMetadata.Maintainer) | ||||
| 		assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) | ||||
| 		assert.Equal(t, "MIT", p.VersionMetadata.License) | ||||
| 		assert.Empty(t, p.FileMetadata.Checksum) | ||||
| 		assert.Equal(t, "Gitea <pack@ag.er>", p.FileMetadata.Packager) | ||||
| 		assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) | ||||
| 		assert.EqualValues(t, 123456, p.FileMetadata.Size) | ||||
| 		assert.Equal(t, "aarch64", p.FileMetadata.Architecture) | ||||
| 		assert.Equal(t, "origin", p.FileMetadata.Origin) | ||||
| 		assert.Equal(t, "1111e709613fbc979651b09ac2bc27c6591a9999", p.FileMetadata.CommitHash) | ||||
| 		assert.Equal(t, "value", p.FileMetadata.InstallIf) | ||||
| 		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) | ||||
| 		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Dependencies) | ||||
| 	}) | ||||
| } | ||||
| @ -24,6 +24,7 @@ var ( | ||||
| 
 | ||||
| 		LimitTotalOwnerCount int64 | ||||
| 		LimitTotalOwnerSize  int64 | ||||
| 		LimitSizeAlpine      int64 | ||||
| 		LimitSizeCargo       int64 | ||||
| 		LimitSizeChef        int64 | ||||
| 		LimitSizeComposer    int64 | ||||
| @ -69,6 +70,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) { | ||||
| 	} | ||||
| 
 | ||||
| 	Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") | ||||
| 	Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") | ||||
| 	Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") | ||||
| 	Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") | ||||
| 	Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") | ||||
|  | ||||
| @ -4,10 +4,13 @@ | ||||
| package util | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 
 | ||||
| 	"github.com/minio/sha256-simd" | ||||
| ) | ||||
| 
 | ||||
| // GenerateKeyPair generates a public and private keypair | ||||
| @ -43,3 +46,16 @@ func pemBlockForPub(pub *rsa.PublicKey) (string, error) { | ||||
| 	}) | ||||
| 	return string(pubBytes), nil | ||||
| } | ||||
| 
 | ||||
| // CreatePublicKeyFingerprint creates a fingerprint of the given key. | ||||
| // The fingerprint is the sha256 sum of the PKIX structure of the key. | ||||
| func CreatePublicKeyFingerprint(key crypto.PublicKey) ([]byte, error) { | ||||
| 	bytes, err := x509.MarshalPKIXPublicKey(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	checksum := sha256.Sum256(bytes) | ||||
| 
 | ||||
| 	return checksum[:], nil | ||||
| } | ||||
|  | ||||
| @ -3212,6 +3212,15 @@ versions = Versions | ||||
| versions.view_all = View all | ||||
| dependency.id = ID | ||||
| dependency.version = Version | ||||
| alpine.registry = Setup this registry by adding the url in your <code>/etc/apk/repositories</code> file: | ||||
| alpine.registry.key = Download the registry public RSA key into the <code>/etc/apk/keys/</code> folder to verify the index signature: | ||||
| alpine.registry.info = Choose $branch and $repository from the list below. | ||||
| alpine.install = To install the package, run the following command: | ||||
| alpine.documentation = For more information on the Alpine registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>. | ||||
| alpine.repository = Repository Info | ||||
| alpine.repository.branches = Branches | ||||
| alpine.repository.repositories = Repositories | ||||
| alpine.repository.architectures = Architectures | ||||
| cargo.registry = Setup this registry in the Cargo configuration file (for example <code>~/.cargo/config.toml</code>): | ||||
| cargo.install = To install the package using Cargo, run the following command: | ||||
| cargo.documentation = For more information on the Cargo registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>. | ||||
|  | ||||
							
								
								
									
										1
									
								
								public/img/svg/gitea-alpine.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/img/svg/gitea-alpine.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 186 162" class="svg gitea-alpine" width="16" height="16" aria-hidden="true"><g fill="#0d597f"><path d="M67 100.75V81.125L52.875 95.25a41.588 41.588 0 0 0 4.3 2.637c1.35.71 2.612 1.25 3.787 1.676a21.12 21.12 0 0 0 3.275.887c1.006.184 1.926.266 2.763.278m72.25-1.625c.025.02.163.137.416.298.255.163.628.372 1.123.578.494.205 1.111.409 1.85.56.745.152 1.612.252 2.625.252.838 0 1.762-.073 2.775-.25a20.93 20.93 0 0 0 3.3-.873 29.25 29.25 0 0 0 3.837-1.676 41.805 41.805 0 0 0 4.375-2.674l-10.712-10.5-35.5-35.625-15.625 15.625-21-21.625-52.75 52.125c1.55 1.075 3 1.95 4.362 2.675a28.324 28.324 0 0 0 3.838 1.675c1.189.414 2.287.696 3.3.872 1.012.177 1.937.251 2.775.251 1.005 0 1.875-.1 2.625-.252a9.726 9.726 0 0 0 1.85-.561c.495-.205.866-.414 1.121-.577s.393-.278.418-.3l23.875-23.875 8.512-8.162 23.625 23.625 8.238 8.475c.024.021.162.137.417.299.255.162.626.37 1.121.577.495.205 1.113.409 1.85.56.745.153 1.625.253 2.625.253.838 0 1.763-.074 2.775-.25a20.966 20.966 0 0 0 3.3-.874 29.323 29.323 0 0 0 3.838-1.675 41.805 41.805 0 0 0 4.375-2.675l-18.875-18.5 3.525-3.525 16.375 16.375 9.55 9.462m-.204-98.75 46.5 80.625-46.5 80.625H46.05L-.45 81.066 46.05.441z"/><path d="M110.75 77 98.363 64.625l.88-.886 12.476 12.337z"/></g></svg> | ||||
| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										253
									
								
								routers/api/packages/alpine/alpine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								routers/api/packages/alpine/alpine.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,253 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package alpine | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/hex" | ||||
| 	"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" | ||||
| 	alpine_service "code.gitea.io/gitea/services/packages/alpine" | ||||
| ) | ||||
| 
 | ||||
| func apiError(ctx *context.Context, status int, obj interface{}) { | ||||
| 	helper.LogAndProcessError(ctx, status, obj, func(message string) { | ||||
| 		ctx.PlainText(status, message) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func GetRepositoryKey(ctx *context.Context) { | ||||
| 	_, pub, err := alpine_service.GetOrCreateKeyPair(ctx.Package.Owner.ID) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	pubPem, _ := pem.Decode([]byte(pub)) | ||||
| 	if pubPem == nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, "failed to decode private key pem") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	pubKey, err := x509.ParsePKIXPublicKey(pubPem.Bytes) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	fingerprint, err := util.CreatePublicKeyFingerprint(pubKey) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ | ||||
| 		ContentType: "application/x-pem-file", | ||||
| 		Filename:    fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func GetRepositoryFile(ctx *context.Context) { | ||||
| 	pv, err := alpine_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	s, pf, err := packages_service.GetFileStreamByPackageVersion( | ||||
| 		ctx, | ||||
| 		pv, | ||||
| 		&packages_service.PackageFileInfo{ | ||||
| 			Filename:     alpine_service.IndexFilename, | ||||
| 			CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")), | ||||
| 		}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, util.ErrNotExist) { | ||||
| 			apiError(ctx, http.StatusNotFound, err) | ||||
| 		} else { | ||||
| 			apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.Close() | ||||
| 
 | ||||
| 	ctx.ServeContent(s, &context.ServeHeaderOptions{ | ||||
| 		Filename:     pf.Name, | ||||
| 		LastModified: pf.CreatedUnix.AsLocalTime(), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func UploadPackageFile(ctx *context.Context) { | ||||
| 	branch := strings.TrimSpace(ctx.Params("branch")) | ||||
| 	repository := strings.TrimSpace(ctx.Params("repository")) | ||||
| 	if branch == "" || repository == "" { | ||||
| 		apiError(ctx, http.StatusBadRequest, "invalid branch or repository") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	upload, close, err := ctx.UploadStream() | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if close { | ||||
| 		defer upload.Close() | ||||
| 	} | ||||
| 
 | ||||
| 	buf, err := packages_module.CreateHashedBufferFromReader(upload) | ||||
| 	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) | ||||
| 		} 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( | ||||
| 		&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) | ||||
| } | ||||
| 
 | ||||
| func DownloadPackageFile(ctx *context.Context) { | ||||
| 	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ | ||||
| 		OwnerID:      ctx.Package.Owner.ID, | ||||
| 		PackageType:  packages_model.TypeAlpine, | ||||
| 		Query:        ctx.Params("filename"), | ||||
| 		CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(pfs) != 1 { | ||||
| 		apiError(ctx, http.StatusNotFound, nil) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	s, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, util.ErrNotExist) { | ||||
| 			apiError(ctx, http.StatusNotFound, err) | ||||
| 		} else { | ||||
| 			apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.Close() | ||||
| 
 | ||||
| 	ctx.ServeContent(s, &context.ServeHeaderOptions{ | ||||
| 		Filename:     pf.Name, | ||||
| 		LastModified: pf.CreatedUnix.AsLocalTime(), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func DeletePackageFile(ctx *context.Context) { | ||||
| 	branch, repository, architecture := ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture") | ||||
| 
 | ||||
| 	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ | ||||
| 		OwnerID:      ctx.Package.Owner.ID, | ||||
| 		PackageType:  packages_model.TypeAlpine, | ||||
| 		Query:        ctx.Params("filename"), | ||||
| 		CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(pfs) != 1 { | ||||
| 		apiError(ctx, http.StatusNotFound, nil) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx.Doer, pfs[0]); err != nil { | ||||
| 		if errors.Is(err, util.ErrNotExist) { | ||||
| 			apiError(ctx, http.StatusNotFound, err) | ||||
| 		} else { | ||||
| 			apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, architecture); err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
| @ -15,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/api/packages/alpine" | ||||
| 	"code.gitea.io/gitea/routers/api/packages/cargo" | ||||
| 	"code.gitea.io/gitea/routers/api/packages/chef" | ||||
| 	"code.gitea.io/gitea/routers/api/packages/composer" | ||||
| @ -107,6 +108,19 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { | ||||
| 	}) | ||||
| 
 | ||||
| 	r.Group("/{username}", func() { | ||||
| 		r.Group("/alpine", func() { | ||||
| 			r.Get("/key", alpine.GetRepositoryKey) | ||||
| 			r.Group("/{branch}/{repository}", func() { | ||||
| 				r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile) | ||||
| 				r.Group("/{architecture}", func() { | ||||
| 					r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile) | ||||
| 					r.Group("/{filename}", func() { | ||||
| 						r.Get("", alpine.DownloadPackageFile) | ||||
| 						r.Delete("", reqPackageAccess(perm.AccessModeWrite), alpine.DeletePackageFile) | ||||
| 					}) | ||||
| 				}) | ||||
| 			}) | ||||
| 		}, reqPackageAccess(perm.AccessModeRead)) | ||||
| 		r.Group("/cargo", func() { | ||||
| 			r.Group("/api/v1/crates", func() { | ||||
| 				r.Get("", cargo.SearchPackages) | ||||
|  | ||||
| @ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) { | ||||
| 	//   in: query | ||||
| 	//   description: package type filter | ||||
| 	//   type: string | ||||
| 	//   enum: [cargo, chef, composer, conan, conda, container, debian, generic, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] | ||||
| 	//   enum: [alpine, cargo, chef, composer, conan, conda, container, debian, generic, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] | ||||
| 	// - name: q | ||||
| 	//   in: query | ||||
| 	//   description: name filter | ||||
|  | ||||
| @ -17,6 +17,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	alpine_module "code.gitea.io/gitea/modules/packages/alpine" | ||||
| 	debian_module "code.gitea.io/gitea/modules/packages/debian" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @ -168,6 +169,27 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 	switch pd.Package.Type { | ||||
| 	case packages_model.TypeContainer: | ||||
| 		ctx.Data["RegistryHost"] = setting.Packages.RegistryHost | ||||
| 	case packages_model.TypeAlpine: | ||||
| 		branches := make(container.Set[string]) | ||||
| 		repositories := make(container.Set[string]) | ||||
| 		architectures := make(container.Set[string]) | ||||
| 
 | ||||
| 		for _, f := range pd.Files { | ||||
| 			for _, pp := range f.Properties { | ||||
| 				switch pp.Name { | ||||
| 				case alpine_module.PropertyBranch: | ||||
| 					branches.Add(pp.Value) | ||||
| 				case alpine_module.PropertyRepository: | ||||
| 					repositories.Add(pp.Value) | ||||
| 				case alpine_module.PropertyArchitecture: | ||||
| 					architectures.Add(pp.Value) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Data["Branches"] = branches.Values() | ||||
| 		ctx.Data["Repositories"] = repositories.Values() | ||||
| 		ctx.Data["Architectures"] = architectures.Values() | ||||
| 	case packages_model.TypeDebian: | ||||
| 		distributions := make(container.Set[string]) | ||||
| 		components := make(container.Set[string]) | ||||
|  | ||||
| @ -23,7 +23,6 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/golang-jwt/jwt/v4" | ||||
| 	"github.com/minio/sha256-simd" | ||||
| ) | ||||
| 
 | ||||
| // ErrInvalidAlgorithmType represents an invalid algorithm error. | ||||
| @ -82,7 +81,7 @@ type rsaSingingKey struct { | ||||
| } | ||||
| 
 | ||||
| func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) { | ||||
| 	kid, err := createPublicKeyFingerprint(key.Public().(*rsa.PublicKey)) | ||||
| 	kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey)) | ||||
| 	if err != nil { | ||||
| 		return rsaSingingKey{}, err | ||||
| 	} | ||||
| @ -133,7 +132,7 @@ type eddsaSigningKey struct { | ||||
| } | ||||
| 
 | ||||
| func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) { | ||||
| 	kid, err := createPublicKeyFingerprint(key.Public().(ed25519.PublicKey)) | ||||
| 	kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey)) | ||||
| 	if err != nil { | ||||
| 		return eddsaSigningKey{}, err | ||||
| 	} | ||||
| @ -184,7 +183,7 @@ type ecdsaSingingKey struct { | ||||
| } | ||||
| 
 | ||||
| func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) { | ||||
| 	kid, err := createPublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) | ||||
| 	kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) | ||||
| 	if err != nil { | ||||
| 		return ecdsaSingingKey{}, err | ||||
| 	} | ||||
| @ -229,19 +228,6 @@ func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) { | ||||
| 	token.Header["kid"] = key.id | ||||
| } | ||||
| 
 | ||||
| // createPublicKeyFingerprint creates a fingerprint of the given key. | ||||
| // The fingerprint is the sha256 sum of the PKIX structure of the key. | ||||
| func createPublicKeyFingerprint(key interface{}) ([]byte, error) { | ||||
| 	bytes, err := x509.MarshalPKIXPublicKey(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	checksum := sha256.Sum256(bytes) | ||||
| 
 | ||||
| 	return checksum[:], nil | ||||
| } | ||||
| 
 | ||||
| // CreateJWTSigningKey creates a signing key from an algorithm / key pair. | ||||
| func CreateJWTSigningKey(algorithm string, key interface{}) (JWTSigningKey, error) { | ||||
| 	var signingMethod jwt.SigningMethod | ||||
|  | ||||
| @ -15,7 +15,7 @@ import ( | ||||
| type PackageCleanupRuleForm struct { | ||||
| 	ID            int64 | ||||
| 	Enabled       bool | ||||
| 	Type          string `binding:"Required;In(cargo,chef,composer,conan,conda,container,debian,generic,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` | ||||
| 	Type          string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,debian,generic,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` | ||||
| 	KeepCount     int    `binding:"In(0,1,5,10,25,50,100)"` | ||||
| 	KeepPattern   string `binding:"RegexPattern"` | ||||
| 	RemoveDays    int    `binding:"In(0,7,14,30,60,90,180)"` | ||||
|  | ||||
							
								
								
									
										328
									
								
								services/packages/alpine/repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								services/packages/alpine/repository.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,328 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package alpine | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"context" | ||||
| 	"crypto" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/hex" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	packages_model "code.gitea.io/gitea/models/packages" | ||||
| 	alpine_model "code.gitea.io/gitea/models/packages/alpine" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"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" | ||||
| ) | ||||
| 
 | ||||
| const IndexFilename = "APKINDEX.tar.gz" | ||||
| 
 | ||||
| // GetOrCreateRepositoryVersion gets or creates the internal repository package | ||||
| // The Alpine registry needs multiple index files which are stored in this package. | ||||
| func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) { | ||||
| 	return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion) | ||||
| } | ||||
| 
 | ||||
| // GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files | ||||
| func GetOrCreateKeyPair(ownerID int64) (string, string, error) { | ||||
| 	priv, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPrivate) | ||||
| 	if err != nil && !errors.Is(err, util.ErrNotExist) { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 
 | ||||
| 	pub, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPublic) | ||||
| 	if err != nil && !errors.Is(err, util.ErrNotExist) { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 
 | ||||
| 	if priv == "" || pub == "" { | ||||
| 		priv, pub, err = util.GenerateKeyPair(4096) | ||||
| 		if err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 
 | ||||
| 		if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPrivate, priv); err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 
 | ||||
| 		if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPublic, pub); err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return priv, pub, nil | ||||
| } | ||||
| 
 | ||||
| // BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures | ||||
| func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { | ||||
| 	pv, err := GetOrCreateRepositoryVersion(ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// 1. Delete all existing repository files | ||||
| 	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, pf := range pfs { | ||||
| 		if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// 2. (Re)Build repository files for existing packages | ||||
| 	branches, err := alpine_model.GetBranches(ctx, ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, branch := range branches { | ||||
| 		repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		for _, repository := range repositories { | ||||
| 			architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			for _, architecture := range architectures { | ||||
| 				if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil { | ||||
| 					return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // BuildSpecificRepositoryFiles builds index files for the repository | ||||
| func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error { | ||||
| 	pv, err := GetOrCreateRepositoryVersion(ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture) | ||||
| } | ||||
| 
 | ||||
| type packageData struct { | ||||
| 	Package         *packages_model.Package | ||||
| 	Version         *packages_model.PackageVersion | ||||
| 	Blob            *packages_model.PackageBlob | ||||
| 	VersionMetadata *alpine_module.VersionMetadata | ||||
| 	FileMetadata    *alpine_module.FileMetadata | ||||
| } | ||||
| 
 | ||||
| type packageCache = map[*packages_model.PackageFile]*packageData | ||||
| 
 | ||||
| // https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format | ||||
| func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error { | ||||
| 	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ | ||||
| 		OwnerID:     ownerID, | ||||
| 		PackageType: packages_model.TypeAlpine, | ||||
| 		Query:       "%.apk", | ||||
| 		Properties: map[string]string{ | ||||
| 			alpine_module.PropertyBranch:       branch, | ||||
| 			alpine_module.PropertyRepository:   repository, | ||||
| 			alpine_module.PropertyArchitecture: architecture, | ||||
| 		}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Delete the package indices if there are no packages | ||||
| 	if len(pfs) == 0 { | ||||
| 		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture)) | ||||
| 		if err != nil && !errors.Is(err, util.ErrNotExist) { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return packages_model.DeleteFileByID(ctx, pf.ID) | ||||
| 	} | ||||
| 
 | ||||
| 	// Cache data needed for all repository files | ||||
| 	cache := make(packageCache) | ||||
| 	for _, pf := range pfs { | ||||
| 		pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		p, err := packages_model.GetPackageByID(ctx, pv.PackageID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		pd := &packageData{ | ||||
| 			Package: p, | ||||
| 			Version: pv, | ||||
| 			Blob:    pb, | ||||
| 		} | ||||
| 
 | ||||
| 		if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if len(pps) > 0 { | ||||
| 			if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		cache[pf] = pd | ||||
| 	} | ||||
| 
 | ||||
| 	var buf bytes.Buffer | ||||
| 	for _, pf := range pfs { | ||||
| 		pd := cache[pf] | ||||
| 
 | ||||
| 		fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum) | ||||
| 		fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name) | ||||
| 		fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version) | ||||
| 		fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture) | ||||
| 		if pd.VersionMetadata.Description != "" { | ||||
| 			fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description) | ||||
| 		} | ||||
| 		if pd.VersionMetadata.ProjectURL != "" { | ||||
| 			fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL) | ||||
| 		} | ||||
| 		if pd.VersionMetadata.License != "" { | ||||
| 			fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License) | ||||
| 		} | ||||
| 		fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size) | ||||
| 		fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size) | ||||
| 		fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin) | ||||
| 		fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer) | ||||
| 		fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate) | ||||
| 		if pd.FileMetadata.CommitHash != "" { | ||||
| 			fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash) | ||||
| 		} | ||||
| 		if len(pd.FileMetadata.Dependencies) > 0 { | ||||
| 			fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " ")) | ||||
| 		} | ||||
| 		if len(pd.FileMetadata.Provides) > 0 { | ||||
| 			fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " ")) | ||||
| 		} | ||||
| 		fmt.Fprint(&buf, "\n") | ||||
| 	} | ||||
| 
 | ||||
| 	unsignedIndexContent, _ := packages_module.NewHashedBuffer() | ||||
| 	h := sha1.New() | ||||
| 
 | ||||
| 	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	priv, _, err := GetOrCreateKeyPair(ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	privPem, _ := pem.Decode([]byte(priv)) | ||||
| 	if privPem == nil { | ||||
| 		return fmt.Errorf("failed to decode private key pem") | ||||
| 	} | ||||
| 
 | ||||
| 	privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	owner, err := user_model.GetUserByID(ctx, ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	signedIndexContent, _ := packages_module.NewHashedBuffer() | ||||
| 
 | ||||
| 	if err := writeGzipStream( | ||||
| 		signedIndexContent, | ||||
| 		fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)), | ||||
| 		sign, | ||||
| 		false, | ||||
| 	); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = packages_service.AddFileToPackageVersionInternal( | ||||
| 		repoVersion, | ||||
| 		&packages_service.PackageFileCreationInfo{ | ||||
| 			PackageFileInfo: packages_service.PackageFileInfo{ | ||||
| 				Filename:     IndexFilename, | ||||
| 				CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), | ||||
| 			}, | ||||
| 			Creator:           user_model.NewGhostUser(), | ||||
| 			Data:              signedIndexContent, | ||||
| 			IsLead:            false, | ||||
| 			OverwriteExisting: true, | ||||
| 		}, | ||||
| 	) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error { | ||||
| 	zw := gzip.NewWriter(w) | ||||
| 	defer zw.Close() | ||||
| 
 | ||||
| 	tw := tar.NewWriter(zw) | ||||
| 	if addTarEnd { | ||||
| 		defer tw.Close() | ||||
| 	} | ||||
| 	hdr := &tar.Header{ | ||||
| 		Name: filename, | ||||
| 		Mode: 0o600, | ||||
| 		Size: int64(len(content)), | ||||
| 	} | ||||
| 	if err := tw.WriteHeader(hdr); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if _, err := tw.Write(content); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @ -351,6 +351,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p | ||||
| 
 | ||||
| 	var typeSpecificSize int64 | ||||
| 	switch packageType { | ||||
| 	case packages_model.TypeAlpine: | ||||
| 		typeSpecificSize = setting.Packages.LimitSizeAlpine | ||||
| 	case packages_model.TypeCargo: | ||||
| 		typeSpecificSize = setting.Packages.LimitSizeCargo | ||||
| 	case packages_model.TypeChef: | ||||
| @ -486,6 +488,47 @@ func RemovePackageVersion(doer *user_model.User, pv *packages_model.PackageVersi | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // RemovePackageFileAndVersionIfUnreferenced deletes the package file and the version if there are no referenced files afterwards | ||||
| func RemovePackageFileAndVersionIfUnreferenced(doer *user_model.User, pf *packages_model.PackageFile) error { | ||||
| 	var pd *packages_model.PackageDescriptor | ||||
| 
 | ||||
| 	if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { | ||||
| 		if err := DeletePackageFile(ctx, pf); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		has, err := packages_model.HasVersionFileReferences(ctx, pf.VersionID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if !has { | ||||
| 			pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			pd, err = packages_model.GetPackageDescriptor(ctx, pv) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			if err := DeletePackageVersionAndReferences(ctx, pv); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if pd != nil { | ||||
| 		notification.NotifyPackageDelete(db.DefaultContext, doer, pd) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // DeletePackageVersionAndReferences deletes the package version and its properties and files | ||||
| func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error { | ||||
| 	if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil { | ||||
|  | ||||
							
								
								
									
										52
									
								
								templates/package/content/alpine.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								templates/package/content/alpine.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| {{if eq .PackageDescriptor.Package.Type "alpine"}} | ||||
| 	<h4 class="ui top attached header">{{.locale.Tr "packages.installation"}}</h4> | ||||
| 	<div class="ui attached segment"> | ||||
| 		<div class="ui form"> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-code"}} {{.locale.Tr "packages.alpine.registry" | Safe}}</label> | ||||
| 				<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div> | ||||
| 				<p>{{.locale.Tr "packages.alpine.registry.info" | Safe}}</p> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.alpine.registry.key" | Safe}}</label> | ||||
| 				<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.alpine.install"}}</label> | ||||
| 				<div class="markup"> | ||||
| 					<pre class="code-block"><code>sudo apk add {{$.PackageDescriptor.Package.Name}}={{$.PackageDescriptor.Version.Version}}</code></pre> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label>{{.locale.Tr "packages.alpine.documentation" "https://docs.gitea.io/en-us/packages/alpine/" | Safe}}</label> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<h4 class="ui top attached header">{{.locale.Tr "packages.alpine.repository"}}</h4> | ||||
| 	<div class="ui attached segment"> | ||||
| 		<table class="ui single line very basic table"> | ||||
| 			<tbody> | ||||
| 				<tr> | ||||
| 					<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.repository.branches"}}</h5></td> | ||||
| 					<td>{{StringUtils.Join .Branches ", "}}</td> | ||||
| 				</tr> | ||||
| 				<tr> | ||||
| 					<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.repository.repositories"}}</h5></td> | ||||
| 					<td>{{StringUtils.Join .Repositories ", "}}</td> | ||||
| 				</tr> | ||||
| 				<tr> | ||||
| 					<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.repository.architectures"}}</h5></td> | ||||
| 					<td>{{StringUtils.Join .Architectures ", "}}</td> | ||||
| 				</tr> | ||||
| 			</tbody> | ||||
| 		</table> | ||||
| 	</div> | ||||
| 
 | ||||
| 	{{if .PackageDescriptor.Metadata.Description}} | ||||
| 		<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4> | ||||
| 		<div class="ui attached segment"> | ||||
| 			{{.PackageDescriptor.Metadata.Description}} | ||||
| 		</div> | ||||
| 	{{end}} | ||||
| {{end}} | ||||
							
								
								
									
										5
									
								
								templates/package/metadata/alpine.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								templates/package/metadata/alpine.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| {{if eq .PackageDescriptor.Package.Type "alpine"}} | ||||
| 	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{.locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} | ||||
| 	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.details.project_site"}}</a></div>{{end}} | ||||
| 	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} | ||||
| {{end}} | ||||
| @ -19,6 +19,7 @@ | ||||
| 					<div class="ui divider"></div> | ||||
| 				</div> | ||||
| 				<div class="twelve wide column"> | ||||
| 					{{template "package/content/alpine" .}} | ||||
| 					{{template "package/content/cargo" .}} | ||||
| 					{{template "package/content/chef" .}} | ||||
| 					{{template "package/content/composer" .}} | ||||
| @ -48,6 +49,7 @@ | ||||
| 							{{end}} | ||||
| 							<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.locale}}</div> | ||||
| 							<div class="item">{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div> | ||||
| 							{{template "package/metadata/alpine" .}} | ||||
| 							{{template "package/metadata/cargo" .}} | ||||
| 							{{template "package/metadata/chef" .}} | ||||
| 							{{template "package/metadata/composer" .}} | ||||
|  | ||||
							
								
								
									
										1
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @ -2409,6 +2409,7 @@ | ||||
|           }, | ||||
|           { | ||||
|             "enum": [ | ||||
|               "alpine", | ||||
|               "cargo", | ||||
|               "chef", | ||||
|               "composer", | ||||
|  | ||||
							
								
								
									
										229
									
								
								tests/integration/api_packages_alpine_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								tests/integration/api_packages_alpine_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,229 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package integration | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/packages" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	alpine_module "code.gitea.io/gitea/modules/packages/alpine" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestPackageAlpine(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 
 | ||||
| 	packageName := "gitea-test" | ||||
| 	packageVersion := "1.4.1-r3" | ||||
| 
 | ||||
| 	base64AlpinePackageContent := `H4sIAAAAAAACA9ML9nT30wsKdtTLzjNJzjYuckjPLElN1DUzMUxMNTa11CsqTtQrKE1ioAAYAIGZ | ||||
| iQmYBgJ02hDENjQxMTAzMzQ1MTVjMDA0MTQ1ZlAwYKADKC0uSSxSUGAYoWDm4sZZtypv75+q2fVT | ||||
| POD1bKkFB22ms+g1z+H4dk7AhC3HwUSj9EbT0Rk3Dn55dHxy/K7Q+Nl/i+L7Z036ypcRvvpZuMiN | ||||
| s7wbZL/klqRGGshv9Gi0qHTgTZfw3HytnJdx9c3NTRp/PHn+Z50uq2pjkilzjtpfd+uzQMw1M7cY | ||||
| i9RXJasnT2M+vDXCesLK7MilJt8sGplj4xUlLMUun9SzY+phFpxWxRXa06AseV9WvzH3jtGGoL5A | ||||
| vQkea+VKPj5R+Cb461tIk97qpa9nJYsJujTNl2B/J1P52H/D2rPr/j19uU8p7cMSq5tmXk51ReXl | ||||
| F/Yddr9XsMpEwFKlXSPo3QSGwnCOG8y2uadjm6ui998WYXNYubjg78N3a7bnXjhrl5fB8voI++LI | ||||
| 1FP5W44e2xf4Ou2wrtyic1Onz7MzMV5ksuno2V/LVG4eN/15X/n2/2vJ2VV+T68aT327dOrhd6e6 | ||||
| q5Y0V82Y83tdqkFa8TW2BvGCZ0ds/iibHVpzKuPcuSULO63/bNmfrnhjWqXzhMSXTb5Cv4vPaxSL | ||||
| 8LFMdqmxbN7+Y+Yi0ZyZhz4UxexLuHHFd1VFvk+kwvniq3P+f9rh52InWnL8Lpvedcecoh1GFSc5 | ||||
| xZ9VBGex2V269HZfwxSVCvP35wQfi2xKX+lYMXtF48n1R65O2PLWpm69RdESMa79dlrTGazsZacu | ||||
| MbMLeSSScPORZde76/MBV6SFJAAEAAAfiwgAAAAAAAID7VRLaxsxEN6zfoUgZ++OVq+1aUIhUDeY | ||||
| pKa49FhmJdkW3ofRysXpr69220t9SCk0gZJ+IGaY56eBmbxY4/m9Q+vCUOTr1fLu4d2H7O8CEpQQ | ||||
| k0y4lAClypgQoBSTQqoMGBMgMnrOXgCnIWJIVLLXCcaoib5110CSij/V7D9eCZ5p5f9o/5VkF/tf | ||||
| MqUzCi+5/6Hv41Nxv/Nffu4fwRVdus4FjM7S+pFiffKNpTxnkMMsALmin5PnHgMtS8rkgvGFBPpp | ||||
| c0tLKDk5HnYdto5e052PDmfRDXE0fnUh2VgucjYLU5h1g0mm5RhGNymMrtEccOfIKTTJsY/xOCyK | ||||
| YqqT+74gExWbmI2VlJ6LeQUcyPFH2lh/9SBuV/wjfXPohDnw8HZKviGD/zYmCZgrgsHsk36u1Bcl | ||||
| SB/8zne/0jV92/qYbKRF38X0niiemN2QxhvXDWOL+7tNGhGeYt+m22mwaR6pddGZNM8FSeRxj8PY | ||||
| X7PaqdqAVlqWXHKnmQGmK43VlqNlILRilbBSMI2jV5Vbu5XGSVsDyGc7yd8B/gK2qgAIAAAfiwgA | ||||
| AAAAAAID7dNNSgMxGAbg7MSCOxcu5wJOv0x+OlkU7K5QoYXqVsxMMihlKMwP1Fu48QQewCN4DfEQ | ||||
| egUz4sYuFKEtFN9n870hWSSQN+7P7GrsrfNV3Y9dW5Z3bNMo0FJ+zmB9EhcJ41KS1lxJpRnxbsWi | ||||
| FduBtm5sFa7C/ifOo7y5Lf2QeiHar6jTaDSbnF5Mp+fzOL/x+aJuy3g+HvGhs8JY4b3yOpMZOZEo | ||||
| lRW+MEoTTw3ZwqU0INNjsAe2VPk/9b/L3/s/kIKzqOtk+IbJGTtmr+bx7WoxOUoun98frk/un14O | ||||
| Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA` | ||||
| 	content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	branches := []string{"v3.16", "v3.17"} | ||||
| 	repositories := []string{"main", "testing"} | ||||
| 
 | ||||
| 	rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name) | ||||
| 
 | ||||
| 	t.Run("RepositoryKey", func(t *testing.T) { | ||||
| 		defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		req := NewRequest(t, "GET", rootURL+"/key") | ||||
| 		resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		assert.Equal(t, "application/x-pem-file", resp.Header().Get("Content-Type")) | ||||
| 		assert.Contains(t, resp.Body.String(), "-----BEGIN PUBLIC KEY-----") | ||||
| 	}) | ||||
| 
 | ||||
| 	for _, branch := range branches { | ||||
| 		for _, repository := range repositories { | ||||
| 			t.Run(fmt.Sprintf("[Branch:%s,Repository:%s]", branch, repository), func(t *testing.T) { | ||||
| 				t.Run("Upload", func(t *testing.T) { | ||||
| 					defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 					uploadURL := fmt.Sprintf("%s/%s/%s", rootURL, branch, repository) | ||||
| 
 | ||||
| 					req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) | ||||
| 					MakeRequest(t, req, http.StatusUnauthorized) | ||||
| 
 | ||||
| 					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) | ||||
| 					AddBasicAuthHeader(req, user.Name) | ||||
| 					MakeRequest(t, req, http.StatusBadRequest) | ||||
| 
 | ||||
| 					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) | ||||
| 					AddBasicAuthHeader(req, user.Name) | ||||
| 					MakeRequest(t, req, http.StatusCreated) | ||||
| 
 | ||||
| 					pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeAlpine) | ||||
| 					assert.NoError(t, err) | ||||
| 					assert.Len(t, pvs, 1) | ||||
| 
 | ||||
| 					pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) | ||||
| 					assert.NoError(t, err) | ||||
| 					assert.Nil(t, pd.SemVer) | ||||
| 					assert.IsType(t, &alpine_module.VersionMetadata{}, pd.Metadata) | ||||
| 					assert.Equal(t, packageName, pd.Package.Name) | ||||
| 					assert.Equal(t, packageVersion, pd.Version.Version) | ||||
| 
 | ||||
| 					pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) | ||||
| 					assert.NoError(t, err) | ||||
| 					assert.NotEmpty(t, pfs) | ||||
| 					assert.Condition(t, func() bool { | ||||
| 						seen := false | ||||
| 						expectedFilename := fmt.Sprintf("%s-%s.apk", packageName, packageVersion) | ||||
| 						expectedCompositeKey := fmt.Sprintf("%s|%s|x86_64", branch, repository) | ||||
| 						for _, pf := range pfs { | ||||
| 							if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { | ||||
| 								if seen { | ||||
| 									return false | ||||
| 								} | ||||
| 								seen = true | ||||
| 
 | ||||
| 								assert.True(t, pf.IsLead) | ||||
| 
 | ||||
| 								pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) | ||||
| 								assert.NoError(t, err) | ||||
| 
 | ||||
| 								for _, pfp := range pfps { | ||||
| 									switch pfp.Name { | ||||
| 									case alpine_module.PropertyBranch: | ||||
| 										assert.Equal(t, branch, pfp.Value) | ||||
| 									case alpine_module.PropertyRepository: | ||||
| 										assert.Equal(t, repository, pfp.Value) | ||||
| 									case alpine_module.PropertyArchitecture: | ||||
| 										assert.Equal(t, "x86_64", pfp.Value) | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						return seen | ||||
| 					}) | ||||
| 				}) | ||||
| 
 | ||||
| 				t.Run("Index", func(t *testing.T) { | ||||
| 					defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 					url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository) | ||||
| 
 | ||||
| 					req := NewRequest(t, "GET", url) | ||||
| 					resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 					assert.Condition(t, func() bool { | ||||
| 						br := bufio.NewReader(resp.Body) | ||||
| 
 | ||||
| 						gzr, err := gzip.NewReader(br) | ||||
| 						assert.NoError(t, err) | ||||
| 
 | ||||
| 						for { | ||||
| 							gzr.Multistream(false) | ||||
| 
 | ||||
| 							tr := tar.NewReader(gzr) | ||||
| 							for { | ||||
| 								hd, err := tr.Next() | ||||
| 								if err == io.EOF { | ||||
| 									break | ||||
| 								} | ||||
| 								assert.NoError(t, err) | ||||
| 
 | ||||
| 								if hd.Name == "APKINDEX" { | ||||
| 									buf, err := io.ReadAll(tr) | ||||
| 									assert.NoError(t, err) | ||||
| 
 | ||||
| 									s := string(buf) | ||||
| 
 | ||||
| 									assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n") | ||||
| 									assert.Contains(t, s, "P:"+packageName+"\n") | ||||
| 									assert.Contains(t, s, "V:"+packageVersion+"\n") | ||||
| 									assert.Contains(t, s, "A:x86_64\n") | ||||
| 									assert.Contains(t, s, "T:Gitea Test Package\n") | ||||
| 									assert.Contains(t, s, "U:https://gitea.io/\n") | ||||
| 									assert.Contains(t, s, "L:MIT\n") | ||||
| 									assert.Contains(t, s, "S:1353\n") | ||||
| 									assert.Contains(t, s, "I:4096\n") | ||||
| 									assert.Contains(t, s, "o:gitea-test\n") | ||||
| 									assert.Contains(t, s, "m:KN4CK3R <kn4ck3r@gitea.io>\n") | ||||
| 									assert.Contains(t, s, "t:1679498030\n") | ||||
| 
 | ||||
| 									return true | ||||
| 								} | ||||
| 							} | ||||
| 
 | ||||
| 							err = gzr.Reset(br) | ||||
| 							if err == io.EOF { | ||||
| 								break | ||||
| 							} | ||||
| 							assert.NoError(t, err) | ||||
| 						} | ||||
| 
 | ||||
| 						return false | ||||
| 					}) | ||||
| 				}) | ||||
| 
 | ||||
| 				t.Run("Download", func(t *testing.T) { | ||||
| 					defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) | ||||
| 					MakeRequest(t, req, http.StatusOK) | ||||
| 				}) | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	t.Run("Delete", func(t *testing.T) { | ||||
| 		defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		for _, branch := range branches { | ||||
| 			for _, repository := range repositories { | ||||
| 				req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) | ||||
| 				MakeRequest(t, req, http.StatusUnauthorized) | ||||
| 
 | ||||
| 				req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) | ||||
| 				AddBasicAuthHeader(req, user.Name) | ||||
| 				MakeRequest(t, req, http.StatusNoContent) | ||||
| 
 | ||||
| 				// Deleting the last file of an architecture should remove that index | ||||
| 				req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)) | ||||
| 				MakeRequest(t, req, http.StatusNotFound) | ||||
| 			} | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										2
									
								
								web_src/svg/gitea-alpine.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web_src/svg/gitea-alpine.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg version="1.1" viewBox="0 0 186 162" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.25 0 0 -1.25 -268 592)" fill="#0d597f"><g transform="translate(268 393)"><path d="m0 0v15.7l-11.3-11.3c1.22-0.847 2.36-1.54 3.44-2.11 1.08-0.567 2.09-1 3.03-1.34 0.941-0.334 1.81-0.562 2.62-0.71 0.804-0.147 1.54-0.213 2.21-0.222m57.8 1.3c0.02-0.017 0.13-0.11 0.333-0.239 0.204-0.13 0.502-0.297 0.898-0.462 0.395-0.164 0.889-0.327 1.48-0.448 0.596-0.122 1.29-0.202 2.1-0.202 0.671 0 1.41 0.059 2.22 0.2 0.812 0.142 1.69 0.367 2.64 0.699 0.953 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-8.57 8.4-28.4 28.5-12.5-12.5-16.8 17.3-42.2-41.7c1.24-0.86 2.4-1.56 3.49-2.14 1.09-0.571 2.12-1.01 3.07-1.34 0.951-0.332 1.83-0.557 2.64-0.698 0.81-0.142 1.55-0.201 2.22-0.201 0.804 0 1.5 0.08 2.1 0.202 0.596 0.121 1.09 0.284 1.48 0.449 0.396 0.164 0.693 0.331 0.897 0.461s0.314 0.223 0.334 0.24l19.1 19.1 6.81 6.53 18.9-18.9 6.59-6.78c0.02-0.017 0.13-0.11 0.334-0.239 0.204-0.13 0.501-0.297 0.897-0.462 0.396-0.164 0.89-0.327 1.48-0.448 0.596-0.122 1.3-0.202 2.1-0.202 0.67 0 1.41 0.059 2.22 0.2 0.811 0.142 1.69 0.367 2.64 0.699 0.952 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-15.1 14.8 2.82 2.82 13.1-13.1 7.64-7.57m-0.163 79 37.2-64.5-37.2-64.5h-74.5l-37.2 64.5 37.2 64.5z" fill="#0d597f"/></g><g transform="translate(303 412)"><path d="m0 0-9.91 9.9 0.705 0.709 9.98-9.87z" fill="#0d597f"/></g></g></svg> | ||||
| After Width: | Height: | Size: 1.4 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 KN4CK3R
						KN4CK3R