diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index 5e03b7101..0abb054b0 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -37,6 +37,7 @@ The following package managers are currently supported: | [Pub]({{< relref "doc/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` | | [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` | | [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` | +| [Vagrant]({{< relref "doc/packages/vagrant.en-us.md" >}}) | - | `vagrant` | **The following paragraphs only apply if Packages are not globally disabled!** diff --git a/docs/content/doc/packages/vagrant.en-us.md b/docs/content/doc/packages/vagrant.en-us.md new file mode 100644 index 000000000..e846de1a2 --- /dev/null +++ b/docs/content/doc/packages/vagrant.en-us.md @@ -0,0 +1,78 @@ +--- +date: "2022-08-23T00:00:00+00:00" +title: "Vagrant Packages Repository" +slug: "packages/vagrant" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "vagrant" + weight: 120 + identifier: "vagrant" +--- + +# Vagrant Packages Repository + +Publish [Vagrant](https://www.vagrantup.com/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Vagrant package registry, you need [Vagrant](https://www.vagrantup.com/downloads) and a tool to make HTTP requests like `curl`. + +## Publish a package + +Publish a Vagrant box by performing a HTTP PUT request to the registry: + +``` +PUT https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}/{package_version}/{provider}.box +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the package. | +| `package_name` | The package name. | +| `package_version` | The package version, semver compatible. | +| `provider` | One of the [supported provider names](https://www.vagrantup.com/docs/providers). | + +Example for uploading a Hyper-V box: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/your/vagrant.box \ + https://gitea.example.com/api/packages/testuser/vagrant/test_system/1.0.0/hyperv.box +``` + +You cannot publish a box if a box of the same name, version and provider already exists. You must delete the existing package first. + +## Install a package + +To install a box from the package registry, execute the following command: + +```shell +vagrant box add "https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}" +``` + +| Parameter | Description | +| -------------- | ----------- | +| `owner` | The owner of the package. | +| `package_name` | The package name. | + +For example: + +```shell +vagrant box add "https://gitea.example.com/api/packages/testuser/vagrant/test_system" +``` + +This will install the latest version of the package. To add a specific version, use the `--box-version` parameter. +If the registry is private you can pass your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) in the `VAGRANT_CLOUD_TOKEN` environment variable. + +## Supported commands + +``` +vagrant box add +``` diff --git a/integrations/api_packages_vagrant_test.go b/integrations/api_packages_vagrant_test.go new file mode 100644 index 000000000..8ce4f24ef --- /dev/null +++ b/integrations/api_packages_vagrant_test.go @@ -0,0 +1,170 @@ +// 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" + "net/http" + "strings" + "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" + "code.gitea.io/gitea/modules/json" + vagrant_module "code.gitea.io/gitea/modules/packages/vagrant" + + "github.com/stretchr/testify/assert" +) + +func TestPackageVagrant(t *testing.T) { + defer prepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + token := "Bearer " + getUserToken(t, user.Name) + + packageName := "test_package" + packageVersion := "1.0.1" + packageDescription := "Test Description" + packageProvider := "virtualbox" + + filename := fmt.Sprintf("%s.box", packageProvider) + + infoContent, _ := json.Marshal(map[string]string{ + "description": packageDescription, + }) + + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + archive := tar.NewWriter(zw) + archive.WriteHeader(&tar.Header{ + Name: "info.json", + Mode: 0o600, + Size: int64(len(infoContent)), + }) + archive.Write(infoContent) + archive.Close() + zw.Close() + content := buf.Bytes() + + root := fmt.Sprintf("/api/packages/%s/vagrant", user.Name) + + t.Run("Authenticate", func(t *testing.T) { + defer PrintCurrentTest(t)() + + authenticateURL := fmt.Sprintf("%s/authenticate", root) + + req := NewRequest(t, "GET", authenticateURL) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "GET", authenticateURL) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusOK) + }) + + boxURL := fmt.Sprintf("%s/%s", root, packageName) + + t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "HEAD", boxURL) + MakeRequest(t, req, http.StatusNotFound) + + uploadURL := fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "HEAD", boxURL) + resp := MakeRequest(t, req, http.StatusOK) + assert.True(t, strings.HasPrefix(resp.HeaderMap.Get("Content-Type"), "application/json")) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeVagrant) + 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, &vagrant_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) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusConflict) + }) + + t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename)) + 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", boxURL) + resp := MakeRequest(t, req, http.StatusOK) + + type providerData struct { + Name string `json:"name"` + URL string `json:"url"` + Checksum string `json:"checksum"` + ChecksumType string `json:"checksum_type"` + } + + type versionMetadata struct { + Version string `json:"version"` + Status string `json:"status"` + DescriptionHTML string `json:"description_html,omitempty"` + DescriptionMarkdown string `json:"description_markdown,omitempty"` + Providers []*providerData `json:"providers"` + } + + type packageMetadata struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"short_description,omitempty"` + Versions []*versionMetadata `json:"versions"` + } + + var result packageMetadata + DecodeJSON(t, resp, &result) + + assert.Equal(t, packageName, result.Name) + assert.Equal(t, packageDescription, result.Description) + assert.Len(t, result.Versions, 1) + version := result.Versions[0] + assert.Equal(t, packageVersion, version.Version) + assert.Equal(t, "active", version.Status) + assert.Len(t, version.Providers, 1) + provider := version.Providers[0] + assert.Equal(t, packageProvider, provider.Name) + assert.Equal(t, "sha512", provider.ChecksumType) + assert.Equal(t, "259bebd6160acad695016d22a45812e26f187aaf78e71a4c23ee3201528346293f991af3468a8c6c5d2a21d7d9e1bdc1bf79b87110b2fddfcc5a0d45963c7c30", provider.Checksum) + }) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index dc753421d..357574a70 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/packages/pub" "code.gitea.io/gitea/modules/packages/pypi" "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/packages/vagrant" "github.com/hashicorp/go-version" ) @@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &pypi.Metadata{} case TypeRubyGems: metadata = &rubygems.Metadata{} + case TypeVagrant: + metadata = &vagrant.Metadata{} default: panic(fmt.Sprintf("unknown package type: %s", string(p.Type))) } diff --git a/models/packages/package.go b/models/packages/package.go index 39b1c83cf..c203901a2 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -42,6 +42,7 @@ const ( TypePub Type = "pub" TypePyPI Type = "pypi" TypeRubyGems Type = "rubygems" + TypeVagrant Type = "vagrant" ) // Name gets the name of the package type @@ -69,6 +70,8 @@ func (pt Type) Name() string { return "PyPI" case TypeRubyGems: return "RubyGems" + case TypeVagrant: + return "Vagrant" } panic(fmt.Sprintf("unknown package type: %s", string(pt))) } @@ -98,6 +101,8 @@ func (pt Type) SVGName() string { return "gitea-python" case TypeRubyGems: return "gitea-rubygems" + case TypeVagrant: + return "gitea-vagrant" } panic(fmt.Sprintf("unknown package type: %s", string(pt))) } diff --git a/modules/packages/vagrant/metadata.go b/modules/packages/vagrant/metadata.go new file mode 100644 index 000000000..278dfab32 --- /dev/null +++ b/modules/packages/vagrant/metadata.go @@ -0,0 +1,97 @@ +// 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 vagrant + +import ( + "archive/tar" + "compress/gzip" + "io" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/validation" +) + +const ( + PropertyProvider = "vagrant.provider" +) + +// Metadata represents the metadata of a Vagrant package +type Metadata struct { + Author string `json:"author,omitempty"` + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` +} + +// ParseMetadataFromBox parses the metdata of a box file +func ParseMetadataFromBox(r io.Reader) (*Metadata, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + 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 == "info.json" { + return ParseInfoFile(tr) + } + } + + return &Metadata{}, nil +} + +// ParseInfoFile parses a info.json file to retrieve the metadata of a Vagrant package +func ParseInfoFile(r io.Reader) (*Metadata, error) { + var values map[string]string + if err := json.NewDecoder(r).Decode(&values); err != nil { + return nil, err + } + + m := &Metadata{} + + // There is no defined format for this file, just try the common keys + for k, v := range values { + switch strings.ToLower(k) { + case "description": + fallthrough + case "short_description": + m.Description = v + case "website": + fallthrough + case "homepage": + fallthrough + case "url": + if validation.IsValidURL(v) { + m.ProjectURL = v + } + case "repository": + fallthrough + case "source": + if validation.IsValidURL(v) { + m.RepositoryURL = v + } + case "author": + fallthrough + case "authors": + m.Author = v + } + } + + return m, nil +} diff --git a/modules/packages/vagrant/metadata_test.go b/modules/packages/vagrant/metadata_test.go new file mode 100644 index 000000000..9720c945a --- /dev/null +++ b/modules/packages/vagrant/metadata_test.go @@ -0,0 +1,111 @@ +// 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 vagrant + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" +) + +const ( + author = "gitea" + description = "Package Description" + projectURL = "https://gitea.io" + repositoryURL = "https://gitea.io/gitea/gitea" +) + +func TestParseMetadataFromBox(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("MissingInfoFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"dummy.txt": {}}) + + metadata, err := ParseMetadataFromBox(data) + assert.NotNil(t, metadata) + assert.NoError(t, err) + }) + + t.Run("Valid", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "description": description, + "author": author, + "website": projectURL, + "repository": repositoryURL, + }) + assert.NoError(t, err) + + data := createArchive(map[string][]byte{"info.json": content}) + + metadata, err := ParseMetadataFromBox(data) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Equal(t, author, metadata.Author) + assert.Equal(t, description, metadata.Description) + assert.Equal(t, projectURL, metadata.ProjectURL) + assert.Equal(t, repositoryURL, metadata.RepositoryURL) + }) +} + +func TestParseInfoFile(t *testing.T) { + t.Run("UnknownKeys", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "package": "", + "dummy": "", + }) + assert.NoError(t, err) + + metadata, err := ParseInfoFile(bytes.NewReader(content)) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Empty(t, metadata.Author) + assert.Empty(t, metadata.Description) + assert.Empty(t, metadata.ProjectURL) + assert.Empty(t, metadata.RepositoryURL) + }) + + t.Run("Valid", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "description": description, + "author": author, + "website": projectURL, + "repository": repositoryURL, + }) + assert.NoError(t, err) + + metadata, err := ParseInfoFile(bytes.NewReader(content)) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Equal(t, author, metadata.Author) + assert.Equal(t, description, metadata.Description) + assert.Equal(t, projectURL, metadata.ProjectURL) + assert.Equal(t, repositoryURL, metadata.RepositoryURL) + }) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e87ee3444..f900e90e7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3131,6 +3131,8 @@ rubygems.dependencies.development = Development Dependencies rubygems.required.ruby = Requires Ruby version rubygems.required.rubygems = Requires RubyGem version rubygems.documentation = For more information on the RubyGems registry, see the documentation. +vagrant.install = To add a Vagrant box, run the following command: +vagrant.documentation = For more information on the Vagrant registry, see the documentation. settings.link = Link this package to a repository settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. settings.link.select = Select Repository diff --git a/public/img/svg/gitea-vagrant.svg b/public/img/svg/gitea-vagrant.svg new file mode 100644 index 000000000..4c1b78cab --- /dev/null +++ b/public/img/svg/gitea-vagrant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 0761cacdf..93f9afca7 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/pub" "code.gitea.io/gitea/routers/api/packages/pypi" "code.gitea.io/gitea/routers/api/packages/rubygems" + "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" ) @@ -266,6 +267,19 @@ func Routes(ctx gocontext.Context) *web.Route { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }) + r.Group("/vagrant", func() { + r.Group("/authenticate", func() { + r.Get("", vagrant.CheckAuthenticate) + }) + r.Group("/{name}", func() { + r.Head("", vagrant.CheckBoxAvailable) + r.Get("", vagrant.EnumeratePackageVersions) + r.Group("/{version}/{provider}", func() { + r.Get("", vagrant.DownloadPackageFile) + r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile) + }) + }) + }) }, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) return r diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go new file mode 100644 index 000000000..7750e5dc4 --- /dev/null +++ b/routers/api/packages/vagrant/vagrant.go @@ -0,0 +1,239 @@ +// 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 vagrant + +import ( + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + vagrant_module "code.gitea.io/gitea/modules/packages/vagrant" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/hashicorp/go-version" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.JSON(status, struct { + Errors []string `json:"errors"` + }{ + Errors: []string{ + message, + }, + }) + }) +} + +func CheckAuthenticate(ctx *context.Context) { + if ctx.Doer == nil { + apiError(ctx, http.StatusUnauthorized, "Invalid access token") + return + } + + ctx.Status(http.StatusOK) +} + +func CheckBoxAvailable(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name")) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + ctx.JSON(http.StatusOK, nil) // needs to be Content-Type: application/json +} + +type packageMetadata struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"short_description,omitempty"` + Versions []*versionMetadata `json:"versions"` +} + +type versionMetadata struct { + Version string `json:"version"` + Status string `json:"status"` + DescriptionHTML string `json:"description_html,omitempty"` + DescriptionMarkdown string `json:"description_markdown,omitempty"` + Providers []*providerData `json:"providers"` +} + +type providerData struct { + Name string `json:"name"` + URL string `json:"url"` + Checksum string `json:"checksum"` + ChecksumType string `json:"checksum_type"` +} + +func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata { + versionURL := baseURL + "/" + url.PathEscape(pd.Version.Version) + + providers := make([]*providerData, 0, len(pd.Files)) + + for _, f := range pd.Files { + providers = append(providers, &providerData{ + Name: f.Properties.GetByName(vagrant_module.PropertyProvider), + URL: versionURL + "/" + url.PathEscape(f.File.Name), + Checksum: f.Blob.HashSHA512, + ChecksumType: "sha512", + }) + } + + return &versionMetadata{ + Status: "active", + Version: pd.Version.Version, + Providers: providers, + } +} + +func EnumeratePackageVersions(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name")) + 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("%sapi/packages/%s/vagrant/%s", setting.AppURL, url.PathEscape(ctx.Package.Owner.Name), url.PathEscape(pds[0].Package.Name)) + + versions := make([]*versionMetadata, 0, len(pds)) + for _, pd := range pds { + versions = append(versions, packageDescriptorToMetadata(baseURL, pd)) + } + + ctx.JSON(http.StatusOK, &packageMetadata{ + Name: pds[0].Package.Name, + Description: pds[len(pds)-1].Metadata.(*vagrant_module.Metadata).Description, + Versions: versions, + }) +} + +func UploadPackageFile(ctx *context.Context) { + boxName := ctx.Params("name") + boxVersion := ctx.Params("version") + _, err := version.NewSemver(boxVersion) + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + boxProvider := ctx.Params("provider") + if !strings.HasSuffix(boxProvider, ".box") { + apiError(ctx, http.StatusBadRequest, err) + return + } + + upload, needsClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needsClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + metadata, err := vagrant_module.ParseMetadataFromBox(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.CreatePackageOrAddFileToExisting( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeVagrant, + Name: boxName, + Version: boxVersion, + }, + SemverCompatible: true, + Creator: ctx.Doer, + Metadata: metadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: strings.ToLower(boxProvider), + }, + Data: buf, + IsLead: true, + Properties: map[string]string{ + vagrant_module.PropertyProvider: strings.TrimSuffix(boxProvider, ".box"), + }, + }, + ) + if err != nil { + if err == packages_model.ErrDuplicatePackageFile { + apiError(ctx, http.StatusConflict, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) +} + +func DownloadPackageFile(ctx *context.Context) { + s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeVagrant, + Name: ctx.Params("name"), + Version: ctx.Params("version"), + }, + &packages_service.PackageFileInfo{ + Filename: ctx.Params("provider"), + }, + ) + if err != nil { + if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer s.Close() + + ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) +} diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 2c0238910..29fa6c85a 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -41,7 +41,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems] + // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] // - name: q // in: query // description: name filter diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 61721532a..06d616347 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -24,6 +24,7 @@ + diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl new file mode 100644 index 000000000..7bd686e6a --- /dev/null +++ b/templates/package/content/vagrant.tmpl @@ -0,0 +1,18 @@ +{{if eq .PackageDescriptor.Package.Type "vagrant"}} +
vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "{{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"