mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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:
		@@ -2442,6 +2442,8 @@ ROUTER = console
 | 
				
			|||||||
;LIMIT_TOTAL_OWNER_COUNT = -1
 | 
					;LIMIT_TOTAL_OWNER_COUNT = -1
 | 
				
			||||||
;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 | 
					;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 | 
				
			||||||
;LIMIT_TOTAL_OWNER_SIZE = -1
 | 
					;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`)
 | 
					;; Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 | 
				
			||||||
;LIMIT_SIZE_CARGO = -1
 | 
					;LIMIT_SIZE_CARGO = -1
 | 
				
			||||||
;; Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 | 
					;; 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`
 | 
					- `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_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_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_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_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`)
 | 
					- `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 |
 | 
					| 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` |
 | 
					| [Cargo]({{< relref "doc/usage/packages/cargo.en-us.md" >}}) | Rust | `cargo` |
 | 
				
			||||||
| [Chef]({{< relref "doc/usage/packages/chef.en-us.md" >}}) | - | `knife` |
 | 
					| [Chef]({{< relref "doc/usage/packages/chef.en-us.md" >}}) | - | `knife` |
 | 
				
			||||||
| [Composer]({{< relref "doc/usage/packages/composer.en-us.md" >}}) | PHP | `composer` |
 | 
					| [Composer]({{< relref "doc/usage/packages/composer.en-us.md" >}}) | PHP | `composer` |
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ menu:
 | 
				
			|||||||
  sidebar:
 | 
					  sidebar:
 | 
				
			||||||
    parent: "packages"
 | 
					    parent: "packages"
 | 
				
			||||||
    name: "Storage"
 | 
					    name: "Storage"
 | 
				
			||||||
    weight: 5
 | 
					    weight: 2
 | 
				
			||||||
    identifier: "storage"
 | 
					    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
 | 
					// GetDistributions gets all available distributions
 | 
				
			||||||
