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