diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b2b5af0af8e..e53ed7ad9fd 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2516,6 +2516,8 @@ ROUTER = console
;LIMIT_SIZE_PYPI = -1
;; Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_RUBYGEMS = -1
+;; Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_SWIFT = -1
;; Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_VAGRANT = -1
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index b67d6cdf5f6..4b9c519cd80 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -1254,6 +1254,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `LIMIT_SIZE_PUB`: **-1**: Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_PYPI`: **-1**: Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_RUBYGEMS`: **-1**: Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_SWIFT`: **-1**: Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_VAGRANT`: **-1**: Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
## Mirror (`mirror`)
diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md
index f93fec63934..08da8ced484 100644
--- a/docs/content/doc/packages/overview.en-us.md
+++ b/docs/content/doc/packages/overview.en-us.md
@@ -40,6 +40,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` |
+| [Swift]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Swift | `swift` |
| [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/swift.en-us.md b/docs/content/doc/packages/swift.en-us.md
new file mode 100644
index 00000000000..61a4c9a55d4
--- /dev/null
+++ b/docs/content/doc/packages/swift.en-us.md
@@ -0,0 +1,93 @@
+---
+date: "2023-01-10T00:00:00+00:00"
+title: "Swift Packages Repository"
+slug: "packages/swift"
+draft: false
+toc: false
+menu:
+ sidebar:
+ parent: "packages"
+ name: "Swift"
+ weight: 95
+ identifier: "swift"
+---
+
+# Swift Packages Repository
+
+Publish [Swift](hhttps://www.swift.org/) packages for your user or organization.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Requirements
+
+To work with the Swift package registry, you need to use [swift](https://www.swift.org/getting-started/) to consume and a HTTP client (like `curl`) to publish packages.
+
+## Configuring the package registry
+
+To register the package registry and provide credentials, execute:
+
+```shell
+swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password}
+```
+
+| Placeholder | Description |
+| ------------ | ----------- |
+| `owner` | The owner of the package. |
+| `username` | Your Gitea username. |
+| `password` | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. |
+
+The login is optional and only needed if the package registry is private.
+
+## Publish a package
+
+First you have to pack the contents of your package:
+
+```shell
+swift package archive-source
+```
+
+To publish the package perform a HTTP PUT request with the package content in the request body.
+
+```shell --user your_username:your_password_or_token \
+curl -X PUT --user {username}:{password} \
+ -H "Accept: application/vnd.swift.registry.v1+json" \
+ -F source-archive=@/path/to/package.zip \
+ -F metadata={metadata} \
+ https://gitea.example.com/api/packages/{owner}/swift/{scope}/{name}/{version}
+```
+
+| Placeholder | Description |
+| ----------- | ----------- |
+| `username` | Your Gitea username. |
+| `password` | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. |
+| `owner` | The owner of the package. |
+| `scope` | The package scope. |
+| `name` | The package name. |
+| `version` | The package version. |
+| `metadata` | (Optional) The metadata of the package. JSON encoded subset of https://schema.org/SoftwareSourceCode |
+
+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 Swift package from the package registry, add it in the `Package.swift` file dependencies list:
+
+```
+dependencies: [
+ .package(id: "{scope}.{name}", from:"{version}")
+]
+```
+
+| Parameter | Description |
+| ----------- | ----------- |
+| `scope` | The package scope. |
+| `name` | The package name. |
+| `version` | The package version. |
+
+Afterwards execute the following command to install it:
+
+```shell
+swift package resolve
+```
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index f4be21e74e2..06699b5d572 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -24,6 +24,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/swift"
"code.gitea.io/gitea/modules/packages/vagrant"
"github.com/hashicorp/go-version"
@@ -159,6 +160,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
metadata = &pypi.Metadata{}
case TypeRubyGems:
metadata = &rubygems.Metadata{}
+ case TypeSwift:
+ metadata = &swift.Metadata{}
case TypeVagrant:
metadata = &vagrant.Metadata{}
default:
diff --git a/models/packages/package.go b/models/packages/package.go
index 32f30fab9b4..ccc9257c312 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -44,6 +44,7 @@ const (
TypePub Type = "pub"
TypePyPI Type = "pypi"
TypeRubyGems Type = "rubygems"
+ TypeSwift Type = "swift"
TypeVagrant Type = "vagrant"
)
@@ -62,6 +63,7 @@ var TypeList = []Type{
TypePub,
TypePyPI,
TypeRubyGems,
+ TypeSwift,
TypeVagrant,
}
@@ -96,6 +98,8 @@ func (pt Type) Name() string {
return "PyPI"
case TypeRubyGems:
return "RubyGems"
+ case TypeSwift:
+ return "Swift"
case TypeVagrant:
return "Vagrant"
}
@@ -133,6 +137,8 @@ func (pt Type) SVGName() string {
return "gitea-python"
case TypeRubyGems:
return "gitea-rubygems"
+ case TypeSwift:
+ return "gitea-swift"
case TypeVagrant:
return "gitea-vagrant"
}
diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go
new file mode 100644
index 00000000000..24c4262ab72
--- /dev/null
+++ b/modules/packages/swift/metadata.go
@@ -0,0 +1,214 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+ "archive/zip"
+ "fmt"
+ "io"
+ "path"
+ "regexp"
+ "strings"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/validation"
+
+ "github.com/hashicorp/go-version"
+)
+
+var (
+ ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing")
+ ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large")
+ ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
+
+ manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
+ toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
+)
+
+const (
+ maxManifestFileSize = 128 * 1024
+
+ PropertyScope = "swift.scope"
+ PropertyName = "swift.name"
+ PropertyRepositoryURL = "swift.repository_url"
+)
+
+// Package represents a Swift package
+type Package struct {
+ RepositoryURLs []string
+ Metadata *Metadata
+}
+
+// Metadata represents the metadata of a Swift package
+type Metadata struct {
+ Description string `json:"description,omitempty"`
+ Keywords []string `json:"keywords,omitempty"`
+ RepositoryURL string `json:"repository_url,omitempty"`
+ License string `json:"license,omitempty"`
+ Author Person `json:"author,omitempty"`
+ Manifests map[string]*Manifest `json:"manifests,omitempty"`
+}
+
+// Manifest represents a Package.swift file
+type Manifest struct {
+ Content string `json:"content"`
+ ToolsVersion string `json:"tools_version,omitempty"`
+}
+
+// https://schema.org/SoftwareSourceCode
+type SoftwareSourceCode struct {
+ Context []string `json:"@context"`
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Description string `json:"description,omitempty"`
+ Keywords []string `json:"keywords,omitempty"`
+ CodeRepository string `json:"codeRepository,omitempty"`
+ License string `json:"license,omitempty"`
+ Author Person `json:"author"`
+ ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
+ RepositoryURLs []string `json:"repositoryURLs,omitempty"`
+}
+
+// https://schema.org/ProgrammingLanguage
+type ProgrammingLanguage struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+}
+
+// https://schema.org/Person
+type Person struct {
+ Type string `json:"@type,omitempty"`
+ GivenName string `json:"givenName,omitempty"`
+ MiddleName string `json:"middleName,omitempty"`
+ FamilyName string `json:"familyName,omitempty"`
+}
+
+func (p Person) String() string {
+ var sb strings.Builder
+ if p.GivenName != "" {
+ sb.WriteString(p.GivenName)
+ }
+ if p.MiddleName != "" {
+ if sb.Len() > 0 {
+ sb.WriteRune(' ')
+ }
+ sb.WriteString(p.MiddleName)
+ }
+ if p.FamilyName != "" {
+ if sb.Len() > 0 {
+ sb.WriteRune(' ')
+ }
+ sb.WriteString(p.FamilyName)
+ }
+ return sb.String()
+}
+
+// ParsePackage parses the Swift package upload
+func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
+ zr, err := zip.NewReader(sr, size)
+ if err != nil {
+ return nil, err
+ }
+
+ p := &Package{
+ Metadata: &Metadata{
+ Manifests: make(map[string]*Manifest),
+ },
+ }
+
+ for _, file := range zr.File {
+ manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
+ if len(manifestMatch) == 0 {
+ continue
+ }
+
+ if file.UncompressedSize64 > maxManifestFileSize {
+ return nil, ErrManifestFileTooLarge
+ }
+
+ f, err := zr.Open(file.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ content, err := io.ReadAll(f)
+
+ if err := f.Close(); err != nil {
+ return nil, err
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ swiftVersion := ""
+ if len(manifestMatch) == 2 && manifestMatch[1] != "" {
+ v, err := version.NewSemver(manifestMatch[1])
+ if err != nil {
+ return nil, ErrInvalidManifestVersion
+ }
+ swiftVersion = TrimmedVersionString(v)
+ }
+
+ manifest := &Manifest{
+ Content: string(content),
+ }
+
+ toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
+ if len(toolsMatch) == 2 {
+ v, err := version.NewSemver(toolsMatch[1])
+ if err != nil {
+ return nil, ErrInvalidManifestVersion
+ }
+
+ manifest.ToolsVersion = TrimmedVersionString(v)
+ }
+
+ p.Metadata.Manifests[swiftVersion] = manifest
+ }
+
+ if _, found := p.Metadata.Manifests[""]; !found {
+ return nil, ErrMissingManifestFile
+ }
+
+ if mr != nil {
+ var ssc *SoftwareSourceCode
+ if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
+ return nil, err
+ }
+
+ p.Metadata.Description = ssc.Description
+ p.Metadata.Keywords = ssc.Keywords
+ p.Metadata.License = ssc.License
+ p.Metadata.Author = Person{
+ GivenName: ssc.Author.GivenName,
+ MiddleName: ssc.Author.MiddleName,
+ FamilyName: ssc.Author.FamilyName,
+ }
+
+ p.Metadata.RepositoryURL = ssc.CodeRepository
+ if !validation.IsValidURL(p.Metadata.RepositoryURL) {
+ p.Metadata.RepositoryURL = ""
+ }
+
+ p.RepositoryURLs = ssc.RepositoryURLs
+ }
+
+ return p, nil
+}
+
+// TrimmedVersionString returns the version string without the patch segment if it is zero
+func TrimmedVersionString(v *version.Version) string {
+ segments := v.Segments64()
+
+ var b strings.Builder
+ fmt.Fprintf(&b, "%d.%d", segments[0], segments[1])
+ if segments[2] != 0 {
+ fmt.Fprintf(&b, ".%d", segments[2])
+ }
+ return b.String()
+}
diff --git a/modules/packages/swift/metadata_test.go b/modules/packages/swift/metadata_test.go
new file mode 100644
index 00000000000..3913c2355ba
--- /dev/null
+++ b/modules/packages/swift/metadata_test.go
@@ -0,0 +1,144 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+ "archive/zip"
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/go-version"
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ packageName = "gitea"
+ packageVersion = "1.0.1"
+ packageDescription = "Package Description"
+ packageRepositoryURL = "https://gitea.io/gitea/gitea"
+ packageAuthor = "KN4CK3R"
+ packageLicense = "MIT"
+)
+
+func TestParsePackage(t *testing.T) {
+ createArchive := func(files map[string][]byte) *bytes.Reader {
+ var buf bytes.Buffer
+ zw := zip.NewWriter(&buf)
+ for filename, content := range files {
+ w, _ := zw.Create(filename)
+ w.Write(content)
+ }
+ zw.Close()
+ return bytes.NewReader(buf.Bytes())
+ }
+
+ t.Run("MissingManifestFile", func(t *testing.T) {
+ data := createArchive(map[string][]byte{"dummy.txt": {}})
+
+ p, err := ParsePackage(data, data.Size(), nil)
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrMissingManifestFile)
+ })
+
+ t.Run("ManifestFileTooLarge", func(t *testing.T) {
+ data := createArchive(map[string][]byte{
+ "Package.swift": make([]byte, maxManifestFileSize+1),
+ })
+
+ p, err := ParsePackage(data, data.Size(), nil)
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrManifestFileTooLarge)
+ })
+
+ t.Run("WithoutMetadata", func(t *testing.T) {
+ content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
+ content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
+
+ data := createArchive(map[string][]byte{
+ "Package.swift": []byte(content1),
+ "Package@swift-5.5.swift": []byte(content2),
+ })
+
+ p, err := ParsePackage(data, data.Size(), nil)
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.NotNil(t, p.Metadata)
+ assert.Empty(t, p.RepositoryURLs)
+ assert.Len(t, p.Metadata.Manifests, 2)
+ m := p.Metadata.Manifests[""]
+ assert.Equal(t, "5.7", m.ToolsVersion)
+ assert.Equal(t, content1, m.Content)
+ m = p.Metadata.Manifests["5.5"]
+ assert.Equal(t, "5.6", m.ToolsVersion)
+ assert.Equal(t, content2, m.Content)
+ })
+
+ t.Run("WithMetadata", func(t *testing.T) {
+ data := createArchive(map[string][]byte{
+ "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
+ })
+
+ p, err := ParsePackage(
+ data,
+ data.Size(),
+ strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
+ )
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.NotNil(t, p.Metadata)
+ assert.Len(t, p.Metadata.Manifests, 1)
+ m := p.Metadata.Manifests[""]
+ assert.Equal(t, "5.7", m.ToolsVersion)
+
+ assert.Equal(t, packageDescription, p.Metadata.Description)
+ assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
+ assert.Equal(t, packageLicense, p.Metadata.License)
+ assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
+ assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
+ assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs)
+ })
+}
+
+func TestTrimmedVersionString(t *testing.T) {
+ cases := []struct {
+ Version *version.Version
+ Expected string
+ }{
+ {
+ Version: version.Must(version.NewVersion("1")),
+ Expected: "1.0",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0")),
+ Expected: "1.0",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0.0")),
+ Expected: "1.0",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0.1")),
+ Expected: "1.0.1",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0+meta")),
+ Expected: "1.0",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0.0+meta")),
+ Expected: "1.0",
+ },
+ {
+ Version: version.Must(version.NewVersion("1.0.1+meta")),
+ Expected: "1.0.1",
+ },
+ }
+
+ for _, c := range cases {
+ assert.Equal(t, c.Expected, TrimmedVersionString(c.Version))
+ }
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index 13599e5a63e..ac0ad62bca3 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -39,6 +39,7 @@ var (
LimitSizePub int64
LimitSizePyPI int64
LimitSizeRubyGems int64
+ LimitSizeSwift int64
LimitSizeVagrant int64
}{
Enabled: true,
@@ -81,6 +82,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
+ Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 677af1397dd..e793c3ef033 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3239,6 +3239,10 @@ 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.
+swift.registry = Setup this registry from the command line:
+swift.install = Add the package in your Package.swift
file:
+swift.install2 = and run the following command:
+swift.documentation = For more information on the Swift 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
diff --git a/public/img/svg/gitea-swift.svg b/public/img/svg/gitea-swift.svg
new file mode 100644
index 00000000000..ebfea951da3
--- /dev/null
+++ b/public/img/svg/gitea-swift.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 0e3d8b7a02d..c0c7b117f69 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -28,6 +28,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/swift"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
@@ -375,6 +376,41 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/swift", func() {
+ r.Group("/{scope}/{name}", func() {
+ r.Group("", func() {
+ r.Get("", swift.EnumeratePackageVersions)
+ r.Get(".json", swift.EnumeratePackageVersions)
+ }, swift.CheckAcceptMediaType(swift.AcceptJSON))
+ r.Group("/{version}", func() {
+ r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
+ r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
+ r.Get("", func(ctx *context.Context) {
+ // Can't use normal routes here: https://github.com/go-chi/chi/issues/781
+
+ version := ctx.Params("version")
+ if strings.HasSuffix(version, ".zip") {
+ swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.SetParams("version", version[:len(version)-4])
+ swift.DownloadPackageFile(ctx)
+ } else {
+ swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
+ if ctx.Written() {
+ return
+ }
+ if strings.HasSuffix(version, ".json") {
+ ctx.SetParams("version", version[:len(version)-5])
+ }
+ swift.PackageVersionMetadata(ctx)
+ }
+ })
+ })
+ })
+ r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/vagrant", func() {
r.Group("/authenticate", func() {
r.Get("", vagrant.CheckAuthenticate)
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
new file mode 100644
index 00000000000..f78f703778b
--- /dev/null
+++ b/routers/api/packages/swift/swift.go
@@ -0,0 +1,464 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "regexp"
+ "sort"
+ "strings"
+
+ 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"
+ swift_module "code.gitea.io/gitea/modules/packages/swift"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+
+ "github.com/hashicorp/go-version"
+)
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
+const (
+ AcceptJSON = "application/vnd.swift.registry.v1+json"
+ AcceptSwift = "application/vnd.swift.registry.v1+swift"
+ AcceptZip = "application/vnd.swift.registry.v1+zip"
+)
+
+var (
+ // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope
+ scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
+ // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name
+ namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
+)
+
+type headers struct {
+ Status int
+ ContentType string
+ Digest string
+ Location string
+ Link string
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
+func setResponseHeaders(resp http.ResponseWriter, h *headers) {
+ if h.ContentType != "" {
+ resp.Header().Set("Content-Type", h.ContentType)
+ }
+ if h.Digest != "" {
+ resp.Header().Set("Digest", "sha256="+h.Digest)
+ }
+ if h.Location != "" {
+ resp.Header().Set("Location", h.Location)
+ }
+ if h.Link != "" {
+ resp.Header().Set("Link", h.Link)
+ }
+ resp.Header().Set("Content-Version", "1")
+ if h.Status != 0 {
+ resp.WriteHeader(h.Status)
+ }
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ // https://www.rfc-editor.org/rfc/rfc7807
+ type Problem struct {
+ Status int `json:"status"`
+ Detail string `json:"detail"`
+ }
+
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ setResponseHeaders(ctx.Resp, &headers{
+ Status: status,
+ ContentType: "application/problem+json",
+ })
+ if err := json.NewEncoder(ctx.Resp).Encode(Problem{
+ Status: status,
+ Detail: message,
+ }); err != nil {
+ log.Error("JSON encode: %v", err)
+ }
+ })
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
+func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) {
+ return func(ctx *context.Context) {
+ accept := ctx.Req.Header.Get("Accept")
+ if accept != "" && accept != requiredAcceptHeader {
+ apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader))
+ }
+ }
+}
+
+func buildPackageID(scope, name string) string {
+ return scope + "." + name
+}
+
+type Release struct {
+ URL string `json:"url"`
+}
+
+type EnumeratePackageVersionsResponse struct {
+ Releases map[string]Release `json:"releases"`
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases
+func EnumeratePackageVersions(ctx *context.Context) {
+ packageScope := ctx.Params("scope")
+ packageName := ctx.Params("name")
+
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, nil)
+ 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/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName)
+
+ releases := make(map[string]Release)
+ for _, pd := range pds {
+ version := pd.SemVer.String()
+ releases[version] = Release{
+ URL: baseURL + version,
+ }
+ }
+
+ setResponseHeaders(ctx.Resp, &headers{
+ Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version),
+ })
+
+ ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{
+ Releases: releases,
+ })
+}
+
+type Resource struct {
+ Name string `json:"id"`
+ Type string `json:"type"`
+ Checksum string `json:"checksum"`
+}
+
+type PackageVersionMetadataResponse struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Resources []Resource `json:"resources"`
+ Metadata *swift_module.SoftwareSourceCode `json:"metadata"`
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2
+func PackageVersionMetadata(ctx *context.Context) {
+ id := buildPackageID(ctx.Params("scope"), ctx.Params("name"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("version"))
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ metadata := pd.Metadata.(*swift_module.Metadata)
+
+ setResponseHeaders(ctx.Resp, &headers{})
+
+ ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{
+ ID: id,
+ Version: pd.Version.Version,
+ Resources: []Resource{
+ {
+ Name: "source-archive",
+ Type: "application/zip",
+ Checksum: pd.Files[0].Blob.HashSHA256,
+ },
+ },
+ Metadata: &swift_module.SoftwareSourceCode{
+ Context: []string{"http://schema.org/"},
+ Type: "SoftwareSourceCode",
+ Name: pd.PackageProperties.GetByName(swift_module.PropertyName),
+ Version: pd.Version.Version,
+ Description: metadata.Description,
+ Keywords: metadata.Keywords,
+ CodeRepository: metadata.RepositoryURL,
+ License: metadata.License,
+ ProgrammingLanguage: swift_module.ProgrammingLanguage{
+ Type: "ComputerLanguage",
+ Name: "Swift",
+ URL: "https://swift.org",
+ },
+ Author: swift_module.Person{
+ Type: "Person",
+ GivenName: metadata.Author.GivenName,
+ MiddleName: metadata.Author.MiddleName,
+ FamilyName: metadata.Author.FamilyName,
+ },
+ },
+ })
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release
+func DownloadManifest(ctx *context.Context) {
+ packageScope := ctx.Params("scope")
+ packageName := ctx.Params("name")
+ packageVersion := ctx.Params("version")
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ swiftVersion := ctx.FormTrim("swift-version")
+ if swiftVersion != "" {
+ v, err := version.NewVersion(swiftVersion)
+ if err == nil {
+ swiftVersion = swift_module.TrimmedVersionString(v)
+ }
+ }
+ m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion]
+ if !ok {
+ setResponseHeaders(ctx.Resp, &headers{
+ Status: http.StatusSeeOther,
+ Location: fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName, packageVersion),
+ })
+ return
+ }
+
+ setResponseHeaders(ctx.Resp, &headers{})
+
+ filename := "Package.swift"
+ if swiftVersion != "" {
+ filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
+ }
+
+ ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{
+ ContentType: "text/x-swift",
+ Filename: filename,
+ LastModified: pv.CreatedUnix.AsLocalTime(),
+ })
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6
+func UploadPackageFile(ctx *context.Context) {
+ packageScope := ctx.Params("scope")
+ packageName := ctx.Params("name")
+
+ v, err := version.NewVersion(ctx.Params("version"))
+
+ if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ packageVersion := v.Core().String()
+
+ file, _, err := ctx.Req.FormFile("source-archive")
+ 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()
+
+ var mr io.Reader
+ metadata := ctx.Req.FormValue("metadata")
+ if metadata != "" {
+ mr = strings.NewReader(metadata)
+ }
+
+ pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
+ 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
+ }
+
+ pv, _, err := packages_service.CreatePackageAndAddFile(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeSwift,
+ Name: buildPackageID(packageScope, packageName),
+ Version: packageVersion,
+ },
+ SemverCompatible: true,
+ Creator: ctx.Doer,
+ Metadata: pck.Metadata,
+ PackageProperties: map[string]string{
+ swift_module.PropertyScope: packageScope,
+ swift_module.PropertyName: packageName,
+ },
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion),
+ },
+ 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
+ }
+
+ for _, url := range pck.RepositoryURLs {
+ _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url)
+ if err != nil {
+ log.Error("InsertProperty failed: %v", err)
+ }
+ }
+
+ setResponseHeaders(ctx.Resp, &headers{})
+
+ ctx.Status(http.StatusCreated)
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4
+func DownloadPackageFile(ctx *context.Context) {
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), 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
+ }
+
+ 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()
+
+ setResponseHeaders(ctx.Resp, &headers{
+ Digest: pd.Files[0].Blob.HashSHA256,
+ })
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ ContentType: "application/zip",
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
+
+type LookupPackageIdentifiersResponse struct {
+ Identifiers []string `json:"identifiers"`
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5
+func LookupPackageIdentifiers(ctx *context.Context) {
+ url := ctx.FormTrim("url")
+ if url == "" {
+ apiError(ctx, http.StatusBadRequest, nil)
+ return
+ }
+
+ pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeSwift,
+ Properties: map[string]string{
+ swift_module.PropertyRepositoryURL: url,
+ },
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ identifiers := make([]string, 0, len(pds))
+ for _, pd := range pds {
+ identifiers = append(identifiers, pd.Package.Name)
+ }
+
+ setResponseHeaders(ctx.Resp, &headers{})
+
+ ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{
+ Identifiers: identifiers,
+ })
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index ab077090d1c..200dc5aaf14 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
- // enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
+ // enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, swift, vagrant]
// - name: q
// in: query
// description: name filter
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index b22ed47c775..699d0fe44f9 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -15,7 +15,7 @@ import (
type PackageCleanupRuleForm struct {
ID int64
Enabled bool
- Type string `binding:"Required;In(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
+ Type string `binding:"Required;In(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,swift,vagrant)"`
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
KeepPattern string `binding:"RegexPattern"`
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 3abca7337c7..dd5c63470b8 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -361,6 +361,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
typeSpecificSize = setting.Packages.LimitSizePyPI
case packages_model.TypeRubyGems:
typeSpecificSize = setting.Packages.LimitSizeRubyGems
+ case packages_model.TypeSwift:
+ typeSpecificSize = setting.Packages.LimitSizeSwift
case packages_model.TypeVagrant:
typeSpecificSize = setting.Packages.LimitSizeVagrant
}
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl
new file mode 100644
index 00000000000..3ff06483b83
--- /dev/null
+++ b/templates/package/content/swift.tmpl
@@ -0,0 +1,40 @@
+{{if eq .PackageDescriptor.Package.Type "swift"}}
+
swift package-registry set
dependencies: [
+ .package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
+]
swift package resolve