func GetDistributions(ctx context.Context, ownerID int64) ([]string, error) {
 | 
					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
 | 
					// GetComponents gets all available components for the given distribution
 | 
				
			||||||
func GetComponents(ctx context.Context, ownerID int64, distribution string) ([]string, error) {
 | 
					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
 | 
					// GetArchitectures gets all available architectures for the given distribution
 | 
				
			||||||
func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) {
 | 
					func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) {
 | 
				
			||||||
	return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyArchitecture)
 | 
						return packages.GetDistinctPropertyValues(
 | 
				
			||||||
}
 | 
							ctx,
 | 
				
			||||||
 | 
							packages.TypeDebian,
 | 
				
			||||||
func getDistinctPropertyValues(ctx context.Context, ownerID int64, distribution, propName string) ([]string, error) {
 | 
							ownerID,
 | 
				
			||||||
	var cond builder.Cond = builder.Eq{
 | 
							packages.PropertyTypeFile,
 | 
				
			||||||
		"package_property.ref_type": packages.PropertyTypeFile,
 | 
							debian_module.PropertyArchitecture,
 | 
				
			||||||
		"package_property.name":     propName,
 | 
							&packages.DistinctPropertyDependency{
 | 
				
			||||||
		"package.type":              packages.TypeDebian,
 | 
								Name:  debian_module.PropertyDistribution,
 | 
				
			||||||
		"package.owner_id":          ownerID,
 | 
								Value: distribution,
 | 
				
			||||||
	}
 | 
							},
 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/json"
 | 
						"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/cargo"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/packages/chef"
 | 
						"code.gitea.io/gitea/modules/packages/chef"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/packages/composer"
 | 
						"code.gitea.io/gitea/modules/packages/composer"
 | 
				
			||||||
@@ -136,6 +137,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var metadata interface{}
 | 
						var metadata interface{}
 | 
				
			||||||
	switch p.Type {
 | 
						switch p.Type {
 | 
				
			||||||
 | 
						case TypeAlpine:
 | 
				
			||||||
 | 
							metadata = &alpine.VersionMetadata{}
 | 
				
			||||||
	case TypeCargo:
 | 
						case TypeCargo:
 | 
				
			||||||
		metadata = &cargo.Metadata{}
 | 
							metadata = &cargo.Metadata{}
 | 
				
			||||||
	case TypeChef:
 | 
						case TypeChef:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ type Type string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// List of supported packages
 | 
					// List of supported packages
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
 | 
						TypeAlpine    Type = "alpine"
 | 
				
			||||||
	TypeCargo     Type = "cargo"
 | 
						TypeCargo     Type = "cargo"
 | 
				
			||||||
	TypeChef      Type = "chef"
 | 
						TypeChef      Type = "chef"
 | 
				
			||||||
	TypeComposer  Type = "composer"
 | 
						TypeComposer  Type = "composer"
 | 
				
			||||||
@@ -51,6 +52,7 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var TypeList = []Type{
 | 
					var TypeList = []Type{
 | 
				
			||||||
 | 
						TypeAlpine,
 | 
				
			||||||
	TypeCargo,
 | 
						TypeCargo,
 | 
				
			||||||
	TypeChef,
 | 
						TypeChef,
 | 
				
			||||||
	TypeComposer,
 | 
						TypeComposer,
 | 
				
			||||||
@@ -74,6 +76,8 @@ var TypeList = []Type{
 | 
				
			|||||||
// Name gets the name of the package type
 | 
					// Name gets the name of the package type
 | 
				
			||||||
func (pt Type) Name() string {
 | 
					func (pt Type) Name() string {
 | 
				
			||||||
	switch pt {
 | 
						switch pt {
 | 
				
			||||||
 | 
						case TypeAlpine:
 | 
				
			||||||
 | 
							return "Alpine"
 | 
				
			||||||
	case TypeCargo:
 | 
						case TypeCargo:
 | 
				
			||||||
		return "Cargo"
 | 
							return "Cargo"
 | 
				
			||||||
	case TypeChef:
 | 
						case TypeChef:
 | 
				
			||||||
@@ -117,6 +121,8 @@ func (pt Type) Name() string {
 | 
				
			|||||||
// SVGName gets the name of the package type svg image
 | 
					// SVGName gets the name of the package type svg image
 | 
				
			||||||
func (pt Type) SVGName() string {
 | 
					func (pt Type) SVGName() string {
 | 
				
			||||||
	switch pt {
 | 
						switch pt {
 | 
				
			||||||
 | 
						case TypeAlpine:
 | 
				
			||||||
 | 
							return "gitea-alpine"
 | 
				
			||||||
	case TypeCargo:
 | 
						case TypeCargo:
 | 
				
			||||||
		return "gitea-cargo"
 | 
							return "gitea-cargo"
 | 
				
			||||||
	case TypeChef:
 | 
						case TypeChef:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					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{})
 | 
						_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{})
 | 
				
			||||||
	return err
 | 
						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
 | 
							LimitTotalOwnerCount int64
 | 
				
			||||||
		LimitTotalOwnerSize  int64
 | 
							LimitTotalOwnerSize  int64
 | 
				
			||||||
 | 
							LimitSizeAlpine      int64
 | 
				
			||||||
		LimitSizeCargo       int64
 | 
							LimitSizeCargo       int64
 | 
				
			||||||
		LimitSizeChef        int64
 | 
							LimitSizeChef        int64
 | 
				
			||||||
		LimitSizeComposer    int64
 | 
							LimitSizeComposer    int64
 | 
				
			||||||
@@ -69,6 +70,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
 | 
						Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
 | 
				
			||||||
 | 
						Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
 | 
				
			||||||
	Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
 | 
						Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
 | 
				
			||||||
	Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
 | 
						Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
 | 
				
			||||||
	Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
 | 
						Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,13 @@
 | 
				
			|||||||
package util
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"crypto"
 | 
				
			||||||
	"crypto/rand"
 | 
						"crypto/rand"
 | 
				
			||||||
	"crypto/rsa"
 | 
						"crypto/rsa"
 | 
				
			||||||
	"crypto/x509"
 | 
						"crypto/x509"
 | 
				
			||||||
	"encoding/pem"
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/minio/sha256-simd"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GenerateKeyPair generates a public and private keypair
 | 
					// GenerateKeyPair generates a public and private keypair
 | 
				
			||||||
@@ -43,3 +46,16 @@ func pemBlockForPub(pub *rsa.PublicKey) (string, error) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
	return string(pubBytes), nil
 | 
						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
 | 
					versions.view_all = View all
 | 
				
			||||||
dependency.id = ID
 | 
					dependency.id = ID
 | 
				
			||||||
dependency.version = Version
 | 
					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.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.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>.
 | 
					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/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"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/cargo"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/packages/chef"
 | 
						"code.gitea.io/gitea/routers/api/packages/chef"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/packages/composer"
 | 
						"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("/{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("/cargo", func() {
 | 
				
			||||||
			r.Group("/api/v1/crates", func() {
 | 
								r.Group("/api/v1/crates", func() {
 | 
				
			||||||
				r.Get("", cargo.SearchPackages)
 | 
									r.Get("", cargo.SearchPackages)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
 | 
				
			|||||||
	//   in: query
 | 
						//   in: query
 | 
				
			||||||
	//   description: package type filter
 | 
						//   description: package type filter
 | 
				
			||||||
	//   type: string
 | 
						//   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
 | 
						// - name: q
 | 
				
			||||||
	//   in: query
 | 
						//   in: query
 | 
				
			||||||
	//   description: name filter
 | 
						//   description: name filter
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/container"
 | 
						"code.gitea.io/gitea/modules/container"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 | 
				
			||||||
	debian_module "code.gitea.io/gitea/modules/packages/debian"
 | 
						debian_module "code.gitea.io/gitea/modules/packages/debian"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
@@ -168,6 +169,27 @@ func ViewPackageVersion(ctx *context.Context) {
 | 
				
			|||||||
	switch pd.Package.Type {
 | 
						switch pd.Package.Type {
 | 
				
			||||||
	case packages_model.TypeContainer:
 | 
						case packages_model.TypeContainer:
 | 
				
			||||||
		ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
 | 
							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:
 | 
						case packages_model.TypeDebian:
 | 
				
			||||||
		distributions := make(container.Set[string])
 | 
							distributions := make(container.Set[string])
 | 
				
			||||||
		components := make(container.Set[string])
 | 
							components := make(container.Set[string])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/golang-jwt/jwt/v4"
 | 
						"github.com/golang-jwt/jwt/v4"
 | 
				
			||||||
	"github.com/minio/sha256-simd"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ErrInvalidAlgorithmType represents an invalid algorithm error.
 | 
					// ErrInvalidAlgorithmType represents an invalid algorithm error.
 | 
				
			||||||
@@ -82,7 +81,7 @@ type rsaSingingKey struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
 | 
					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 {
 | 
						if err != nil {
 | 
				
			||||||
		return rsaSingingKey{}, err
 | 
							return rsaSingingKey{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -133,7 +132,7 @@ type eddsaSigningKey struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) {
 | 
					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 {
 | 
						if err != nil {
 | 
				
			||||||
		return eddsaSigningKey{}, err
 | 
							return eddsaSigningKey{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -184,7 +183,7 @@ type ecdsaSingingKey struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
 | 
					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 {
 | 
						if err != nil {
 | 
				
			||||||
		return ecdsaSingingKey{}, err
 | 
							return ecdsaSingingKey{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -229,19 +228,6 @@ func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
 | 
				
			|||||||
	token.Header["kid"] = key.id
 | 
						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.
 | 
					// CreateJWTSigningKey creates a signing key from an algorithm / key pair.
 | 
				
			||||||
func CreateJWTSigningKey(algorithm string, key interface{}) (JWTSigningKey, error) {
 | 
					func CreateJWTSigningKey(algorithm string, key interface{}) (JWTSigningKey, error) {
 | 
				
			||||||
	var signingMethod jwt.SigningMethod
 | 
						var signingMethod jwt.SigningMethod
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ import (
 | 
				
			|||||||
type PackageCleanupRuleForm struct {
 | 
					type PackageCleanupRuleForm struct {
 | 
				
			||||||
	ID            int64
 | 
						ID            int64
 | 
				
			||||||
	Enabled       bool
 | 
						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)"`
 | 
						KeepCount     int    `binding:"In(0,1,5,10,25,50,100)"`
 | 
				
			||||||
	KeepPattern   string `binding:"RegexPattern"`
 | 
						KeepPattern   string `binding:"RegexPattern"`
 | 
				
			||||||
	RemoveDays    int    `binding:"In(0,7,14,30,60,90,180)"`
 | 
						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
 | 
						var typeSpecificSize int64
 | 
				
			||||||
	switch packageType {
 | 
						switch packageType {
 | 
				
			||||||
 | 
						case packages_model.TypeAlpine:
 | 
				
			||||||
 | 
							typeSpecificSize = setting.Packages.LimitSizeAlpine
 | 
				
			||||||
	case packages_model.TypeCargo:
 | 
						case packages_model.TypeCargo:
 | 
				
			||||||
		typeSpecificSize = setting.Packages.LimitSizeCargo
 | 
							typeSpecificSize = setting.Packages.LimitSizeCargo
 | 
				
			||||||
	case packages_model.TypeChef:
 | 
						case packages_model.TypeChef:
 | 
				
			||||||
@@ -486,6 +488,47 @@ func RemovePackageVersion(doer *user_model.User, pv *packages_model.PackageVersi
 | 
				
			|||||||
	return nil
 | 
						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
 | 
					// DeletePackageVersionAndReferences deletes the package version and its properties and files
 | 
				
			||||||
func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error {
 | 
					func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error {
 | 
				
			||||||
	if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
 | 
						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 class="ui divider"></div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="twelve wide column">
 | 
									<div class="twelve wide column">
 | 
				
			||||||
 | 
										{{template "package/content/alpine" .}}
 | 
				
			||||||
					{{template "package/content/cargo" .}}
 | 
										{{template "package/content/cargo" .}}
 | 
				
			||||||
					{{template "package/content/chef" .}}
 | 
										{{template "package/content/chef" .}}
 | 
				
			||||||
					{{template "package/content/composer" .}}
 | 
										{{template "package/content/composer" .}}
 | 
				
			||||||
@@ -48,6 +49,7 @@
 | 
				
			|||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
							<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.locale}}</div>
 | 
												<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>
 | 
												<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/cargo" .}}
 | 
				
			||||||
							{{template "package/metadata/chef" .}}
 | 
												{{template "package/metadata/chef" .}}
 | 
				
			||||||
							{{template "package/metadata/composer" .}}
 | 
												{{template "package/metadata/composer" .}}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -2409,6 +2409,7 @@
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "enum": [
 | 
					            "enum": [
 | 
				
			||||||
 | 
					              "alpine",
 | 
				
			||||||
              "cargo",
 | 
					              "cargo",
 | 
				
			||||||
              "chef",
 | 
					              "chef",
 | 
				
			||||||
              "composer",
 | 
					              "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  | 
		Reference in New Issue
	
	Block a user