Add package registry quota limits (#21584)
Related #20471 This PR adds global quota limits for the package registry. Settings for individual users/orgs can be added in a seperate PR using the settings table. Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
cb83288530
commit
20674dd05d
@ -44,6 +44,7 @@ func TestMigratePackages(t *testing.T) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: "a.go",
|
||||
},
|
||||
Creator: creator,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
})
|
||||
|
@ -2335,6 +2335,35 @@ ROUTER = console
|
||||
;;
|
||||
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
|
||||
;CHUNKED_UPLOAD_PATH = tmp/package-upload
|
||||
;;
|
||||
;; Maxmimum count of package versions a single owner can have (`-1` means no limits)
|
||||
;LIMIT_TOTAL_OWNER_COUNT = -1
|
||||
;; Maxmimum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_TOTAL_OWNER_SIZE = -1
|
||||
;; Maxmimum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_COMPOSER = -1
|
||||
;; Maxmimum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_CONAN = -1
|
||||
;; Maxmimum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_CONTAINER = -1
|
||||
;; Maxmimum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_GENERIC = -1
|
||||
;; Maxmimum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_HELM = -1
|
||||
;; Maxmimum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_MAVEN = -1
|
||||
;; Maxmimum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_NPM = -1
|
||||
;; Maxmimum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_NUGET = -1
|
||||
;; Maxmimum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_PUB = -1
|
||||
;; Maxmimum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_PYPI = -1
|
||||
;; Maxmimum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_RUBYGEMS = -1
|
||||
;; Maxmimum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_VAGRANT = -1
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -1138,6 +1138,20 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
|
||||
|
||||
- `ENABLED`: **true**: Enable/Disable package registry capabilities
|
||||
- `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload`
|
||||
- `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maxmimum count of package versions a single owner can have (`-1` means no limits)
|
||||
- `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maxmimum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_COMPOSER`: **-1**: Maxmimum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_CONAN`: **-1**: Maxmimum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_CONTAINER`: **-1**: Maxmimum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_GENERIC`: **-1**: Maxmimum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_HELM`: **-1**: Maxmimum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_MAVEN`: **-1**: Maxmimum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_NPM`: **-1**: Maxmimum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_NUGET`: **-1**: Maxmimum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_PUB`: **-1**: Maxmimum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_PYPI`: **-1**: Maxmimum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_RUBYGEMS`: **-1**: Maxmimum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_VAGRANT`: **-1**: Maxmimum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
|
||||
## Mirror (`mirror`)
|
||||
|
||||
|
@ -199,3 +199,13 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag
|
||||
count, err := sess.FindAndCount(&pfs)
|
||||
return pfs, count, err
|
||||
}
|
||||
|
||||
// CalculateBlobSize sums up all blob sizes matching the search options.
|
||||
// It does NOT respect the deduplication of blobs.
|
||||
func CalculateBlobSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Table("package_file").
|
||||
Where(opts.toConds()).
|
||||
Join("INNER", "package_blob", "package_blob.id = package_file.blob_id").
|
||||
SumInt(new(PackageBlob), "size")
|
||||
}
|
||||
|
@ -319,3 +319,12 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
|
||||
count, err := sess.FindAndCount(&pvs)
|
||||
return pvs, count, err
|
||||
}
|
||||
|
||||
// CountVersions counts all versions of packages matching the search options
|
||||
func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where(opts.toConds()).
|
||||
Table("package_version").
|
||||
Join("INNER", "package", "package.id = package_version.package_id").
|
||||
Count(new(PackageVersion))
|
||||
}
|
||||
|
@ -5,11 +5,15 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// Package registry settings
|
||||
@ -19,8 +23,24 @@ var (
|
||||
Enabled bool
|
||||
ChunkedUploadPath string
|
||||
RegistryHost string
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeComposer int64
|
||||
LimitSizeConan int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeHelm int64
|
||||
LimitSizeMaven int64
|
||||
LimitSizeNpm int64
|
||||
LimitSizeNuGet int64
|
||||
LimitSizePub int64
|
||||
LimitSizePyPI int64
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeVagrant int64
|
||||
}{
|
||||
Enabled: true,
|
||||
LimitTotalOwnerCount: -1,
|
||||
}
|
||||
)
|
||||
|
||||
@ -43,4 +63,32 @@ func newPackages() {
|
||||
if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil {
|
||||
log.Error("Unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err)
|
||||
}
|
||||
|
||||
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
|
||||
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
|
||||
Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
|
||||
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
|
||||
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
|
||||
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
|
||||
Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
|
||||
Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM")
|
||||
Packages.LimitSizeNuGet = mustBytes(sec, "LIMIT_SIZE_NUGET")
|
||||
Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
|
||||
Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
|
||||
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
||||
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
||||
}
|
||||
|
||||
func mustBytes(section *ini.Section, key string) int64 {
|
||||
const noLimit = "-1"
|
||||
|
||||
value := section.Key(key).MustString(noLimit)
|
||||
if value == noLimit {
|
||||
return -1
|
||||
}
|
||||
bytes, err := humanize.ParseBytes(value)
|
||||
if err != nil || bytes > math.MaxInt64 {
|
||||
return -1
|
||||
}
|
||||
return int64(bytes)
|
||||
}
|
||||
|
31
modules/setting/packages_test.go
Normal file
31
modules/setting/packages_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestMustBytes(t *testing.T) {
|
||||
test := func(value string) int64 {
|
||||
sec, _ := ini.Empty().NewSection("test")
|
||||
sec.NewKey("VALUE", value)
|
||||
|
||||
return mustBytes(sec, "VALUE")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, -1, test(""))
|
||||
assert.EqualValues(t, -1, test("-1"))
|
||||
assert.EqualValues(t, 0, test("0"))
|
||||
assert.EqualValues(t, 1, test("1"))
|
||||
assert.EqualValues(t, 10000, test("10000"))
|
||||
assert.EqualValues(t, 1000000, test("1 mb"))
|
||||
assert.EqualValues(t, 1048576, test("1mib"))
|
||||
assert.EqualValues(t, 1782579, test("1.7mib"))
|
||||
assert.EqualValues(t, -1, test("1 yib")) // too large
|
||||
}
|
@ -235,16 +235,20 @@ func UploadPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -348,6 +348,7 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey
|
||||
Filename: strings.ToLower(filename),
|
||||
CompositeKey: fileKey,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: isConanfileFile,
|
||||
Properties: map[string]string{
|
||||
@ -416,11 +417,14 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey
|
||||
pfci,
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -104,16 +104,20 @@ func UploadPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -186,17 +186,21 @@ func UploadPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: createFilename(metadata),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -266,6 +266,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: params.Filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: false,
|
||||
OverwriteExisting: params.IsMeta,
|
||||
@ -312,11 +313,14 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
pfci,
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -180,16 +180,20 @@ func UploadPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: npmPackage.Filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -374,16 +374,20 @@ func UploadPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -428,6 +432,7 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: false,
|
||||
},
|
||||
@ -438,6 +443,8 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
@ -452,6 +459,7 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
Filename: strings.ToLower(pdb.Name),
|
||||
CompositeKey: strings.ToLower(pdb.ID),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: pdb.Content,
|
||||
IsLead: false,
|
||||
Properties: map[string]string{
|
||||
@ -463,6 +471,8 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
@ -199,16 +199,20 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(pck.Version + ".tar.gz"),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -162,16 +162,20 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: fileHeader.Filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -242,16 +242,20 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -193,6 +193,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(boxProvider),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
Properties: map[string]string{
|
||||
@ -201,11 +202,14 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@ -19,10 +20,17 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/notification"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrQuotaTypeSize = errors.New("maximum allowed package type size exceeded")
|
||||
ErrQuotaTotalSize = errors.New("maximum allowed package storage quota exceeded")
|
||||
ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
|
||||
)
|
||||
|
||||
// PackageInfo describes a package
|
||||
type PackageInfo struct {
|
||||
Owner *user_model.User
|
||||
@ -50,6 +58,7 @@ type PackageFileInfo struct {
|
||||
// PackageFileCreationInfo describes a package file to create
|
||||
type PackageFileCreationInfo struct {
|
||||
PackageFileInfo
|
||||
Creator *user_model.User
|
||||
Data packages_module.HashedSizeReader
|
||||
IsLead bool
|
||||
Properties map[string]string
|
||||
@ -78,7 +87,7 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
|
||||
pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, &pvci.PackageInfo, pfci)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if blobCreated && removeBlob {
|
||||
@ -164,6 +173,10 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
|
||||
}
|
||||
|
||||
if versionCreated {
|
||||
if err := checkCountQuotaExceeded(ctx, pvci.Creator, pvci.Owner); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
for name, value := range pvci.VersionProperties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
@ -188,7 +201,7 @@ func AddFileToExistingPackage(pvi *PackageInfo, pfci *PackageFileCreationInfo) (
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
|
||||
pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pvi, pfci)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
@ -224,9 +237,13 @@ func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.Packag
|
||||
}
|
||||
}
|
||||
|
||||
func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
|
||||
|
||||
if err := checkSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
@ -285,6 +302,80 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers
|
||||
return pf, pb, !exists, nil
|
||||
}
|
||||
|
||||
func checkCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) error {
|
||||
if doer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
if setting.Packages.LimitTotalOwnerCount > -1 {
|
||||
totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: owner.ID,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CountVersions failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if totalCount > setting.Packages.LimitTotalOwnerCount {
|
||||
return ErrQuotaTotalCount
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, packageType packages_model.Type, uploadSize int64) error {
|
||||
if doer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
var typeSpecificSize int64
|
||||
switch packageType {
|
||||
case packages_model.TypeComposer:
|
||||
typeSpecificSize = setting.Packages.LimitSizeComposer
|
||||
case packages_model.TypeConan:
|
||||
typeSpecificSize = setting.Packages.LimitSizeConan
|
||||
case packages_model.TypeContainer:
|
||||
typeSpecificSize = setting.Packages.LimitSizeContainer
|
||||
case packages_model.TypeGeneric:
|
||||
typeSpecificSize = setting.Packages.LimitSizeGeneric
|
||||
case packages_model.TypeHelm:
|
||||
typeSpecificSize = setting.Packages.LimitSizeHelm
|
||||
case packages_model.TypeMaven:
|
||||
typeSpecificSize = setting.Packages.LimitSizeMaven
|
||||
case packages_model.TypeNpm:
|
||||
typeSpecificSize = setting.Packages.LimitSizeNpm
|
||||
case packages_model.TypeNuGet:
|
||||
typeSpecificSize = setting.Packages.LimitSizeNuGet
|
||||
case packages_model.TypePub:
|
||||
typeSpecificSize = setting.Packages.LimitSizePub
|
||||
case packages_model.TypePyPI:
|
||||
typeSpecificSize = setting.Packages.LimitSizePyPI
|
||||
case packages_model.TypeRubyGems:
|
||||
typeSpecificSize = setting.Packages.LimitSizeRubyGems
|
||||
case packages_model.TypeVagrant:
|
||||
typeSpecificSize = setting.Packages.LimitSizeVagrant
|
||||
}
|
||||
if typeSpecificSize > -1 && typeSpecificSize < uploadSize {
|
||||
return ErrQuotaTypeSize
|
||||
}
|
||||
|
||||
if setting.Packages.LimitTotalOwnerSize > -1 {
|
||||
totalSize, err := packages_model.CalculateBlobSize(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: owner.ID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CalculateBlobSize failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if totalSize+uploadSize > setting.Packages.LimitTotalOwnerSize {
|
||||
return ErrQuotaTotalSize
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePackageVersionByNameAndVersion deletes a package version and all associated files
|
||||
func RemovePackageVersionByNameAndVersion(doer *user_model.User, pvi *PackageInfo) error {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
"code.gitea.io/gitea/tests"
|
||||
@ -166,6 +167,39 @@ func TestPackageAccess(t *testing.T) {
|
||||
uploadPackage(admin, user, http.StatusCreated)
|
||||
}
|
||||
|
||||
func TestPackageQuota(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
limitTotalOwnerCount, limitTotalOwnerSize, limitSizeGeneric := setting.Packages.LimitTotalOwnerCount, setting.Packages.LimitTotalOwnerSize, setting.Packages.LimitSizeGeneric
|
||||
|
||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
|
||||
|
||||
uploadPackage := func(doer *user_model.User, version string, expectedStatus int) {
|
||||
url := fmt.Sprintf("/api/packages/%s/generic/test-package/%s/file.bin", user.Name, version)
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1}))
|
||||
AddBasicAuthHeader(req, doer.Name)
|
||||
MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
// Exceeded quota result in StatusForbidden for normal users but admins are always allowed to upload.
|
||||
|
||||
setting.Packages.LimitTotalOwnerCount = 0
|
||||
uploadPackage(user, "1.0", http.StatusForbidden)
|
||||
uploadPackage(admin, "1.0", http.StatusCreated)
|
||||
setting.Packages.LimitTotalOwnerCount = limitTotalOwnerCount
|
||||
|
||||
setting.Packages.LimitTotalOwnerSize = 0
|
||||
uploadPackage(user, "1.1", http.StatusForbidden)
|
||||
uploadPackage(admin, "1.1", http.StatusCreated)
|
||||
setting.Packages.LimitTotalOwnerSize = limitTotalOwnerSize
|
||||
|
||||
setting.Packages.LimitSizeGeneric = 0
|
||||
uploadPackage(user, "1.2", http.StatusForbidden)
|
||||
uploadPackage(admin, "1.2", http.StatusCreated)
|
||||
setting.Packages.LimitSizeGeneric = limitSizeGeneric
|
||||
}
|
||||
|
||||
func TestPackageCleanup(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user