mirror of https://github.com/go-gitea/gitea
Add Go package registry (#24687)
Fixes #7608 This PR adds a Go package registry usable with the Go proxy protocol. ![grafik](https://github.com/go-gitea/gitea/assets/1666336/328feb5c-3df2-4f9d-8eae-fe3126d14c37)pull/24711/head^2
parent
53a00017bb
commit
5968c63a11
@ -0,0 +1,77 @@ |
|||||||
|
--- |
||||||
|
date: "2023-05-10T00:00:00+00:00" |
||||||
|
title: "Go Packages Repository" |
||||||
|
slug: "go" |
||||||
|
weight: 45 |
||||||
|
draft: false |
||||||
|
toc: false |
||||||
|
menu: |
||||||
|
sidebar: |
||||||
|
parent: "packages" |
||||||
|
name: "Go" |
||||||
|
weight: 45 |
||||||
|
identifier: "go" |
||||||
|
--- |
||||||
|
|
||||||
|
# Go Packages Repository |
||||||
|
|
||||||
|
Publish Go packages for your user or organization. |
||||||
|
|
||||||
|
**Table of Contents** |
||||||
|
|
||||||
|
{{< toc >}} |
||||||
|
|
||||||
|
## Publish a package |
||||||
|
|
||||||
|
To publish a Go package perform a HTTP `PUT` operation with the package content in the request body. |
||||||
|
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. |
||||||
|
The package must follow the [documented structure](https://go.dev/ref/mod#zip-files). |
||||||
|
|
||||||
|
``` |
||||||
|
PUT https://gitea.example.com/api/packages/{owner}/go/upload |
||||||
|
``` |
||||||
|
|
||||||
|
| Parameter | Description | |
||||||
|
| --------- | ----------- | |
||||||
|
| `owner` | The owner of the package. | |
||||||
|
|
||||||
|
To authenticate to the package registry, you need to provide [custom HTTP headers or use HTTP Basic authentication]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}): |
||||||
|
|
||||||
|
```shell |
||||||
|
curl --user your_username:your_password_or_token \ |
||||||
|
--upload-file path/to/file.zip \ |
||||||
|
https://gitea.example.com/api/packages/testuser/go/upload |
||||||
|
``` |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
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 is invalid. | |
||||||
|
| `409 Conflict` | A package with the same name exist already. | |
||||||
|
|
||||||
|
## Install a package |
||||||
|
|
||||||
|
To install a Go package instruct Go to use the package registry as proxy: |
||||||
|
|
||||||
|
```shell |
||||||
|
# use latest version |
||||||
|
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name} |
||||||
|
# or |
||||||
|
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@latest |
||||||
|
# use specific version |
||||||
|
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@{package_version} |
||||||
|
``` |
||||||
|
|
||||||
|
| Parameter | Description | |
||||||
|
| ----------------- | ----------- | |
||||||
|
| `owner` | The owner of the package. | |
||||||
|
| `package_name` | The package name. | |
||||||
|
| `package_version` | The package version. | |
||||||
|
|
||||||
|
If the owner of the packages is private you need to [provide credentials](https://go.dev/ref/mod#private-module-proxy-auth). |
||||||
|
|
||||||
|
More information about the `GOPROXY` environment variable and how to protect against data leaks can be found in [the documentation](https://go.dev/ref/mod#private-modules). |
@ -0,0 +1,94 @@ |
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package goproxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/zip" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
PropertyGoMod = "go.mod" |
||||||
|
|
||||||
|
maxGoModFileSize = 16 * 1024 * 1024 // https://go.dev/ref/mod#zip-path-size-constraints
|
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure") |
||||||
|
ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large") |
||||||
|
) |
||||||
|
|
||||||
|
type Package struct { |
||||||
|
Name string |
||||||
|
Version string |
||||||
|
GoMod string |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePackage parses the Go package file
|
||||||
|
// https://go.dev/ref/mod#zip-files
|
||||||
|
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { |
||||||
|
archive, err := zip.NewReader(r, size) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var p *Package |
||||||
|
|
||||||
|
for _, file := range archive.File { |
||||||
|
nameAndVersion := path.Dir(file.Name) |
||||||
|
|
||||||
|
parts := strings.SplitN(nameAndVersion, "@", 2) |
||||||
|
if len(parts) != 2 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
versionParts := strings.SplitN(parts[1], "/", 2) |
||||||
|
|
||||||
|
if p == nil { |
||||||
|
p = &Package{ |
||||||
|
Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]), |
||||||
|
Version: versionParts[0], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(versionParts) > 1 { |
||||||
|
// files are expected in the "root" folder
|
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if path.Base(file.Name) == "go.mod" { |
||||||
|
if file.UncompressedSize64 > maxGoModFileSize { |
||||||
|
return nil, ErrGoModFileTooLarge |
||||||
|
} |
||||||
|
|
||||||
|
f, err := archive.Open(file.Name) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
bytes, err := io.ReadAll(&io.LimitedReader{R: f, N: maxGoModFileSize}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
p.GoMod = string(bytes) |
||||||
|
|
||||||
|
return p, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if p == nil { |
||||||
|
return nil, ErrInvalidStructure |
||||||
|
} |
||||||
|
|
||||||
|
p.GoMod = fmt.Sprintf("module %s", p.Name) |
||||||
|
|
||||||
|
return p, nil |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package goproxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/zip" |
||||||
|
"bytes" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
packageName = "gitea.com/go-gitea/gitea" |
||||||
|
packageVersion = "v0.0.1" |
||||||
|
) |
||||||
|
|
||||||
|
func TestParsePackage(t *testing.T) { |
||||||
|
createArchive := func(files map[string][]byte) *bytes.Reader { |
||||||
|
var buf bytes.Buffer |
||||||
|
zw := zip.NewWriter(&buf) |
||||||
|
for name, content := range files { |
||||||
|
w, _ := zw.Create(name) |
||||||
|
w.Write(content) |
||||||
|
} |
||||||
|
zw.Close() |
||||||
|
return bytes.NewReader(buf.Bytes()) |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("EmptyPackage", func(t *testing.T) { |
||||||
|
data := createArchive(nil) |
||||||
|
|
||||||
|
p, err := ParsePackage(data, int64(data.Len())) |
||||||
|
assert.Nil(t, p) |
||||||
|
assert.ErrorIs(t, err, ErrInvalidStructure) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("InvalidNameOrVersionStructure", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{ |
||||||
|
packageName + "/" + packageVersion + "/go.mod": {}, |
||||||
|
}) |
||||||
|
|
||||||
|
p, err := ParsePackage(data, int64(data.Len())) |
||||||
|
assert.Nil(t, p) |
||||||
|
assert.ErrorIs(t, err, ErrInvalidStructure) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("GoModFileInWrongDirectory", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{ |
||||||
|
packageName + "@" + packageVersion + "/subdir/go.mod": {}, |
||||||
|
}) |
||||||
|
|
||||||
|
p, err := ParsePackage(data, int64(data.Len())) |
||||||
|
assert.NotNil(t, p) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, packageName, p.Name) |
||||||
|
assert.Equal(t, packageVersion, p.Version) |
||||||
|
assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) { |
||||||
|
data := createArchive(map[string][]byte{ |
||||||
|
packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"), |
||||||
|
packageName + "@" + packageVersion + "/go.mod": []byte("valid"), |
||||||
|
}) |
||||||
|
|
||||||
|
p, err := ParsePackage(data, int64(data.Len())) |
||||||
|
assert.NotNil(t, p) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, packageName, p.Name) |
||||||
|
assert.Equal(t, packageVersion, p.Version) |
||||||
|
assert.Equal(t, "valid", p.GoMod) |
||||||
|
}) |
||||||
|
} |
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,226 @@ |
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package goproxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"sort" |
||||||
|
"time" |
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
packages_module "code.gitea.io/gitea/modules/packages" |
||||||
|
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
"code.gitea.io/gitea/routers/api/packages/helper" |
||||||
|
packages_service "code.gitea.io/gitea/services/packages" |
||||||
|
) |
||||||
|
|
||||||
|
func apiError(ctx *context.Context, status int, obj interface{}) { |
||||||
|
helper.LogAndProcessError(ctx, status, obj, func(message string) { |
||||||
|
ctx.PlainText(status, message) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func EnumeratePackageVersions(ctx *context.Context) { |
||||||
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeGo, ctx.Params("name")) |
||||||
|
if err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
if len(pvs) == 0 { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
sort.Slice(pvs, func(i, j int) bool { |
||||||
|
return pvs[i].CreatedUnix < pvs[j].CreatedUnix |
||||||
|
}) |
||||||
|
|
||||||
|
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") |
||||||
|
|
||||||
|
for _, pv := range pvs { |
||||||
|
fmt.Fprintln(ctx.Resp, pv.Version) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func PackageVersionMetadata(ctx *context.Context) { |
||||||
|
pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version")) |
||||||
|
if err != nil { |
||||||
|
if errors.Is(err, util.ErrNotExist) { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
} else { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, struct { |
||||||
|
Version string `json:"Version"` |
||||||
|
Time time.Time `json:"Time"` |
||||||
|
}{ |
||||||
|
Version: pv.Version, |
||||||
|
Time: pv.CreatedUnix.AsLocalTime(), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func PackageVersionGoModContent(ctx *context.Context) { |
||||||
|
pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version")) |
||||||
|
if err != nil { |
||||||
|
if errors.Is(err, util.ErrNotExist) { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
} else { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, goproxy_module.PropertyGoMod) |
||||||
|
if err != nil || len(pps) != 1 { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.PlainText(http.StatusOK, pps[0].Value) |
||||||
|
} |
||||||
|
|
||||||
|
func DownloadPackageFile(ctx *context.Context) { |
||||||
|
pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version")) |
||||||
|
if err != nil { |
||||||
|
if errors.Is(err, util.ErrNotExist) { |
||||||
|
apiError(ctx, http.StatusNotFound, err) |
||||||
|
} else { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) |
||||||
|
if err != nil || len(pfs) != 1 { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
s, _, 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: pfs[0].Name, |
||||||
|
LastModified: pfs[0].CreatedUnix.AsLocalTime(), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (*packages_model.PackageVersion, error) { |
||||||
|
var pv *packages_model.PackageVersion |
||||||
|
|
||||||
|
if version == "latest" { |
||||||
|
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ |
||||||
|
OwnerID: ownerID, |
||||||
|
Type: packages_model.TypeGo, |
||||||
|
Name: packages_model.SearchValue{ |
||||||
|
Value: name, |
||||||
|
ExactMatch: true, |
||||||
|
}, |
||||||
|
IsInternal: util.OptionalBoolFalse, |
||||||
|
Sort: packages_model.SortCreatedDesc, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if len(pvs) != 1 { |
||||||
|
return nil, packages_model.ErrPackageNotExist |
||||||
|
} |
||||||
|
|
||||||
|
pv = pvs[0] |
||||||
|
} else { |
||||||
|
var err error |
||||||
|
pv, err = packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeGo, name, version) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return pv, nil |
||||||
|
} |
||||||
|
|
||||||
|
func UploadPackage(ctx *context.Context) { |
||||||
|
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 := goproxy_module.ParsePackage(buf, buf.Size()) |
||||||
|
if err != nil { |
||||||
|
if errors.Is(err, util.ErrInvalidArgument) { |
||||||
|
apiError(ctx, http.StatusBadRequest, err) |
||||||
|
} else { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := buf.Seek(0, io.SeekStart); err != nil { |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
_, _, err = packages_service.CreatePackageAndAddFile( |
||||||
|
&packages_service.PackageCreationInfo{ |
||||||
|
PackageInfo: packages_service.PackageInfo{ |
||||||
|
Owner: ctx.Package.Owner, |
||||||
|
PackageType: packages_model.TypeGo, |
||||||
|
Name: pck.Name, |
||||||
|
Version: pck.Version, |
||||||
|
}, |
||||||
|
Creator: ctx.Doer, |
||||||
|
VersionProperties: map[string]string{ |
||||||
|
goproxy_module.PropertyGoMod: pck.GoMod, |
||||||
|
}, |
||||||
|
}, |
||||||
|
&packages_service.PackageFileCreationInfo{ |
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{ |
||||||
|
Filename: fmt.Sprintf("%v.zip", pck.Version), |
||||||
|
}, |
||||||
|
Creator: ctx.Doer, |
||||||
|
Data: buf, |
||||||
|
IsLead: true, |
||||||
|
}, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
switch err { |
||||||
|
case packages_model.ErrDuplicatePackageVersion: |
||||||
|
apiError(ctx, http.StatusConflict, err) |
||||||
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: |
||||||
|
apiError(ctx, http.StatusForbidden, err) |
||||||
|
default: |
||||||
|
apiError(ctx, http.StatusInternalServerError, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Status(http.StatusCreated) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
{{if eq .PackageDescriptor.Package.Type "go"}} |
||||||
|
<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.go.install"}}</label> |
||||||
|
<div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label>{{.locale.Tr "packages.go.documentation" "https://docs.gitea.io/en-us/usage/packages/go" | Safe}}</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
@ -0,0 +1,166 @@ |
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/zip" |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"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" |
||||||
|
"code.gitea.io/gitea/tests" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestPackageGo(t *testing.T) { |
||||||
|
defer tests.PrepareTestEnv(t)() |
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||||
|
|
||||||
|
packageName := "gitea.com/go-gitea/gitea" |
||||||
|
packageVersion := "v0.0.1" |
||||||
|
packageVersion2 := "v0.0.2" |
||||||
|
goModContent := `module "gitea.com/go-gitea/gitea"` |
||||||
|
|
||||||
|
createArchive := func(files map[string][]byte) []byte { |
||||||
|
var buf bytes.Buffer |
||||||
|
zw := zip.NewWriter(&buf) |
||||||
|
for name, content := range files { |
||||||
|
w, _ := zw.Create(name) |
||||||
|
w.Write(content) |
||||||
|
} |
||||||
|
zw.Close() |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
url := fmt.Sprintf("/api/packages/%s/go", user.Name) |
||||||
|
|
||||||
|
t.Run("Upload", func(t *testing.T) { |
||||||
|
defer tests.PrintCurrentTest(t)() |
||||||
|
|
||||||
|
content := createArchive(nil) |
||||||
|
|
||||||
|
req := NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) |
||||||
|
MakeRequest(t, req, http.StatusUnauthorized) |
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) |
||||||
|
AddBasicAuthHeader(req, user.Name) |
||||||
|
MakeRequest(t, req, http.StatusBadRequest) |
||||||
|
|
||||||
|
content = createArchive(map[string][]byte{ |
||||||
|
packageName + "@" + packageVersion + "/go.mod": []byte(goModContent), |
||||||
|
}) |
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) |
||||||
|
AddBasicAuthHeader(req, user.Name) |
||||||
|
MakeRequest(t, req, http.StatusCreated) |
||||||
|
|
||||||
|
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGo) |
||||||
|
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.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, packageVersion+".zip", 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) |
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) |
||||||
|
AddBasicAuthHeader(req, user.Name) |
||||||
|
MakeRequest(t, req, http.StatusConflict) |
||||||
|
|
||||||
|
time.Sleep(time.Second) |
||||||
|
|
||||||
|
content = createArchive(map[string][]byte{ |
||||||
|
packageName + "@" + packageVersion2 + "/go.mod": []byte(goModContent), |
||||||
|
}) |
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) |
||||||
|
AddBasicAuthHeader(req, user.Name) |
||||||
|
MakeRequest(t, req, http.StatusCreated) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("List", func(t *testing.T) { |
||||||
|
defer tests.PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/list", url, packageName)) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
assert.Equal(t, packageVersion+"\n"+packageVersion2+"\n", resp.Body.String()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Info", func(t *testing.T) { |
||||||
|
defer tests.PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.info", url, packageName, packageVersion)) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
type Info struct { |
||||||
|
Version string `json:"Version"` |
||||||
|
Time time.Time `json:"Time"` |
||||||
|
} |
||||||
|
|
||||||
|
info := &Info{} |
||||||
|
DecodeJSON(t, resp, &info) |
||||||
|
|
||||||
|
assert.Equal(t, packageVersion, info.Version) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.info", url, packageName)) |
||||||
|
resp = MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
info = &Info{} |
||||||
|
DecodeJSON(t, resp, &info) |
||||||
|
|
||||||
|
assert.Equal(t, packageVersion2, info.Version) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@latest", url, packageName)) |
||||||
|
resp = MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
info = &Info{} |
||||||
|
DecodeJSON(t, resp, &info) |
||||||
|
|
||||||
|
assert.Equal(t, packageVersion2, info.Version) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("GoMod", func(t *testing.T) { |
||||||
|
defer tests.PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.mod", url, packageName, packageVersion)) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
assert.Equal(t, goModContent, resp.Body.String()) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.mod", url, packageName)) |
||||||
|
resp = MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
assert.Equal(t, goModContent, resp.Body.String()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Download", func(t *testing.T) { |
||||||
|
defer tests.PrintCurrentTest(t)() |
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.zip", url, packageName, packageVersion)) |
||||||
|
MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.zip", url, packageName)) |
||||||
|
MakeRequest(t, req, http.StatusOK) |
||||||
|
}) |
||||||
|
} |
After Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in new issue