Add support for Pub packages (#20560)
* Added support for Pub packages. * Update docs/content/doc/packages/overview.en-us.md Co-authored-by: Gergely Nagy <algernon@users.noreply.github.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Gergely Nagy <algernon@users.noreply.github.com> Co-authored-by: Lauris BH <lauris@nix.lv>tokarchuk/v1.18
parent
d4326afb25
commit
f55af4675c
@ -0,0 +1,83 @@ |
|||||||
|
--- |
||||||
|
date: "2022-07-31T00:00:00+00:00" |
||||||
|
title: "Pub Packages Repository" |
||||||
|
slug: "packages/pub" |
||||||
|
draft: false |
||||||
|
toc: false |
||||||
|
menu: |
||||||
|
sidebar: |
||||||
|
parent: "packages" |
||||||
|
name: "Pub" |
||||||
|
weight: 90 |
||||||
|
identifier: "pub" |
||||||
|
--- |
||||||
|
|
||||||
|
# Pub Packages Repository |
||||||
|
|
||||||
|
Publish [Pub](https://dart.dev/guides/packages) packages for your user or organization. |
||||||
|
|
||||||
|
**Table of Contents** |
||||||
|
|
||||||
|
{{< toc >}} |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
To work with the Pub package registry, you need to use the tools [dart](https://dart.dev/tools/dart-tool) and/or [flutter](https://docs.flutter.dev/reference/flutter-cli). |
||||||
|
|
||||||
|
The following examples use dart. |
||||||
|
|
||||||
|
## Configuring the package registry |
||||||
|
|
||||||
|
To register the package registry and provide credentials, execute: |
||||||
|
|
||||||
|
```shell |
||||||
|
dart pub token add https://gitea.example.com/api/packages/{owner}/pub |
||||||
|
``` |
||||||
|
|
||||||
|
| Placeholder | Description | |
||||||
|
| ------------ | ----------- | |
||||||
|
| `owner` | The owner of the package. | |
||||||
|
|
||||||
|
You need to provide your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). |
||||||
|
|
||||||
|
## Publish a package |
||||||
|
|
||||||
|
To publish a package, edit the `pubspec.yaml` and add the following line: |
||||||
|
|
||||||
|
```yaml |
||||||
|
publish_to: https://gitea.example.com/api/packages/{owner}/pub |
||||||
|
``` |
||||||
|
|
||||||
|
| Placeholder | Description | |
||||||
|
| ------------ | ----------- | |
||||||
|
| `owner` | The owner of the package. | |
||||||
|
|
||||||
|
Now you can publish the package by running the following command: |
||||||
|
|
||||||
|
```shell |
||||||
|
dart pub publish |
||||||
|
``` |
||||||
|
|
||||||
|
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. |
||||||
|
|
||||||
|
## Install a package |
||||||
|
|
||||||
|
To install a Pub package from the package registry, execute the following command: |
||||||
|
|
||||||
|
```shell |
||||||
|
dart pub add {package_name} --hosted-url=https://gitea.example.com/api/packages/{owner}/pub/ |
||||||
|
``` |
||||||
|
|
||||||
|
| Parameter | Description | |
||||||
|
| ----------------- | ----------- | |
||||||
|
| `owner` | The owner of the package. | |
||||||
|
| `package_name` | The package name. | |
||||||
|
|
||||||
|
For example: |
||||||
|
|
||||||
|
```shell |
||||||
|
# use latest version |
||||||
|
dart pub add mypackage --hosted-url=https://gitea.example.com/api/packages/testuser/pub/ |
||||||
|
# specify version |
||||||
|
dart pub add mypackage:1.0.8 --hosted-url=https://gitea.example.com/api/packages/testuser/pub/ |
||||||
|
``` |
@ -0,0 +1,179 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/tar" |
||||||
|
"bytes" |
||||||
|
"compress/gzip" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"mime/multipart" |
||||||
|
"net/http" |
||||||
|
"net/http/httptest" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"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" |
||||||
|
pub_module "code.gitea.io/gitea/modules/packages/pub" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestPackagePub(t *testing.T) { |
||||||
|
defer prepareTestEnv(t)() |
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) |
||||||
|
|
||||||
|
token := "Bearer " + getUserToken(t, user.Name) |
||||||
|
|
||||||
|
packageName := "test_package" |
||||||
|
packageVersion := "1.0.1" |
||||||
|
packageDescription := "Test Description" |
||||||
|
|
||||||
|
filename := fmt.Sprintf("%s.tar.gz", packageVersion) |
||||||
|
|
||||||
|
pubspecContent := `name: ` + packageName + ` |
||||||
|
version: ` + packageVersion + ` |
||||||
|
description: ` + packageDescription |
||||||
|
|
||||||
|
var buf bytes.Buffer |
||||||
|
zw := gzip.NewWriter(&buf) |
||||||
|
archive := tar.NewWriter(zw) |
||||||
|
archive.WriteHeader(&tar.Header{ |
||||||
|
Name: "pubspec.yaml", |
||||||
|
Mode: 0o600, |
||||||
|
Size: int64(len(pubspecContent)), |
||||||
|
}) |
||||||
|
archive.Write([]byte(pubspecContent)) |
||||||
|
archive.Close() |
||||||
|
zw.Close() |
||||||
|
content := buf.Bytes() |
||||||
|
|
||||||
|
root := fmt.Sprintf("/api/packages/%s/pub", user.Name) |
||||||
|
|
||||||
|
t.Run("Upload", func(t *testing.T) { |
||||||
|
defer PrintCurrentTest(t)() |
||||||
|
|
||||||
|
uploadURL := root + "/api/packages/versions/new" |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", uploadURL) |
||||||
|
MakeRequest(t, req, http.StatusUnauthorized) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", uploadURL) |
||||||
|
addTokenAuthHeader(req, token) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
type UploadRequest struct { |
||||||
|
URL string `json:"url"` |
||||||
|
Fields map[string]string `json:"fields"` |
||||||
|
} |
||||||
|
|
||||||
|
var result UploadRequest |
||||||
|
DecodeJSON(t, resp, &result) |
||||||
|
|
||||||
|
assert.Empty(t, result.Fields) |
||||||
|
|
||||||
|
uploadFile := func(t *testing.T, url string, content []byte, expectedStatus int) *httptest.ResponseRecorder { |
||||||
|
body := &bytes.Buffer{} |
||||||
|
writer := multipart.NewWriter(body) |
||||||
|
part, _ := writer.CreateFormFile("file", "dummy.tar.gz") |
||||||
|
_, _ = io.Copy(part, bytes.NewReader(content)) |
||||||
|
|
||||||
|
_ = writer.Close() |
||||||
|
|
||||||
|
req := NewRequestWithBody(t, "POST", url, body) |
||||||
|
req.Header.Add("Content-Type", writer.FormDataContentType()) |
||||||
|
addTokenAuthHeader(req, token) |
||||||
|
return MakeRequest(t, req, expectedStatus) |
||||||
|
} |
||||||
|
|
||||||
|
resp = uploadFile(t, result.URL, content, http.StatusNoContent) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", resp.Header().Get("Location")) |
||||||
|
addTokenAuthHeader(req, token) |
||||||
|
MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePub) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Len(t, pvs, 1) |
||||||
|
|
||||||
|
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.NotNil(t, pd.SemVer) |
||||||
|
assert.IsType(t, &pub_module.Metadata{}, 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.Len(t, pfs, 1) |
||||||
|
assert.Equal(t, filename, pfs[0].Name) |
||||||
|
assert.True(t, pfs[0].IsLead) |
||||||
|
|
||||||
|
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, int64(len(content)), pb.Size) |
||||||
|
|
||||||
|
resp = uploadFile(t, result.URL, content, http.StatusBadRequest) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Download", func(t *testing.T) { |
||||||
|
defer PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/api/packages/%s/%s", root, packageName, packageVersion)) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
type VersionMetadata struct { |
||||||
|
Version string `json:"version"` |
||||||
|
ArchiveURL string `json:"archive_url"` |
||||||
|
Published time.Time `json:"published"` |
||||||
|
Pubspec interface{} `json:"pubspec,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
var result VersionMetadata |
||||||
|
DecodeJSON(t, resp, &result) |
||||||
|
|
||||||
|
assert.Equal(t, packageVersion, result.Version) |
||||||
|
assert.NotNil(t, result.Pubspec) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", result.ArchiveURL) |
||||||
|
resp = MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
assert.Equal(t, content, resp.Body.Bytes()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("EnumeratePackageVersions", func(t *testing.T) { |
||||||
|
defer PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/api/packages/%s", root, packageName)) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
type VersionMetadata struct { |
||||||
|
Version string `json:"version"` |
||||||
|
ArchiveURL string `json:"archive_url"` |
||||||
|
Published time.Time `json:"published"` |
||||||
|
Pubspec interface{} `json:"pubspec,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type PackageVersions struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Latest *VersionMetadata `json:"latest"` |
||||||
|
Versions []*VersionMetadata `json:"versions"` |
||||||
|
} |
||||||
|
|
||||||
|
var result PackageVersions |
||||||
|
DecodeJSON(t, resp, &result) |
||||||
|
|
||||||
|
assert.Equal(t, packageName, result.Name) |
||||||
|
assert.NotNil(t, result.Latest) |
||||||
|
assert.Len(t, result.Versions, 1) |
||||||
|
assert.Equal(t, result.Latest.Version, result.Versions[0].Version) |
||||||
|
assert.Equal(t, packageVersion, result.Latest.Version) |
||||||
|
assert.NotNil(t, result.Latest.Pubspec) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package pub |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/tar" |
||||||
|
"compress/gzip" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/validation" |
||||||
|
|
||||||
|
"github.com/hashicorp/go-version" |
||||||
|
"gopkg.in/yaml.v2" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrMissingPubspecFile = errors.New("Pubspec file is missing") |
||||||
|
ErrPubspecFileTooLarge = errors.New("Pubspec file is too large") |
||||||
|
ErrInvalidName = errors.New("Package name is invalid") |
||||||
|
ErrInvalidVersion = errors.New("Package version is invalid") |
||||||
|
) |
||||||
|
|
||||||
|
var namePattern = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`) |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub-dev/blob/4d582302a8d10152a5cd6129f65bf4f4dbca239d/pkg/pub_package_reader/lib/pub_package_reader.dart#L143
|
||||||
|
const maxPubspecFileSize = 128 * 1024 |
||||||
|
|
||||||
|
// Package represents a Pub package
|
||||||
|
type Package struct { |
||||||
|
Name string |
||||||
|
Version string |
||||||
|
Metadata *Metadata |
||||||
|
} |
||||||
|
|
||||||
|
// Metadata represents the metadata of a Pub package
|
||||||
|
type Metadata struct { |
||||||
|
Description string `json:"description,omitempty"` |
||||||
|
ProjectURL string `json:"project_url,omitempty"` |
||||||
|
RepositoryURL string `json:"repository_url,omitempty"` |
||||||
|
DocumentationURL string `json:"documentation_url,omitempty"` |
||||||
|
Readme string `json:"readme,omitempty"` |
||||||
|
Pubspec interface{} `json:"pubspec"` |
||||||
|
} |
||||||
|
|
||||||
|
type pubspecPackage struct { |
||||||
|
Name string `yaml:"name"` |
||||||
|
Version string `yaml:"version"` |
||||||
|
Description string `yaml:"description"` |
||||||
|
Homepage string `yaml:"homepage"` |
||||||
|
Repository string `yaml:"repository"` |
||||||
|
Documentation string `yaml:"documentation"` |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePackage parses the Pub package file
|
||||||
|
func ParsePackage(r io.Reader) (*Package, error) { |
||||||
|
gzr, err := gzip.NewReader(r) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer gzr.Close() |
||||||
|
|
||||||
|
var p *Package |
||||||
|
var readme string |
||||||
|
|
||||||
|
tr := tar.NewReader(gzr) |
||||||
|
for { |
||||||
|
hd, err := tr.Next() |
||||||
|
if err == io.EOF { |
||||||
|
break |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if hd.Typeflag != tar.TypeReg { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if hd.Name == "pubspec.yaml" { |
||||||
|
if hd.Size > maxPubspecFileSize { |
||||||
|
return nil, ErrPubspecFileTooLarge |
||||||
|
} |
||||||
|
p, err = ParsePubspecMetadata(tr) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} else if strings.ToLower(hd.Name) == "readme.md" { |
||||||
|
data, err := io.ReadAll(tr) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
readme = string(data) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if p == nil { |
||||||
|
return nil, ErrMissingPubspecFile |
||||||
|
} |
||||||
|
|
||||||
|
p.Metadata.Readme = readme |
||||||
|
|
||||||
|
return p, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePubspecMetadata parses a Pubspec file to retrieve the metadata of a Pub package
|
||||||
|
func ParsePubspecMetadata(r io.Reader) (*Package, error) { |
||||||
|
buf, err := io.ReadAll(io.LimitReader(r, maxPubspecFileSize)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var p pubspecPackage |
||||||
|
if err := yaml.Unmarshal(buf, &p); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if !namePattern.MatchString(p.Name) { |
||||||
|
return nil, ErrInvalidName |
||||||
|
} |
||||||
|
|
||||||
|
v, err := version.NewSemver(p.Version) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrInvalidVersion |
||||||
|
} |
||||||
|
|
||||||
|
if !validation.IsValidURL(p.Homepage) { |
||||||
|
p.Homepage = "" |
||||||
|
} |
||||||
|
if !validation.IsValidURL(p.Repository) { |
||||||
|
p.Repository = "" |
||||||
|
} |
||||||
|
|
||||||
|
var pubspec interface{} |
||||||
|
if err := yaml.Unmarshal(buf, &pubspec); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &Package{ |
||||||
|
Name: p.Name, |
||||||
|
Version: v.String(), |
||||||
|
Metadata: &Metadata{ |
||||||
|
Description: p.Description, |
||||||
|
ProjectURL: p.Homepage, |
||||||
|
RepositoryURL: p.Repository, |
||||||
|
DocumentationURL: p.Documentation, |
||||||
|
Pubspec: pubspec, |
||||||
|
}, |
||||||
|
}, nil |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package pub |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/tar" |
||||||
|
"bytes" |
||||||
|
"compress/gzip" |
||||||
|
"io" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
packageName = "gitea" |
||||||
|
packageVersion = "1.0.1" |
||||||
|
description = "Package Description" |
||||||
|
projectURL = "https://gitea.io" |
||||||
|
repositoryURL = "https://gitea.io/gitea/gitea" |
||||||
|
documentationURL = "https://docs.gitea.io" |
||||||
|
) |
||||||
|
|
||||||
|
const pubspecContent = `name: ` + packageName + ` |
||||||
|
version: ` + packageVersion + ` |
||||||
|
description: ` + description + ` |
||||||
|
homepage: ` + projectURL + ` |
||||||
|
repository: ` + repositoryURL + ` |
||||||
|
documentation: ` + documentationURL + ` |
||||||
|
|
||||||
|
environment: |
||||||
|
sdk: '>=2.16.0 <3.0.0' |
||||||
|
|
||||||
|
dependencies: |
||||||
|
flutter: |
||||||
|
sdk: flutter |
||||||
|
path: '>=1.8.0 <3.0.0' |
||||||
|
|
||||||
|
dev_dependencies: |
||||||
|
http: '>=0.13.0'` |
||||||
|
|
||||||
|
func TestParsePackage(t *testing.T) { |
||||||
|
createArchive := func(files map[string][]byte) io.Reader { |
||||||
|
var buf bytes.Buffer |
||||||
|
zw := gzip.NewWriter(&buf) |
||||||
|
tw := tar.NewWriter(zw) |
||||||
|
for filename, content := range files { |
||||||
|
hdr := &tar.Header{ |
||||||
|
Name: filename, |
||||||
|
Mode: 0o600, |
||||||
|
Size: int64(len(content)), |
||||||
|
} |
||||||
|
tw.WriteHeader(hdr) |
||||||
|
tw.Write(content) |
||||||
|
} |
||||||
|
tw.Close() |
||||||
|
zw.Close() |
||||||
|
return &buf |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("MissingPubspecFile", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{"dummy.txt": {}}) |
||||||
|
|
||||||
|
pp, err := ParsePackage(data) |
||||||
|
assert.Nil(t, pp) |
||||||
|
assert.ErrorIs(t, err, ErrMissingPubspecFile) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("PubspecFileTooLarge", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{"pubspec.yaml": make([]byte, 200*1024)}) |
||||||
|
|
||||||
|
pp, err := ParsePackage(data) |
||||||
|
assert.Nil(t, pp) |
||||||
|
assert.ErrorIs(t, err, ErrPubspecFileTooLarge) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("InvalidPubspecFile", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{"pubspec.yaml": {}}) |
||||||
|
|
||||||
|
pp, err := ParsePackage(data) |
||||||
|
assert.Nil(t, pp) |
||||||
|
assert.Error(t, err) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent)}) |
||||||
|
|
||||||
|
pp, err := ParsePackage(data) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.NotNil(t, pp) |
||||||
|
assert.Empty(t, pp.Metadata.Readme) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("ValidWithReadme", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent), "README.md": []byte("readme")}) |
||||||
|
|
||||||
|
pp, err := ParsePackage(data) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.NotNil(t, pp) |
||||||
|
assert.Equal(t, "readme", pp.Metadata.Readme) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestParsePubspecMetadata(t *testing.T) { |
||||||
|
t.Run("InvalidName", func(t *testing.T) { |
||||||
|
for _, name := range []string{"123abc", "ab-cd"} { |
||||||
|
pp, err := ParsePubspecMetadata(strings.NewReader(`name: ` + name)) |
||||||
|
assert.Nil(t, pp) |
||||||
|
assert.ErrorIs(t, err, ErrInvalidName) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("InvalidVersion", func(t *testing.T) { |
||||||
|
pp, err := ParsePubspecMetadata(strings.NewReader(`name: dummy |
||||||
|
version: invalid`)) |
||||||
|
assert.Nil(t, pp) |
||||||
|
assert.ErrorIs(t, err, ErrInvalidVersion) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) { |
||||||
|
pp, err := ParsePubspecMetadata(strings.NewReader(pubspecContent)) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.NotNil(t, pp) |
||||||
|
|
||||||
|
assert.Equal(t, packageName, pp.Name) |
||||||
|
assert.Equal(t, packageVersion, pp.Version) |
||||||
|
assert.Equal(t, description, pp.Metadata.Description) |
||||||
|
assert.Equal(t, projectURL, pp.Metadata.ProjectURL) |
||||||
|
assert.Equal(t, repositoryURL, pp.Metadata.RepositoryURL) |
||||||
|
assert.Equal(t, documentationURL, pp.Metadata.DocumentationURL) |
||||||
|
assert.NotNil(t, pp.Metadata.Pubspec) |
||||||
|
}) |
||||||
|
} |
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,275 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package pub |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/json" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
packages_module "code.gitea.io/gitea/modules/packages" |
||||||
|
pub_module "code.gitea.io/gitea/modules/packages/pub" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/routers/api/packages/helper" |
||||||
|
packages_service "code.gitea.io/gitea/services/packages" |
||||||
|
) |
||||||
|
|
||||||
|
func jsonResponse(ctx *context.Context, status int, obj interface{}) { |
||||||
|
resp := ctx.Resp |
||||||
|
resp.Header().Set("Content-Type", "application/vnd.pub.v2+json") |
||||||
|
resp.WriteHeader(status) |
||||||
|
if err := json.NewEncoder(resp).Encode(obj); err != nil { |
||||||
|
log.Error("JSON encode: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func apiError(ctx *context.Context, status int, obj interface{}) { |
||||||
|
type Error struct { |
||||||
|
Code string `json:"code"` |
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
|
type ErrorWrapper struct { |
||||||
|
Error Error `json:"error"` |
||||||
|
} |
||||||
|
|
||||||
|
helper.LogAndProcessError(ctx, status, obj, func(message string) { |
||||||
|
jsonResponse(ctx, status, ErrorWrapper{ |
||||||
|
Error: Error{ |
||||||
|
Code: http.StatusText(status), |
||||||
|
Message: message, |
||||||
|
}, |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
type packageVersions struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Latest *versionMetadata `json:"latest"` |
||||||
|
Versions []*versionMetadata `json:"versions"` |
||||||
|
} |
||||||
|
|
||||||
|
type versionMetadata struct { |
||||||
|
Version string `json:"version"` |
||||||
|
ArchiveURL string `json:"archive_url"` |
||||||
|
Published time.Time `json:"published"` |
||||||
|
Pubspec interface{} `json:"pubspec,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata { |
||||||
|
return &versionMetadata{ |
||||||
|
Version: pd.Version.Version, |
||||||
|
ArchiveURL: fmt.Sprintf("%s/files/%s.tar.gz", baseURL, url.PathEscape(pd.Version.Version)), |
||||||
|
Published: time.Unix(int64(pd.Version.CreatedUnix), 0), |
||||||
|
Pubspec: pd.Metadata.(*pub_module.Metadata).Pubspec, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func baseURL(ctx *context.Context) string { |
||||||
|
return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pub/api/packages" |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#list-all-versions-of-a-package
|
||||||
|
func EnumeratePackageVersions(ctx *context.Context) { |
||||||
|
packageName := ctx.Params("id") |
||||||
|
|
||||||
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
if len(pvs) == 0 { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
sort.Slice(pds, func(i, j int) bool { |
||||||
|
return pds[i].SemVer.LessThan(pds[j].SemVer) |
||||||
|
}) |
||||||
|
|
||||||
|
baseURL := fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pds[0].Package.Name)) |
||||||
|
|
||||||
|
versions := make([]*versionMetadata, 0, len(pds)) |
||||||
|
for _, pd := range pds { |
||||||
|
versions = append(versions, packageDescriptorToMetadata(baseURL, pd)) |
||||||
|
} |
||||||
|
|
||||||
|
jsonResponse(ctx, http.StatusOK, &packageVersions{ |
||||||
|
Name: pds[0].Package.Name, |
||||||
|
Latest: packageDescriptorToMetadata(baseURL, pds[0]), |
||||||
|
Versions: versions, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-inspect-a-specific-version-of-a-package
|
||||||
|
func PackageVersionMetadata(ctx *context.Context) { |
||||||
|
packageName := ctx.Params("id") |
||||||
|
packageVersion := ctx.Params("version") |
||||||
|
|
||||||
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) |
||||||
|
if err != nil { |
||||||
|
if err == packages_model.ErrPackageNotExist { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
return |
||||||
|
} |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pd, err := packages_model.GetPackageDescriptor(ctx, pv) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
jsonResponse(ctx, http.StatusOK, packageDescriptorToMetadata( |
||||||
|
fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pd.Package.Name)), |
||||||
|
pd, |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
|
||||||
|
func RequestUpload(ctx *context.Context) { |
||||||
|
type UploadRequest struct { |
||||||
|
URL string `json:"url"` |
||||||
|
Fields map[string]string `json:"fields"` |
||||||
|
} |
||||||
|
|
||||||
|
jsonResponse(ctx, http.StatusOK, UploadRequest{ |
||||||
|
URL: baseURL(ctx) + "/versions/new/upload", |
||||||
|
Fields: make(map[string]string), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
|
||||||
|
func UploadPackageFile(ctx *context.Context) { |
||||||
|
file, _, err := ctx.Req.FormFile("file") |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusBadRequest, err) |
||||||
|
return |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
defer buf.Close() |
||||||
|
|
||||||
|
pck, err := pub_module.ParsePackage(buf) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := buf.Seek(0, io.SeekStart); err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
_, _, err = packages_service.CreatePackageAndAddFile( |
||||||
|
&packages_service.PackageCreationInfo{ |
||||||
|
PackageInfo: packages_service.PackageInfo{ |
||||||
|
Owner: ctx.Package.Owner, |
||||||
|
PackageType: packages_model.TypePub, |
||||||
|
Name: pck.Name, |
||||||
|
Version: pck.Version, |
||||||
|
}, |
||||||
|
SemverCompatible: true, |
||||||
|
Creator: ctx.Doer, |
||||||
|
Metadata: pck.Metadata, |
||||||
|
}, |
||||||
|
&packages_service.PackageFileCreationInfo{ |
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{ |
||||||
|
Filename: strings.ToLower(pck.Version + ".tar.gz"), |
||||||
|
}, |
||||||
|
Data: buf, |
||||||
|
IsLead: true, |
||||||
|
}, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
if err == packages_model.ErrDuplicatePackageVersion { |
||||||
|
apiError(ctx, http.StatusBadRequest, err) |
||||||
|
return |
||||||
|
} |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Resp.Header().Set("Location", fmt.Sprintf("%s/versions/new/finalize/%s/%s", baseURL(ctx), url.PathEscape(pck.Name), url.PathEscape(pck.Version))) |
||||||
|
ctx.Status(http.StatusNoContent) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
|
||||||
|
func FinalizePackage(ctx *context.Context) { |
||||||
|
packageName := ctx.Params("id") |
||||||
|
packageVersion := ctx.Params("version") |
||||||
|
|
||||||
|
_, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) |
||||||
|
if err != nil { |
||||||
|
if err == packages_model.ErrPackageNotExist { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
return |
||||||
|
} |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
type Success struct { |
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
|
type SuccessWrapper struct { |
||||||
|
Success Success `json:"success"` |
||||||
|
} |
||||||
|
|
||||||
|
jsonResponse(ctx, http.StatusOK, SuccessWrapper{Success{}}) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-download-a-specific-version-of-a-package
|
||||||
|
func DownloadPackageFile(ctx *context.Context) { |
||||||
|
packageName := ctx.Params("id") |
||||||
|
packageVersion := strings.TrimSuffix(ctx.Params("version"), ".tar.gz") |
||||||
|
|
||||||
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) |
||||||
|
if err != nil { |
||||||
|
if err == packages_model.ErrPackageNotExist { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
return |
||||||
|
} |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pd, err := packages_model.GetPackageDescriptor(ctx, pv) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pf := pd.Files[0].File |
||||||
|
|
||||||
|
s, _, err := packages_service.GetPackageFileStream(ctx, pf) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
defer s.Close() |
||||||
|
|
||||||
|
ctx.ServeStream(s, pf.Name) |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
{{if eq .PackageDescriptor.Package.Type "pub"}} |
||||||
|
<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-terminal"}} {{.locale.Tr "packages.pub.install"}}</label> |
||||||
|
<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url={{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/pub/</code></pre></div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label>{{.locale.Tr "packages.pub.documentation" | Safe}}</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}} |
||||||
|
<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4> |
||||||
|
{{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div>{{end}} |
||||||
|
{{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment">{{RenderMarkdownToHtml .PackageDescriptor.Metadata.Readme}}</div>{{end}} |
||||||
|
{{end}} |
||||||
|
{{end}} |
@ -0,0 +1,5 @@ |
|||||||
|
{{if eq .PackageDescriptor.Package.Type "pub"}} |
||||||
|
{{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.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.pub.details.repository_site"}}</a></div>{{end}} |
||||||
|
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.pub.details.documentation_site"}}</a></div>{{end}} |
||||||
|
{{end}} |
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in new issue